WebSocket connection indicator

This commit is contained in:
2025-05-05 20:59:52 +08:00
parent e4e562918c
commit f266968644
13 changed files with 580 additions and 212 deletions

View File

@ -0,0 +1,93 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'profile.g.dart';
@riverpod
Future<SnAccount> account(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get("/accounts/$uname");
return SnAccount.fromJson(resp.data);
}
@RoutePage()
class AccountProfileScreen extends HookConsumerWidget {
final String name;
const AccountProfileScreen({
super.key,
@PathParam("name") required this.name,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final accountAsync = ref.watch(accountProvider(name));
return accountAsync.when(
data:
(data) => AppScaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 180,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
background:
data.profile.backgroundId != null
? CloudImageWidget(
fileId: data.profile.backgroundId!,
)
: Container(
color:
Theme.of(context).appBarTheme.backgroundColor,
),
title: Text(
data.name,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.profile.bio ?? '',
style: const TextStyle(fontSize: 16),
),
],
),
),
),
],
),
),
error:
(error, stackTrace) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: Text(error.toString())),
),
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: CircularProgressIndicator()),
),
);
}
}

View File

@ -0,0 +1,149 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'profile.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$accountHash() => r'39003ef3250181b9290e0562329c7801d4841941';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [account].
@ProviderFor(account)
const accountProvider = AccountFamily();
/// See also [account].
class AccountFamily extends Family<AsyncValue<SnAccount>> {
/// See also [account].
const AccountFamily();
/// See also [account].
AccountProvider call(String uname) {
return AccountProvider(uname);
}
@override
AccountProvider getProviderOverride(covariant AccountProvider provider) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'accountProvider';
}
/// See also [account].
class AccountProvider extends AutoDisposeFutureProvider<SnAccount> {
/// See also [account].
AccountProvider(String uname)
: this._internal(
(ref) => account(ref as AccountRef, uname),
from: accountProvider,
name: r'accountProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountHash,
dependencies: AccountFamily._dependencies,
allTransitiveDependencies: AccountFamily._allTransitiveDependencies,
uname: uname,
);
AccountProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<SnAccount> Function(AccountRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountProvider._internal(
(ref) => create(ref as AccountRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnAccount> createElement() {
return _AccountProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountRef on AutoDisposeFutureProviderRef<SnAccount> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountProviderElement
extends AutoDisposeFutureProviderElement<SnAccount>
with AccountRef {
_AccountProviderElement(super.provider);
@override
String get uname => (origin as AccountProvider).uname;
}
// 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

@ -10,7 +10,6 @@ import 'package:island/route.gr.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher_string.dart';

View File

@ -18,7 +18,6 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';

View File

@ -15,7 +15,6 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:styled_widget/styled_widget.dart';
part 'room_detail.freezed.dart';
@ -42,7 +41,7 @@ class ChatDetailScreen extends HookConsumerWidget {
offset: Offset(1.0, 1.0),
);
return Scaffold(
return AppScaffold(
body: roomState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),

View File

@ -85,27 +85,34 @@ class _PostListController extends StateNotifier<List<SnPost>> {
if (isLoading || hasReachedMax) return;
isLoading = true;
final response = await _dio.get(
'/posts',
queryParameters: {'offset': offset, 'take': take},
);
try {
final response = await _dio.get(
'/posts',
queryParameters: {'offset': offset, 'take': take},
);
final List<SnPost> fetched =
(response.data as List)
.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList();
final List<SnPost> fetched =
(response.data as List)
.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
.toList();
final headerTotal = int.tryParse(response.headers['x-total']?.first ?? '');
if (headerTotal != null) total = headerTotal;
final headerTotal = int.tryParse(response.headers['x-total']?.first ?? '');
if (headerTotal != null) total = headerTotal;
state = [...state, ...fetched];
offset += fetched.length;
if (state.length >= total) hasReachedMax = true;
if (!mounted) return; // Check if the notifier is still mounted
isLoading = false;
state = [...state, ...fetched];
offset += fetched.length;
if (state.length >= total) hasReachedMax = true;
} finally {
if (mounted) {
isLoading = false;
}
}
}
void updateOne(int index, SnPost post) {
if (!mounted) return; // Check if the notifier is still mounted
final updatedPosts = [...state];
updatedPosts[index] = post;
state = updatedPosts;

View File

@ -23,7 +23,6 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:markdown_editor_plus/widgets/markdown_auto_preview.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:styled_widget/styled_widget.dart';
@RoutePage()

View File

@ -14,7 +14,6 @@ import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';