Pin code

🐛 Bug fixes
This commit is contained in:
2025-06-22 00:38:51 +08:00
parent f73cf10a54
commit 006841cf82
15 changed files with 401 additions and 120 deletions

View File

@ -66,10 +66,10 @@ class AccountScreen extends HookConsumerWidget {
}
return AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation
noBackground: isWide,
appBar: AppBar(title: const Text('account').tr()),
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
body: SingleChildScrollView(
padding: getTabbedPadding(context),
child: Column(
children: <Widget>[
Card(
@ -144,7 +144,7 @@ class AccountScreen extends HookConsumerWidget {
level: user.value!.profile.level,
experience: user.value!.profile.experience,
progress: user.value!.profile.levelingProgress,
).padding(horizontal: 8),
).padding(horizontal: 12),
Row(
children: [
Expanded(

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -138,13 +139,13 @@ class AuthFactorSheet extends HookConsumerWidget {
children: [
if (factor.enabledAt == null)
Badge(
label: Text('authFactorDisabled'.tr()),
label: Text('authFactorDisabled').tr(),
textColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
)
else
Badge(
label: Text('authFactorEnabled'.tr()),
label: Text('authFactorEnabled').tr(),
textColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
),
@ -217,6 +218,8 @@ class AuthFactorNewSheet extends HookConsumerWidget {
}
}
final width = math.min(400, MediaQuery.of(context).size.width);
return SheetScaffold(
titleText: 'authFactorNew'.tr(),
child: Column(
@ -248,7 +251,7 @@ class AuthFactorNewSheet extends HookConsumerWidget {
}
},
),
if (factorType.value == 0)
if ([0].contains(factorType.value))
TextField(
controller: secretController,
decoration: InputDecoration(
@ -259,6 +262,20 @@ class AuthFactorNewSheet extends HookConsumerWidget {
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
)
else if ([4].contains(factorType.value))
OtpTextField(
showCursor: false,
numberOfFields: 6,
obscureText: false,
showFieldAsBox: true,
focusedBorderColor: Theme.of(context).colorScheme.primary,
fieldWidth: (width / 6) - 10,
keyboardType: TextInputType.number,
onSubmit: (String verificationCode) {
secretController.text = verificationCode;
},
textStyle: Theme.of(context).textTheme.titleLarge!,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),

View File

@ -40,6 +40,7 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
Symbols.notifications_active,
),
3: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
};
@RoutePage()

View File

@ -368,10 +368,10 @@ class ChatListScreen extends HookConsumerWidget {
ref.invalidate(chatroomsJoinedProvider);
}),
child: ListView.builder(
padding:
callState.isConnected
? EdgeInsets.only(bottom: 96)
: EdgeInsets.zero,
padding: getTabbedPadding(
context,
bottom: callState.isConnected ? 96 : null,
),
itemCount:
items
.where(

View File

@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
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';
@ -46,10 +47,8 @@ class ExploreShellScreen extends ConsumerWidget {
}
}
@RoutePage()
class ExploreScreen extends ConsumerWidget {
class ExploreScreen extends HookConsumerWidget {
final bool isAside;
const ExploreScreen({super.key, this.isAside = false});
@ -60,12 +59,67 @@ class ExploreScreen extends ConsumerWidget {
return const EmptyPageHolder();
}
final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier);
final tabController = useTabController(initialLength: 3);
final currentFilter = useState<String?>(null);
useEffect(() {
void listener() {
switch (tabController.index) {
case 0:
currentFilter.value = null;
break;
case 1:
currentFilter.value = 'subscriptions';
break;
case 2:
currentFilter.value = 'friends';
break;
}
}
tabController.addListener(listener);
return () => tabController.removeListener(listener);
}, [tabController]);
final activitiesNotifier = ref.watch(
activityListNotifierProvider(currentFilter.value).notifier,
);
return TourTriggerWidget(
child: AppScaffold(
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(title: const Text('explore').tr()),
appBar: AppBar(
toolbarHeight: 0,
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
heroTag: Key("explore-page-fab"),
onPressed: () {
@ -78,32 +132,49 @@ class ExploreScreen extends ConsumerWidget {
child: const Icon(Symbols.edit),
),
floatingActionButtonLocation: TabbedFabLocation(context),
body: RefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: PagingHelperView(
provider: activityListNotifierProvider,
futureRefreshable: activityListNotifierProvider.future,
notifierRefreshable: activityListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => Center(
child: _ActivityListView(
data: data,
widgetCount: widgetCount,
endItemView: endItemView,
activitiesNotifier: activitiesNotifier,
),
),
),
body: TabBarView(
controller: tabController,
children: [
_buildActivityList(ref, null),
_buildActivityList(ref, 'subscriptions'),
_buildActivityList(ref, 'friends'),
],
),
),
);
}
Widget _buildActivityList(WidgetRef ref, String? filter) {
final activitiesNotifier = ref.watch(
activityListNotifierProvider(filter).notifier,
);
return RefreshIndicator(
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
child: PagingHelperView(
provider: activityListNotifierProvider(filter),
futureRefreshable: activityListNotifierProvider(filter).future,
notifierRefreshable: activityListNotifierProvider(filter).notifier,
contentBuilder:
(data, widgetCount, endItemView) => Center(
child: _ActivityListView(
data: data,
widgetCount: widgetCount,
endItemView: endItemView,
activitiesNotifier: activitiesNotifier,
contentOnly: filter != null,
),
),
),
);
}
}
class _ActivityListView extends HookConsumerWidget {
final CursorPagingData<SnActivity> data;
final int widgetCount;
final Widget endItemView;
final bool contentOnly;
final ActivityListNotifier activitiesNotifier;
const _ActivityListView({
@ -111,6 +182,7 @@ class _ActivityListView extends HookConsumerWidget {
required this.widgetCount,
required this.endItemView,
required this.activitiesNotifier,
this.contentOnly = false,
});
@override
@ -119,7 +191,8 @@ class _ActivityListView extends HookConsumerWidget {
return CustomScrollView(
slivers: [
if (user.hasValue) SliverToBoxAdapter(child: CheckInWidget()),
if (user.hasValue && !contentOnly)
SliverToBoxAdapter(child: CheckInWidget()),
SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
@ -178,7 +251,7 @@ class _ActivityListView extends HookConsumerWidget {
return Column(children: [itemWidget, const Divider(height: 1)]);
},
),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
SliverGap(getTabbedPadding(context).bottom),
],
);
}
@ -188,16 +261,23 @@ class _ActivityListView extends HookConsumerWidget {
class ActivityListNotifier extends _$ActivityListNotifier
with CursorPagingNotifierMixin<SnActivity> {
@override
Future<CursorPagingData<SnActivity>> build() => fetch(cursor: null);
Future<CursorPagingData<SnActivity>> build(String? filter) =>
fetch(cursor: null);
@override
Future<CursorPagingData<SnActivity>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider);
final take = 20;
final queryParameters = {
if (cursor != null) 'cursor': cursor,
'take': take,
if (filter != null) 'filter': filter,
};
final response = await client.get(
'/activities',
queryParameters: {if (cursor != null) 'cursor': cursor, 'take': take},
queryParameters: queryParameters,
);
final List<SnActivity> items =

View File

@ -7,25 +7,174 @@ part of 'explore.dart';
// **************************************************************************
String _$activityListNotifierHash() =>
r'c9683035f7a66a2f331689e274642b60064fbb2e';
r'14ec2f211c86e1e64a9a34b142d0e8f78ff6361a';
/// 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));
}
}
abstract class _$ActivityListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnActivity>> {
late final String? filter;
FutureOr<CursorPagingData<SnActivity>> build(String? filter);
}
/// See also [ActivityListNotifier].
@ProviderFor(ActivityListNotifier)
final activityListNotifierProvider = AutoDisposeAsyncNotifierProvider<
ActivityListNotifier,
CursorPagingData<SnActivity>
>.internal(
ActivityListNotifier.new,
name: r'activityListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$activityListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
const activityListNotifierProvider = ActivityListNotifierFamily();
/// See also [ActivityListNotifier].
class ActivityListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnActivity>>> {
/// See also [ActivityListNotifier].
const ActivityListNotifierFamily();
/// See also [ActivityListNotifier].
ActivityListNotifierProvider call(String? filter) {
return ActivityListNotifierProvider(filter);
}
@override
ActivityListNotifierProvider getProviderOverride(
covariant ActivityListNotifierProvider provider,
) {
return call(provider.filter);
}
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'activityListNotifierProvider';
}
/// See also [ActivityListNotifier].
class ActivityListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ActivityListNotifier,
CursorPagingData<SnActivity>
> {
/// See also [ActivityListNotifier].
ActivityListNotifierProvider(String? filter)
: this._internal(
() => ActivityListNotifier()..filter = filter,
from: activityListNotifierProvider,
name: r'activityListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$activityListNotifierHash,
dependencies: ActivityListNotifierFamily._dependencies,
allTransitiveDependencies:
ActivityListNotifierFamily._allTransitiveDependencies,
filter: filter,
);
ActivityListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.filter,
}) : super.internal();
final String? filter;
@override
FutureOr<CursorPagingData<SnActivity>> runNotifierBuild(
covariant ActivityListNotifier notifier,
) {
return notifier.build(filter);
}
@override
Override overrideWith(ActivityListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ActivityListNotifierProvider._internal(
() => create()..filter = filter,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
filter: filter,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ActivityListNotifier,
CursorPagingData<SnActivity>
>
createElement() {
return _ActivityListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ActivityListNotifierProvider && other.filter == filter;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, filter.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ActivityListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnActivity>> {
/// The parameter `filter` of this provider.
String? get filter;
}
class _ActivityListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ActivityListNotifier,
CursorPagingData<SnActivity>
>
with ActivityListNotifierRef {
_ActivityListNotifierProviderElement(super.provider);
@override
String? get filter => (origin as ActivityListNotifierProvider).filter;
}
typedef _$ActivityListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnActivity>>;
// 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

@ -161,48 +161,34 @@ class PublisherProfileScreen extends HookConsumerWidget {
),
],
),
actions: [
subStatus.when(
data:
(status) => IconButton(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Icons.remove_circle
: Icons.add_circle,
shadows: [appbarShadow],
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
width: 48,
height: 48,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
),
const Gap(8),
],
),
SliverToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
ProfilePictureWidget(file: data.picture, radius: 32),
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor:
Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(
file: data.picture,
radius: 32,
),
),
onTap: () {
Navigator.pop(context, true);
context.router.pushPath('/account/${data.name}');
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@ -242,19 +228,49 @@ class PublisherProfileScreen extends HookConsumerWidget {
uname: data.account!.name,
padding: EdgeInsets.zero,
),
OutlinedButton.icon(
onPressed: () {
Navigator.pop(context);
context.router.pushPath(
'/account/${data.name}',
);
},
icon: const Icon(Symbols.launch),
label: Text('accountProfileView').tr(),
style: ButtonStyle(
visualDensity: VisualDensity(vertical: -2),
),
).padding(top: 8),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(
vertical: -2,
),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
)
.padding(top: 8),
],
),
),

View File

@ -13,6 +13,7 @@ import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/route.gr.dart';
import 'package:island/services/file.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
@ -93,9 +94,7 @@ class RealmListScreen extends HookConsumerWidget {
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom,
),
padding: getTabbedPadding(context),
itemCount: value.length,
itemBuilder: (context, item) {
return ListTile(

View File

@ -2,7 +2,6 @@ import 'dart:ui';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/route.gr.dart';
import 'package:island/screens/notification.dart';
@ -55,12 +54,6 @@ class TabsScreen extends HookConsumerWidget {
builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context);
// Check if current route is a tab route
final currentRoute = context.router.topRoute;
final isTabRoute = routes.any(
(route) => route.routeName == currentRoute.name,
);
return Stack(
children: [
Positioned.fill(child: child),