/// Context panel showing backlinks and outlinks for the active file. library; import 'dart:async'; import 'package:clide/kernel/kernel.dart'; import 'package:clide/widgets/widgets.dart'; import 'package:flutter/widgets.dart'; import 'backlinks_controller.dart'; class BacklinksView extends StatefulWidget { const BacklinksView({super.key}); @override State createState() => _BacklinksViewState(); } class _BacklinksViewState extends State { BacklinksController? _controller; @override void didChangeDependencies() { super.didChangeDependencies(); if (_controller != null) return; final kernel = ClideKernel.of(context); _controller = BacklinksController(ipc: kernel.ipc, events: kernel.events); } @override void dispose() { _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final c = _controller; if (c == null) return const SizedBox.shrink(); return ListenableBuilder( listenable: c, builder: (context, _) { final tokens = ClideTheme.of(context).surface; if (c.activePath == null) { return const Padding( padding: EdgeInsets.all(12), child: ClideText( 'Open a file to see its links.', muted: true, ), ); } return Semantics( label: 'backlinks for ${c.activePath}', container: true, explicitChildNodes: true, child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ClideText( c.activePath!.split('/').last, color: tokens.globalForeground, ), ), if (c.error != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ClideText( c.error!, color: tokens.statusError, fontSize: clideFontCaption, ), ), if (c.loading) const Padding( padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true), ), _LinkGroup( label: 'Backlinks', links: c.backlinks, pathKey: 'source', ), _LinkGroup( label: 'Outlinks', links: c.outlinks, pathKey: 'target', ), ], ), ), ); }, ); } } class _LinkGroup extends StatelessWidget { const _LinkGroup({ required this.label, required this.links, required this.pathKey, }); final String label; final List> links; final String pathKey; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.only(left: 12, right: 8, top: 8, bottom: 2), child: ClideText( '$label (${links.length})', fontSize: clideFontCaption, muted: true, ), ), if (links.isEmpty) const Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2), child: ClideText('None', fontSize: clideFontCaption, muted: true), ), for (final link in links) _LinkRow(link: link, pathKey: pathKey), ], ); } } class _LinkRow extends StatelessWidget { const _LinkRow({required this.link, required this.pathKey}); final Map link; final String pathKey; @override Widget build(BuildContext context) { final tokens = ClideTheme.of(context).surface; final target = link[pathKey] as String? ?? ''; final alias = link['alias'] as String?; final display = alias ?? target; return Semantics( button: true, label: target, child: ClideTappable( onTap: () { if (!target.startsWith('http')) { final kernel = ClideKernel.of(context); unawaited(kernel.ipc.request('editor.open', args: {'path': target})); } }, builder: (context, hovered, _) => Container( color: hovered ? tokens.sidebarItemHover : null, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), child: ClideText( display, maxLines: 1, overflow: TextOverflow.ellipsis, color: target.startsWith('http') ? tokens.statusInfo : tokens.sidebarForeground, ), ), ), ); } }