import 'package:flutter/material.dart'; import '../../services/adb_service.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key, this.adbDiagnosticsLoader}); final AdbDiagnosticsLoader? adbDiagnosticsLoader; @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { AdbRuntimeDiagnostics? _diagnostics; bool _loading = false; Object? _loadError; Future _loadDiagnostics() async { final loader = widget.adbDiagnosticsLoader; if (loader == null) return; setState(() { _loading = true; _loadError = null; }); try { final diag = await loader(); if (!mounted) return; setState(() { _diagnostics = diag; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _loadError = e; _loading = false; }); } } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(28, 0, 28, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Material( color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Jenkins', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 14), Row( children: [ Expanded( child: TextField( decoration: const InputDecoration( labelText: 'Base URL', prefixIcon: Icon(Icons.dns_outlined), ), ), ), const SizedBox(width: 12), Expanded( child: TextField( decoration: const InputDecoration( labelText: '기본 Job', prefixIcon: Icon(Icons.account_tree_outlined), ), ), ), ], ), const SizedBox(height: 14), SegmentedButton( segments: const [ ButtonSegment( value: 'manual', label: Text('Token'), icon: Icon(Icons.key), ), ButtonSegment( value: 'webview', label: Text('Web Login'), icon: Icon(Icons.web_asset), ), ], selected: const {'webview'}, onSelectionChanged: (_) {}, ), ], ), ), ), const SizedBox(height: 14), Material( color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: colorScheme.outlineVariant), ), child: Padding( padding: const EdgeInsets.all(18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('ADB', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 14), Row( children: [ Expanded( child: TextField( decoration: const InputDecoration( labelText: 'adb path', prefixIcon: Icon(Icons.terminal), ), ), ), const SizedBox(width: 12), OutlinedButton.icon( onPressed: () {}, icon: const Icon(Icons.search), label: const Text('찾기'), ), ], ), const SizedBox(height: 8), SwitchListTile( contentPadding: EdgeInsets.zero, value: true, onChanged: (_) {}, title: const Text('설치 전 동일 패키지 업데이트'), secondary: const Icon(Icons.update), ), if (widget.adbDiagnosticsLoader != null) ...[ const Divider(height: 24), _AdbDiagnosticsSection( diagnostics: _diagnostics, loading: _loading, loadError: _loadError, onRefresh: _loadDiagnostics, ), ], ], ), ), ), ], ), ); } } class _AdbDiagnosticsSection extends StatelessWidget { const _AdbDiagnosticsSection({ required this.diagnostics, required this.loading, required this.loadError, required this.onRefresh, }); final AdbRuntimeDiagnostics? diagnostics; final bool loading; final Object? loadError; final VoidCallback onRefresh; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'ADB 진단', style: textTheme.labelMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 8), if (loading) Icon( key: const ValueKey('adb-diag-loading'), Icons.hourglass_empty, size: 14, color: colorScheme.onSurfaceVariant, ) else InkWell( onTap: onRefresh, child: Icon( Icons.refresh, size: 16, color: colorScheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: 8), if (loadError != null) Text( '진단 로드 실패', style: textTheme.bodySmall?.copyWith(color: colorScheme.error), ) else if (diagnostics == null && !loading) Text( '진단 정보 없음', style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ) else if (diagnostics != null) _DiagRows(diagnostics: diagnostics!), ], ); } } class _DiagRows extends StatelessWidget { const _DiagRows({required this.diagnostics}); final AdbRuntimeDiagnostics diagnostics; @override Widget build(BuildContext context) { final rt = diagnostics.runtime; final sha = diagnostics.sha256; final shortSha = sha != null && sha.length >= 12 ? sha.substring(0, 12) : sha; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _DiagRow( key: const ValueKey('diag-source'), label: '소스', value: _sourceLabel(rt.source, rt.fallbackUsed), ), _DiagRow( key: const ValueKey('diag-path'), label: '경로', value: rt.executablePath, ), _DiagRow( key: const ValueKey('diag-port'), label: '서버 포트', value: rt.serverPort.toString(), ), if (diagnostics.version != null) _DiagRow( key: const ValueKey('diag-version'), label: '버전', value: diagnostics.version!.split('\n').first, ), if (diagnostics.versionError != null) _DiagRow( key: const ValueKey('diag-version-error'), label: '버전 오류', value: diagnostics.versionError!, isError: true, ), if (shortSha != null) _DiagRow( key: const ValueKey('diag-hash'), label: 'SHA-256', value: shortSha, ), ], ); } String _sourceLabel(AdbExecutableSource source, bool fallback) { return switch (source) { AdbExecutableSource.bundled => 'bundled', AdbExecutableSource.custom => 'custom', AdbExecutableSource.system => fallback ? 'system (fallback)' : 'system', }; } } class _DiagRow extends StatelessWidget { const _DiagRow({ super.key, required this.label, required this.value, this.isError = false, }); final String label; final String value; final bool isError; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 72, child: Text( label, style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), ), Expanded( child: Text( value, maxLines: 1, overflow: TextOverflow.ellipsis, style: textTheme.bodySmall?.copyWith( color: isError ? colorScheme.error : null, fontFamily: 'monospace', ), ), ), ], ), ); } }