mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 13:47:38 +01:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYndZhdUrRGFEjBazQ
157 lines
5.3 KiB
Dart
157 lines
5.3 KiB
Dart
// Connect / save / test / sync actions for the GitHub mirror screen.
|
|
//
|
|
// Split out of `github_mirror_screen.dart` for the repo's 250-line cap. A
|
|
// `part` rather than a separate library: every one of these methods drives
|
|
// the screen's private state (`_busy`, `_status`, `_storedToken`, the four
|
|
// controllers) and calls its private helpers, none of which are reachable
|
|
// across a library boundary in Dart.
|
|
//
|
|
// A mixin rather than an extension, because these call `setState`, which is
|
|
// `@protected` -- an extension is not a subclass of `State`, so the analyzer
|
|
// (correctly) rejects it there.
|
|
|
|
part of 'github_mirror_screen.dart';
|
|
|
|
mixin _GitHubMirrorActions on State<GitHubMirrorScreen> {
|
|
// The four text controllers stay declared on the State, which creates and
|
|
// disposes them; the mixin only reads and writes their text.
|
|
TextEditingController get _ownerController;
|
|
TextEditingController get _repoController;
|
|
TextEditingController get _tokenController;
|
|
TextEditingController get _clientIdController;
|
|
|
|
bool _busy = false;
|
|
Widget? _status;
|
|
String _storedToken = '';
|
|
SyncSettings _currentSettings() {
|
|
final typed = _tokenController.text.trim();
|
|
return SyncSettings(
|
|
owner: _ownerController.text.trim(),
|
|
repo: _repoController.text.trim(),
|
|
// The `_storedToken` side is web-only (the wrapper holds the real
|
|
// token), so it is unreachable from a VM test.
|
|
token: typed.isEmpty && !SyncSettings.exposesTokenValue
|
|
// coverage:ignore-line
|
|
? _storedToken
|
|
: typed,
|
|
clientId: _clientIdController.text.trim(),
|
|
);
|
|
}
|
|
|
|
/// Shows the outcome as an icon cue; [semanticLabel] is spoken, never
|
|
/// painted.
|
|
void _showCue(CueKind kind, String semanticLabel) {
|
|
if (!mounted) return;
|
|
setState(
|
|
() => _status = StatusCue(
|
|
key: UniqueKey(),
|
|
kind: kind,
|
|
semanticLabel: semanticLabel,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _connectGitHub() async {
|
|
var clientId = _clientIdController.text.trim();
|
|
if (clientId.isEmpty) {
|
|
final entered = await showDialog<String>(
|
|
context: context,
|
|
builder: (_) => const _ClientIdSetupDialog(),
|
|
);
|
|
if (entered == null || entered.isEmpty) return;
|
|
clientId = entered;
|
|
if (!mounted) return;
|
|
setState(() => _clientIdController.text = clientId);
|
|
await _currentSettings().save();
|
|
}
|
|
final auth = createDeviceAuth(clientId, httpClient: widget.httpClient);
|
|
try {
|
|
final device = await auth.requestDeviceCode();
|
|
if (!mounted) return;
|
|
final token = await showDialog<String>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => DeviceCodeDialog(device: device, auth: auth),
|
|
);
|
|
if (token != null && token.isNotEmpty) {
|
|
setState(() {
|
|
_storedToken = token;
|
|
if (SyncSettings.exposesTokenValue) _tokenController.text = token;
|
|
});
|
|
_showCue(CueKind.info, 'Connected — syncing…');
|
|
await _currentSettings().save();
|
|
await _syncAfterConnect();
|
|
}
|
|
} on Exception catch (e) {
|
|
_showCue(CueKind.error, 'Could not start device flow: $e');
|
|
} finally {
|
|
auth.close();
|
|
}
|
|
}
|
|
|
|
/// Runs a sync right after connecting so the device-flow token is proven
|
|
/// to work immediately, with clear confirmation either way.
|
|
Future<void> _syncAfterConnect() async {
|
|
final settings = _currentSettings();
|
|
final client = createGitHubClient(settings, httpClient: widget.httpClient);
|
|
try {
|
|
await runSync(await syncBackend(client));
|
|
await SyncHealth.recordSuccess();
|
|
_showCue(CueKind.ok, 'Connected and synced.');
|
|
} on Exception catch (e) {
|
|
await SyncHealth.recordFailure();
|
|
_showCue(CueKind.error, 'Connected, but sync failed: $e');
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
setState(() => _busy = true);
|
|
await _currentSettings().save();
|
|
if (!mounted) return;
|
|
setState(() => _busy = false);
|
|
_showCue(CueKind.ok, 'Saved.');
|
|
}
|
|
|
|
Future<void> _testConnection() async {
|
|
setState(() => _busy = true);
|
|
final settings = _currentSettings();
|
|
final client = createGitHubClient(settings, httpClient: widget.httpClient);
|
|
try {
|
|
final ok = await client.canAccessRepo();
|
|
if (ok) {
|
|
_showCue(CueKind.ok, 'GitHub connection OK.');
|
|
} else {
|
|
_showCue(CueKind.error, 'GitHub connection failed.');
|
|
}
|
|
} on Exception catch (e) {
|
|
_showCue(CueKind.error, 'GitHub connection failed: $e');
|
|
} finally {
|
|
client.close();
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _syncNow() async {
|
|
setState(() => _busy = true);
|
|
final settings = _currentSettings();
|
|
await settings.save();
|
|
final client = createGitHubClient(settings, httpClient: widget.httpClient);
|
|
try {
|
|
await runSync(await syncBackend(client));
|
|
// Clears any stored failure: this is the button a user reaches
|
|
// *because* the banner told them syncing had stopped, so a successful
|
|
// run here must dismiss the warning it caused.
|
|
await SyncHealth.recordSuccess();
|
|
_showCue(CueKind.ok, 'Synced.');
|
|
} on Exception catch (e) {
|
|
await SyncHealth.recordFailure();
|
|
_showCue(CueKind.error, 'Sync failed: $e');
|
|
} finally {
|
|
client.close();
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
}
|