- Update usb-install.md milestone documentation - Improve ADB service functionality and error handling - Update app shell and settings page for usb install feature - Add test coverage for adb service and widget - Update dependencies in pubspec.yaml - Archive completed task artifacts
343 lines
10 KiB
Dart
343 lines
10 KiB
Dart
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<SettingsPage> createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends State<SettingsPage> {
|
|
AdbRuntimeDiagnostics? _diagnostics;
|
|
bool _loading = false;
|
|
Object? _loadError;
|
|
|
|
Future<void> _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<String>(
|
|
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',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|