Migrate ClideTheme.of(context) reads across the widget, feature, and shell layers to the unified ClideSettings.theme.of(context) facade (D-101), so theme/i18n/fonts/settings share one widget-facing entry. The facade delegates straight to ClideTheme, so behaviour is unchanged — goldens are unmoved. The low-level theme provider keeps its direct ClideTheme.of: the facade is built on it, and the two kernel sites (ClideTheme's own definition + the panels drag-resize widget) stay direct to avoid a widgets→kernel import cycle. Dead controller.dart/kernel.dart imports left by the sweep removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
4.5 KiB
Dart
142 lines
4.5 KiB
Dart
/// 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<BacklinksView> createState() => _BacklinksViewState();
|
|
}
|
|
|
|
class _BacklinksViewState extends State<BacklinksView> {
|
|
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 = ClideSettings.theme.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<Map<String, Object?>> 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<String, Object?> link;
|
|
final String pathKey;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = ClideSettings.theme.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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|