Realm discovery and more detailed realm

This commit is contained in:
2025-06-27 21:10:53 +08:00
parent f511612a53
commit 9d115a5712
16 changed files with 288 additions and 102 deletions

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:material_symbols_icons/symbols.dart';
class RealmCard extends ConsumerWidget {
final SnRealm realm;
const RealmCard({super.key, required this.realm});
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(apiClientProvider);
Widget imageWidget;
if (realm.picture != null) {
final imageUrl = '${client.options.baseUrl}/files/${realm.picture!.id}';
imageWidget = Image.network(
imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
);
} else {
imageWidget = Container(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Center(
child: Icon(
Symbols.photo_camera,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
);
}
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.push('/realms/${realm.slug}');
},
child: AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
children: [
imageWidget,
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent,
],
),
),
padding: const EdgeInsets.all(8),
child: Text(
realm.name,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'realm_list.g.dart';
@riverpod
class RealmListNotifier extends _$RealmListNotifier
with CursorPagingNotifierMixin<SnRealm> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnRealm>> build() {
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnRealm>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/discovery/realms',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final realms = data.map((json) => SnRealm.fromJson(json)).toList();
final hasMore = offset + realms.length < total;
final nextCursor = hasMore ? (offset + realms.length).toString() : null;
return CursorPagingData(
items: realms,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class SliverRealmList extends HookConsumerWidget {
const SliverRealmList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: realmListNotifierProvider,
futureRefreshable: realmListNotifierProvider.future,
notifierRefreshable: realmListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final realm = data.items[index];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: RealmCard(realm: realm),
);
},
),
);
}
}

View File

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'realm_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$realmListNotifierHash() => r'440eb8c61db2059699191b904b6518a0b01ccd25';
/// See also [RealmListNotifier].
@ProviderFor(RealmListNotifier)
final realmListNotifierProvider = AutoDisposeAsyncNotifierProvider<
RealmListNotifier,
CursorPagingData<SnRealm>
>.internal(
RealmListNotifier.new,
name: r'realmListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$realmListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$RealmListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnRealm>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/realm.dart';
import 'package:island/widgets/content/cloud_files.dart';
class RealmTile extends HookConsumerWidget {
final SnRealm realm;
const RealmTile({super.key, required this.realm});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListTile(
leading: ProfilePictureWidget(file: realm.picture),
title: Text(realm.name),
subtitle: Text(realm.description),
onTap: () => context.push('/realms/${realm.slug}'),
);
}
}