All sidebar and detail panes use clideFontSmall/clideFontBadge/ clideFontCaption instead of hardcoded sizes — single place to tune. Decision detail view subscribes to the message bus for selection events (same pattern as ticket controller) and publishes focus after loading. Both decisions and tickets sidebar views subscribe to the focus channel, expand the accordion section, highlight the active card, and scroll it into view. Clickable DQRT links wired in decision detail, ticket detail, and markdown viewer via onRecordTap. Ticket descriptions now render through ClideMarkdown instead of plain text. Co-Authored-By: Claude <noreply@anthropic.com>
100 lines
2.8 KiB
Dart
100 lines
2.8 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:clide/kernel/kernel.dart';
|
|
import 'package:clide/widgets/widgets.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
class MarkdownViewer extends StatefulWidget {
|
|
const MarkdownViewer({super.key});
|
|
|
|
@override
|
|
State<MarkdownViewer> createState() => _MarkdownViewerState();
|
|
}
|
|
|
|
class _MarkdownViewerState extends State<MarkdownViewer> {
|
|
String? _path;
|
|
String? _content;
|
|
String? _error;
|
|
StreamSubscription<DaemonEvent>? _eventSub;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_eventSub != null) return;
|
|
final kernel = ClideKernel.of(context);
|
|
_eventSub = kernel.events.on<DaemonEvent>().listen((e) {
|
|
if (e.kind == 'editor.buffer_activated') {
|
|
final path = e.data['path'] as String?;
|
|
if (path != null && path.endsWith('.md')) {
|
|
_loadFile(path);
|
|
}
|
|
}
|
|
});
|
|
final activeTab = kernel.panels.activeTabIn(Slots.workspace);
|
|
if (activeTab == 'editor.active') {
|
|
unawaited(_loadActiveBuffer());
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_eventSub?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadActiveBuffer() async {
|
|
final kernel = ClideKernel.of(context);
|
|
final resp = await kernel.ipc.request('editor.active');
|
|
if (!mounted || !resp.ok) return;
|
|
final path = resp.data['path'] as String?;
|
|
if (path != null && path.endsWith('.md')) {
|
|
await _loadFile(path);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadFile(String path) async {
|
|
final kernel = ClideKernel.of(context);
|
|
final resp = await kernel.ipc.request('files.read', args: {'path': path});
|
|
if (!mounted) return;
|
|
if (resp.ok) {
|
|
setState(() {
|
|
_path = path;
|
|
_content = resp.data['content'] as String? ?? '';
|
|
_error = null;
|
|
});
|
|
} else {
|
|
setState(() => _error = resp.error?.message);
|
|
}
|
|
}
|
|
|
|
void _navigateToRecord(BuildContext context, String id) {
|
|
final kernel = ClideKernel.of(context);
|
|
if (id.startsWith('T-')) {
|
|
kernel.messages.publish('builtin.tickets', 'selection', {'id': id});
|
|
} else {
|
|
kernel.messages.publish('builtin.decisions', 'selection', {'id': id});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_error != null) {
|
|
return Padding(padding: const EdgeInsets.all(12), child: ClideText(_error!, muted: true));
|
|
}
|
|
if (_content == null) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(12),
|
|
child: ClideText('Open a .md file to preview it here.', muted: true),
|
|
);
|
|
}
|
|
return ClidePaneChrome(
|
|
title: _path ?? 'viewer',
|
|
subtitle: '${_content!.split('\n').length} lines',
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(12),
|
|
child: ClideMarkdown(_content!, onRecordTap: (id) => _navigateToRecord(context, id)),
|
|
),
|
|
);
|
|
}
|
|
}
|