appsok/lib/src/features/settings/settings_page.dart
toki ce27006e05 feat: usb install milestone update and service improvements
- 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
2026-06-13 07:57:54 +09:00

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',
),
),
),
],
),
);
}
}