👽 Update API to microservices

♻️ Refactor router pushes
This commit is contained in:
2025-07-17 14:35:09 +08:00
parent a7454edec0
commit e6c58b7b63
109 changed files with 9156 additions and 344 deletions

View File

@@ -105,7 +105,7 @@ class AccountProfileCard extends HookConsumerWidget {
FilledButton.tonalIcon(
onPressed: () {
Navigator.pop(context);
context.push('/account/${data.name}');
context.pushNamed('accountProfile', pathParameters: {'name': data.name});
},
icon: const Icon(Symbols.launch),
label: Text('accountProfileView').tr(),

View File

@@ -18,7 +18,7 @@ Future<List<SnAccount>> searchAccounts(Ref ref, {required String query}) async {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get(
'/accounts/search',
'/id/accounts/search',
queryParameters: {'query': query},
);

View File

@@ -16,7 +16,9 @@ part 'account_session_sheet.g.dart';
@riverpod
Future<List<SnAuthDevice>> authDevices(Ref ref) async {
final resp = await ref.watch(apiClientProvider).get('/accounts/me/devices');
final resp = await ref
.watch(apiClientProvider)
.get('/id/accounts/me/devices');
final sessionId = resp.headers.value('x-auth-session');
final data =
resp.data.map<SnAuthDevice>((e) {
@@ -122,7 +124,7 @@ class AccountSessionSheet extends HookConsumerWidget {
if (!confirm || !context.mounted) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.delete('/accounts/me/sessions/$sessionId');
await apiClient.delete('/id/accounts/me/sessions/$sessionId');
ref.invalidate(authDevicesProvider);
} catch (err) {
showErrorAlert(err);

View File

@@ -6,7 +6,7 @@ part of 'account_session_sheet.dart';
// RiverpodGenerator
// **************************************************************************
String _$authDevicesHash() => r'19807110962206a9637075d03cd372233cae2f49';
String _$authDevicesHash() => r'8bc41a1ffc37df8e757c977b4ddae11db8faaeb5';
/// See also [authDevices].
@ProviderFor(authDevices)

View File

@@ -1,11 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/account/event_details_widget.dart';
import 'package:table_calendar/table_calendar.dart';
/// A reusable widget for displaying an event calendar with event details
@@ -123,57 +122,10 @@ class EventCalendarWidget extends HookConsumerWidget {
events.value
?.where((e) => isSameDay(e.date, selectedDay.value))
.firstOrNull;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(DateFormat.EEEE().format(selectedDay.value))
.fontSize(16)
.bold()
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
Text(DateFormat.yMd().format(selectedDay.value))
.fontSize(12)
.textColor(
Theme.of(context).colorScheme.onSecondaryContainer,
),
const Gap(16),
if (event?.checkInResult != null)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${event!.checkInResult!.level}',
).tr().fontSize(16).bold(),
for (final tip in event.checkInResult!.tips)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Icon(
Symbols.circle,
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(tip.title).bold(),
Text(tip.content),
],
),
),
],
).padding(top: 8),
],
),
if (event?.checkInResult == null &&
(event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
return EventDetailsWidget(
selectedDay: selectedDay.value,
event: event,
);
},
),
),

View File

@@ -0,0 +1,63 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/activity.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class EventDetailsWidget extends StatelessWidget {
final DateTime selectedDay;
final SnEventCalendarEntry? event;
const EventDetailsWidget({
super.key,
required this.selectedDay,
required this.event,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(DateFormat.EEEE().format(selectedDay))
.fontSize(16)
.bold()
.textColor(Theme.of(context).colorScheme.onSecondaryContainer),
Text(DateFormat.yMd().format(selectedDay))
.fontSize(12)
.textColor(Theme.of(context).colorScheme.onSecondaryContainer),
const Gap(16),
if (event?.checkInResult != null)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'checkInResultLevel${event!.checkInResult!.level}',
).tr().fontSize(16).bold(),
for (final tip in event!.checkInResult!.tips)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
Icon(
Symbols.circle,
size: 12,
fill: 1,
).padding(top: 4, right: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text(tip.title).bold(), Text(tip.content)],
),
),
],
).padding(top: 8),
],
),
if (event?.checkInResult == null && (event?.statuses.isEmpty ?? true))
Text('eventCalanderEmpty').tr(),
],
).padding(vertical: 24, horizontal: 24);
}
}

View File

@@ -17,7 +17,7 @@ part 'status.g.dart';
Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get('/accounts/$uname/statuses');
final resp = await apiClient.get('/id/accounts/$uname/statuses');
return SnAccountStatus.fromJson(resp.data);
} catch (err) {
if (err is DioException) {

View File

@@ -6,7 +6,7 @@ part of 'status.dart';
// RiverpodGenerator
// **************************************************************************
String _$accountStatusHash() => r'8c3ba5242da1d1e75e3cbf1f2934ff7d5683d0d6';
String _$accountStatusHash() => r'c861a0565d6229fd35666bba7cb2f5c6b7298e46';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -32,7 +32,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
submitting.value = true;
final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/accounts/me/statuses');
await apiClient.delete('/id/accounts/me/statuses');
if (!context.mounted) return;
ref.invalidate(accountStatusProvider(user.value!.name));
Navigator.pop(context);

View File

@@ -16,7 +16,7 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
if (roomId.isEmpty) return null;
try {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/chat/realtime/$roomId');
final resp = await apiClient.get('/sphere/chat/realtime/$roomId');
return SnRealtimeCall.fromJson(resp.data);
} catch (e) {
if (e is DioException && e.response?.statusCode == 404) {
@@ -42,9 +42,9 @@ class AudioCallButton extends HookConsumerWidget {
Future<void> handleJoin() async {
isLoading.value = true;
try {
await apiClient.post('/chat/realtime/$roomId');
await apiClient.post('/sphere/chat/realtime/$roomId');
if (context.mounted) {
context.push('/chat/$roomId/call');
context.pushNamed('chatCall', pathParameters: {'id': roomId});
}
} catch (e) {
showErrorAlert(e);
@@ -56,7 +56,7 @@ class AudioCallButton extends HookConsumerWidget {
Future<void> handleEnd() async {
isLoading.value = true;
try {
await apiClient.delete('/chat/realtime/$roomId');
await apiClient.delete('/sphere/chat/realtime/$roomId');
callNotifier.dispose(); // Clean up call resources
} catch (e) {
showErrorAlert(e);
@@ -96,7 +96,7 @@ class AudioCallButton extends HookConsumerWidget {
tooltip: 'Join Ongoing Call',
onPressed: () {
if (context.mounted) {
context.push('/chat/$roomId/call');
context.pushNamed('chatCall', pathParameters: {'id': roomId});
}
},
);

View File

@@ -6,7 +6,7 @@ part of 'call_button.dart';
// RiverpodGenerator
// **************************************************************************
String _$ongoingCallHash() => r'ab7337bcd4d766897bd6d6a38f418c6bdd15eb94';
String _$ongoingCallHash() => r'48031badb79efa07aefb3a4fc51635be457bd3f9';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -360,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(all: 16),
),
onTap: () {
context.push('/chat/${callNotifier.roomId!}/call');
context.pushNamed('chatCall', pathParameters: {'id': callNotifier.roomId!});
},
);
}

View File

@@ -22,7 +22,7 @@ part 'check_in.g.dart';
Future<SnCheckInResult?> checkInResultToday(Ref ref) async {
final client = ref.watch(apiClientProvider);
try {
final resp = await client.get('/accounts/me/check-in');
final resp = await client.get('/id/accounts/me/check-in');
return SnCheckInResult.fromJson(resp.data);
} catch (err) {
if (err is DioException) {
@@ -45,7 +45,7 @@ class CheckInWidget extends HookConsumerWidget {
final client = ref.read(apiClientProvider);
try {
await client.post(
'/accounts/me/check-in',
'/id/accounts/me/check-in',
data: captchatTk == null ? null : jsonEncode(captchatTk),
);
ref.invalidate(checkInResultTodayProvider);
@@ -136,7 +136,10 @@ class CheckInWidget extends HookConsumerWidget {
if (todayResult.valueOrNull == null) {
checkIn();
} else {
context.push('/account/me/calendar');
context.pushNamed(
'accountCalendar',
pathParameters: {'name': 'me'},
);
}
},
icon: AnimatedSwitcher(

View File

@@ -7,7 +7,7 @@ part of 'check_in.dart';
// **************************************************************************
String _$checkInResultTodayHash() =>
r'0e2af6c1f419b2ee74ee38b6fb5d8071498e75c8';
r'402e3a3be0d441ae12b2370d19d09bf81326933f';
/// See also [checkInResultToday].
@ProviderFor(checkInResultToday)

View File

@@ -80,7 +80,9 @@ class MarkdownTextContent extends HookConsumerWidget {
final url = Uri.tryParse(href);
if (url != null) {
if (url.scheme == 'solian') {
context.push(['', url.host, ...url.pathSegments].join('/'));
if (url.host == 'account') {
context.pushNamed('accountProfile', pathParameters: {'name': url.pathSegments[0]});
}
return;
}
final whitelistDomains = ['solian.app', 'solsynth.dev'];

View File

@@ -72,7 +72,7 @@ class PostItem extends HookConsumerWidget {
children: [
GestureDetector(
onTap: () {
context.push('/publishers/${item.publisher.name}');
context.pushNamed('publisherProfile', pathParameters: {'name': item.publisher.name});
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
@@ -254,7 +254,7 @@ class PostItem extends HookConsumerWidget {
GestureDetector(
child: ProfilePictureWidget(file: item.publisher.picture),
onTap: () {
context.push('/publishers/${item.publisher.name}');
context.pushNamed('publisherProfile', pathParameters: {'name': item.publisher.name});
},
),
Expanded(
@@ -427,7 +427,7 @@ class PostItem extends HookConsumerWidget {
),
onTap: () {
if (isOpenable) {
context.push('/posts/${item.id}');
context.pushNamed('postDetail', pathParameters: {'id': item.id});
}
},
),
@@ -496,7 +496,7 @@ class PostItem extends HookConsumerWidget {
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context.push('/posts/${item.id}/edit').then((value) {
context.pushNamed('postEdit', pathParameters: {'id': item.id}).then((value) {
if (value != null) {
onRefresh?.call();
}
@@ -515,7 +515,7 @@ class PostItem extends HookConsumerWidget {
if (confirm) {
final client = ref.watch(apiClientProvider);
client
.delete('/posts/${item.id}')
.delete('/sphere/posts/${item.id}')
.catchError((err) {
showErrorAlert(err);
return err;
@@ -541,8 +541,8 @@ class PostItem extends HookConsumerWidget {
title: 'reply'.tr(),
image: MenuImage.icon(Symbols.reply),
callback: () {
context.push(
'/posts/compose',
context.pushNamed(
'postCompose',
extra: PostComposeInitialState(replyingTo: item),
);
},
@@ -551,8 +551,8 @@ class PostItem extends HookConsumerWidget {
title: 'forward'.tr(),
image: MenuImage.icon(Symbols.forward),
callback: () {
context.push(
'/posts/compose',
context.pushNamed(
'postCompose',
extra: PostComposeInitialState(forwardingTo: item),
);
},
@@ -732,7 +732,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
),
],
),
).gestures(onTap: () => context.push('/posts/${referencePost.id}'));
).gestures(onTap: () => context.pushNamed('postDetail', pathParameters: {'id': referencePost.id}));
}
class PostReactionList extends HookConsumerWidget {

View File

@@ -45,7 +45,7 @@ class PostItemCreator extends HookConsumerWidget {
title: 'edit'.tr(),
image: MenuImage.icon(Symbols.edit),
callback: () {
context.push('/posts/${item.id}/edit').then((value) {
context.pushNamed('postEdit', pathParameters: {'id': item.id}).then((value) {
if (value != null) {
onRefresh?.call();
}
@@ -61,7 +61,7 @@ class PostItemCreator extends HookConsumerWidget {
if (confirm) {
final client = ref.watch(apiClientProvider);
client
.delete('/posts/${item.id}')
.delete('/sphere/posts/${item.id}')
.catchError((err) {
showErrorAlert(err);
return err;
@@ -80,7 +80,7 @@ class PostItemCreator extends HookConsumerWidget {
image: MenuImage.icon(Symbols.link),
callback: () {
// Copy post link to clipboard
context.push('/posts/${item.id}');
context.pushNamed('postDetail', pathParameters: {'id': item.id});
},
),
],
@@ -94,7 +94,7 @@ class PostItemCreator extends HookConsumerWidget {
borderRadius: BorderRadius.circular(12),
onTap: () {
if (isOpenable) {
context.push('/posts/${item.id}');
context.pushNamed('postDetail', pathParameters: {'id': item.id});
}
},
child: Padding(

View File

@@ -30,7 +30,10 @@ class PostListNotifier extends _$PostListNotifier
if (pubName != null) 'pub': pubName,
};
final response = await client.get('/posts', queryParameters: queryParams);
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final posts = data.map((json) => SnPost.fromJson(json)).toList();

View File

@@ -6,7 +6,7 @@ part of 'post_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$postListNotifierHash() => r'a2a273cbf96393a84a66bd6ae8e88058704f3195';
String _$postListNotifierHash() => r'2e4fb36123d3f97ac1edf9945043251d4eb519a2';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -43,8 +43,7 @@ class PublisherModal extends HookConsumerWidget {
const Gap(12),
ElevatedButton(
onPressed: () {
context.push('/creators/publishers/new')
.then((value) {
context.pushNamed('creatorNew').then((value) {
if (value != null) {
ref.invalidate(
publishersManagedProvider,

View File

@@ -29,7 +29,7 @@ class PublisherCard extends ConsumerWidget {
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.push('/publishers/${publisher.name}');
context.pushNamed('publisherProfile', pathParameters: {'name': publisher.name});
},
child: AspectRatio(
aspectRatio: 16 / 7,

View File

@@ -31,7 +31,7 @@ class RealmCard extends ConsumerWidget {
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
context.push('/realms/${realm.slug}');
context.pushNamed('realmDetail', pathParameters: {'slug': realm.slug});
},
child: AspectRatio(
aspectRatio: 16 / 7,

View File

@@ -14,7 +14,7 @@ class RealmTile extends HookConsumerWidget {
leading: ProfilePictureWidget(file: realm.picture),
title: Text(realm.name),
subtitle: Text(realm.description),
onTap: () => context.push('/realms/${realm.slug}'),
onTap: () => context.pushNamed('realmDetail', pathParameters: {'slug': realm.slug}),
);
}
}

View File

@@ -178,7 +178,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Navigate to compose screen
if (mounted) {
context.push('/posts/compose', extra: initialState);
context.pushNamed('postCompose', extra: initialState);
Navigator.of(context).pop(); // Close the share sheet
}
} catch (e) {
@@ -323,7 +323,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
// Navigate to chat if requested
if (shouldNavigate == true && mounted) {
context.push('/chat/${chatRoom.id}');
context.push('/sphere/chat/${chatRoom.id}');
}
}
} catch (e) {

View File

@@ -17,7 +17,7 @@ class WebArticleCard extends StatelessWidget {
});
void _onTap(BuildContext context) {
context.push('/feeds/articles/${article.id}');
context.pushNamed('articleDetail', pathParameters: {'id': article.id});
}
@override