Merge pull request #279 from cogwheel0/refactor-notes-platform-controls

refactor(notes): Improve code formatting and replace platform-specific refresh controls
This commit is contained in:
cogwheel
2025-12-15 18:47:18 +05:30
committed by GitHub
4 changed files with 93 additions and 35 deletions

View File

@@ -2030,6 +2030,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Messages Area fills entire space with pull-to-refresh
Positioned.fill(
child: ConduitRefreshIndicator(
// Position indicator below the floating app bar
edgeOffset:
MediaQuery.of(context).padding.top +
kToolbarHeight,
onRefresh: () async {
// Reload active conversation messages from server
final api = ref.read(apiServiceProvider);

View File

@@ -13,6 +13,7 @@ import '../../../shared/theme/theme_extensions.dart';
import '../../chat/providers/chat_providers.dart' as chat;
import '../../../core/utils/debug_logger.dart';
import '../../../core/services/navigation_service.dart';
import '../../../shared/widgets/loading_states.dart';
import '../../../shared/widgets/themed_dialogs.dart';
import 'package:conduit/l10n/app_localizations.dart';
import '../../../core/utils/user_display_name.dart';
@@ -112,19 +113,6 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
// Legacy helper removed: drawer now uses slivers with lazy delegates.
Widget _buildRefreshableScrollableSlivers({required List<Widget> slivers}) {
if (Platform.isIOS) {
final scroll = CustomScrollView(
key: const PageStorageKey<String>('chats_drawer_scroll'),
controller: _listController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
CupertinoSliverRefreshControl(onRefresh: _refreshChats),
...slivers,
],
);
return CupertinoScrollbar(controller: _listController, child: scroll);
}
final scroll = CustomScrollView(
key: const PageStorageKey<String>('chats_drawer_scroll'),
controller: _listController,
@@ -132,10 +120,20 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
cacheExtent: 800,
slivers: slivers,
);
return RefreshIndicator(
final refreshableScroll = ConduitRefreshIndicator(
onRefresh: _refreshChats,
child: Scrollbar(controller: _listController, child: scroll),
child: scroll,
);
if (Platform.isIOS) {
return CupertinoScrollbar(
controller: _listController,
child: refreshableScroll,
);
}
return Scrollbar(controller: _listController, child: refreshableScroll);
}
@override

View File

@@ -16,6 +16,7 @@ import '../../../core/widgets/error_boundary.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/utils/ui_utils.dart';
import '../../../shared/widgets/improved_loading_states.dart';
import '../../../shared/widgets/loading_states.dart';
import '../../../shared/widgets/themed_dialogs.dart';
import '../../../shared/widgets/middle_ellipsis_text.dart';
import '../../../shared/utils/conversation_context_menu.dart';
@@ -346,18 +347,7 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
}
Widget _buildRefreshableScrollView(List<Widget> slivers) {
if (Platform.isIOS) {
return CustomScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
CupertinoSliverRefreshControl(onRefresh: _refreshNotes),
...slivers,
],
);
}
return RefreshIndicator(
return ConduitRefreshIndicator(
onRefresh: _refreshNotes,
child: CustomScrollView(
controller: _scrollController,
@@ -551,7 +541,9 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
Text(
preview,
style: AppTypography.bodySmallStyle.copyWith(
color: sidebarTheme.foreground.withValues(alpha: 0.6),
color: sidebarTheme.foreground.withValues(
alpha: 0.6,
),
height: 1.4,
),
maxLines: 2,
@@ -566,23 +558,30 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
Platform.isIOS
? CupertinoIcons.clock
: Icons.schedule_rounded,
color: sidebarTheme.foreground.withValues(alpha: 0.4),
color: sidebarTheme.foreground.withValues(
alpha: 0.4,
),
size: 12,
),
const SizedBox(width: 4),
Text(
timeText,
style: AppTypography.tiny.copyWith(
color: sidebarTheme.foreground.withValues(alpha: 0.5),
color: sidebarTheme.foreground.withValues(
alpha: 0.5,
),
fontWeight: FontWeight.w500,
),
),
if (note.user != null && note.user!.name != null) ...[
if (note.user != null &&
note.user!.name != null) ...[
const SizedBox(width: Spacing.sm),
Text(
'·',
style: AppTypography.tiny.copyWith(
color: sidebarTheme.foreground.withValues(alpha: 0.3),
color: sidebarTheme.foreground.withValues(
alpha: 0.3,
),
),
),
const SizedBox(width: Spacing.sm),
@@ -590,7 +589,9 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
child: Text(
note.user!.name!,
style: AppTypography.tiny.copyWith(
color: sidebarTheme.foreground.withValues(alpha: 0.5),
color: sidebarTheme.foreground.withValues(
alpha: 0.5,
),
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
@@ -618,7 +619,8 @@ class _NotesListPageState extends ConsumerState<NotesListPage> {
minWidth: TouchTarget.badge,
minHeight: TouchTarget.badge,
),
onPressed: () => _showNoteContextMenu(buttonContext, note),
onPressed: () =>
_showNoteContextMenu(buttonContext, note),
),
),
],

View File

@@ -433,23 +433,77 @@ class LoadingButton extends StatelessWidget {
}
}
/// Refresh indicator with Conduit styling
/// Refresh indicator with Conduit styling.
///
/// Uses platform-appropriate refresh controls:
/// - iOS: Native Cupertino-style refresh control (when child is CustomScrollView)
/// - Android/Other: Material RefreshIndicator
///
/// Set [edgeOffset] to position the indicator below an app bar or other
/// overlay. For example, use `MediaQuery.of(context).padding.top + kToolbarHeight`
/// to position below a transparent/floating app bar.
///
/// Note: On iOS with a CustomScrollView child, [edgeOffset] is ignored since
/// CupertinoSliverRefreshControl naturally positions itself based on scroll
/// content. The scroll view's existing padding should handle app bar clearance.
class ConduitRefreshIndicator extends StatelessWidget {
final Widget child;
final Future<void> Function() onRefresh;
/// The distance from the top of the scroll view where the refresh indicator
/// will appear. Useful for positioning below a floating/transparent app bar.
///
/// Note: This is only effective on Android/non-iOS platforms, or on iOS when
/// the child is not a CustomScrollView. For iOS with CustomScrollView, the
/// refresh control naturally positions based on scroll content.
final double edgeOffset;
const ConduitRefreshIndicator({
super.key,
required this.child,
required this.onRefresh,
this.edgeOffset = 0.0,
});
@override
Widget build(BuildContext context) {
// On iOS, try to use CupertinoSliverRefreshControl for native feel
// when the child is directly a CustomScrollView
if (Platform.isIOS && child is CustomScrollView) {
final csv = child as CustomScrollView;
return CustomScrollView(
key: csv.key,
controller: csv.controller,
scrollDirection: csv.scrollDirection,
reverse: csv.reverse,
primary: csv.primary,
physics: csv.physics,
shrinkWrap: csv.shrinkWrap,
cacheExtent: csv.cacheExtent,
keyboardDismissBehavior: csv.keyboardDismissBehavior,
clipBehavior: csv.clipBehavior,
center: csv.center,
anchor: csv.anchor,
semanticChildCount: csv.semanticChildCount,
dragStartBehavior: csv.dragStartBehavior,
restorationId: csv.restorationId,
slivers: [
// CupertinoSliverRefreshControl naturally positions itself based on
// scroll content; the scroll view's existing padding handles app bar
// clearance, so no edgeOffset adjustment is needed here.
CupertinoSliverRefreshControl(onRefresh: onRefresh),
...csv.slivers,
],
);
}
// For Android, other platforms, or when child is not a CustomScrollView,
// use Material RefreshIndicator which works with any scrollable
return RefreshIndicator(
onRefresh: onRefresh,
color: context.conduitTheme.buttonPrimary,
backgroundColor: context.conduitTheme.surfaceBackground,
edgeOffset: edgeOffset,
child: child,
);
}