mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 13:07:46 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
github_device_auth tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
sync_settings_ui tests / test (push) Waiting to run
Glyph + spoken label replaces every app-written string on the shared Sync settings screen and the GitHub device-code dialog (kuhy's textgate rule). Old sentences are semantics labels; tests find them there. The device user code stays visible (GitHub's data). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
176 lines
5.3 KiB
Dart
176 lines
5.3 KiB
Dart
part of 'sync_settings_screen.dart';
|
|
|
|
/// The two sections the screen composes.
|
|
///
|
|
/// Split from sync_settings_screen.dart for the 250-line cap;
|
|
/// a `part` rather than an import because both sections are
|
|
/// library-private and could not be imported at all.
|
|
class _FirebaseSection extends StatelessWidget {
|
|
const _FirebaseSection({
|
|
required this.connected,
|
|
required this.busy,
|
|
required this.googleAvailable,
|
|
required this.googleUnavailableReason,
|
|
required this.emailController,
|
|
required this.passwordController,
|
|
required this.onConnectPassword,
|
|
required this.onConnectGoogle,
|
|
required this.onDisconnect,
|
|
});
|
|
|
|
final bool connected;
|
|
final bool busy;
|
|
final bool googleAvailable;
|
|
final String? googleUnavailableReason;
|
|
final TextEditingController emailController;
|
|
final TextEditingController passwordController;
|
|
final VoidCallback onConnectPassword;
|
|
final VoidCallback onConnectGoogle;
|
|
final VoidCallback onDisconnect;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (connected) {
|
|
return Row(
|
|
children: [
|
|
const Icon(Icons.cloud_done, size: 20),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: Text(emailController.text)),
|
|
IconButton(
|
|
onPressed: busy ? null : onDisconnect,
|
|
icon: const Icon(Icons.link_off, semanticLabel: 'Disconnect'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (googleAvailable) ...[
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: FilledButton(
|
|
onPressed: busy ? null : onConnectGoogle,
|
|
child: const Icon(
|
|
Icons.account_circle,
|
|
semanticLabel: 'Sign in with Google',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
] else if (googleUnavailableReason != null) ...[
|
|
// Disabled, not hidden. A null onPressed cannot be tapped, so this
|
|
// is not the "control that can never succeed" trap -- that one
|
|
// looks live and reports a fake cancellation. This one says why,
|
|
// which beats a screen that silently offers less on one platform.
|
|
// The why is a block glyph beside it, the reason spoken only.
|
|
Row(
|
|
children: [
|
|
const FilledButton(
|
|
onPressed: null,
|
|
child: Icon(
|
|
Icons.account_circle,
|
|
semanticLabel: 'Sign in with Google',
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Icon(
|
|
Icons.block,
|
|
color: Theme.of(context).colorScheme.error,
|
|
semanticLabel: googleUnavailableReason,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
ExpansionTile(
|
|
initiallyExpanded: !googleAvailable,
|
|
title: const Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Icon(
|
|
Icons.password,
|
|
semanticLabel: 'Use the account password instead',
|
|
),
|
|
),
|
|
tilePadding: EdgeInsets.zero,
|
|
childrenPadding: const EdgeInsets.only(bottom: 8),
|
|
children: [
|
|
TextField(
|
|
controller: emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
autocorrect: false,
|
|
decoration: const InputDecoration(
|
|
prefixIcon: Icon(
|
|
Icons.alternate_email,
|
|
semanticLabel: 'Sync account email',
|
|
),
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: passwordController,
|
|
obscureText: true,
|
|
autocorrect: false,
|
|
decoration: const InputDecoration(
|
|
prefixIcon: Icon(
|
|
Icons.lock_outline,
|
|
semanticLabel: 'Sync account password',
|
|
),
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: FilledButton(
|
|
onPressed: busy ? null : onConnectPassword,
|
|
child: const Icon(
|
|
Icons.cloud_done,
|
|
semanticLabel: 'Connect Firebase',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BackupSection extends StatelessWidget {
|
|
const _BackupSection({
|
|
required this.backup,
|
|
required this.onExport,
|
|
required this.onImport,
|
|
});
|
|
|
|
final BackupSlot backup;
|
|
final VoidCallback onExport;
|
|
final VoidCallback onImport;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: onExport,
|
|
child: Icon(Icons.upload, semanticLabel: 'Export ${backup.label}'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: onImport,
|
|
child: Icon(
|
|
Icons.download,
|
|
semanticLabel: 'Import ${backup.label}',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|