Compare commits
17 Commits
e4cd0c99df
...
v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
d7746d14e4
|
|||
|
648d5225f6
|
|||
|
9d4d0f2e48
|
|||
|
fe386163f4
|
|||
|
ac2cee10e5
|
|||
|
9c370647dd
|
|||
|
7516e197fe
|
|||
|
71c372ab6c
|
|||
|
25f23f7f93
|
|||
|
51853698b9
|
|||
|
39ed5393ab
|
|||
|
782b3f1b08
|
|||
|
3ef2f13dd3
|
|||
|
36b0f55a47
|
|||
|
bc7a6e865e
|
|||
|
2ff60fc4ff
|
|||
|
ea93aa144e
|
@@ -12,6 +12,8 @@
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
@@ -159,4 +161,4 @@
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -618,6 +618,7 @@
|
||||
"tagsHint": "Enter tags, separated by commas",
|
||||
"categories": "Categories",
|
||||
"categoriesHint": "Enter categories, separated by commas",
|
||||
"categoriesAndTags": "Categories & Tags",
|
||||
"chatNotJoined": "You have not joined this chat yet.",
|
||||
"chatUnableJoin": "You can't join this chat due to it's access control settings.",
|
||||
"chatJoin": "Join the Chat",
|
||||
|
||||
@@ -257,6 +257,8 @@ PODS:
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- pointer_interceptor_ios (0.0.1):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
@@ -351,6 +353,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
@@ -458,6 +461,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/pasteboard/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
pointer_interceptor_ios:
|
||||
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||
protocol_handler_ios:
|
||||
@@ -539,6 +544,7 @@ SPEC CHECKSUMS:
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
pointer_interceptor_ios: da06a662d5bfd329602b45b2ab41bc0fb5fdb0f0
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -14,7 +14,7 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
final indexedCloudFileListProvider = AsyncNotifierProvider(
|
||||
final indexedCloudFileListProvider = AsyncNotifierProvider.autoDispose(
|
||||
IndexedCloudFileListNotifier.new,
|
||||
);
|
||||
|
||||
@@ -76,12 +76,12 @@ class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<String> folders =
|
||||
(response.data['folders'] as List).map((e) => e as String).toList();
|
||||
final List<SnCloudFileIndex> files =
|
||||
(response.data['files'] as List)
|
||||
.map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final List<String> folders = (response.data['folders'] as List)
|
||||
.map((e) => e as String)
|
||||
.toList();
|
||||
final List<SnCloudFileIndex> files = (response.data['files'] as List)
|
||||
.map((e) => SnCloudFileIndex.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final List<FileListItem> items = [
|
||||
...folders.map((folderName) => FileListItem.folder(folderName)),
|
||||
@@ -92,7 +92,7 @@ class IndexedCloudFileListNotifier extends AsyncNotifier<List<FileListItem>>
|
||||
}
|
||||
}
|
||||
|
||||
final unindexedFileListProvider = AsyncNotifierProvider(
|
||||
final unindexedFileListProvider = AsyncNotifierProvider.autoDispose(
|
||||
UnindexedFileListNotifier.new,
|
||||
);
|
||||
|
||||
@@ -165,13 +165,13 @@ class UnindexedFileListNotifier extends AsyncNotifier<List<FileListItem>>
|
||||
|
||||
totalCount = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||
|
||||
final List<SnCloudFile> files =
|
||||
(response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final List<SnCloudFile> files = (response.data as List)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final List<FileListItem> items =
|
||||
files.map((file) => FileListItem.unindexedFile(file)).toList();
|
||||
final List<FileListItem> items = files
|
||||
.map((file) => FileListItem.unindexedFile(file))
|
||||
.toList();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
52
lib/pods/post/post_categories.dart
Normal file
52
lib/pods/post/post_categories.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
// Post Categories Notifier
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post_category.dart';
|
||||
import 'package:island/models/post_tag.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
|
||||
final postCategoriesProvider =
|
||||
AsyncNotifierProvider.autoDispose<
|
||||
PostCategoriesNotifier,
|
||||
List<SnPostCategory>
|
||||
>(PostCategoriesNotifier.new);
|
||||
|
||||
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
|
||||
with AsyncPaginationController<SnPostCategory> {
|
||||
@override
|
||||
Future<List<SnPostCategory>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts/categories',
|
||||
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final data = response.data as List;
|
||||
return data.map((json) => SnPostCategory.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Post Tags Notifier
|
||||
final postTagsProvider =
|
||||
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
|
||||
PostTagsNotifier.new,
|
||||
);
|
||||
|
||||
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
|
||||
with AsyncPaginationController<SnPostTag> {
|
||||
@override
|
||||
Future<List<SnPostTag>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts/tags',
|
||||
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final data = response.data as List;
|
||||
return data.map((json) => SnPostTag.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/activity.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
|
||||
final activityListProvider =
|
||||
AsyncNotifierProvider<ActivityListNotifier, List<SnTimelineEvent>>(
|
||||
ActivityListNotifier.new,
|
||||
);
|
||||
final activityListProvider = AsyncNotifierProvider.autoDispose(
|
||||
ActivityListNotifier.new,
|
||||
);
|
||||
|
||||
class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
|
||||
with
|
||||
@@ -28,8 +26,6 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
|
||||
if (cursor != null) 'cursor': cursor,
|
||||
'take': pageSize,
|
||||
if (currentFilter != null) 'filter': currentFilter,
|
||||
if (kDebugMode)
|
||||
'debugInclude': 'realms,publishers,articles,shuffledPosts',
|
||||
};
|
||||
|
||||
final response = await client.get(
|
||||
@@ -37,10 +33,9 @@ class ActivityListNotifier extends AsyncNotifier<List<SnTimelineEvent>>
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<SnTimelineEvent> items =
|
||||
(response.data as List)
|
||||
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
final List<SnTimelineEvent> items = (response.data as List)
|
||||
.map((e) => SnTimelineEvent.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty';
|
||||
|
||||
|
||||
@@ -5,16 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/webfeed.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
final webFeedListProvider = FutureProvider.family<List<SnWebFeed>, String>((
|
||||
ref,
|
||||
pubName,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final response = await client.get('/sphere/publishers/$pubName/feeds');
|
||||
return (response.data as List)
|
||||
.map((json) => SnWebFeed.fromJson(json))
|
||||
.toList();
|
||||
});
|
||||
final webFeedListProvider = FutureProvider.autoDispose
|
||||
.family<List<SnWebFeed>, String>((ref, pubName) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final response = await client.get('/sphere/publishers/$pubName/feeds');
|
||||
return (response.data as List)
|
||||
.map((json) => SnWebFeed.fromJson(json))
|
||||
.toList();
|
||||
});
|
||||
|
||||
class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
||||
final ({String pubName, String? feedId}) arg;
|
||||
@@ -51,10 +49,9 @@ class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final url = '/sphere/publishers/${feed.publisherId}/feeds';
|
||||
|
||||
final response =
|
||||
feed.id.isEmpty
|
||||
? await client.post(url, data: feed.toJson())
|
||||
: await client.patch('$url/${feed.id}', data: feed.toJson());
|
||||
final response = feed.id.isEmpty
|
||||
? await client.post(url, data: feed.toJson())
|
||||
: await client.patch('$url/${feed.id}', data: feed.toJson());
|
||||
|
||||
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
|
||||
} catch (error, stackTrace) {
|
||||
|
||||
112
lib/route.dart
112
lib/route.dart
@@ -105,10 +105,9 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'articleCompose',
|
||||
path: '/articles/compose',
|
||||
builder:
|
||||
(context, state) => ArticleComposeScreen(
|
||||
initialState: state.extra as PostComposeInitialState?,
|
||||
),
|
||||
builder: (context, state) => ArticleComposeScreen(
|
||||
initialState: state.extra as PostComposeInitialState?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'articleEdit',
|
||||
@@ -190,12 +189,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'explore',
|
||||
path: '/',
|
||||
pageBuilder:
|
||||
(context, state) => CustomTransitionPage(
|
||||
key: const ValueKey('explore'),
|
||||
child: const ExploreScreen(),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
key: const ValueKey('explore'),
|
||||
child: const ExploreScreen(),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postSearch',
|
||||
@@ -220,11 +218,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return PostCategoryDetailScreen(slug: slug, isCategory: true);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postTags',
|
||||
path: '/posts/tags',
|
||||
builder: (context, state) => const PostTagsListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postTagDetail',
|
||||
path: '/posts/tags/:slug',
|
||||
@@ -260,12 +253,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
|
||||
// Chat tab
|
||||
ShellRoute(
|
||||
pageBuilder:
|
||||
(context, state, child) => CustomTransitionPage(
|
||||
key: const ValueKey('chat'),
|
||||
child: ChatShellScreen(child: child),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
pageBuilder: (context, state, child) => CustomTransitionPage(
|
||||
key: const ValueKey('chat'),
|
||||
child: ChatShellScreen(child: child),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'chatList',
|
||||
@@ -303,12 +295,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'realmList',
|
||||
path: '/realms',
|
||||
pageBuilder:
|
||||
(context, state) => CustomTransitionPage(
|
||||
key: const ValueKey('realms'),
|
||||
child: const RealmListScreen(),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
key: const ValueKey('realms'),
|
||||
child: const RealmListScreen(),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'realmNew',
|
||||
@@ -336,12 +327,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
|
||||
// Account tab
|
||||
ShellRoute(
|
||||
pageBuilder:
|
||||
(context, state, child) => CustomTransitionPage(
|
||||
key: const ValueKey('account'),
|
||||
child: AccountShellScreen(child: child),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
pageBuilder: (context, state, child) => CustomTransitionPage(
|
||||
key: const ValueKey('account'),
|
||||
child: AccountShellScreen(child: child),
|
||||
transitionsBuilder: _tabPagesTransitionBuilder,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'account',
|
||||
@@ -352,8 +342,8 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'stickerMarketplace',
|
||||
path: '/stickers',
|
||||
builder:
|
||||
(context, state) => const MarketplaceStickersScreen(),
|
||||
builder: (context, state) =>
|
||||
const MarketplaceStickersScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'stickerPackDetail',
|
||||
@@ -368,8 +358,8 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'webFeedMarketplace',
|
||||
path: '/feeds',
|
||||
builder:
|
||||
(context, state) => const MarketplaceWebFeedsScreen(),
|
||||
builder: (context, state) =>
|
||||
const MarketplaceWebFeedsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'webFeedDetail',
|
||||
@@ -516,29 +506,25 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'developerHub',
|
||||
path: '/developers',
|
||||
builder:
|
||||
(context, state) => DeveloperHubScreen(
|
||||
initialPublisherName:
|
||||
state.uri.queryParameters['publisher'],
|
||||
initialProjectId: state.uri.queryParameters['project'],
|
||||
),
|
||||
builder: (context, state) => DeveloperHubScreen(
|
||||
initialPublisherName: state.uri.queryParameters['publisher'],
|
||||
initialProjectId: state.uri.queryParameters['project'],
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'developerProjectNew',
|
||||
path: ':name/projects/new',
|
||||
builder:
|
||||
(context, state) => NewProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
),
|
||||
builder: (context, state) => NewProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectEdit',
|
||||
path: ':name/projects/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
builder: (context, state) => EditProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectDetail',
|
||||
@@ -558,22 +544,20 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
name: 'developerAppDetail',
|
||||
path: 'apps/:appId',
|
||||
builder:
|
||||
(context, state) => AppDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
appId: state.pathParameters['appId']!,
|
||||
),
|
||||
builder: (context, state) => AppDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
appId: state.pathParameters['appId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotDetail',
|
||||
path: 'bots/:botId',
|
||||
builder:
|
||||
(context, state) => BotDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
botId: state.pathParameters['botId']!,
|
||||
),
|
||||
builder: (context, state) => BotDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
botId: state.pathParameters['botId']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ Future<double> socialCredits(Ref ref) async {
|
||||
return response.data?.toDouble() ?? 0.0;
|
||||
}
|
||||
|
||||
final socialCreditHistoryNotifierProvider = AsyncNotifierProvider(
|
||||
final socialCreditHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
|
||||
SocialCreditHistoryNotifier.new,
|
||||
);
|
||||
|
||||
@@ -45,11 +45,10 @@ class SocialCreditHistoryNotifier
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
|
||||
final records =
|
||||
response.data
|
||||
.map((json) => SnSocialCreditRecord.fromJson(json))
|
||||
.cast<SnSocialCreditRecord>()
|
||||
.toList();
|
||||
final records = response.data
|
||||
.map((json) => SnSocialCreditRecord.fromJson(json))
|
||||
.cast<SnSocialCreditRecord>()
|
||||
.toList();
|
||||
|
||||
return records;
|
||||
}
|
||||
@@ -68,39 +67,36 @@ class SocialCreditsTab extends HookConsumerWidget {
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: socialCredits
|
||||
.when(
|
||||
data:
|
||||
(credits) => Stack(
|
||||
data: (credits) => Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
credits < 100
|
||||
? 'socialCreditsLevelPoor'.tr()
|
||||
: credits < 150
|
||||
? 'socialCreditsLevelNormal'.tr()
|
||||
: credits < 200
|
||||
? 'socialCreditsLevelGood'.tr()
|
||||
: 'socialCreditsLevelExcellent'.tr(),
|
||||
).tr().bold().fontSize(20),
|
||||
Text(
|
||||
'${credits.toStringAsFixed(2)} pts',
|
||||
).fontSize(14),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(value: credits / 200),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Symbols.info),
|
||||
tooltip: 'socialCreditsDescription'.tr(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
credits < 100
|
||||
? 'socialCreditsLevelPoor'.tr()
|
||||
: credits < 150
|
||||
? 'socialCreditsLevelNormal'.tr()
|
||||
: credits < 200
|
||||
? 'socialCreditsLevelGood'.tr()
|
||||
: 'socialCreditsLevelExcellent'.tr(),
|
||||
).tr().bold().fontSize(20),
|
||||
Text('${credits.toStringAsFixed(2)} pts').fontSize(14),
|
||||
const Gap(8),
|
||||
LinearProgressIndicator(value: credits / 200),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Symbols.info),
|
||||
tooltip: 'socialCreditsDescription'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
error: (_, _) => Text('Error loading credits'),
|
||||
loading: () => const LinearProgressIndicator(),
|
||||
)
|
||||
@@ -119,15 +115,14 @@ class SocialCreditsTab extends HookConsumerWidget {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(
|
||||
record.reason,
|
||||
style:
|
||||
isExpired
|
||||
? TextStyle(
|
||||
decoration: TextDecoration.lineThrough,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.8),
|
||||
)
|
||||
: null,
|
||||
style: isExpired
|
||||
? TextStyle(
|
||||
decoration: TextDecoration.lineThrough,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withOpacity(0.8),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
subtitle: Row(
|
||||
spacing: 4,
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
final levelingHistoryNotifierProvider = AsyncNotifierProvider(
|
||||
final levelingHistoryNotifierProvider = AsyncNotifierProvider.autoDispose(
|
||||
LevelingHistoryNotifier.new,
|
||||
);
|
||||
|
||||
@@ -35,11 +35,10 @@ class LevelingHistoryNotifier extends AsyncNotifier<List<SnExperienceRecord>>
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
|
||||
final List<SnExperienceRecord> records =
|
||||
response.data
|
||||
.map((json) => SnExperienceRecord.fromJson(json))
|
||||
.cast<SnExperienceRecord>()
|
||||
.toList();
|
||||
final List<SnExperienceRecord> records = response.data
|
||||
.map((json) => SnExperienceRecord.fromJson(json))
|
||||
.cast<SnExperienceRecord>()
|
||||
.toList();
|
||||
|
||||
return records;
|
||||
}
|
||||
@@ -162,8 +161,9 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
stopIndicatorRadius: 0,
|
||||
trackGap: 0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
],
|
||||
@@ -186,38 +186,35 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
notifier: levelingHistoryNotifierProvider.notifier,
|
||||
isRefreshable: false,
|
||||
isSliver: true,
|
||||
itemBuilder:
|
||||
(context, idx, record) => ListTile(
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(record.reason),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
record.createdAt.formatRelative(context),
|
||||
).fontSize(13),
|
||||
Text('·').fontSize(13).bold(),
|
||||
Text(record.createdAt.formatSystem()).fontSize(13),
|
||||
],
|
||||
).opacity(0.8),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
spacing: 8,
|
||||
itemBuilder: (context, idx, record) => ListTile(
|
||||
title: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(record.reason),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
|
||||
),
|
||||
if (record.bonusMultiplier != 1.0)
|
||||
Text('x${record.bonusMultiplier}'),
|
||||
record.createdAt.formatRelative(context),
|
||||
).fontSize(13),
|
||||
Text('·').fontSize(13).bold(),
|
||||
Text(record.createdAt.formatSystem()).fontSize(13),
|
||||
],
|
||||
),
|
||||
minTileHeight: 56,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
).opacity(0.8),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text('${record.delta > 0 ? '+' : ''}${record.delta} EXP'),
|
||||
if (record.bonusMultiplier != 1.0)
|
||||
Text('x${record.bonusMultiplier}'),
|
||||
],
|
||||
),
|
||||
minTileHeight: 56,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
),
|
||||
),
|
||||
|
||||
SliverGap(20),
|
||||
@@ -249,11 +246,10 @@ class LevelStairsPainter extends CustomPainter {
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint =
|
||||
Paint()
|
||||
..color = surfaceColor.withOpacity(0.2)
|
||||
..strokeWidth = 1.5
|
||||
..style = PaintingStyle.stroke;
|
||||
final paint = Paint()
|
||||
..color = surfaceColor.withOpacity(0.2)
|
||||
..strokeWidth = 1.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
// Draw connecting lines between stairs
|
||||
for (int i = 0; i < totalLevels - 1; i++) {
|
||||
|
||||
@@ -29,7 +29,7 @@ Future<List<SnRelationship>> sentFriendRequest(Ref ref) async {
|
||||
.toList();
|
||||
}
|
||||
|
||||
final relationshipListNotifierProvider = AsyncNotifierProvider(
|
||||
final relationshipListNotifierProvider = AsyncNotifierProvider.autoDispose(
|
||||
RelationshipListNotifier.new,
|
||||
);
|
||||
|
||||
@@ -45,11 +45,10 @@ class RelationshipListNotifier extends AsyncNotifier<List<SnRelationship>>
|
||||
queryParameters: {'offset': fetchedCount.toString(), 'take': take},
|
||||
);
|
||||
|
||||
final List<SnRelationship> items =
|
||||
(response.data as List)
|
||||
.map((e) => SnRelationship.fromJson(e as Map<String, dynamic>))
|
||||
.cast<SnRelationship>()
|
||||
.toList();
|
||||
final List<SnRelationship> items = (response.data as List)
|
||||
.map((e) => SnRelationship.fromJson(e as Map<String, dynamic>))
|
||||
.cast<SnRelationship>()
|
||||
.toList();
|
||||
|
||||
totalCount = int.tryParse(response.headers['x-total']?.first ?? '') ?? 0;
|
||||
|
||||
@@ -83,8 +82,9 @@ class RelationshipListTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final account =
|
||||
showRelatedAccount ? relationship.related : relationship.account;
|
||||
final account = showRelatedAccount
|
||||
? relationship.related
|
||||
: relationship.account;
|
||||
final isPending =
|
||||
relationship.status == 0 && relationship.relatedId == currentUserId;
|
||||
final isWaiting =
|
||||
@@ -138,64 +138,56 @@ class RelationshipListTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
subtitle: Text('@${account.name}'),
|
||||
trailing:
|
||||
showActions
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isPending && onAccept != null)
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: submitting ? null : onAccept,
|
||||
icon: const Icon(Symbols.check),
|
||||
),
|
||||
if (isPending && onDecline != null)
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: submitting ? null : onDecline,
|
||||
icon: const Icon(Symbols.close),
|
||||
),
|
||||
if (isWaiting && onCancel != null)
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: submitting ? null : onCancel,
|
||||
icon: const Icon(Symbols.close),
|
||||
),
|
||||
if (isEstablished && onUpdateStatus != null)
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Symbols.more_vert),
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
if (relationship.status >= 100) // If friend
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.block),
|
||||
title: Text('blockUser').tr(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onTap:
|
||||
() => onUpdateStatus?.call(
|
||||
relationship,
|
||||
-100,
|
||||
),
|
||||
)
|
||||
else if (relationship.status <= -100) // If blocked
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.person_add),
|
||||
title: Text('unblockUser').tr(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onTap:
|
||||
() =>
|
||||
onUpdateStatus?.call(relationship, 100),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
trailing: showActions
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isPending && onAccept != null)
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: submitting ? null : onAccept,
|
||||
icon: const Icon(Symbols.check),
|
||||
),
|
||||
if (isPending && onDecline != null)
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: submitting ? null : onDecline,
|
||||
icon: const Icon(Symbols.close),
|
||||
),
|
||||
if (isWaiting && onCancel != null)
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: submitting ? null : onCancel,
|
||||
icon: const Icon(Symbols.close),
|
||||
),
|
||||
if (isEstablished && onUpdateStatus != null)
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(Symbols.more_vert),
|
||||
itemBuilder: (context) => [
|
||||
if (relationship.status >= 100) // If friend
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.block),
|
||||
title: Text('blockUser').tr(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onTap: () => onUpdateStatus?.call(relationship, -100),
|
||||
)
|
||||
else if (relationship.status <= -100) // If blocked
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.person_add),
|
||||
title: Text('unblockUser').tr(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onTap: () => onUpdateStatus?.call(relationship, 100),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -299,6 +291,7 @@ class RelationshipScreen extends HookConsumerWidget {
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: PaginationList(
|
||||
padding: EdgeInsets.zero,
|
||||
provider: relationshipListNotifierProvider,
|
||||
notifier: relationshipListNotifierProvider.notifier,
|
||||
itemBuilder: (context, index, relationship) {
|
||||
@@ -380,28 +373,26 @@ class _SentFriendRequestsSheet extends HookConsumerWidget {
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: requests.when(
|
||||
data:
|
||||
(items) =>
|
||||
items.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'friendSentRequestEmpty'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final request = items[index];
|
||||
return RelationshipListTile(
|
||||
relationship: request,
|
||||
onCancel: () => cancelRequest(request),
|
||||
currentUserId: user.value?.id,
|
||||
showRelatedAccount: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
data: (items) => items.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'friendSentRequestEmpty'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final request = items[index];
|
||||
return RelationshipListTile(
|
||||
relationship: request,
|
||||
onCancel: () => cancelRequest(request),
|
||||
currentUserId: user.value?.id,
|
||||
showRelatedAccount: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
),
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:island/widgets/navigation/fab_menu.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
import 'package:island/pods/chat/chat_room.dart';
|
||||
@@ -50,84 +51,124 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
if (validMembers.isNotEmpty) {
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
if (userInfo.value != null) {
|
||||
validMembers =
|
||||
validMembers
|
||||
.where((e) => e.accountId != userInfo.value!.id)
|
||||
.toList();
|
||||
validMembers = validMembers
|
||||
.where((e) => e.accountId != userInfo.value!.id)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildSubtitle() {
|
||||
if (subtitle != null) return subtitle!;
|
||||
|
||||
return summary.when(
|
||||
data: (data) {
|
||||
if (data == null) {
|
||||
return isDirect && room.description == null
|
||||
? Text(
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (data.unreadCount > 0)
|
||||
Text(
|
||||
'unreadMessages'.plural(data.unreadCount),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
child: summary.when(
|
||||
data: (data) => Container(
|
||||
key: const ValueKey('data'),
|
||||
child: data == null
|
||||
? isDirect && room.description == null
|
||||
? Text(
|
||||
validMembers
|
||||
.map((e) => '@${e.account.name}')
|
||||
.join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(
|
||||
room.description ?? 'descriptionNone'.tr(),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (data.unreadCount > 0)
|
||||
Text(
|
||||
'unreadMessages'.plural(data.unreadCount),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
if (data.lastMessage == null)
|
||||
Text(
|
||||
room.description ?? 'descriptionNone'.tr(),
|
||||
maxLines: 1,
|
||||
)
|
||||
else
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Badge(
|
||||
label: Text(
|
||||
data.lastMessage!.sender.account.nick,
|
||||
),
|
||||
textColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
(data.lastMessage!.content?.isNotEmpty ?? false)
|
||||
? data.lastMessage!.content!
|
||||
: 'messageNone'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
RelativeTime(
|
||||
context,
|
||||
).format(data.lastMessage!.createdAt),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (data.lastMessage == null)
|
||||
Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1)
|
||||
else
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Badge(
|
||||
label: Text(data.lastMessage!.sender.account.nick),
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
(data.lastMessage!.content?.isNotEmpty ?? false)
|
||||
? data.lastMessage!.content!
|
||||
: 'messageNone'.tr(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
RelativeTime(
|
||||
context,
|
||||
).format(data.lastMessage!.createdAt),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error:
|
||||
(_, _) =>
|
||||
isDirect && room.description == null
|
||||
? Text(
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(
|
||||
room.description ?? 'descriptionNone'.tr(),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
loading: () => Container(
|
||||
key: const ValueKey('loading'),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final seed = DateTime.now().microsecondsSinceEpoch;
|
||||
final len = 4 + (seed % 17); // 4..20 inclusive
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
var s = seed;
|
||||
final buffer = StringBuffer();
|
||||
for (var i = 0; i < len; i++) {
|
||||
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
||||
buffer.write(chars[s % chars.length]);
|
||||
}
|
||||
return Skeletonizer(
|
||||
enabled: true,
|
||||
child: Text(buffer.toString()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
error: (_, _) => Container(
|
||||
key: const ValueKey('error'),
|
||||
child: isDirect && room.description == null
|
||||
? Text(
|
||||
validMembers.map((e) => '@${e.account.name}').join(', '),
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(room.description ?? 'descriptionNone'.tr(), maxLines: 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,17 +190,15 @@ class ChatRoomListTile extends HookConsumerWidget {
|
||||
loading: () => false,
|
||||
error: (_, _) => false,
|
||||
),
|
||||
child:
|
||||
(isDirect && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId:
|
||||
validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id == null
|
||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||
child: (isDirect && room.picture?.id == null)
|
||||
? SplitAvatarWidget(
|
||||
filesId: validMembers
|
||||
.map((e) => e.account.profile.picture?.id)
|
||||
.toList(),
|
||||
)
|
||||
: room.picture?.id == null
|
||||
? CircleAvatar(child: Text(room.name![0].toUpperCase()))
|
||||
: ProfilePictureWidget(fileId: room.picture?.id),
|
||||
),
|
||||
title: Text(titleText),
|
||||
subtitle: buildSubtitle(),
|
||||
@@ -199,74 +238,67 @@ class ChatListBodyWidget extends HookConsumerWidget {
|
||||
builder: (context, ref, _) {
|
||||
final summaryState = ref.watch(chatSummaryProvider);
|
||||
return summaryState.maybeWhen(
|
||||
loading:
|
||||
() => const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
loading: () => const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: chats.when(
|
||||
data:
|
||||
(items) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(() {
|
||||
ref.invalidate(chatRoomJoinedProvider);
|
||||
}),
|
||||
child: SuperListView.builder(
|
||||
padding: EdgeInsets.only(bottom: 96),
|
||||
itemCount:
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 && item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems =
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.toList();
|
||||
final item = filteredItems[index];
|
||||
return ChatRoomListTile(
|
||||
room: item,
|
||||
isDirect: item.type == 1,
|
||||
onTap: () {
|
||||
if (isWideScreen(context)) {
|
||||
context.replaceNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
} else {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
data: (items) => RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() {
|
||||
ref.invalidate(chatRoomJoinedProvider);
|
||||
}),
|
||||
child: SuperListView.builder(
|
||||
padding: EdgeInsets.only(bottom: 96),
|
||||
itemCount: items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 && item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems = items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 && item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.toList();
|
||||
final item = filteredItems[index];
|
||||
return ChatRoomListTile(
|
||||
room: item,
|
||||
isDirect: item.type == 1,
|
||||
onTap: () {
|
||||
if (isWideScreen(context)) {
|
||||
context.replaceNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
} else {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(chatRoomJoinedProvider);
|
||||
},
|
||||
),
|
||||
error: (error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(chatRoomJoinedProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -552,53 +584,47 @@ class _ChatInvitesSheet extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
child: invites.when(
|
||||
data:
|
||||
(items) =>
|
||||
items.isEmpty
|
||||
? Center(
|
||||
child:
|
||||
Text(
|
||||
'invitesEmpty',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invite = items[index];
|
||||
return ChatRoomListTile(
|
||||
room: invite.chatRoom!,
|
||||
isDirect: invite.chatRoom!.type == 1,
|
||||
subtitle: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (invite.chatRoom!.type == 1)
|
||||
Badge(
|
||||
label: const Text('directMessage').tr(),
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
textColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
],
|
||||
data: (items) => items.isEmpty
|
||||
? Center(
|
||||
child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invite = items[index];
|
||||
return ChatRoomListTile(
|
||||
room: invite.chatRoom!,
|
||||
isDirect: invite.chatRoom!.type == 1,
|
||||
subtitle: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (invite.chatRoom!.type == 1)
|
||||
Badge(
|
||||
label: const Text('directMessage').tr(),
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
onPressed: () => acceptInvite(invite),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => declineInvite(invite),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
onPressed: () => acceptInvite(invite),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => declineInvite(invite),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
),
|
||||
|
||||
@@ -98,11 +98,10 @@ class PublisherMemberListNotifier extends AsyncNotifier<List<SnPublisherMember>>
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final members =
|
||||
response.data
|
||||
.map((e) => SnPublisherMember.fromJson(e))
|
||||
.cast<SnPublisherMember>()
|
||||
.toList();
|
||||
final members = response.data
|
||||
.map((e) => SnPublisherMember.fromJson(e))
|
||||
.cast<SnPublisherMember>()
|
||||
.toList();
|
||||
|
||||
return members;
|
||||
}
|
||||
@@ -173,14 +172,12 @@ class PublisherSelector extends StatelessWidget {
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.arrow_drop_down),
|
||||
iconSize: 19,
|
||||
iconEnabledColor:
|
||||
isWideScreen(context)
|
||||
? null
|
||||
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor:
|
||||
isWideScreen(context)
|
||||
? null
|
||||
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconEnabledColor: isWideScreen(context)
|
||||
? null
|
||||
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor: isWideScreen(context)
|
||||
? null
|
||||
: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -204,16 +201,24 @@ class _PublisherUnselectedWidget extends HookConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
if (!hasPublishers) ...[
|
||||
const Icon(
|
||||
Symbols.info,
|
||||
fill: 1,
|
||||
size: 32,
|
||||
).padding(bottom: 6, top: 24),
|
||||
Text(
|
||||
'creatorHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).tr(),
|
||||
if (publishers.isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: const CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
...([
|
||||
const Icon(
|
||||
Symbols.info,
|
||||
fill: 1,
|
||||
size: 32,
|
||||
).padding(bottom: 6, top: 24),
|
||||
Text(
|
||||
'creatorHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).tr(),
|
||||
]),
|
||||
const Gap(24),
|
||||
],
|
||||
if (hasPublishers)
|
||||
@@ -288,14 +293,14 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) =>
|
||||
EditPublisherScreen(name: currentPublisher.value!.name),
|
||||
builder: (context) =>
|
||||
EditPublisherScreen(name: currentPublisher.value!.name),
|
||||
).then((value) async {
|
||||
if (value == null) return;
|
||||
final data = await ref.refresh(publishersManagedProvider.future);
|
||||
currentPublisher.value =
|
||||
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
|
||||
currentPublisher.value = data
|
||||
.where((e) => e.id == currentPublisher.value!.id)
|
||||
.firstOrNull;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,29 +320,26 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
final List<DropdownMenuItem<SnPublisher>> publishersMenu = publishers.when(
|
||||
data:
|
||||
(data) =>
|
||||
data
|
||||
.map(
|
||||
(item) => DropdownMenuItem<SnPublisher>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: item.picture?.id,
|
||||
),
|
||||
title: Text(item.nick),
|
||||
subtitle: Text('@${item.name}'),
|
||||
trailing:
|
||||
currentPublisher.value?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
data: (data) => data
|
||||
.map(
|
||||
(item) => DropdownMenuItem<SnPublisher>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: item.picture?.id,
|
||||
),
|
||||
title: Text(item.nick),
|
||||
subtitle: Text('@${item.name}'),
|
||||
trailing: currentPublisher.value?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
loading: () => [],
|
||||
error: (_, _) => [],
|
||||
);
|
||||
@@ -443,10 +445,9 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _PublisherMemberListSheet(
|
||||
publisherUname: currentPublisher.value!.name,
|
||||
),
|
||||
builder: (context) => _PublisherMemberListSheet(
|
||||
publisherUname: currentPublisher.value!.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -567,51 +568,49 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: publisherStats.when(
|
||||
data:
|
||||
(stats) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child:
|
||||
currentPublisher.value == null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _PublisherUnselectedWidget(
|
||||
onPublisherSelected: (publisher) {
|
||||
currentPublisher.value = publisher;
|
||||
},
|
||||
),
|
||||
).center()
|
||||
: isWide
|
||||
? Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
PublisherSelector(
|
||||
currentPublisher: currentPublisher.value,
|
||||
publishersMenu: publishersMenu,
|
||||
onChanged: (value) {
|
||||
currentPublisher.value = value;
|
||||
},
|
||||
),
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
heatmap: publisherHeatmap.value,
|
||||
).padding(horizontal: 12),
|
||||
buildNavigationWidget(true),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
heatmap: publisherHeatmap.value,
|
||||
).padding(horizontal: 16),
|
||||
buildNavigationWidget(false),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (stats) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: currentPublisher.value == null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _PublisherUnselectedWidget(
|
||||
onPublisherSelected: (publisher) {
|
||||
currentPublisher.value = publisher;
|
||||
},
|
||||
),
|
||||
).center()
|
||||
: isWide
|
||||
? Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const SizedBox.shrink(),
|
||||
PublisherSelector(
|
||||
currentPublisher: currentPublisher.value,
|
||||
publishersMenu: publishersMenu,
|
||||
onChanged: (value) {
|
||||
currentPublisher.value = value;
|
||||
},
|
||||
),
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
heatmap: publisherHeatmap.value,
|
||||
).padding(horizontal: 12),
|
||||
buildNavigationWidget(true),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
heatmap: publisherHeatmap.value,
|
||||
).padding(horizontal: 16),
|
||||
buildNavigationWidget(false),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
),
|
||||
@@ -876,11 +875,10 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _PublisherMemberRoleSheet(
|
||||
publisherUname: publisherUname,
|
||||
member: member,
|
||||
),
|
||||
builder: (context) => _PublisherMemberRoleSheet(
|
||||
publisherUname: publisherUname,
|
||||
member: member,
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
memberNotifier.refresh();
|
||||
@@ -991,23 +989,19 @@ class _PublisherMemberRoleSheet extends HookConsumerWidget {
|
||||
onSelected: (int selection) {
|
||||
roleController.text = selection.toString();
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'memberRole'.tr(),
|
||||
helperText: 'memberRoleHint'.tr(),
|
||||
),
|
||||
onTapOutside: (event) => focusNode.unfocus(),
|
||||
);
|
||||
},
|
||||
fieldViewBuilder:
|
||||
(context, controller, focusNode, onFieldSubmitted) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'memberRole'.tr(),
|
||||
helperText: 'memberRoleHint'.tr(),
|
||||
),
|
||||
onTapOutside: (event) => focusNode.unfocus(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
FilledButton.icon(
|
||||
@@ -1085,57 +1079,49 @@ class _PublisherInviteSheet extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
child: invites.when(
|
||||
data:
|
||||
(items) =>
|
||||
items.isEmpty
|
||||
? Center(
|
||||
child:
|
||||
Text(
|
||||
'invitesEmpty',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invite = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: invite.publisher!.picture?.id,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(invite.publisher!.nick),
|
||||
subtitle:
|
||||
Text(
|
||||
invite.role >= 100
|
||||
? 'permissionOwner'
|
||||
: invite.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
onPressed: () => acceptInvite(invite),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => declineInvite(invite),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
data: (items) => items.isEmpty
|
||||
? Center(
|
||||
child: Text('invitesEmpty', textAlign: TextAlign.center).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final invite = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: invite.publisher!.picture?.id,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(invite.publisher!.nick),
|
||||
subtitle: Text(
|
||||
invite.role >= 100
|
||||
? 'permissionOwner'
|
||||
: invite.role >= 50
|
||||
? 'permissionModerator'
|
||||
: 'permissionMember',
|
||||
).tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
onPressed: () => acceptInvite(invite),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => declineInvite(invite),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(publisherInvitesProvider),
|
||||
),
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(publisherInvitesProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,11 +44,10 @@ class PollListNotifier extends AsyncNotifier<List<SnPollWithStats>>
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final items =
|
||||
response.data
|
||||
.map((json) => SnPollWithStats.fromJson(json))
|
||||
.cast<SnPollWithStats>()
|
||||
.toList();
|
||||
final items = response.data
|
||||
.map((json) => SnPollWithStats.fromJson(json))
|
||||
.cast<SnPollWithStats>()
|
||||
.toList();
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -91,6 +90,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
||||
body: ExtendedRefreshIndicator(
|
||||
onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future),
|
||||
child: PaginationList(
|
||||
footerSkeletonMaxWidth: 640,
|
||||
provider: pollListNotifierProvider(pubName),
|
||||
notifier: pollListNotifierProvider(pubName).notifier,
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
@@ -119,10 +119,9 @@ class _CreatorPollItem extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final ended = pollWithStats.endedAt;
|
||||
final endedText =
|
||||
ended == null
|
||||
? 'No end'
|
||||
: MaterialLocalizations.of(context).formatFullDate(ended);
|
||||
final endedText = ended == null
|
||||
? 'No end'
|
||||
: MaterialLocalizations.of(context).formatFullDate(ended);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
@@ -152,78 +151,69 @@ class _CreatorPollItem extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<SnPoll>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
builder: (context) => PollEditorScreen(
|
||||
initialPublisher: pubName,
|
||||
initialPollId: pollWithStats.id,
|
||||
),
|
||||
);
|
||||
if (result != null && context.mounted) {
|
||||
ref.invalidate(pollListNotifierProvider(pubName));
|
||||
}
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Gap(16),
|
||||
Text('delete').tr().textColor(Colors.red),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Delete Poll'),
|
||||
content: Text('Are you sure you want to delete this poll?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final result = await showModalBottomSheet<SnPoll>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
builder:
|
||||
(context) => PollEditorScreen(
|
||||
initialPublisher: pubName,
|
||||
initialPollId: pollWithStats.id,
|
||||
),
|
||||
);
|
||||
if (result != null && context.mounted) {
|
||||
ref.invalidate(pollListNotifierProvider(pubName));
|
||||
}
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Gap(16),
|
||||
Text('delete').tr().textColor(Colors.red),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('Delete Poll'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this poll?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(false),
|
||||
child: Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(true),
|
||||
child: Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete(
|
||||
'/sphere/polls/${pollWithStats.id}',
|
||||
);
|
||||
ref.invalidate(pollListNotifierProvider(pubName));
|
||||
showSnackBar('Poll deleted successfully');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/sphere/polls/${pollWithStats.id}');
|
||||
ref.invalidate(pollListNotifierProvider(pubName));
|
||||
showSnackBar('Poll deleted successfully');
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
|
||||
@@ -12,7 +12,6 @@ import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
final siteListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
|
||||
@@ -38,11 +37,10 @@ class SiteListNotifier extends AsyncNotifier<List<SnPublicationSite>>
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final items =
|
||||
response.data
|
||||
.map((json) => SnPublicationSite.fromJson(json))
|
||||
.cast<SnPublicationSite>()
|
||||
.toList();
|
||||
final items = response.data
|
||||
.map((json) => SnPublicationSite.fromJson(json))
|
||||
.cast<SnPublicationSite>()
|
||||
.toList();
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -70,23 +68,17 @@ class CreatorSiteListScreen extends HookConsumerWidget {
|
||||
onPressed: () => _createSite(context),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
body: ExtendedRefreshIndicator(
|
||||
onRefresh: () => ref.refresh(siteListNotifierProvider(pubName).future),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(8),
|
||||
PaginationList(
|
||||
provider: siteListNotifierProvider(pubName),
|
||||
notifier: siteListNotifierProvider(pubName).notifier,
|
||||
itemBuilder: (context, index, site) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _CreatorSiteItem(site: site, pubName: pubName),
|
||||
).center();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: PaginationList(
|
||||
footerSkeletonMaxWidth: 640,
|
||||
provider: siteListNotifierProvider(pubName),
|
||||
notifier: siteListNotifierProvider(pubName).notifier,
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
itemBuilder: (context, index, site) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _CreatorSiteItem(site: site, pubName: pubName),
|
||||
).center();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -148,73 +140,53 @@ class _CreatorSiteItem extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SiteForm(
|
||||
pubName: pubName,
|
||||
siteSlug: site.slug,
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const Gap(16),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) =>
|
||||
SiteForm(pubName: pubName, siteSlug: site.slug),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Gap(16),
|
||||
Text('delete').tr().textColor(Colors.red),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final confirmed = await showConfirmAlert(
|
||||
'publicationSiteDeleteConfirm'.tr(),
|
||||
'deleteSite'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete(
|
||||
'/zone/sites/$pubName/${site.slug}',
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Gap(16),
|
||||
Text('delete').tr().textColor(Colors.red),
|
||||
],
|
||||
),
|
||||
onTap: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('deleteSite'.tr()),
|
||||
content: Text('deleteSiteConfirm'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() =>
|
||||
Navigator.of(context).pop(false),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(true),
|
||||
child: Text('delete'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete(
|
||||
'/zone/sites/$pubName/${site.slug}',
|
||||
);
|
||||
ref.invalidate(siteListNotifierProvider(pubName));
|
||||
showSnackBar('siteDeletedSuccess'.tr());
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
ref.invalidate(siteListNotifierProvider(pubName));
|
||||
showSnackBar('siteDeletedSuccess'.tr());
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -41,11 +41,10 @@ class StickersScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createStickerPack'.tr(),
|
||||
child: StickerPackForm(pubName: pubName),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createStickerPack'.tr(),
|
||||
child: StickerPackForm(pubName: pubName),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPacksProvider(pubName));
|
||||
@@ -54,24 +53,23 @@ class StickersScreen extends HookConsumerWidget {
|
||||
},
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
body:
|
||||
isWideScreen(context)
|
||||
? Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
body: isWideScreen(context)
|
||||
? Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: content,
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: content,
|
||||
),
|
||||
)
|
||||
: content,
|
||||
),
|
||||
)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -83,6 +81,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PaginationList(
|
||||
padding: EdgeInsets.zero,
|
||||
provider: stickerPacksProvider(pubName),
|
||||
notifier: stickerPacksProvider(pubName).notifier,
|
||||
itemBuilder: (context, index, sticker) {
|
||||
@@ -97,40 +96,38 @@ class SliverStickerPacksList extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: sticker.name,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
onPressed: () {
|
||||
final id = sticker.id;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createSticker'.tr(),
|
||||
child: StickerForm(packId: id),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPackContentProvider(id));
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
StickerPackActionMenu(
|
||||
pubName: pubName,
|
||||
packId: sticker.id,
|
||||
iconShadow: Shadow(),
|
||||
),
|
||||
],
|
||||
child: StickerPackDetailContent(
|
||||
id: sticker.id,
|
||||
pubName: pubName,
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: sticker.name,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
onPressed: () {
|
||||
final id = sticker.id;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createSticker'.tr(),
|
||||
child: StickerForm(packId: id),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPackContentProvider(id));
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
StickerPackActionMenu(
|
||||
pubName: pubName,
|
||||
packId: sticker.id,
|
||||
iconShadow: Shadow(),
|
||||
),
|
||||
],
|
||||
child: StickerPackDetailContent(
|
||||
id: sticker.id,
|
||||
pubName: pubName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -165,11 +162,10 @@ class StickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final stickers =
|
||||
response.data
|
||||
.map((e) => SnStickerPack.fromJson(e))
|
||||
.cast<SnStickerPack>()
|
||||
.toList();
|
||||
final stickers = response.data
|
||||
.map((e) => SnStickerPack.fromJson(e))
|
||||
.cast<SnStickerPack>()
|
||||
.toList();
|
||||
|
||||
return stickers;
|
||||
} catch (err) {
|
||||
@@ -262,10 +258,9 @@ class StickerPackForm extends HookConsumerWidget {
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child:
|
||||
(icon.value?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(fileId: icon.value!),
|
||||
child: (icon.value?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(fileId: icon.value!),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -273,10 +268,9 @@ class StickerPackForm extends HookConsumerWidget {
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => CloudFilePicker(
|
||||
allowedTypes: {UniversalFileType.image},
|
||||
),
|
||||
builder: (context) => CloudFilePicker(
|
||||
allowedTypes: {UniversalFileType.image},
|
||||
),
|
||||
).then((value) {
|
||||
if (value == null) return;
|
||||
icon.value = value[0].id;
|
||||
@@ -300,8 +294,8 @@ class StickerPackForm extends HookConsumerWidget {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
@@ -314,8 +308,8 @@ class StickerPackForm extends HookConsumerWidget {
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: prefixController,
|
||||
@@ -332,8 +326,8 @@ class StickerPackForm extends HookConsumerWidget {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -39,19 +39,18 @@ class AppDetailScreen extends HookConsumerWidget {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
onPressed:
|
||||
appData.value == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
'developerAppEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': appId,
|
||||
},
|
||||
);
|
||||
},
|
||||
onPressed: appData.value == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
'developerAppEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': appId,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
@@ -85,24 +84,34 @@ class AppDetailScreen extends HookConsumerWidget {
|
||||
controller: tabController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_AppOverview(app: app),
|
||||
AppSecretsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
appId: appId,
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: _AppOverview(app: app),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: AppSecretsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
appId: appId,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
customAppProvider(publisherName, projectId, appId),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(
|
||||
customAppProvider(publisherName, projectId, appId),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -115,6 +124,7 @@ class _AppOverview extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
@@ -125,13 +135,12 @@ class _AppOverview extends StatelessWidget {
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child:
|
||||
app.background != null
|
||||
? CloudFileWidget(
|
||||
item: app.background!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
child: app.background != null
|
||||
? CloudFileWidget(
|
||||
item: app.background!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/custom_app_secret.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
@@ -53,37 +54,36 @@ class AppSecretsScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'newSecretGenerated'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('copySecretHint'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(newSecret),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: newSecret));
|
||||
},
|
||||
icon: const Icon(Symbols.copy_all),
|
||||
label: Text('copy'.tr()),
|
||||
),
|
||||
],
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'newSecretGenerated'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('copySecretHint'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(newSecret),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: newSecret));
|
||||
},
|
||||
icon: const Icon(Symbols.copy_all),
|
||||
label: Text('copy'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).whenComplete(() {
|
||||
ref.invalidate(
|
||||
customAppSecretsProvider(publisherName, projectId, appId),
|
||||
@@ -114,22 +114,38 @@ class AppSecretsScreen extends HookConsumerWidget {
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Gap(16),
|
||||
TextFormField(
|
||||
controller: expiresInController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'expiresIn'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SwitchListTile(
|
||||
title: Text('isOidc'.tr()),
|
||||
value: isOidc.value,
|
||||
onChanged: (value) => isOidc.value = value,
|
||||
const Gap(16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SwitchListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text('isOidc'.tr()),
|
||||
value: isOidc.value,
|
||||
onChanged: (value) => isOidc.value = value,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
@@ -175,14 +191,9 @@ class AppSecretsScreen extends HookConsumerWidget {
|
||||
return secrets.when(
|
||||
data: (data) {
|
||||
return RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
customAppSecretsProvider(
|
||||
publisherName,
|
||||
projectId,
|
||||
appId,
|
||||
).future,
|
||||
),
|
||||
onRefresh: () => ref.refresh(
|
||||
customAppSecretsProvider(publisherName, projectId, appId).future,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
@@ -240,14 +251,12 @@ class AppSecretsScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
customAppSecretsProvider(publisherName, projectId, appId),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(
|
||||
customAppSecretsProvider(publisherName, projectId, appId),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,15 +76,14 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
@@ -95,10 +94,8 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
customAppsProvider(publisherName, projectId).future,
|
||||
),
|
||||
onRefresh: () =>
|
||||
ref.refresh(customAppsProvider(publisherName, projectId).future),
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
@@ -110,15 +107,14 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createCustomApp'.tr(),
|
||||
child: NewCustomAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
@@ -146,31 +142,20 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 150,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (app.background != null)
|
||||
CloudFileWidget(
|
||||
item: app.background!,
|
||||
fit: BoxFit.cover,
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
if (app.picture != null)
|
||||
Positioned(
|
||||
left: 16,
|
||||
bottom: 16,
|
||||
child: ProfilePictureWidget(
|
||||
fileId: app.picture!.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (app.background != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: CloudFileWidget(
|
||||
item: app.background!,
|
||||
fit: BoxFit.cover,
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(app.name),
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: app.picture?.id,
|
||||
fallbackIcon: Symbols.apps,
|
||||
),
|
||||
subtitle: Text(
|
||||
app.slug,
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
@@ -180,52 +165,48 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
right: 12,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'editCustomApp'.tr(),
|
||||
child: EditAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
id: app.id,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'editCustomApp'.tr(),
|
||||
child: EditAppScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
id: app.id,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
@@ -264,14 +245,11 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
customAppsProvider(publisherName, projectId),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () =>
|
||||
ref.invalidate(customAppsProvider(publisherName, projectId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,19 +36,18 @@ class BotDetailScreen extends HookConsumerWidget {
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.edit),
|
||||
onPressed:
|
||||
botData.value == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
'developerBotEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': botId,
|
||||
},
|
||||
);
|
||||
},
|
||||
onPressed: botData.value == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
'developerBotEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'id': botId,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
@@ -84,24 +83,33 @@ class BotDetailScreen extends HookConsumerWidget {
|
||||
controller: tabController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_BotOverview(bot: bot),
|
||||
BotKeysScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
botId: botId,
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: _BotOverview(bot: bot),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: BotKeysScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
botId: botId,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
botProvider(publisherName, projectId, botId),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () =>
|
||||
ref.invalidate(botProvider(publisherName, projectId, botId)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -124,13 +132,12 @@ class _BotOverview extends StatelessWidget {
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child:
|
||||
bot.account.profile.background != null
|
||||
? CloudFileWidget(
|
||||
item: bot.account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
child: bot.account.profile.background != null
|
||||
? CloudFileWidget(
|
||||
item: bot.account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
|
||||
@@ -53,37 +53,36 @@ class BotKeysScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'newKeyGenerated'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('copyKeyHint'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(token),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: token));
|
||||
},
|
||||
icon: const Icon(Symbols.copy_all),
|
||||
label: Text('copy'.tr()),
|
||||
),
|
||||
],
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'newKeyGenerated'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('copyKeyHint'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(token),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: token));
|
||||
},
|
||||
icon: const Icon(Symbols.copy_all),
|
||||
label: Text('copy'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
).whenComplete(() {
|
||||
ref.invalidate(botKeysProvider(publisherName, projectId, botId));
|
||||
});
|
||||
@@ -94,45 +93,50 @@ class BotKeysScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'newBotKey'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: keyNameController,
|
||||
decoration: InputDecoration(labelText: 'keyName'.tr()),
|
||||
autofocus: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
heightFactor: 0.7,
|
||||
titleText: 'newBotKey'.tr(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: keyNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'keyName'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
if (keyNameController.text.isEmpty) return;
|
||||
final keyName = keyNameController.text;
|
||||
Navigator.pop(context); // Close the sheet
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
|
||||
data: {'label': keyName},
|
||||
);
|
||||
final newApiKey = SnAccountApiKey.fromJson(resp.data);
|
||||
showNewKeySheet(newApiKey);
|
||||
} catch (e) {
|
||||
showErrorAlert(e.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('create'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: () async {
|
||||
if (keyNameController.text.isEmpty) return;
|
||||
final keyName = keyNameController.text;
|
||||
Navigator.pop(context); // Close the sheet
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final resp = await client.post(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys',
|
||||
data: {'label': keyName},
|
||||
);
|
||||
final newApiKey = SnAccountApiKey.fromJson(resp.data);
|
||||
showNewKeySheet(newApiKey);
|
||||
} catch (e) {
|
||||
showErrorAlert(e.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
label: Text('create'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,92 +193,79 @@ class BotKeysScreen extends HookConsumerWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.add),
|
||||
title: Text('newBotKey'.tr()),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: createKey,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child:
|
||||
data.isEmpty
|
||||
? Center(child: Text('noBotKeys'.tr()))
|
||||
: RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
botKeysProvider(
|
||||
publisherName,
|
||||
projectId,
|
||||
botId,
|
||||
).future,
|
||||
child: data.isEmpty
|
||||
? Center(child: Text('noBotKeys'.tr()))
|
||||
: RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(
|
||||
botKeysProvider(publisherName, projectId, botId).future,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final apiKey = data[index];
|
||||
return ListTile(
|
||||
title: Text(apiKey.label),
|
||||
subtitle: Text(apiKey.createdAt.formatSystem()),
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 12,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final apiKey = data[index];
|
||||
return ListTile(
|
||||
title: Text(apiKey.label),
|
||||
subtitle: Text(apiKey.createdAt.formatSystem()),
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 12,
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'rotate',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.refresh),
|
||||
const Gap(12),
|
||||
Text('rotateKey'.tr()),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'rotate',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.refresh),
|
||||
const Gap(12),
|
||||
Text('rotateKey'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'revoke',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'revoke',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.delete,
|
||||
color: Colors.red,
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'revoke'.tr(),
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Text(
|
||||
'revoke'.tr(),
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'rotate') {
|
||||
rotateKey(apiKey.id);
|
||||
} else if (value == 'revoke') {
|
||||
revokeKey(apiKey.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'rotate') {
|
||||
rotateKey(apiKey.id);
|
||||
} else if (value == 'revoke') {
|
||||
revokeKey(apiKey.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
botKeysProvider(publisherName, projectId, botId),
|
||||
),
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () =>
|
||||
ref.invalidate(botKeysProvider(publisherName, projectId, botId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,15 +54,14 @@ class BotsScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
@@ -73,8 +72,8 @@ class BotsScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(botsProvider(publisherName, projectId).future),
|
||||
onRefresh: () =>
|
||||
ref.refresh(botsProvider(publisherName, projectId).future),
|
||||
child: Column(
|
||||
children: [
|
||||
const Gap(8),
|
||||
@@ -86,15 +85,14 @@ class BotsScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createBot'.tr(),
|
||||
child: NewBotScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: projectId,
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.add),
|
||||
@@ -108,23 +106,30 @@ class BotsScreen extends HookConsumerWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final bot = data[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||
),
|
||||
leading: CircleAvatar(
|
||||
child:
|
||||
bot.account.profile.picture != null
|
||||
? ProfilePictureWidget(
|
||||
file: bot.account.profile.picture!,
|
||||
)
|
||||
: const Icon(Symbols.smart_toy),
|
||||
),
|
||||
title: Text(bot.account.nick),
|
||||
subtitle: Text(bot.account.name),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
child: Column(
|
||||
children: [
|
||||
if (bot.account.profile.background != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: CloudFileWidget(
|
||||
item: bot.account.profile.background!,
|
||||
fit: BoxFit.cover,
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
),
|
||||
ListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8.0),
|
||||
),
|
||||
),
|
||||
leading: ProfilePictureWidget(
|
||||
fallbackIcon: Symbols.smart_toy,
|
||||
file: bot.account.profile.picture,
|
||||
),
|
||||
title: Text(bot.account.nick),
|
||||
subtitle: Text(bot.account.name),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
@@ -152,13 +157,12 @@ class BotsScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'editBot'.tr(),
|
||||
child: EditBotScreen(
|
||||
publisherName: publisherName,
|
||||
@@ -167,36 +171,40 @@ class BotsScreen extends HookConsumerWidget {
|
||||
isModal: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteBotHint'.tr(),
|
||||
'deleteBot'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
botsProvider(publisherName, projectId),
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteBotHint'.tr(),
|
||||
'deleteBot'.tr(),
|
||||
isDanger: true,
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(
|
||||
apiClientProvider,
|
||||
);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
botsProvider(publisherName, projectId),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'developerBotDetail',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'botId': bot.id,
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'developerBotDetail',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
'botId': bot.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -207,12 +215,10 @@ class BotsScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry:
|
||||
() => ref.invalidate(botsProvider(publisherName, projectId)),
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(botsProvider(publisherName, projectId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +68,11 @@ class DeveloperHubScreen extends HookConsumerWidget {
|
||||
developers.value?.firstOrNull,
|
||||
);
|
||||
|
||||
final projects =
|
||||
currentDeveloper.value?.publisher?.name != null
|
||||
? ref.watch(
|
||||
devProjectsProvider(currentDeveloper.value!.publisher!.name),
|
||||
)
|
||||
: const AsyncValue<List<DevProject>>.data([]);
|
||||
final projects = currentDeveloper.value?.publisher?.name != null
|
||||
? ref.watch(
|
||||
devProjectsProvider(currentDeveloper.value!.publisher!.name),
|
||||
)
|
||||
: const AsyncValue<List<DevProject>>.data([]);
|
||||
|
||||
final currentProject = useState<DevProject?>(
|
||||
projects.value?.where((p) => p.id == initialProjectId).firstOrNull,
|
||||
@@ -126,14 +125,13 @@ class DeveloperHubScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createProject'.tr(),
|
||||
child: ProjectForm(
|
||||
publisherName:
|
||||
currentDeveloper.value!.publisher!.name,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'createProject'.tr(),
|
||||
child: ProjectForm(
|
||||
publisherName:
|
||||
currentDeveloper.value!.publisher!.name,
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
@@ -211,108 +209,96 @@ class _MainContentSection extends HookConsumerWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: developerStats.when(
|
||||
data:
|
||||
(stats) =>
|
||||
currentDeveloper == null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _DeveloperUnselectedWidget(
|
||||
onDeveloperSelected: onDeveloperSelected,
|
||||
data: (stats) => currentDeveloper == null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _DeveloperUnselectedWidget(
|
||||
onDeveloperSelected: onDeveloperSelected,
|
||||
),
|
||||
).center()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Developer Stats
|
||||
if (stats != null) ...[
|
||||
Text(
|
||||
'Overview',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
).center()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Developer Stats
|
||||
if (stats != null) ...[
|
||||
Text(
|
||||
'Overview',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(
|
||||
const Gap(16),
|
||||
_DeveloperStatsWidget(stats: stats),
|
||||
const Gap(24),
|
||||
],
|
||||
|
||||
// Projects Section
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Projects',
|
||||
style: Theme.of(context).textTheme.titleLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onCreateProject,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: const Text('Create Project'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A73E8),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
const Gap(16),
|
||||
_DeveloperStatsWidget(stats: stats),
|
||||
const Gap(24),
|
||||
],
|
||||
|
||||
// Projects Section
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Projects',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onCreateProject,
|
||||
icon: const Icon(Symbols.add),
|
||||
label: const Text('Create Project'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF1A73E8),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
|
||||
// Projects List
|
||||
projects.value?.isNotEmpty ?? false
|
||||
? Column(
|
||||
children:
|
||||
projects.value!
|
||||
.map(
|
||||
(project) => _ProjectListTile(
|
||||
project: project,
|
||||
publisherName:
|
||||
currentDeveloper!
|
||||
.publisher!
|
||||
.name,
|
||||
onProjectSelected:
|
||||
onProjectSelected,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(48),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'No projects available',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
|
||||
// Projects List
|
||||
projects.value?.isNotEmpty ?? false
|
||||
? Column(
|
||||
children: projects.value!
|
||||
.map(
|
||||
(project) => _ProjectListTile(
|
||||
project: project,
|
||||
publisherName:
|
||||
currentDeveloper!.publisher!.name,
|
||||
onProjectSelected: onProjectSelected,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(48),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'No projects available',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () {
|
||||
ref.invalidate(
|
||||
developerStatsProvider(currentDeveloper?.publisher?.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
error: (err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () {
|
||||
ref.invalidate(
|
||||
developerStatsProvider(currentDeveloper?.publisher?.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -335,29 +321,26 @@ class DeveloperSelector extends HookConsumerWidget {
|
||||
final developers = ref.watch(developersProvider);
|
||||
|
||||
final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when(
|
||||
data:
|
||||
(data) =>
|
||||
data
|
||||
.map(
|
||||
(item) => DropdownMenuItem<SnDeveloper>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: item.publisher?.picture?.id,
|
||||
),
|
||||
title: Text(item.publisher!.nick),
|
||||
subtitle: Text('@${item.publisher!.name}'),
|
||||
trailing:
|
||||
currentDeveloper?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
data: (data) => data
|
||||
.map(
|
||||
(item) => DropdownMenuItem<SnDeveloper>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: item.publisher?.picture?.id,
|
||||
),
|
||||
title: Text(item.publisher!.nick),
|
||||
subtitle: Text('@${item.publisher!.name}'),
|
||||
trailing: currentDeveloper?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
loading: () => [],
|
||||
error: (_, _) => [],
|
||||
);
|
||||
@@ -446,38 +429,36 @@ class ProjectSelector extends HookConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<DropdownMenuItem<DevProject>> projectsMenu =
|
||||
projects.value!
|
||||
.map(
|
||||
(item) => DropdownMenuItem<DevProject>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
item.name.isNotEmpty ? item.name[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
final List<DropdownMenuItem<DevProject>> projectsMenu = projects.value!
|
||||
.map(
|
||||
(item) => DropdownMenuItem<DevProject>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
item.name.isNotEmpty ? item.name[0].toUpperCase() : '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: Text(item.name),
|
||||
subtitle: Text(
|
||||
item.description ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing:
|
||||
currentProject?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
title: Text(item.name),
|
||||
subtitle: Text(
|
||||
item.description ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: currentProject?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<DevProject>(
|
||||
@@ -496,50 +477,47 @@ class ProjectSelector extends HookConsumerWidget {
|
||||
final isWider = isWiderScreen(context);
|
||||
return projectsMenu
|
||||
.map(
|
||||
(e) =>
|
||||
isWider
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
e.value?.name.isNotEmpty ?? false
|
||||
? e.value!.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
e.value?.name ?? '?',
|
||||
(e) => isWider
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
child: Text(
|
||||
e.value?.name.isNotEmpty ?? false
|
||||
? e.value!.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(right: 8)
|
||||
: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
e.value?.name.isNotEmpty ?? false
|
||||
? e.value!.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
const Gap(8),
|
||||
Text(
|
||||
e.value?.name ?? '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(right: 8)
|
||||
: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
e.value?.name.isNotEmpty ?? false
|
||||
? e.value!.name[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
@@ -590,45 +568,40 @@ class _ProjectListTile extends HookConsumerWidget {
|
||||
subtitle: Text(project.description ?? ''),
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 17),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text('delete', style: const TextStyle(color: Colors.red)).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'editProject'.tr(),
|
||||
child: ProjectForm(
|
||||
publisherName: publisherName,
|
||||
project: project,
|
||||
),
|
||||
),
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'editProject'.tr(),
|
||||
child: ProjectForm(
|
||||
publisherName: publisherName,
|
||||
project: project,
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(devProjectsProvider(publisherName));
|
||||
@@ -735,16 +708,24 @@ class _DeveloperUnselectedWidget extends HookConsumerWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!hasDevelopers) ...[
|
||||
const Icon(
|
||||
Symbols.info,
|
||||
fill: 1,
|
||||
size: 32,
|
||||
).padding(bottom: 6, top: 24),
|
||||
Text(
|
||||
'developerHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).tr(),
|
||||
if (developers.isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: const CircularProgressIndicator(),
|
||||
)
|
||||
else
|
||||
...([
|
||||
const Icon(
|
||||
Symbols.info,
|
||||
fill: 1,
|
||||
size: 32,
|
||||
).padding(bottom: 6, top: 24),
|
||||
Text(
|
||||
'developerHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).tr(),
|
||||
]),
|
||||
const Gap(24),
|
||||
],
|
||||
if (hasDevelopers)
|
||||
@@ -818,16 +799,15 @@ class ProjectForm extends HookConsumerWidget {
|
||||
'description': descriptionController.text,
|
||||
};
|
||||
|
||||
final resp =
|
||||
isEditing
|
||||
? await client.put(
|
||||
'/develop/developers/$publisherName/projects/${project!.id}',
|
||||
data: data,
|
||||
)
|
||||
: await client.post(
|
||||
'/develop/developers/$publisherName/projects',
|
||||
data: data,
|
||||
);
|
||||
final resp = isEditing
|
||||
? await client.put(
|
||||
'/develop/developers/$publisherName/projects/${project!.id}',
|
||||
data: data,
|
||||
)
|
||||
: await client.post(
|
||||
'/develop/developers/$publisherName/projects',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop(DevProject.fromJson(resp.data));
|
||||
@@ -860,8 +840,8 @@ class ProjectForm extends HookConsumerWidget {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
@@ -878,8 +858,8 @@ class ProjectForm extends HookConsumerWidget {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
@@ -892,8 +872,8 @@ class ProjectForm extends HookConsumerWidget {
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -934,38 +914,34 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
|
||||
return SheetScaffold(
|
||||
titleText: 'enrollDeveloper'.tr(),
|
||||
child: publishers.when(
|
||||
data:
|
||||
(items) =>
|
||||
items.isEmpty
|
||||
? Center(
|
||||
child:
|
||||
Text(
|
||||
'noDevelopersToEnroll',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final publisher = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: publisher.picture?.id,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () => enroll(publisher),
|
||||
);
|
||||
},
|
||||
data: (items) => items.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'noDevelopersToEnroll',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
)
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final publisher = items[index];
|
||||
return ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
fileId: publisher.picture?.id,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () => enroll(publisher),
|
||||
);
|
||||
},
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(publishersManagedProvider),
|
||||
),
|
||||
error: (error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(publishersManagedProvider),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ class ProjectDetailView extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
final currentDest = useState(0);
|
||||
|
||||
useEffect(() {
|
||||
tabController.addListener(() {
|
||||
if (tabController.indexIsChanging) {
|
||||
currentDest.value = tabController.index;
|
||||
}
|
||||
});
|
||||
return null;
|
||||
});
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
@@ -38,14 +48,13 @@ class ProjectDetailView extends HookConsumerWidget {
|
||||
child: NavigationRail(
|
||||
extended: isWiderScreen(context),
|
||||
scrollable: true,
|
||||
labelType:
|
||||
isWiderScreen(context)
|
||||
? null
|
||||
: NavigationRailLabelType.selected,
|
||||
labelType: isWiderScreen(context)
|
||||
? null
|
||||
: NavigationRailLabelType.selected,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedIndex: tabController.index,
|
||||
onDestinationSelected:
|
||||
(index) => tabController.animateTo(index),
|
||||
selectedIndex: currentDest.value,
|
||||
onDestinationSelected: (index) =>
|
||||
tabController.animateTo(index),
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.apps),
|
||||
|
||||
@@ -33,7 +33,12 @@ class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
|
||||
Future<List<SnWebArticle>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParams = {'limit': pageSize, 'offset': fetchedCount.toString()};
|
||||
final queryParams = {
|
||||
'limit': pageSize,
|
||||
'offset': fetchedCount.toString(),
|
||||
'feedId': arg.feedId,
|
||||
'publisherId': arg.publisherId,
|
||||
}..removeWhere((key, value) => value == null);
|
||||
|
||||
try {
|
||||
final response = await client.get(
|
||||
@@ -41,13 +46,10 @@ class ArticlesListNotifier extends AsyncNotifier<List<SnWebArticle>>
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
final articles =
|
||||
response.data
|
||||
.map(
|
||||
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
|
||||
)
|
||||
.cast<SnWebArticle>()
|
||||
.toList();
|
||||
final articles = response.data
|
||||
.map((json) => SnWebArticle.fromJson(json as Map<String, dynamic>))
|
||||
.cast<SnWebArticle>()
|
||||
.toList();
|
||||
|
||||
totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
|
||||
|
||||
@@ -81,6 +83,7 @@ class SliverArticlesList extends ConsumerWidget {
|
||||
ArticleListQuery(feedId: feedId, publisherId: publisherId),
|
||||
);
|
||||
return PaginationList(
|
||||
spacing: 12,
|
||||
provider: provider,
|
||||
notifier: provider.notifier,
|
||||
isRefreshable: false,
|
||||
@@ -184,18 +187,16 @@ 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')),
|
||||
),
|
||||
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')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,10 @@ class MarketplaceWebFeedContentNotifier
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final articles =
|
||||
response.data
|
||||
.map((json) => SnWebArticle.fromJson(json))
|
||||
.cast<SnWebArticle>()
|
||||
.toList();
|
||||
final articles = response.data
|
||||
.map((json) => SnWebArticle.fromJson(json))
|
||||
.cast<SnWebArticle>()
|
||||
.toList();
|
||||
|
||||
return articles;
|
||||
}
|
||||
@@ -116,31 +115,30 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
|
||||
// Feed meta
|
||||
feed
|
||||
.when(
|
||||
data:
|
||||
(data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
data: (data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(data.description ?? 'descriptionNone'.tr()),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(data.description ?? 'descriptionNone'.tr()),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.rss_feed, size: 16),
|
||||
Text(
|
||||
'webFeedArticleCount'.plural(
|
||||
feedNotifier.totalCount ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.link, size: 16),
|
||||
SelectableText(data.url),
|
||||
],
|
||||
).opacity(0.85),
|
||||
const Icon(Symbols.rss_feed, size: 16),
|
||||
Text(
|
||||
'webFeedArticleCount'.plural(
|
||||
feedNotifier.totalCount ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.link, size: 16),
|
||||
SelectableText(data.url),
|
||||
],
|
||||
).opacity(0.85),
|
||||
],
|
||||
),
|
||||
error: (err, _) => Text(err.toString()),
|
||||
loading: () => CircularProgressIndicator().center(),
|
||||
)
|
||||
@@ -149,10 +147,12 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
|
||||
// Articles list
|
||||
Expanded(
|
||||
child: PaginationList(
|
||||
spacing: 8,
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
provider: marketplaceWebFeedContentNotifierProvider(id),
|
||||
notifier: marketplaceWebFeedContentNotifierProvider(id).notifier,
|
||||
itemBuilder: (context, index, article) {
|
||||
return WebArticleCard(article: article);
|
||||
return WebArticleCard(article: article).padding(horizontal: 12);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -165,29 +165,25 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: subscribed.when(
|
||||
data:
|
||||
(isSubscribed) => FilledButton.icon(
|
||||
onPressed:
|
||||
isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
|
||||
icon: Icon(
|
||||
isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
|
||||
),
|
||||
label: Text(
|
||||
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
error:
|
||||
(_, _) => OutlinedButton.icon(
|
||||
onPressed: subscribeToFeed,
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe').tr(),
|
||||
),
|
||||
data: (isSubscribed) => FilledButton.icon(
|
||||
onPressed: isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
|
||||
icon: Icon(
|
||||
isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
|
||||
),
|
||||
label: Text(
|
||||
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
).center(),
|
||||
error: (_, _) => OutlinedButton.icon(
|
||||
onPressed: subscribeToFeed,
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe').tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
final marketplaceWebFeedsNotifierProvider = AsyncNotifierProvider(
|
||||
final marketplaceWebFeedsNotifierProvider = AsyncNotifierProvider.autoDispose(
|
||||
MarketplaceWebFeedsNotifier.new,
|
||||
);
|
||||
|
||||
@@ -38,11 +38,10 @@ class MarketplaceWebFeedsNotifier extends AsyncNotifier<List<SnWebFeed>>
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final feeds =
|
||||
response.data
|
||||
.map((e) => SnWebFeed.fromJson(e))
|
||||
.cast<SnWebFeed>()
|
||||
.toList();
|
||||
final feeds = response.data
|
||||
.map((e) => SnWebFeed.fromJson(e))
|
||||
.cast<SnWebFeed>()
|
||||
.toList();
|
||||
|
||||
return feeds;
|
||||
}
|
||||
@@ -92,8 +91,8 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
trailing: [
|
||||
if (query.value != null && query.value!.isNotEmpty)
|
||||
IconButton(
|
||||
@@ -128,6 +127,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget {
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, feed) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(feed.title),
|
||||
subtitle: Text(feed.description ?? ''),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
|
||||
@@ -23,7 +23,7 @@ class DiscoveryRealmsScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(80),
|
||||
SliverGap(88),
|
||||
SliverRealmList(
|
||||
query: currentQuery.value,
|
||||
key: ValueKey(currentQuery.value),
|
||||
|
||||
@@ -102,7 +102,7 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
// Listen for post creation events to refresh activities
|
||||
useEffect(() {
|
||||
final subscription = eventBus.on<PostCreatedEvent>().listen((event) {
|
||||
ref.invalidate(activityListProvider);
|
||||
ref.read(activityListProvider.notifier).refresh();
|
||||
});
|
||||
return subscription.cancel;
|
||||
}, []);
|
||||
@@ -183,25 +183,13 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categories').tr(),
|
||||
Text('categoriesAndTags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.label),
|
||||
const Gap(12),
|
||||
Text('tags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postTags');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -490,25 +478,13 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categories').tr(),
|
||||
Text('categoriesAndTags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.label),
|
||||
const Gap(12),
|
||||
Text('tags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postTags');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -18,11 +18,92 @@ import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
part 'notification.g.dart';
|
||||
|
||||
class SkeletonNotificationTile extends StatelessWidget {
|
||||
const SkeletonNotificationTile({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const fakeTitle = 'New notification';
|
||||
const fakeSubtitle = 'You have a new message from someone';
|
||||
const fakeContent =
|
||||
'This is a preview of the notification content. It may contain formatted text.';
|
||||
const List<String> fakeImageIds = []; // Empty list for no images
|
||||
const String? fakePfp = null; // No profile picture
|
||||
|
||||
return ListTile(
|
||||
isThreeLine: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
leading: fakePfp != null
|
||||
? ProfilePictureWidget(fileId: fakePfp, radius: 20)
|
||||
: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Icon(
|
||||
Symbols.notifications,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
title: const Text(fakeTitle),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(fakeSubtitle).bold(),
|
||||
Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text('Loading...').fontSize(11),
|
||||
Skeleton.ignore(child: Text('·').fontSize(11).bold()),
|
||||
Text('Now').fontSize(11),
|
||||
],
|
||||
).opacity(0.75).padding(bottom: 4),
|
||||
MarkdownTextContent(
|
||||
content: fakeContent,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
if (fakeImageIds.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: fakeImageIds.map((imageId) {
|
||||
return SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CloudImageWidget(
|
||||
fileId: imageId,
|
||||
aspectRatio: 1,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.blue,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class NotificationUnreadCountNotifier
|
||||
extends _$NotificationUnreadCountNotifier {
|
||||
@@ -82,7 +163,7 @@ class NotificationUnreadCountNotifier
|
||||
}
|
||||
}
|
||||
|
||||
final notificationListProvider = AsyncNotifierProvider(
|
||||
final notificationListProvider = AsyncNotifierProvider.autoDispose(
|
||||
NotificationListNotifier.new,
|
||||
);
|
||||
|
||||
@@ -182,6 +263,7 @@ class NotificationSheet extends HookConsumerWidget {
|
||||
child: PaginationList(
|
||||
provider: notificationListProvider,
|
||||
notifier: notificationListProvider.notifier,
|
||||
footerSkeletonChild: const SkeletonNotificationTile(),
|
||||
itemBuilder: (context, index, notification) {
|
||||
final pfp = notification.meta['pfp'] as String?;
|
||||
final images = notification.meta['images'] as List?;
|
||||
|
||||
@@ -2,118 +2,113 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post_category.dart';
|
||||
import 'package:island/models/post_tag.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
import 'package:island/pods/post/post_categories.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
// Post Categories Notifier
|
||||
final postCategoriesProvider = AsyncNotifierProvider.autoDispose<
|
||||
PostCategoriesNotifier,
|
||||
List<SnPostCategory>
|
||||
>(PostCategoriesNotifier.new);
|
||||
|
||||
class PostCategoriesNotifier extends AsyncNotifier<List<SnPostCategory>>
|
||||
with AsyncPaginationController<SnPostCategory> {
|
||||
@override
|
||||
Future<List<SnPostCategory>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts/categories',
|
||||
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final data = response.data as List;
|
||||
return data.map((json) => SnPostCategory.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Post Tags Notifier
|
||||
final postTagsProvider =
|
||||
AsyncNotifierProvider.autoDispose<PostTagsNotifier, List<SnPostTag>>(
|
||||
PostTagsNotifier.new,
|
||||
);
|
||||
|
||||
class PostTagsNotifier extends AsyncNotifier<List<SnPostTag>>
|
||||
with AsyncPaginationController<SnPostTag> {
|
||||
@override
|
||||
Future<List<SnPostTag>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/sphere/posts/tags',
|
||||
queryParameters: {'offset': fetchedCount, 'take': 20, 'order': 'usage'},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final data = response.data as List;
|
||||
return data.map((json) => SnPostTag.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class PostCategoriesListScreen extends ConsumerWidget {
|
||||
class PostCategoriesListScreen extends HookConsumerWidget {
|
||||
const PostCategoriesListScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('categories').tr()),
|
||||
body: PaginationList(
|
||||
provider: postCategoriesProvider,
|
||||
notifier: postCategoriesProvider.notifier,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, category) {
|
||||
return ListTile(
|
||||
leading: const Icon(Symbols.category),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text(category.categoryDisplayTitle),
|
||||
subtitle: Text('postCount'.plural(category.usage)),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'postCategoryDetail',
|
||||
pathParameters: {'slug': category.slug},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('categoriesAndTags').tr(),
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'categories'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'tags'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(children: [_CategoriesTab(), _TagsTab()]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PostTagsListScreen extends ConsumerWidget {
|
||||
const PostTagsListScreen({super.key});
|
||||
class _CategoriesTab extends ConsumerWidget {
|
||||
const _CategoriesTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('tags').tr()),
|
||||
body: PaginationList(
|
||||
provider: postTagsProvider,
|
||||
notifier: postTagsProvider.notifier,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, tag) {
|
||||
return ListTile(
|
||||
title: Text(tag.name ?? '#${tag.slug}'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.label),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
subtitle: Text('postCount'.plural(tag.usage)),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'postTagDetail',
|
||||
pathParameters: {'slug': tag.slug},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
return PaginationList(
|
||||
provider: postCategoriesProvider,
|
||||
notifier: postCategoriesProvider.notifier,
|
||||
footerSkeletonMaxWidth: 640,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, category) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.category),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text(category.categoryDisplayTitle),
|
||||
subtitle: Text('postCount'.plural(category.usage)),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'postCategoryDetail',
|
||||
pathParameters: {'slug': category.slug},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TagsTab extends ConsumerWidget {
|
||||
const _TagsTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PaginationList(
|
||||
provider: postTagsProvider,
|
||||
notifier: postTagsProvider.notifier,
|
||||
footerSkeletonMaxWidth: 640,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, tag) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
child: ListTile(
|
||||
title: Text(tag.name ?? '#${tag.slug}'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.label),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
subtitle: Text('postCount'.plural(tag.usage)),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'postTagDetail',
|
||||
pathParameters: {'slug': tag.slug},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/post_item_skeleton.dart';
|
||||
import 'package:island/widgets/posts/post_filter.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/pods/post/post_list.dart';
|
||||
@@ -79,43 +80,21 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: isWideScreen(context)
|
||||
? null
|
||||
: AppBar(
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'search'.tr(),
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor,
|
||||
),
|
||||
onChanged: onSearchChanged,
|
||||
onSubmitted: (value) {
|
||||
onSearchChanged(value, skipDebounce: true);
|
||||
},
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
showFilters.value
|
||||
? Icons.filter_alt
|
||||
: Icons.filter_alt_outlined,
|
||||
),
|
||||
onPressed: toggleFilterDisplay,
|
||||
tooltip: 'toggleFilters'.tr(),
|
||||
),
|
||||
],
|
||||
appBar: AppBar(
|
||||
title: Text('searchPosts'.tr()),
|
||||
actions: [
|
||||
if (!isWideScreen(context))
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
showFilters.value
|
||||
? Icons.filter_alt
|
||||
: Icons.filter_alt_outlined,
|
||||
),
|
||||
onPressed: toggleFilterDisplay,
|
||||
tooltip: 'toggleFilters'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final searchState = ref.watch(
|
||||
@@ -152,10 +131,7 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(16),
|
||||
if (showFilters.value && !isWideScreen(context))
|
||||
SliverToBoxAdapter(child: buildFilterPanel()),
|
||||
// Use PaginationList with isSliver=true
|
||||
const SliverGap(12),
|
||||
PaginationList(
|
||||
provider: postListProvider(
|
||||
PostListQueryConfig(id: kSearchPostListId),
|
||||
@@ -165,6 +141,12 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
).notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
footerSkeletonChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: const PostItemSkeleton(),
|
||||
),
|
||||
itemBuilder: (context, index, post) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
@@ -246,6 +228,28 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
)
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(4),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: SearchBar(
|
||||
elevation: WidgetStateProperty.all(4),
|
||||
controller: searchController,
|
||||
hintText: 'search'.tr(),
|
||||
leading: const Icon(Icons.search),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onChanged: onSearchChanged,
|
||||
onSubmitted: (value) {
|
||||
onSearchChanged(value, skipDebounce: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showFilters.value)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
@@ -255,7 +259,6 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Use PaginationList with isSliver=true
|
||||
PaginationList(
|
||||
provider: postListProvider(
|
||||
PostListQueryConfig(id: kSearchPostListId),
|
||||
@@ -265,6 +268,10 @@ class PostSearchScreen extends HookConsumerWidget {
|
||||
).notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
footerSkeletonChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: const PostItemSkeleton(),
|
||||
),
|
||||
itemBuilder: (context, index, post) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
|
||||
@@ -127,102 +127,88 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
|
||||
// Stickers grid
|
||||
Expanded(
|
||||
child: packContent.when(
|
||||
data:
|
||||
(stickers) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
marketplaceStickerPackContentProvider(
|
||||
packId: id,
|
||||
).future,
|
||||
),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
data: (stickers) => RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(
|
||||
marketplaceStickerPackContentProvider(packId: id).future,
|
||||
),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 96,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 96,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
),
|
||||
itemCount: stickers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sticker = stickers[index];
|
||||
return Tooltip(
|
||||
message: ':${p?.prefix ?? ''}${sticker.slug}:',
|
||||
child: ClipRRect(
|
||||
itemCount: stickers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sticker = stickers[index];
|
||||
return Tooltip(
|
||||
message: ':${p?.prefix ?? ''}${sticker.slug}:',
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.image.id,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text(
|
||||
'Error: $err',
|
||||
).textAlignment(TextAlign.center).center(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
error: (err, _) => Text(
|
||||
'Error: $err',
|
||||
).textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: owned.when(
|
||||
data:
|
||||
(isOwned) => FilledButton.icon(
|
||||
onPressed:
|
||||
isOwned
|
||||
? removePackFromMyCollection
|
||||
: addPackToMyCollection,
|
||||
icon: Icon(
|
||||
isOwned ? Symbols.remove_circle : Symbols.add_circle,
|
||||
),
|
||||
label: Text(
|
||||
isOwned ? 'removePack'.tr() : 'addPack'.tr(),
|
||||
),
|
||||
),
|
||||
loading:
|
||||
() => const SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
error:
|
||||
(_, _) => OutlinedButton.icon(
|
||||
onPressed: addPackToMyCollection,
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('addPack').tr(),
|
||||
),
|
||||
data: (isOwned) => FilledButton.icon(
|
||||
onPressed: isOwned
|
||||
? removePackFromMyCollection
|
||||
: addPackToMyCollection,
|
||||
icon: Icon(
|
||||
isOwned ? Symbols.remove_circle : Symbols.add_circle,
|
||||
),
|
||||
label: Text(isOwned ? 'removePack'.tr() : 'addPack'.tr()),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
).center(),
|
||||
error: (_, _) => OutlinedButton.icon(
|
||||
onPressed: addPackToMyCollection,
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('addPack').tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
);
|
||||
},
|
||||
error:
|
||||
(err, _) =>
|
||||
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
||||
error: (err, _) =>
|
||||
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -28,9 +28,8 @@ sealed class MarketplaceStickerQuery with _$MarketplaceStickerQuery {
|
||||
}) = _MarketplaceStickerQuery;
|
||||
}
|
||||
|
||||
final marketplaceStickerPacksNotifierProvider = AsyncNotifierProvider(
|
||||
MarketplaceStickerPacksNotifier.new,
|
||||
);
|
||||
final marketplaceStickerPacksNotifierProvider =
|
||||
AsyncNotifierProvider.autoDispose(MarketplaceStickerPacksNotifier.new);
|
||||
|
||||
class MarketplaceStickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
|
||||
with
|
||||
@@ -60,11 +59,10 @@ class MarketplaceStickerPacksNotifier extends AsyncNotifier<List<SnStickerPack>>
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final stickers =
|
||||
response.data
|
||||
.map((e) => SnStickerPack.fromJson(e))
|
||||
.cast<SnStickerPack>()
|
||||
.toList();
|
||||
final stickers = response.data
|
||||
.map((e) => SnStickerPack.fromJson(e))
|
||||
.cast<SnStickerPack>()
|
||||
.toList();
|
||||
|
||||
return stickers;
|
||||
}
|
||||
@@ -112,14 +110,12 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
|
||||
onPressed: () {
|
||||
query.value = query.value.copyWith(byUsage: !query.value.byUsage);
|
||||
},
|
||||
icon:
|
||||
query.value.byUsage
|
||||
? const Icon(Symbols.local_fire_department)
|
||||
: const Icon(Symbols.access_time),
|
||||
tooltip:
|
||||
query.value.byUsage
|
||||
? 'orderByPopularity'.tr()
|
||||
: 'orderByReleaseDate'.tr(),
|
||||
icon: query.value.byUsage
|
||||
? const Icon(Symbols.local_fire_department)
|
||||
: const Icon(Symbols.access_time),
|
||||
tooltip: query.value.byUsage
|
||||
? 'orderByPopularity'.tr()
|
||||
: 'orderByReleaseDate'.tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
@@ -137,8 +133,8 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
trailing: [
|
||||
if (query.value.query != null && query.value.query!.isNotEmpty)
|
||||
IconButton(
|
||||
@@ -171,26 +167,53 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
provider: marketplaceStickerPacksNotifierProvider,
|
||||
notifier: marketplaceStickerPacksNotifierProvider.notifier,
|
||||
itemBuilder:
|
||||
(context, idx, pack) => Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
itemBuilder: (context, idx, pack) => Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Column(
|
||||
children: [
|
||||
if (pack.stickers.isNotEmpty)
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
math.min(pack.stickers.length, 4),
|
||||
(index) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: index < 3 ? 8 : 0,
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 80,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.tertiaryContainer,
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
file: pack.stickers[index].image,
|
||||
),
|
||||
).clipRRect(all: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pack.stickers.length > 4)
|
||||
const SizedBox(height: 8),
|
||||
if (pack.stickers.length > 4)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
math.min(pack.stickers.length, 4),
|
||||
math.min(pack.stickers.length - 4, 4),
|
||||
(index) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: index < 3 ? 8 : 0,
|
||||
@@ -203,89 +226,55 @@ class MarketplaceStickersScreen extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.tertiaryContainer,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.tertiaryContainer,
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
file: pack.stickers[index].image,
|
||||
file: pack.stickers[index + 4].image,
|
||||
),
|
||||
).clipRRect(all: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (pack.stickers.length > 4)
|
||||
const SizedBox(height: 8),
|
||||
if (pack.stickers.length > 4)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
math.min(pack.stickers.length - 4, 4),
|
||||
(index) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: index < 3 ? 8 : 0,
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 80,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.tertiaryContainer,
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
file:
|
||||
pack.stickers[index + 4].image,
|
||||
),
|
||||
).clipRRect(all: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.tertiaryContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
file: pack.icon ?? pack.stickers.first.image,
|
||||
),
|
||||
).width(40).height(40).clipRRect(all: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
title: Text(pack.name),
|
||||
subtitle: Text(pack.description),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
// Navigate to user-facing sticker pack detail page.
|
||||
// Adjust the route name/parameters if your app uses different ones.
|
||||
context.pushNamed(
|
||||
'stickerPackDetail',
|
||||
pathParameters: {'packId': pack.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
).clipRRect(topLeft: 8, topRight: 8),
|
||||
ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.tertiaryContainer,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
file: pack.icon ?? pack.stickers.firstOrNull?.image,
|
||||
),
|
||||
).width(40).height(40).clipRRect(all: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
title: Text(pack.name),
|
||||
subtitle: Text(pack.description),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
// Navigate to user-facing sticker pack detail page.
|
||||
// Adjust the route name/parameters if your app uses different ones.
|
||||
context.pushNamed(
|
||||
'stickerPackDetail',
|
||||
pathParameters: {'packId': pack.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/misc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
@@ -17,21 +18,27 @@ class PaginationList<T> extends HookConsumerWidget {
|
||||
final ProviderListenable<AsyncValue<List<T>>> provider;
|
||||
final Refreshable<PaginationController<T>> notifier;
|
||||
final Widget? Function(BuildContext, int, T) itemBuilder;
|
||||
final Widget? Function(BuildContext, int, T)? seperatorBuilder;
|
||||
final double? spacing;
|
||||
final bool isRefreshable;
|
||||
final bool isSliver;
|
||||
final bool showDefaultWidgets;
|
||||
final EdgeInsets? padding;
|
||||
final Widget? footerSkeletonChild;
|
||||
final double? footerSkeletonMaxWidth;
|
||||
const PaginationList({
|
||||
super.key,
|
||||
required this.provider,
|
||||
required this.notifier,
|
||||
required this.itemBuilder,
|
||||
this.seperatorBuilder,
|
||||
this.spacing,
|
||||
this.isRefreshable = true,
|
||||
this.isSliver = false,
|
||||
this.showDefaultWidgets = true,
|
||||
this.padding,
|
||||
this.footerSkeletonChild,
|
||||
this.footerSkeletonMaxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -39,70 +46,149 @@ class PaginationList<T> extends HookConsumerWidget {
|
||||
final data = ref.watch(provider);
|
||||
final noti = ref.watch(notifier);
|
||||
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = List<Widget>.generate(
|
||||
10,
|
||||
(_) => Skeletonizer(
|
||||
enabled: true,
|
||||
effect: ShimmerEffect(
|
||||
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
highlightColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
// For sliver cases, avoid animation to prevent complex sliver issues
|
||||
if (isSliver) {
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = List<Widget>.generate(
|
||||
10,
|
||||
(_) => Skeletonizer(
|
||||
enabled: true,
|
||||
effect: ShimmerEffect(
|
||||
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
highlightColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child:
|
||||
footerSkeletonChild ??
|
||||
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
|
||||
),
|
||||
);
|
||||
return SliverList.list(children: content);
|
||||
}
|
||||
|
||||
if (data.hasError) {
|
||||
final content = ResponseErrorWidget(
|
||||
error: data.error,
|
||||
onRetry: noti.refresh,
|
||||
);
|
||||
return SliverFillRemaining(child: content);
|
||||
}
|
||||
|
||||
final listView = SuperSliverList.separated(
|
||||
itemCount: (data.value?.length ?? 0) + 1,
|
||||
itemBuilder: (context, idx) {
|
||||
if (idx == data.value?.length) {
|
||||
return PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||
);
|
||||
}
|
||||
final entry = data.value?[idx];
|
||||
if (entry != null) return itemBuilder(context, idx, entry);
|
||||
return null;
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
if (seperatorBuilder != null) {
|
||||
final entry = data.value?[index];
|
||||
if (entry != null) {
|
||||
return seperatorBuilder!(context, index, entry) ??
|
||||
const SizedBox();
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
if (spacing != null && spacing! > 0) {
|
||||
return Gap(spacing!);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
return isSliver
|
||||
? SliverList.list(children: content)
|
||||
: ListView(children: content);
|
||||
|
||||
return isRefreshable
|
||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
|
||||
: listView;
|
||||
}
|
||||
|
||||
if (data.hasError) {
|
||||
final content = ResponseErrorWidget(
|
||||
error: data.error,
|
||||
onRetry: noti.refresh,
|
||||
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
|
||||
Widget buildContent() {
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = List<Widget>.generate(
|
||||
10,
|
||||
(_) => Skeletonizer(
|
||||
enabled: true,
|
||||
effect: ShimmerEffect(
|
||||
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
highlightColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child:
|
||||
footerSkeletonChild ??
|
||||
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
|
||||
),
|
||||
);
|
||||
return SizedBox(
|
||||
key: const ValueKey('loading'),
|
||||
child: ListView(children: content),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.hasError) {
|
||||
final content = ResponseErrorWidget(
|
||||
error: data.error,
|
||||
onRetry: noti.refresh,
|
||||
);
|
||||
return SizedBox(key: const ValueKey('error'), child: content);
|
||||
}
|
||||
|
||||
final listView = SuperListView.separated(
|
||||
padding: padding,
|
||||
itemCount: (data.value?.length ?? 0) + 1,
|
||||
itemBuilder: (context, idx) {
|
||||
if (idx == data.value?.length) {
|
||||
return PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||
);
|
||||
}
|
||||
final entry = data.value?[idx];
|
||||
if (entry != null) return itemBuilder(context, idx, entry);
|
||||
return null;
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
if (seperatorBuilder != null) {
|
||||
final entry = data.value?[index];
|
||||
if (entry != null) {
|
||||
return seperatorBuilder!(context, index, entry) ??
|
||||
const SizedBox();
|
||||
}
|
||||
return const SizedBox();
|
||||
}
|
||||
if (spacing != null && spacing! > 0) {
|
||||
return Gap(spacing!);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
key: const ValueKey('data'),
|
||||
child: isRefreshable
|
||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
|
||||
: listView,
|
||||
);
|
||||
return isSliver ? SliverFillRemaining(child: content) : content;
|
||||
}
|
||||
|
||||
final listView = isSliver
|
||||
? SuperSliverList.builder(
|
||||
itemCount: (data.value?.length ?? 0) + 1,
|
||||
itemBuilder: (context, idx) {
|
||||
if (idx == data.value?.length) {
|
||||
return PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
);
|
||||
}
|
||||
final entry = data.value?[idx];
|
||||
if (entry != null) return itemBuilder(context, idx, entry);
|
||||
return null;
|
||||
},
|
||||
)
|
||||
: SuperListView.builder(
|
||||
padding: padding,
|
||||
itemCount: (data.value?.length ?? 0) + 1,
|
||||
itemBuilder: (context, idx) {
|
||||
if (idx == data.value?.length) {
|
||||
return PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
);
|
||||
}
|
||||
final entry = data.value?[idx];
|
||||
if (entry != null) return itemBuilder(context, idx, entry);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
return isRefreshable
|
||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
|
||||
: listView;
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: buildContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +200,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
|
||||
final bool isSliver;
|
||||
final bool showDefaultWidgets;
|
||||
final Widget? footerSkeletonChild;
|
||||
final double? footerSkeletonMaxWidth;
|
||||
const PaginationWidget({
|
||||
super.key,
|
||||
required this.provider,
|
||||
@@ -123,6 +210,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
|
||||
this.isSliver = false,
|
||||
this.showDefaultWidgets = true,
|
||||
this.footerSkeletonChild,
|
||||
this.footerSkeletonMaxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -130,44 +218,102 @@ class PaginationWidget<T> extends HookConsumerWidget {
|
||||
final data = ref.watch(provider);
|
||||
final noti = ref.watch(notifier);
|
||||
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = List<Widget>.generate(
|
||||
10,
|
||||
(_) => Skeletonizer(
|
||||
enabled: true,
|
||||
effect: ShimmerEffect(
|
||||
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
highlightColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
// For sliver cases, avoid animation to prevent complex sliver issues
|
||||
if (isSliver) {
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = List<Widget>.generate(
|
||||
10,
|
||||
(_) => Skeletonizer(
|
||||
enabled: true,
|
||||
effect: ShimmerEffect(
|
||||
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
highlightColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child:
|
||||
footerSkeletonChild ??
|
||||
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
|
||||
),
|
||||
);
|
||||
return SliverList.list(children: content);
|
||||
}
|
||||
|
||||
if (data.hasError) {
|
||||
final content = ResponseErrorWidget(
|
||||
error: data.error,
|
||||
onRetry: noti.refresh,
|
||||
);
|
||||
return SliverFillRemaining(child: content);
|
||||
}
|
||||
|
||||
final footer = PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||
);
|
||||
return isSliver
|
||||
? SliverList.list(children: content)
|
||||
: ListView(children: content);
|
||||
final content = contentBuilder(data.value ?? [], footer);
|
||||
|
||||
return isRefreshable
|
||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
|
||||
: content;
|
||||
}
|
||||
|
||||
if (data.hasError) {
|
||||
final content = ResponseErrorWidget(
|
||||
error: data.error,
|
||||
onRetry: noti.refresh,
|
||||
// For non-sliver cases, use AnimatedSwitcher for smooth transitions
|
||||
Widget buildContent() {
|
||||
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
|
||||
final content = List<Widget>.generate(
|
||||
10,
|
||||
(_) => Skeletonizer(
|
||||
enabled: true,
|
||||
effect: ShimmerEffect(
|
||||
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
highlightColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child:
|
||||
footerSkeletonChild ??
|
||||
_DefaultSkeletonChild(maxWidth: footerSkeletonMaxWidth),
|
||||
),
|
||||
);
|
||||
return SizedBox(
|
||||
key: const ValueKey('loading'),
|
||||
child: ListView(children: content),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.hasError) {
|
||||
final content = ResponseErrorWidget(
|
||||
error: data.error,
|
||||
onRetry: noti.refresh,
|
||||
);
|
||||
return SizedBox(key: const ValueKey('error'), child: content);
|
||||
}
|
||||
|
||||
final footer = PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
skeletonMaxWidth: footerSkeletonMaxWidth,
|
||||
);
|
||||
final content = contentBuilder(data.value ?? [], footer);
|
||||
|
||||
return SizedBox(
|
||||
key: const ValueKey('data'),
|
||||
child: isRefreshable
|
||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
|
||||
: content,
|
||||
);
|
||||
return isSliver ? SliverFillRemaining(child: content) : content;
|
||||
}
|
||||
|
||||
final footer = PaginationListFooter(
|
||||
noti: noti,
|
||||
data: data,
|
||||
skeletonChild: footerSkeletonChild,
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: buildContent(),
|
||||
);
|
||||
final content = contentBuilder(data.value ?? [], footer);
|
||||
|
||||
return isRefreshable
|
||||
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
|
||||
: content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +321,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
|
||||
final PaginationController<T> noti;
|
||||
final AsyncValue<List<T>> data;
|
||||
final Widget? skeletonChild;
|
||||
final double? skeletonMaxWidth;
|
||||
final bool isSliver;
|
||||
|
||||
const PaginationListFooter({
|
||||
@@ -182,6 +329,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
|
||||
required this.noti,
|
||||
required this.data,
|
||||
this.skeletonChild,
|
||||
this.skeletonMaxWidth,
|
||||
this.isSliver = false,
|
||||
});
|
||||
|
||||
@@ -196,7 +344,7 @@ class PaginationListFooter<T> extends HookConsumerWidget {
|
||||
highlightColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: skeletonChild ?? _DefaultSkeletonChild(),
|
||||
child: skeletonChild ?? _DefaultSkeletonChild(maxWidth: skeletonMaxWidth),
|
||||
);
|
||||
final child = hasBeenVisible.value
|
||||
? data.isLoading
|
||||
@@ -225,14 +373,24 @@ class PaginationListFooter<T> extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class _DefaultSkeletonChild extends StatelessWidget {
|
||||
const _DefaultSkeletonChild();
|
||||
final double? maxWidth;
|
||||
const _DefaultSkeletonChild({this.maxWidth});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
final content = ListTile(
|
||||
title: Text('Some data'),
|
||||
subtitle: const Text('Subtitle here'),
|
||||
trailing: const Icon(Icons.ac_unit),
|
||||
);
|
||||
if (maxWidth != null) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth!),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,33 +9,14 @@ import 'package:island/models/post_category.dart';
|
||||
import 'package:island/models/post_tag.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/post/post_categories.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'compose_settings_sheet.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnPostCategory>> postCategories(Ref ref) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/posts/categories');
|
||||
final categories =
|
||||
resp.data
|
||||
.map((e) => SnPostCategory.fromJson(e))
|
||||
.cast<SnPostCategory>()
|
||||
.toList();
|
||||
// Remove duplicates based on id
|
||||
final uniqueCategories = <String, SnPostCategory>{};
|
||||
for (final category in categories) {
|
||||
uniqueCategories[category.id] = category;
|
||||
}
|
||||
return uniqueCategories.values.toList();
|
||||
}
|
||||
|
||||
class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
final ComposeState state;
|
||||
|
||||
@@ -121,39 +102,38 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
void showVisibilitySheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'postVisibility'.tr(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
0,
|
||||
Symbols.public,
|
||||
'postVisibilityPublic',
|
||||
),
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
1,
|
||||
Symbols.group,
|
||||
'postVisibilityFriends',
|
||||
),
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
2,
|
||||
Symbols.link_off,
|
||||
'postVisibilityUnlisted',
|
||||
),
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
3,
|
||||
Symbols.lock,
|
||||
'postVisibilityPrivate',
|
||||
),
|
||||
],
|
||||
builder: (context) => SheetScaffold(
|
||||
titleText: 'postVisibility'.tr(),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
0,
|
||||
Symbols.public,
|
||||
'postVisibilityPublic',
|
||||
),
|
||||
),
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
1,
|
||||
Symbols.group,
|
||||
'postVisibilityFriends',
|
||||
),
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
2,
|
||||
Symbols.link_off,
|
||||
'postVisibilityUnlisted',
|
||||
),
|
||||
buildVisibilityOption(
|
||||
context,
|
||||
3,
|
||||
Symbols.lock,
|
||||
'postVisibilityPrivate',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,8 +162,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
||||
// Tags field
|
||||
@@ -209,51 +189,48 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children:
|
||||
currentTags.map((tag) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
children: currentTags.map((tag) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'#$tag',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
const Gap(4),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final newTags = List<String>.from(
|
||||
state.tags.value,
|
||||
)..remove(tag);
|
||||
state.tags.value = newTags;
|
||||
},
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'#$tag',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
final newTags = List<String>.from(
|
||||
state.tags.value,
|
||||
)..remove(tag);
|
||||
state.tags.value = newTags;
|
||||
},
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
// Tag input with autocomplete
|
||||
TypeAheadField<SnPostTag>(
|
||||
@@ -274,8 +251,8 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
},
|
||||
suggestionsCallback:
|
||||
(pattern) => _fetchTagSuggestions(pattern, ref),
|
||||
suggestionsCallback: (pattern) =>
|
||||
_fetchTagSuggestions(pattern, ref),
|
||||
itemBuilder: (context, suggestion) {
|
||||
return ListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
@@ -314,55 +291,49 @@ class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
|
||||
items:
|
||||
(postCategories.value ?? <SnPostCategory>[]).map((item) {
|
||||
return DropdownMenuItem(
|
||||
value: item,
|
||||
enabled: false,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, menuSetState) {
|
||||
final isSelected = state.categories.value.contains(
|
||||
item,
|
||||
);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
isSelected
|
||||
? state.categories.value =
|
||||
state.categories.value
|
||||
.where((e) => e != item)
|
||||
.toList()
|
||||
: state.categories.value = [
|
||||
...state.categories.value,
|
||||
item,
|
||||
];
|
||||
menuSetState(() {});
|
||||
},
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (isSelected)
|
||||
const Icon(Icons.check_box_outlined)
|
||||
else
|
||||
const Icon(Icons.check_box_outline_blank),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.categoryDisplayTitle,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
items: (postCategories.value ?? <SnPostCategory>[]).map((item) {
|
||||
return DropdownMenuItem(
|
||||
value: item,
|
||||
enabled: false,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, menuSetState) {
|
||||
final isSelected = state.categories.value.contains(item);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
isSelected
|
||||
? state.categories.value = state.categories.value
|
||||
.where((e) => e != item)
|
||||
.toList()
|
||||
: state.categories.value = [
|
||||
...state.categories.value,
|
||||
item,
|
||||
];
|
||||
menuSetState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (isSelected)
|
||||
const Icon(Icons.check_box_outlined)
|
||||
else
|
||||
const Icon(Icons.check_box_outline_blank),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.categoryDisplayTitle,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
value: currentCategories.isEmpty ? null : currentCategories.last,
|
||||
onChanged: (_) {},
|
||||
selectedItemBuilder: (context) {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'compose_settings_sheet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(postCategories)
|
||||
const postCategoriesProvider = PostCategoriesProvider._();
|
||||
|
||||
final class PostCategoriesProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnPostCategory>>,
|
||||
List<SnPostCategory>,
|
||||
FutureOr<List<SnPostCategory>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SnPostCategory>>,
|
||||
$FutureProvider<List<SnPostCategory>> {
|
||||
const PostCategoriesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'postCategoriesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$postCategoriesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnPostCategory>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnPostCategory>> create(Ref ref) {
|
||||
return postCategories(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$postCategoriesHash() => r'8799c10eb91cf8c8c7ea72eff3475e1eaa7b9a2b';
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/compose_card.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
@@ -32,16 +37,21 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
SnPost? originalPost,
|
||||
PostComposeInitialState? initialState,
|
||||
}) {
|
||||
// Check if editing an article
|
||||
if (originalPost != null && originalPost.type == 1) {
|
||||
context.pushNamed('articleEdit', pathParameters: {'id': originalPost.id});
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
return showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder:
|
||||
(context) => PostComposeSheet(
|
||||
originalPost: originalPost,
|
||||
initialState: initialState,
|
||||
isBottomSheet: true,
|
||||
),
|
||||
builder: (context) => PostComposeSheet(
|
||||
originalPost: originalPost,
|
||||
initialState: initialState,
|
||||
isBottomSheet: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,10 +62,9 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
final prompted = useState(false);
|
||||
|
||||
// Fetch full post data if we're editing a post
|
||||
final fullPostData =
|
||||
originalPost != null
|
||||
? ref.watch(postProvider(originalPost!.id))
|
||||
: const AsyncValue.data(null);
|
||||
final fullPostData = originalPost != null
|
||||
? ref.watch(postProvider(originalPost!.id))
|
||||
: const AsyncValue.data(null);
|
||||
|
||||
// Use the full post data if available, otherwise fall back to originalPost
|
||||
final effectiveOriginalPost = fullPostData.when(
|
||||
@@ -115,7 +124,11 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
}, [drafts, prompted.value]);
|
||||
|
||||
// Dispose state when widget is disposed
|
||||
useEffect(() => () => ComposeLogic.dispose(state), []);
|
||||
useEffect(
|
||||
() =>
|
||||
() => ComposeLogic.dispose(state),
|
||||
[],
|
||||
);
|
||||
|
||||
// Helper methods for actions
|
||||
void showSettingsSheet() {
|
||||
@@ -145,26 +158,31 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
IconButton(
|
||||
onPressed:
|
||||
(state.submitting.value || state.currentPublisher.value == null)
|
||||
? null
|
||||
: performSubmit,
|
||||
icon:
|
||||
state.submitting.value
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(
|
||||
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
|
||||
),
|
||||
tooltip:
|
||||
effectiveOriginalPost != null
|
||||
? 'postUpdate'.tr()
|
||||
: 'postPublish'.tr(),
|
||||
? null
|
||||
: performSubmit,
|
||||
icon: state.submitting.value
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(
|
||||
effectiveOriginalPost != null ? Symbols.edit : Symbols.upload,
|
||||
),
|
||||
tooltip: effectiveOriginalPost != null
|
||||
? 'postUpdate'.tr()
|
||||
: 'postPublish'.tr(),
|
||||
),
|
||||
];
|
||||
|
||||
// Tablet will show a virtual keyboard, so we adjust the height factor accordingly
|
||||
final isTablet =
|
||||
isWideScreen(context) &&
|
||||
!kIsWeb &&
|
||||
(Platform.isAndroid || Platform.isAndroid);
|
||||
|
||||
return SheetScaffold(
|
||||
heightFactor: isTablet ? 0.95 : 0.8,
|
||||
titleText: 'postCompose'.tr(),
|
||||
actions: actions,
|
||||
child: PostComposeCard(
|
||||
@@ -192,29 +210,28 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
final restore = await showDialog<bool>(
|
||||
context: ref.context,
|
||||
useRootNavigator: true,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('restoreDraftTitle'.tr()),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('restoreDraftMessage'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
_buildCompactDraftPreview(context, latestDraft),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('no'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text('yes'.tr()),
|
||||
),
|
||||
],
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('restoreDraftTitle'.tr()),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('restoreDraftMessage'.tr()),
|
||||
const SizedBox(height: 16),
|
||||
_buildCompactDraftPreview(context, latestDraft),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('no'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text('yes'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (restore == true) {
|
||||
// Delete the old draft
|
||||
@@ -226,10 +243,9 @@ class PostComposeSheet extends HookConsumerWidget {
|
||||
description: latestDraft.description,
|
||||
content: latestDraft.content,
|
||||
visibility: latestDraft.visibility,
|
||||
attachments:
|
||||
latestDraft.attachments
|
||||
.map((e) => UniversalFile.fromAttachment(e))
|
||||
.toList(),
|
||||
attachments: latestDraft.attachments
|
||||
.map((e) => UniversalFile.fromAttachment(e))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ class SliverPostList extends HookConsumerWidget {
|
||||
notifier: provider.notifier,
|
||||
isRefreshable: false,
|
||||
isSliver: true,
|
||||
footerSkeletonChild: const PostItemSkeleton(),
|
||||
footerSkeletonChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: const PostItemSkeleton(),
|
||||
),
|
||||
itemBuilder: (context, index, post) {
|
||||
if (maxWidth != null) {
|
||||
return Center(
|
||||
|
||||
@@ -48,12 +48,24 @@ class PostRepliesList extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final provider = postRepliesProvider(postId);
|
||||
|
||||
final skeletonItem = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: const PostItemSkeleton(),
|
||||
);
|
||||
|
||||
return PaginationList(
|
||||
provider: provider,
|
||||
notifier: provider.notifier,
|
||||
isRefreshable: false,
|
||||
isSliver: true,
|
||||
footerSkeletonChild: const PostItemSkeleton(),
|
||||
footerSkeletonChild: maxWidth == null
|
||||
? skeletonItem
|
||||
: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth!),
|
||||
child: skeletonItem,
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, index, item) {
|
||||
final contentWidget = Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/paging.dart';
|
||||
import 'package:island/widgets/paging/pagination_list.dart';
|
||||
import 'package:island/widgets/realm/realm_card.dart';
|
||||
import 'package:island/widgets/realm/realm_list_tile.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
final realmListNotifierProvider = AsyncNotifierProvider.autoDispose
|
||||
@@ -51,25 +50,12 @@ class SliverRealmList extends HookConsumerWidget {
|
||||
notifier: provider.notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
spacing: 8,
|
||||
itemBuilder: (context, index, realm) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child:
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
child: RealmCard(realm: realm),
|
||||
).center(),
|
||||
),
|
||||
if (index <
|
||||
(ref.read(provider).value?.length ?? 0) -
|
||||
1) // Add gap except for last item? Actually PaginationList handles loading indicator which might look like last item.
|
||||
// Wait, ref.read(provider).value?.length might change.
|
||||
// Simpler to just add bottom padding to all, or Gap.
|
||||
const Gap(8),
|
||||
],
|
||||
);
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
child: RealmListTile(realm: realm),
|
||||
).center();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
@@ -10,6 +11,7 @@ import 'package:island/pods/site_files.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/sites/file_upload_dialog.dart';
|
||||
import 'package:island/widgets/sites/file_item.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
@@ -53,6 +55,9 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Symbols.upload),
|
||||
onSelected: (String choice) async {
|
||||
if (!kIsWeb) {
|
||||
await Permission.storage.request();
|
||||
}
|
||||
List<File> files = [];
|
||||
List<Map<String, dynamic>>? results;
|
||||
if (choice == 'files') {
|
||||
@@ -65,17 +70,17 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
selectedFiles.files.isEmpty) {
|
||||
return; // User canceled
|
||||
}
|
||||
files =
|
||||
selectedFiles.files
|
||||
.map((f) => File(f.path!))
|
||||
.toList();
|
||||
files = selectedFiles.files
|
||||
.map((f) => File(f.path!))
|
||||
.toList();
|
||||
} else if (choice == 'folder') {
|
||||
final dirPath =
|
||||
await FilePicker.platform.getDirectoryPath();
|
||||
final dirPath = await FilePicker.platform
|
||||
.getDirectoryPath();
|
||||
if (dirPath == null) return;
|
||||
results = await _getFilesRecursive(dirPath);
|
||||
files =
|
||||
results.map((m) => m['file'] as File).toList();
|
||||
files = results
|
||||
.map((m) => m['file'] as File)
|
||||
.toList();
|
||||
if (files.isEmpty) {
|
||||
showSnackBar('noFilesFoundInFolder'.tr());
|
||||
return;
|
||||
@@ -88,51 +93,46 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => FileUploadDialog(
|
||||
selectedFiles: files,
|
||||
site: site,
|
||||
relativePaths:
|
||||
results
|
||||
?.map(
|
||||
(m) => m['relativePath'] as String,
|
||||
)
|
||||
.toList(),
|
||||
onUploadComplete: () {
|
||||
// Refresh file list
|
||||
ref.invalidate(
|
||||
siteFilesProvider(
|
||||
siteId: site.id,
|
||||
path: currentPath.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
builder: (context) => FileUploadDialog(
|
||||
selectedFiles: files,
|
||||
site: site,
|
||||
relativePaths: results
|
||||
?.map((m) => m['relativePath'] as String)
|
||||
.toList(),
|
||||
onUploadComplete: () {
|
||||
// Refresh file list
|
||||
ref.invalidate(
|
||||
siteFilesProvider(
|
||||
siteId: site.id,
|
||||
path: currentPath.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder:
|
||||
(BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'files',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.file_copy),
|
||||
Gap(12),
|
||||
Text('siteFiles'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'folder',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.folder),
|
||||
Gap(12),
|
||||
Text('siteFolder'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem<String>(
|
||||
value: 'files',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.file_copy),
|
||||
Gap(12),
|
||||
Text('siteFiles'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'folder',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Symbols.folder),
|
||||
Gap(12),
|
||||
Text('siteFolder'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
style: ButtonStyle(
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
@@ -156,19 +156,17 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
IconButton(
|
||||
icon: Icon(Symbols.arrow_back),
|
||||
onPressed: () {
|
||||
final pathParts =
|
||||
currentPath.value!
|
||||
.split('/')
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
final pathParts = currentPath.value!
|
||||
.split('/')
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
if (pathParts.isEmpty) {
|
||||
currentPath.value = null;
|
||||
} else {
|
||||
pathParts.removeLast();
|
||||
currentPath.value =
|
||||
pathParts.isEmpty
|
||||
? null
|
||||
: pathParts.join('/');
|
||||
currentPath.value = pathParts.isEmpty
|
||||
? null
|
||||
: pathParts.join('/');
|
||||
}
|
||||
},
|
||||
visualDensity: const VisualDensity(
|
||||
@@ -185,11 +183,10 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
child: Text('siteRoot'.tr()),
|
||||
),
|
||||
...() {
|
||||
final parts =
|
||||
currentPath.value!
|
||||
.split('/')
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
final parts = currentPath.value!
|
||||
.split('/')
|
||||
.where((part) => part.isNotEmpty)
|
||||
.toList();
|
||||
final widgets = <Widget>[];
|
||||
String currentBuilder = '';
|
||||
for (final part in parts) {
|
||||
@@ -200,8 +197,8 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
widgets.addAll([
|
||||
const Text(' / '),
|
||||
InkWell(
|
||||
onTap:
|
||||
() => currentPath.value = pathToSet,
|
||||
onTap: () =>
|
||||
currentPath.value = pathToSet,
|
||||
child: Text(part),
|
||||
),
|
||||
]);
|
||||
@@ -253,33 +250,31 @@ class FileManagementSection extends HookConsumerWidget {
|
||||
return FileItem(
|
||||
file: file,
|
||||
site: site,
|
||||
onNavigateDirectory:
|
||||
(path) => currentPath.value = path,
|
||||
onNavigateDirectory: (path) =>
|
||||
currentPath.value = path,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading:
|
||||
() => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('failedToLoadFiles'.tr()),
|
||||
const Gap(8),
|
||||
ElevatedButton(
|
||||
onPressed:
|
||||
() => ref.invalidate(
|
||||
siteFilesProvider(
|
||||
siteId: site.id,
|
||||
path: currentPath.value,
|
||||
),
|
||||
),
|
||||
child: Text('retry'.tr()),
|
||||
loading: () =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text('failedToLoadFiles'.tr()),
|
||||
const Gap(8),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(
|
||||
siteFilesProvider(
|
||||
siteId: site.id,
|
||||
path: currentPath.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text('retry'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -19,64 +19,50 @@ class SiteActionMenu extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PopupMenuButton<String>(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.edit,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const Gap(16),
|
||||
Text('edit'.tr()),
|
||||
],
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.edit,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Gap(16),
|
||||
Text('delete'.tr()).textColor(Colors.red),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const Gap(16),
|
||||
Text('edit'.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const Gap(16),
|
||||
Text('delete'.tr()).textColor(Colors.red),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SiteForm(pubName: pubName, siteSlug: site.slug),
|
||||
builder: (context) =>
|
||||
SiteForm(pubName: pubName, siteSlug: site.slug),
|
||||
).then((_) {
|
||||
// Refresh site data after potential edit
|
||||
ref.invalidate(publicationSiteDetailProvider(pubName, site.slug));
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('deleteSite'.tr()),
|
||||
content: Text('publicationSiteDeleteConfirm'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text('delete'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
final confirmed = await showConfirmAlert(
|
||||
'publicationSiteDeleteConfirm'.tr(),
|
||||
'deleteSite'.tr(),
|
||||
isDanger: true,
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/models/webfeed.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class WebArticleCard extends StatelessWidget {
|
||||
final SnWebArticle article;
|
||||
@@ -22,9 +23,6 @@ class WebArticleCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Card(
|
||||
@@ -32,108 +30,41 @@ class WebArticleCard extends StatelessWidget {
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => _onTap(context),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Image or fallback
|
||||
article.preview?.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: article.preview!.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
)
|
||||
: ColoredBox(
|
||||
color: colorScheme.secondaryContainer,
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.article_outlined,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Gradient overlay
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.7),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (article.preview?.imageUrl != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: article.preview!.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
// Title
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showDetails)
|
||||
const SizedBox(height: 8)
|
||||
else
|
||||
Spacer(),
|
||||
Text(
|
||||
article.title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.3,
|
||||
),
|
||||
maxLines: showDetails ? 3 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (showDetails &&
|
||||
article.author?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
article.author!,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showDetails) const Spacer(),
|
||||
if (showDetails && article.publishedAt != null) ...[
|
||||
Text(
|
||||
'${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
Text(
|
||||
article.feed?.title ?? 'Unknown Source',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.white70,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text(article.title),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${article.createdAt.formatSystem()} · ${article.createdAt.formatRelative(context)}',
|
||||
),
|
||||
Text(
|
||||
article.feed?.title ?? 'Unknown Source',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
84
pubspec.lock
84
pubspec.lock
@@ -493,26 +493,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114"
|
||||
sha256: "3669e1b68d7bffb60192ac6ba9fd2c0306804d7a00e5879f6364c69ecde53a7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.29.0"
|
||||
version: "2.30.0"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43"
|
||||
sha256: afe4d1d2cfce6606c86f11a6196e974a2ddbfaa992956ce61e054c9b1899c769
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.29.0"
|
||||
version: "2.30.0"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: b7534bf320aac5213259aac120670ba67b63a1fd010505babc436ff86083818f
|
||||
sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.7"
|
||||
version: "0.2.8"
|
||||
dropdown_button2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1917,6 +1917,54 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+3"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.0.1"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.1"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3+5"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2607,10 +2655,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c"
|
||||
sha256: "162435ede92bcc793ea939fdc0452eef0a73d11f8ed053b58a89792fba749da5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.42.0"
|
||||
version: "0.42.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2783,42 +2831,42 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker
|
||||
sha256: "5ab800e29d91ce7728fa218c8a7d46f6c228202ac89af650c3f82cb938a64ba6"
|
||||
sha256: "1f94c986b282ccb4efdb3bf0b2549bf314aa20c9e35cbde2c8111d6dd31a7b9a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
version: "5.1.4"
|
||||
talker_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_dio_logger
|
||||
sha256: "214a31d2ecc488ae6abf623ca9dac3831d34e66195f633bd1909a9d0c282ab8c"
|
||||
sha256: "775cad84e935f3c47b7cbfb07f1ab459850b2df28893eb798b1d537eea9feb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
version: "5.1.4"
|
||||
talker_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_flutter
|
||||
sha256: "79158cf0fe3fd2bcdb1dc6f5a870cb623f18d0b59a4fd87414f53ce446d80a45"
|
||||
sha256: "60350aef6b2f5f66e9d1350dc6ab60f9800ab2c4904912f635899dae64f44229"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
version: "5.1.4"
|
||||
talker_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_logger
|
||||
sha256: bc75612ace4dbb82fbad36181ff27e95b1ee152c719c2fea6b8ac59c4f091cd2
|
||||
sha256: c2d58daa0d99518c83b4941967e7527dc4d98328b145bbf07a638bbaf4f0e039
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
version: "5.1.4"
|
||||
talker_riverpod_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: talker_riverpod_logger
|
||||
sha256: "7040a9e4efcd6b41a169b607bfd1db6cbe0a22182659006979531f63155a1426"
|
||||
sha256: "3b236ee086f5f2943da997b70f03e42ae4f9a0c155b35491f9e94a239a9908ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.3"
|
||||
version: "5.1.4"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
19
pubspec.yaml
19
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 3.5.0+150
|
||||
version: 3.5.0+151
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.0
|
||||
@@ -84,8 +84,8 @@ dependencies:
|
||||
firebase_core: ^4.2.1
|
||||
web_socket_channel: ^3.0.3
|
||||
material_symbols_icons: ^4.2892.0
|
||||
drift: ^2.29.0
|
||||
drift_flutter: ^0.2.7
|
||||
drift: ^2.30.0
|
||||
drift_flutter: ^0.2.8
|
||||
path: ^1.9.1
|
||||
collection: ^1.19.1
|
||||
markdown_editor_plus: ^0.2.15
|
||||
@@ -154,11 +154,11 @@ dependencies:
|
||||
dart_ipc: ^1.0.1
|
||||
pretty_diff_text: ^2.1.0
|
||||
window_manager: ^0.5.1
|
||||
talker: ^5.1.3
|
||||
talker_flutter: ^5.1.3
|
||||
talker_logger: ^5.1.3
|
||||
talker_dio_logger: ^5.1.3
|
||||
talker_riverpod_logger: ^5.1.3
|
||||
talker: ^5.1.4
|
||||
talker_flutter: ^5.1.4
|
||||
talker_logger: ^5.1.4
|
||||
talker_dio_logger: ^5.1.4
|
||||
talker_riverpod_logger: ^5.1.4
|
||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||
swipe_to: ^1.0.6
|
||||
fl_heatmap: ^0.4.6
|
||||
@@ -171,6 +171,7 @@ dependencies:
|
||||
http_parser: ^4.1.2
|
||||
flutter_code_editor: ^0.3.5
|
||||
skeletonizer: ^2.1.1
|
||||
permission_handler: ^12.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -188,7 +189,7 @@ dev_dependencies:
|
||||
riverpod_generator: ^3.0.3
|
||||
custom_lint: ^0.8.1
|
||||
riverpod_lint: ^3.0.3
|
||||
drift_dev: ^2.29.0
|
||||
drift_dev: ^2.30.0
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
msix: ^3.16.12
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||
@@ -73,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||
PasteboardPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -21,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_libs_windows_video
|
||||
media_kit_video
|
||||
pasteboard
|
||||
permission_handler_windows
|
||||
protocol_handler_windows
|
||||
record_windows
|
||||
screen_retriever_windows
|
||||
|
||||
Reference in New Issue
Block a user