CH-J SM Mobilepengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J SM Mobile

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / SM Mobile 175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a Ditambah: · Dibina:
gates.dart Ditambah: Tarikh tidak direkodkan 12.5 KB · 347 baris
Kod Muat turun fail
1import 'package:flutter/material.dart';
2import '../core/app_controller.dart';
3import '../core/failure.dart';
4import '../l10n/strings.dart';
5import '../services/ssh_service.dart';
6import 'common.dart';
7import 'password_fields.dart';
9Widget 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 );
37class Onboarding extends StatefulWidget {
38 final AppController controller;
39 const Onboarding(this.controller, {super.key});
40 @override
41 State<Onboarding> createState() => _OnboardingState();
44class _OnboardingState extends State<Onboarding> {
45 int step = 0;
46 bool consent = false, busy = false, declined = false;
47 @override
48 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 == 0
91 ? 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: busy
102 ? null
103 : (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: busy
121 ? null
122 : () => setState(() {
123 declined = true;
124 consent = false;
125 }),
126 child: Text(context.tr('decline')),
127 ),
128 FilledButton(
129 onPressed: busy || (step == 1 && !consent)
130 ? null
131 : () 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 }
170class VaultGate extends StatefulWidget {
171 final AppController controller;
172 const VaultGate(this.controller, {super.key});
173 @override
174 State<VaultGate> createState() => _VaultGateState();
177class _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 @override
189 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 in
196 /// the foreground — once per locked period, so a cancelled or failed
197 /// 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 @override
245 void dispose() {
246 c.removeListener(_maybeAutoBiometric);
247 password.dispose();
248 repeated.dispose();
249 super.dispose();
250 }
252 @override
253 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.initialized
295 ? TextInputAction.done
296 : 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.biometricAttempted
332 ? 'tryBiometricAgain'
333 : 'unlockBiometric',
334 ),
335 ),
336 ),
337 ],
338 if (busy) const LinearProgressIndicator(),
339 ],
340 ),
341 ),
342 ),
343 ),
344 ),
345 );
346 }
SHA-256720d01c744b93d9fe1171b87eeaad2931b5e211053e57bbf9ff846f6af8dd058

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkib46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Fail