Performance: - Move JSON parsing to background isolate via Isolate.run() (eliminates 2-4s UI freeze) - Cache filteredEntries with key-based invalidation (eliminates O(n) recomputation) - Debounce search queries at 300ms (prevents cascade rebuilds on keystroke) - Flatten timeline from 2-level turn×response Column to single virtualized ListView.builder - Add RepaintBoundary per timeline item (isolates repaints during scroll) - Use context.select for granular rebuilds instead of top-level context.watch - Lazy ExpandableCard: child not built until first expand (replaces AnimatedCrossFade) - Use IndexedStack in AppShell (preserves screen state across tab switches) Fixes: - Collect parse errors instead of silently swallowing them - Show parse error count in timeline filter bar - Fix overflow in tokens screen pie chart legend Build: - Configure Developer ID signing with hardened runtime for production distribution - Enable secure timestamps for notarization - Update app name to Claude Session Viewer - Signed, notarized, stapled DMG distribution
117 lines
3.1 KiB
Dart
117 lines
3.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../theme/app_theme.dart';
|
|
|
|
class ExpandableCard extends StatefulWidget {
|
|
final Widget header;
|
|
final Widget child;
|
|
final bool initiallyExpanded;
|
|
final Color? backgroundColor;
|
|
final Color? borderColor;
|
|
|
|
const ExpandableCard({
|
|
super.key,
|
|
required this.header,
|
|
required this.child,
|
|
this.initiallyExpanded = false,
|
|
this.backgroundColor,
|
|
this.borderColor,
|
|
});
|
|
|
|
@override
|
|
State<ExpandableCard> createState() => _ExpandableCardState();
|
|
}
|
|
|
|
class _ExpandableCardState extends State<ExpandableCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late bool _expanded;
|
|
late AnimationController _controller;
|
|
late Animation<double> _rotation;
|
|
bool _hasBeenExpanded = false; // Track if ever expanded to lazy-build
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_expanded = widget.initiallyExpanded;
|
|
_hasBeenExpanded = _expanded;
|
|
_controller = AnimationController(
|
|
duration: const Duration(milliseconds: 200),
|
|
vsync: this,
|
|
value: _expanded ? 1.0 : 0.0,
|
|
);
|
|
_rotation = Tween<double>(begin: 0, end: 0.25).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _toggle() {
|
|
setState(() {
|
|
_expanded = !_expanded;
|
|
if (_expanded) {
|
|
_hasBeenExpanded = true;
|
|
_controller.forward();
|
|
} else {
|
|
_controller.reverse();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: widget.backgroundColor ?? AppColors.surface,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: widget.borderColor ?? AppColors.surfaceBorder,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
InkWell(
|
|
onTap: _toggle,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
child: Row(
|
|
children: [
|
|
RotationTransition(
|
|
turns: _rotation,
|
|
child: const Icon(
|
|
Icons.chevron_right,
|
|
size: 18,
|
|
color: AppColors.textSecondary,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: widget.header),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Lazy animated expand: only build child once ever expanded
|
|
if (_hasBeenExpanded)
|
|
ClipRect(
|
|
child: SizeTransition(
|
|
sizeFactor: _controller,
|
|
axisAlignment: -1.0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
|
child: widget.child,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|