Migrate the user-facing strings in builtin.tickets, builtin.pql, builtin.decisions, and builtin.problems (list/detail views, section headers, filters, empty/loading/error states, type chips) to ClideSettings.i18n; extend each extension's catalog. The detail/links tabs that had a bare title now wire titleKey + i18nNamespace so the tab title resolves too. Context threaded into the static label helpers. No en_US behaviour change (D-21). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
160 lines
6.0 KiB
Dart
160 lines
6.0 KiB
Dart
/// Sidebar panel showing project diagnostics from pql.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:clide/kernel/kernel.dart';
|
|
import 'package:clide/widgets/widgets.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import 'problems_controller.dart';
|
|
|
|
class ProblemsView extends StatefulWidget {
|
|
const ProblemsView({super.key});
|
|
|
|
@override
|
|
State<ProblemsView> createState() => _ProblemsViewState();
|
|
}
|
|
|
|
class _ProblemsViewState extends State<ProblemsView> {
|
|
ProblemsController? _controller;
|
|
String _filter = '';
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_controller != null) return;
|
|
final kernel = ClideKernel.of(context);
|
|
_controller = ProblemsController(ipc: kernel.ipc);
|
|
unawaited(_controller!.refresh());
|
|
}
|
|
|
|
@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;
|
|
return Semantics(
|
|
label: ClideSettings.i18n.string(context, 'semantics.panel', namespace: 'builtin.problems', placeholder: 'problems panel'),
|
|
container: true,
|
|
explicitChildNodes: true,
|
|
child: () {
|
|
final lf = _filter.toLowerCase();
|
|
final filtered = lf.isEmpty
|
|
? c.problems
|
|
: c.problems.where((p) => p.message.toLowerCase().contains(lf) || p.source.toLowerCase().contains(lf)).toList();
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
ClideFilterBox(
|
|
address: 'problems.panel',
|
|
hint: ClideSettings.i18n.string(context, 'filter.hint', namespace: 'builtin.problems', placeholder: 'Filter problems…'),
|
|
onChanged: (v) => setState(() => _filter = v),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: ClideText(
|
|
ClideSettings.i18n.interpolated(
|
|
context,
|
|
'count',
|
|
namespace: 'builtin.problems',
|
|
placeholder: 'Problems ({count})',
|
|
replacers: [I18nReplacer(from: '{count}', replace: '${filtered.length}')],
|
|
),
|
|
fontSize: clideFontCaption,
|
|
color: tokens.sidebarForeground,
|
|
),
|
|
),
|
|
Semantics(
|
|
button: true,
|
|
label: ClideSettings.i18n.string(context, 'refresh.semantics', namespace: 'builtin.problems', placeholder: 'refresh problems'),
|
|
child: GestureDetector(
|
|
onTap: () => unawaited(c.refresh()),
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: ClideText(
|
|
ClideSettings.i18n.string(context, 'refresh.label', namespace: 'builtin.problems', placeholder: 'Refresh'),
|
|
fontSize: clideFontCaption,
|
|
color: tokens.sidebarForeground,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (c.loading && c.problems.isEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: ClideText(ClideSettings.i18n.string(context, 'scanning', namespace: 'builtin.problems', placeholder: 'Scanning…'), muted: true),
|
|
),
|
|
if (!c.loading && filtered.isEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: ClideText(
|
|
ClideSettings.i18n.string(context, 'empty', namespace: 'builtin.problems', placeholder: 'No problems found.'),
|
|
muted: true,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [for (final p in filtered) _ProblemRow(problem: p)],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ProblemRow extends StatelessWidget {
|
|
const _ProblemRow({required this.problem});
|
|
final Problem problem;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = ClideSettings.theme.of(context).surface;
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
ClideText(problem.source, fontSize: clideFontMono, color: tokens.statusWarning, fontFamily: ClideSettings.fonts.monoOf(context)),
|
|
const SizedBox(width: 6),
|
|
Expanded(child: ClideText(problem.message, color: tokens.sidebarForeground, maxLines: 2)),
|
|
],
|
|
),
|
|
if (problem.hint != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 44, top: 2),
|
|
child: ClideText(problem.hint!, fontSize: clideFontMono, muted: true, fontFamily: ClideSettings.fonts.monoOf(context)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|