المصدر المنشور
تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J SM Mobile
تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.
CH-J / SM Mobile
175 ملفات
sm-mobile-20261016T185235Z-8e93ecc1b59a
تاريخ الإضافة: · تاريخ البناء:
1
import 'package:flutter/material.dart';2
import '../core/app_controller.dart';3
import '../core/failure.dart';4
import '../l10n/strings.dart';5
import '../services/ssh_service.dart';6
import 'common.dart';7
import 'password_fields.dart';9
Widget languagePicker(BuildContext context, AppController c) =>10
DropdownButtonFormField<String>(11
initialValue: c.locale?.languageCode ?? 'system',12
isExpanded: true,13
decoration: InputDecoration(labelText: context.tr('language')),14
items: ['system', 'cs', 'de', 'en']15
.map(16
(v) => DropdownMenuItem(17
value: v,18
child: Text(switch (v) {19
'cs' => 'Čeština',20
'de' => 'Deutsch',21
'en' => 'English',22
_ => context.tr('system'),23
}),24
),25
)26
.toList(),27
onChanged: (v) async {28
c.locale = v == 'system' ? null : Locale(v!);29
try {30
await c.saveSettings();31
} catch (e) {32
if (context.mounted) showFailure(context, e);33
}34
},35
);37
class Onboarding extends StatefulWidget {38
final AppController controller;39
const Onboarding(this.controller, {super.key});40
@override41
State<Onboarding> createState() => _OnboardingState();42
}44
class _OnboardingState extends State<Onboarding> {45
int step = 0;46
bool consent = false, busy = false, declined = false;47
@override48
Widget build(BuildContext context) {49
final c = widget.controller;50
final lang = Localizations.localeOf(context).languageCode;51
return Scaffold(52
appBar: AppBar(title: const Text('CH-J · SM Mobile')),53
body: SafeArea(54
child: Center(55
child: ConstrainedBox(56
constraints: const BoxConstraints(maxWidth: 760),57
child: Padding(58
padding: const EdgeInsets.all(20),59
child: LayoutBuilder(60
builder: (context, constraints) => SingleChildScrollView(61
child: SizedBox(62
height: constraints.maxHeight.clamp(63
600 * MediaQuery.textScalerOf(context).scale(1),64
double.infinity,65
),66
child: Column(67
crossAxisAlignment: CrossAxisAlignment.stretch,68
children: [69
if (step == 0) ...[70
Text(71
context.tr('welcome'),72
style: Theme.of(context).textTheme.headlineSmall,73
),74
const SizedBox(height: 16),75
languagePicker(context, c),76
const SizedBox(height: 16),77
Text(context.tr('localOnly')),78
const SizedBox(height: 16),79
],80
Text(81
context.tr(step == 0 ? 'guidance' : 'license'),82
style: Theme.of(context).textTheme.titleLarge,83
),84
const SizedBox(height: 12),85
Expanded(86
child: Card(87
child: SingleChildScrollView(88
padding: const EdgeInsets.all(16),89
child: SelectableText(90
step == 091
? c.legal.guidance[lang] ??92
c.legal.guidance['en']!93
: c.legal.license,94
),95
),96
),97
),98
if (step == 1)99
CheckboxListTile(100
value: consent,101
onChanged: busy102
? null103
: (v) => setState(() => consent = v!),104
title: Text(context.tr('consent')),105
contentPadding: EdgeInsets.zero,106
),107
if (declined)108
Text(109
context.tr('licenseDeclined'),110
style: TextStyle(111
color: Theme.of(context).colorScheme.error,112
),113
),114
Wrap(115
spacing: 12,116
alignment: WrapAlignment.end,117
children: [118
if (step == 1)119
TextButton(120
onPressed: busy121
? null122
: () => setState(() {123
declined = true;124
consent = false;125
}),126
child: Text(context.tr('decline')),127
),128
FilledButton(129
onPressed: busy || (step == 1 && !consent)130
? null131
: () async {132
if (step == 0) {133
setState(() => step = 1);134
return;135
}136
setState(() => busy = true);137
try {138
await c.acceptLicense();139
} catch (e) {140
if (context.mounted) {141
showFailure(context, e);142
}143
} finally {144
if (mounted) {145
setState(() => busy = false);146
}147
}148
},149
child: Text(150
context.tr(151
step == 0 ? 'continueAction' : 'accept',152
),153
),154
),155
],156
),157
],158
),159
),160
),161
),162
),163
),164
),165
),166
);167
}168
}170
class VaultGate extends StatefulWidget {171
final AppController controller;172
const VaultGate(this.controller, {super.key});173
@override174
State<VaultGate> createState() => _VaultGateState();175
}177
class _VaultGateState extends State<VaultGate> {178
final password = TextEditingController(), repeated = TextEditingController();179
bool busy = false;180
// Why the last biometric attempt did not unlock (localization key).181
String? biometricError;183
AppController get c => widget.controller;185
bool get _biometricReady =>186
c.initialized && c.biometric && c.biometricVersion >= 2;188
@override189
void initState() {190
super.initState();191
c.addListener(_maybeAutoBiometric);192
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAutoBiometric());193
}195
/// Starts the biometric prompt by itself when the lock screen appears in196
/// the foreground — once per locked period, so a cancelled or failed197
/// prompt is never repeated in a loop.198
void _maybeAutoBiometric() {199
if (!mounted ||200
busy ||201
!_biometricReady ||202
!c.foreground ||203
c.authenticating ||204
c.biometricAttempted ||205
!c.accepted) {206
return;207
}208
WidgetsBinding.instance.addPostFrameCallback((_) {209
if (mounted && !busy && !c.biometricAttempted && c.foreground) {210
open(biometric: true);211
}212
});213
}215
Future<void> open({bool biometric = false}) async {216
setState(() {217
busy = true;218
if (biometric) biometricError = null;219
});220
try {221
if (biometric) {222
await c.biometricUnlock(context.tr('biometricReason'));223
} else {224
if (!c.initialized && password.text != repeated.text) {225
throw const Failure('passwordMismatch');226
}227
await c.openVault(password.text, create: !c.initialized);228
}229
password.clear();230
repeated.clear();231
} catch (e) {232
if (!mounted) return;233
if (biometric) {234
// Shown on the page; the password field stays ready below.235
setState(() => biometricError = failureCode(e));236
} else {237
showFailure(context, e);238
}239
} finally {240
if (mounted) setState(() => busy = false);241
}242
}244
@override245
void dispose() {246
c.removeListener(_maybeAutoBiometric);247
password.dispose();248
repeated.dispose();249
super.dispose();250
}252
@override253
Widget build(BuildContext context) {254
c.biometricReasonText = context.tr('biometricReason');255
c.biometricNegativeText = context.tr('usePassword');256
final scheme = Theme.of(context).colorScheme;257
final notice = biometricError ?? c.biometricNotice;258
return Scaffold(259
appBar: AppBar(title: const Text('SM Mobile')),260
body: SafeArea(261
child: Center(262
child: SingleChildScrollView(263
padding: const EdgeInsets.all(24),264
child: ConstrainedBox(265
constraints: const BoxConstraints(maxWidth: 450),266
child: Column(267
crossAxisAlignment: CrossAxisAlignment.stretch,268
children: [269
Icon(Icons.lock_outline, size: 56, color: scheme.primary),270
const SizedBox(height: 20),271
Text(272
context.tr(c.initialized ? 'vault' : 'createVault'),273
style: Theme.of(context).textTheme.headlineSmall,274
textAlign: TextAlign.center,275
),276
const SizedBox(height: 20),277
if (!c.initialized) ...[278
Text(context.tr('vaultAdvice')),279
const SizedBox(height: 20),280
],281
if (notice != null && c.initialized) ...[282
Card(283
color: scheme.secondaryContainer,284
child: Padding(285
padding: const EdgeInsets.all(12),286
child: Text(context.tr(notice)),287
),288
),289
const SizedBox(height: 12),290
],291
PasswordField(292
controller: password,293
label: context.tr('masterPassword'),294
action: c.initialized295
? TextInputAction.done296
: TextInputAction.next,297
onChanged: (_) {298
if (!c.initialized) setState(() {});299
},300
onSubmitted: (_) {301
if (!busy && c.initialized) open();302
},303
),304
if (!c.initialized) ...[305
const SizedBox(height: 8),306
PasswordAdvice(password.text),307
const SizedBox(height: 16),308
PasswordField(309
controller: repeated,310
label: context.tr('confirmPassword'),311
action: TextInputAction.done,312
onSubmitted: (_) {313
if (!busy) open();314
},315
),316
],317
const SizedBox(height: 20),318
FilledButton(319
onPressed: busy ? null : open,320
child: Text(321
context.tr(c.initialized ? 'unlock' : 'createVault'),322
),323
),324
if (_biometricReady) ...[325
const SizedBox(height: 8),326
OutlinedButton.icon(327
onPressed: busy ? null : () => open(biometric: true),328
icon: const Icon(Icons.fingerprint),329
label: Text(330
context.tr(331
c.biometricAttempted332
? 'tryBiometricAgain'333
: 'unlockBiometric',334
),335
),336
),337
],338
if (busy) const LinearProgressIndicator(),339
],340
),341
),342
),343
),344
),345
);346
}347
}SHA-256
720d01c744b93d9fe1171b87eeaad2931b5e211053e57bbf9ff846f6af8dd058CH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.
SHA-256 للأرشيف
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655