Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions lib/components/layout/device_bottom_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,30 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';

import '../../core/theme/color_tokens.dart';
import '../../core/utils/smooth_scroll_controller.dart';
import '../../models/device_info.dart';
import '../../server/providers/server_providers.dart';

/// Bottom bar showing connected devices with glow animation on selected item.
class DeviceBottomBar extends ConsumerWidget {
class DeviceBottomBar extends ConsumerStatefulWidget {
const DeviceBottomBar({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<DeviceBottomBar> createState() => _DeviceBottomBarState();
}

class _DeviceBottomBarState extends ConsumerState<DeviceBottomBar> {
final _scrollController = SmoothScrollController();

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Comment on lines +18 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Flutter's default ScrollBehavior route unmodified vertical mouse wheel scrollDelta to horizontal-only ListViews, or does it require Shift by default?

💡 Result:

Yes, Flutter's default ScrollBehavior requires the Shift key to be held down when using a vertical mouse wheel to scroll a horizontal ListView [1][2]. This behavior is governed by the pointerAxisModifiers property in the ScrollBehavior class, which defaults to including LogicalKeyboardKey.shiftLeft and LogicalKeyboardKey.shiftRight [1][3]. When these keys are pressed in combination with a mouse scroll event, Flutter flips the scroll axes, allowing vertical wheel input to move a horizontal ScrollView [1][3]. This is considered the standard desktop convention for horizontal scrolling [2]. If you wish to modify this behavior, you can override the ScrollBehavior in your app (e.g., via ScrollConfiguration or within your MaterialApp/CupertinoApp) to customize the pointerAxisModifiers set [1][4][5]. However, simply clearing the set does not automatically cause a horizontal list to scroll with a vertical wheel; it effectively disables the axis-flipping mechanism [4][6]. Top results: [1], [2], [5]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and nearby scroll-related code.
git ls-files 'lib/components/layout/device_bottom_bar.dart' 'lib/**' | sed -n '1,120p'

echo
echo "== device_bottom_bar.dart =="
wc -l lib/components/layout/device_bottom_bar.dart
cat -n lib/components/layout/device_bottom_bar.dart | sed -n '1,220p'

echo
echo "== Search for SmoothScrollController usage =="
rg -n "SmoothScrollController|pointerScroll|ScrollBehavior|ScrollConfiguration|ListView\.separated|Axis\.horizontal" lib -S

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 17929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the smooth-scrolling controller comes from a package or local helper, inspect its API shape.
rg -n "class SmoothScrollController|extends ScrollController|pointerScroll" . -S

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 450


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l lib/core/utils/smooth_scroll_controller.dart
cat -n lib/core/utils/smooth_scroll_controller.dart | sed -n '1,220p'

Repository: ridelinktechs/devconnect-manage-kit

Length of output: 4391


Horizontal wheel scrolling still needs a behavior override lib/components/layout/device_bottom_bar.dart:80-82 — Flutter keeps horizontal ListView wheel scrolling on Shift+wheel by default, so SmoothScrollController won’t change plain mouse-wheel input here. Add a ScrollBehavior/ScrollConfiguration if desktop wheel scrolling is expected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/components/layout/device_bottom_bar.dart` around lines 18 - 25, The
horizontal `ListView` in `DeviceBottomBar` still needs an explicit desktop
scroll behavior override because `SmoothScrollController` does not change the
default Shift+wheel handling. Update the `build` path for
`DeviceBottomBar`/`_DeviceBottomBarState` to wrap the horizontal list with a
`ScrollConfiguration` or custom `ScrollBehavior` so mouse-wheel input is handled
as intended on desktop, while keeping the existing `_scrollController` and
disposal logic intact.


@override
Widget build(BuildContext context) {
final ref = this.ref;
final devices = ref.watch(connectedDevicesProvider);
final selectedId = ref.watch(selectedDeviceProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
Expand Down Expand Up @@ -63,6 +78,7 @@ class DeviceBottomBar extends ConsumerWidget {
// Device chips
Expanded(
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 2),
itemCount: devices.length,
Expand Down
24 changes: 21 additions & 3 deletions lib/components/layout/sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../core/providers/tab_visibility_provider.dart';
import '../../core/theme/color_tokens.dart';
import '../../core/theme/theme_provider.dart';
import '../../core/utils/smooth_scroll_controller.dart';
import '../../server/providers/server_providers.dart';

class SidebarItem {
Expand Down Expand Up @@ -82,7 +83,7 @@ final sidebarItems = [
),
];

class Sidebar extends ConsumerWidget {
class Sidebar extends ConsumerStatefulWidget {
final int selectedIndex;
final ValueChanged<int> onItemSelected;

Expand All @@ -93,7 +94,21 @@ class Sidebar extends ConsumerWidget {
});

@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<Sidebar> createState() => _SidebarState();
}

class _SidebarState extends ConsumerState<Sidebar> {
final _scrollController = SmoothScrollController();

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final ref = this.ref;
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final devices = ref.watch(connectedDevicesProvider);
Expand All @@ -107,6 +122,8 @@ class Sidebar extends ConsumerWidget {
);
}

final selectedIndex = widget.selectedIndex;

return Container(
width: 68,
decoration: BoxDecoration(
Expand Down Expand Up @@ -158,6 +175,7 @@ class Sidebar extends ConsumerWidget {
child: Builder(builder: (context) {
final enabledTabs = ref.watch(tabVisibilityProvider);
return ListView.builder(
controller: _scrollController,
itemCount: sidebarItems.length,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) {
Expand All @@ -170,7 +188,7 @@ class Sidebar extends ConsumerWidget {
label: item.label,
isSelected: isSelected,
isLocked: isLocked,
onTap: () => onItemSelected(index),
onTap: () => widget.onItemSelected(index),
);
},
);
Expand Down
10 changes: 9 additions & 1 deletion lib/components/text/text_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,20 @@ class _SelectableEllipsis extends StatelessWidget {
painter.dispose();

if (!didOverflow) {
// Text fits within [maxLines] — we pass `maxLines: null` here
// intentionally. Flutter's SelectableText (backed by a multiline
// TextField) reserves vertical height for every line slot up to the
// maxLines limit even when the content is short, which creates
// unwanted whitespace in compact rows (e.g. network header values).
// Since the TextPainter check above already confirmed the text does
// NOT exceed the constraint, clamping is unnecessary and null
// lets the widget collapse to its natural single-line height.
return SelectableText(data,
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
maxLines: maxLines,
maxLines: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Hardcoding maxLines: null inside _SelectableEllipsis disables the ellipsis truncation behavior globally and makes the maxLines parameter of this widget dead code. Any caller passing a specific maxLines constraint will have it silently ignored, which can cause text to wrap unexpectedly and break layouts. If the goal was to prevent vertical space reservation for short values, this should be handled conditionally or at the caller level rather than hardcoding null here.

Suggested change
maxLines: null,
maxLines: maxLines,

textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
semanticsLabel: semanticsLabel,
Expand Down
31 changes: 28 additions & 3 deletions lib/components/viewers/json_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import '../../core/theme/color_tokens.dart';
import '../../core/utils/code_generator.dart';
import '../../core/utils/toast_utils.dart';
import '../../core/utils/code_highlighter.dart';
import '../../core/utils/smooth_scroll_controller.dart';

class JsonViewer extends StatelessWidget {
class JsonViewer extends StatefulWidget {
final dynamic data;
final bool initiallyExpanded;

Expand All @@ -23,16 +24,31 @@ class JsonViewer extends StatelessWidget {
this.initiallyExpanded = true,
});

@override
State<JsonViewer> createState() => _JsonViewerState();
}

class _JsonViewerState extends State<JsonViewer> {
final _scrollController = SmoothScrollController();

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

String _formatAll() {
try {
return const JsonEncoder.withIndent(' ').convert(data);
return const JsonEncoder.withIndent(' ').convert(widget.data);
} catch (_) {
return data?.toString() ?? 'null';
return widget.data?.toString() ?? 'null';
}
}

@override
Widget build(BuildContext context) {
final data = widget.data;
final initiallyExpanded = widget.initiallyExpanded;
if (data == null) {
return Text('null', style: Theme.of(context).textTheme.labelMedium);
}
Expand All @@ -56,6 +72,7 @@ class JsonViewer extends StatelessWidget {
const SizedBox(height: 6),
SelectionArea(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand Down Expand Up @@ -410,6 +427,13 @@ class _JsonPrettyViewerState extends State<JsonPrettyViewer> {
bool? _lastIsDark;
/// Per-line TextSpan cache — built lazily per visible line.
final Map<int, List<TextSpan>> _lineSpanCache = {};
final _scrollController = SmoothScrollController();

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

@override
void initState() {
Expand Down Expand Up @@ -605,6 +629,7 @@ class _JsonPrettyViewerState extends State<JsonPrettyViewer> {
builder: (context, constraints) {
final bounded = constraints.maxHeight.isFinite;
return ListView.builder(
controller: _scrollController,
itemCount: lineCount,
shrinkWrap: !bounded,
physics: bounded
Expand Down
16 changes: 16 additions & 0 deletions lib/core/theme/theme_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,22 @@ final smoothScrollEnabledProvider =
(ref) => SmoothScrollEnabledNotifier(),
);

/// How long the smooth scroll animation runs (in milliseconds).
class SmoothScrollDurationNotifier extends StateNotifier<int> {
SmoothScrollDurationNotifier()
: super(AppPreferences().get<int>('smoothScrollDuration', 250) ?? 250);

void set(int v) {
state = v;
AppPreferences().set('smoothScrollDuration', v);
}
}

final smoothScrollDurationProvider =
StateNotifierProvider<SmoothScrollDurationNotifier, int>(
(ref) => SmoothScrollDurationNotifier(),
);

// ═══════════════════════════════════════════════════════════════════
// Server start error (transient, not persisted)
// ═══════════════════════════════════════════════════════════════════
Expand Down
43 changes: 43 additions & 0 deletions lib/core/utils/log_message_summary.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:convert';

/// Convert a raw log message into a one-line preview suitable for
/// list-row titles and search-result previews.
///
/// Special handling for JSON objects/arrays so the user sees something
/// like `Object {3 keys: foo, bar, baz}` instead of just `{` (the first
/// character of the pretty-printed payload that the SDK sent over the
/// wire, which the list-row `maxLines: 1` would otherwise truncate to).
///
/// Non-JSON messages are returned unchanged.
String summarizeLogMessage(String message) {
final trimmed = message.trimLeft();
if (trimmed.isEmpty) return message;
if (trimmed[0] != '{' && trimmed[0] != '[') return message;

// Skip expensive JSON parsing for very large payloads — decoding a
// multi-MB log synchronously on the main thread can cause noticeable
// jank. In practice a JSON preview is not useful for such payloads.
if (trimmed.length > 5000) return message;

// Try to parse as JSON — RN's `toStr` (and Flutter's `jsonEncode`) ship
// pretty-printed payloads, so we can't rely on a single line.
dynamic parsed;
try {
parsed = jsonDecode(trimmed);
Comment on lines +24 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Synchronously calling jsonDecode on potentially large log messages on the main thread can block the UI and cause noticeable jank, especially when mapping over many events in providers or lists. Consider adding a length threshold check (e.g., skipping parsing if trimmed.length > 5000) to protect against performance degradation with large payloads.

  if (trimmed.length > 5000) return message;\n\n  dynamic parsed;\n  try {\n    parsed = jsonDecode(trimmed);

} catch (_) {
return message; // not valid JSON — show the original text
}

if (parsed is Map) {
final keys = parsed.keys.cast<String>().toList();
if (keys.isEmpty) return 'Object {}';
final preview = keys.take(3).join(', ');
final more = keys.length > 3 ? ', …' : '';
return 'Object {${keys.length} key${keys.length == 1 ? '' : 's'}: $preview$more}';
}
if (parsed is List) {
if (parsed.isEmpty) return 'Array []';
return 'Array [${parsed.length}]';
}
return message;
}
3 changes: 2 additions & 1 deletion lib/core/utils/smooth_scroll_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SmoothScrollPosition extends ScrollPositionWithSingleContext {
// Calculate target pixels starting from current target (if animating) or current position
final double basePixels = _targetPixels ?? pixels;
final double target = (basePixels + delta * 2.2).clamp(minScrollExtent, maxScrollExtent);
final durationMs = AppPreferences().get<int>('smoothScrollDuration', 250) ?? 250;

if (target != pixels) {
_targetPixels = target;
Expand All @@ -47,7 +48,7 @@ class SmoothScrollPosition extends ScrollPositionWithSingleContext {
// in `beginActivity` always cleared `_targetPixels` mid-animation.
animateTo(
target,
duration: const Duration(milliseconds: 250),
duration: Duration(milliseconds: durationMs),
curve: Curves.easeOutQuart,
).whenComplete(() {
if (_targetPixels == target) {
Expand Down
Loading
Loading