CH-J SM Mobileservera pārvaldība virs SSH
Izvēlne
Publicētais avots

CH-J SM Mobile

Pārlūkot mapes un failus konkrētai lietojumprogrammas izdošanai.

Lejupielādēt avota ZIP
CH-J / SM Mobile 134 faili
sm-mobile-20261016T050549Z-3678f2a05fa9 Pievienots: · Būvēts:
Faili
settings_page.dart Pievienots: Datums nav reģistrēts 10,6 KB · 331 rindas
Lejupielādēt failu
1import 'dart:typed_data';
2import 'package:file_picker/file_picker.dart';
3import 'package:flutter/material.dart';
4import '../core/app_controller.dart';
5import '../core/build_info.dart';
6import '../core/failure.dart';
7import '../l10n/strings.dart';
8import '../models/server_profile.dart';
9import '../services/local_files.dart';
10import '../services/profile_transfer.dart';
11import 'common.dart';
12import 'gates.dart';
14class SettingsPage extends StatefulWidget {
15 final AppController controller;
16 const SettingsPage(this.controller, {super.key});
17 @override
18 State<SettingsPage> createState() => _SettingsPageState();
21class _SettingsPageState extends State<SettingsPage> {
22 bool busy = false;
23 Future<void> action(Future<void> Function() task) async {
24 setState(() => busy = true);
25 try {
26 await task();
27 } catch (e) {
28 if (mounted) showFailure(context, e);
29 } finally {
30 if (mounted) setState(() => busy = false);
31 }
32 }
34 Future<void> importProfiles() async {
35 final c = widget.controller;
36 final epoch = c.vault.generation;
37 final file = await c.withSystemDialog(
38 () => FilePicker.pickFile(
39 type: FileType.custom,
40 allowedExtensions: ['json'],
41 ),
42 );
43 if (file == null ||
44 !mounted ||
45 epoch != c.vault.generation ||
46 !c.vault.unlocked) {
47 return;
48 }
49 final profiles = ProfileTransfer.decode(
50 await readBounded(file.readAsByteStream(), ProfileTransfer.maxBytes),
51 );
52 if (!mounted) return;
53 final policy = await Navigator.push<String>(
54 context,
55 MaterialPageRoute(
56 builder: (_) => ImportPreview(profiles, c.vault.profiles),
57 ),
58 );
59 if (policy == null || !c.vault.unlocked || epoch != c.vault.generation) {
60 return;
61 }
62 await c.vault.change((d) {
63 final list = d['profiles'] as List;
64 final seen = {
65 for (final p in list)
66 ServerProfile.fromJson(Map<String, dynamic>.from(p)).identity: p,
67 };
68 for (final p in profiles) {
69 final match = seen[p.identity];
70 if (match != null && policy == 'skipDuplicates') continue;
71 final next = ServerProfile(
72 id: match != null && policy == 'updateDuplicates'
73 ? match['id']
74 : null,
75 name: p.name,
76 host: p.host,
77 username: p.username,
78 port: p.port,
79 );
80 if (match != null && policy == 'updateDuplicates') {
81 list[list.indexOf(match)] = next.toJson();
82 (d['secrets'] as Map).remove(next.id);
83 } else {
84 list.add(next.toJson());
85 }
86 seen[p.identity] = list.lastWhere((r) => r['id'] == next.id);
87 }
88 });
89 c.ssh.disconnect(clear: true);
90 c.changed();
91 }
93 @override
94 Widget build(BuildContext context) {
95 final c = widget.controller;
96 return ListView(
97 padding: const EdgeInsets.all(20),
98 children: [
99 Text(
100 'CH-J · SM Mobile $appVersion',
101 style: Theme.of(context).textTheme.titleLarge,
102 ),
103 Text(buildId),
104 const SizedBox(height: 12),
105 Text(context.tr('localOnly')),
106 const SizedBox(height: 24),
107 languagePicker(context, c),
108 const SizedBox(height: 16),
109 DropdownButtonFormField<ThemeMode>(
110 initialValue: c.theme,
111 decoration: InputDecoration(labelText: context.tr('theme')),
112 items: ThemeMode.values
113 .map(
114 (t) =>
115 DropdownMenuItem(value: t, child: Text(context.tr(t.name))),
116 )
117 .toList(),
118 onChanged: (t) => action(() async {
119 c.theme = t!;
120 await c.saveSettings();
121 }),
122 ),
123 const SizedBox(height: 16),
124 SwitchListTile.adaptive(
125 title: Text(context.tr('biometric')),
126 value: c.biometric,
127 onChanged: busy
128 ? null
129 : (v) => action(
130 () => c.setBiometric(v, context.tr('biometricReason')),
131 ),
132 ),
133 SwitchListTile.adaptive(
134 title: Text(context.tr('lockBackground')),
135 value: c.lockBackground,
136 onChanged: (v) => action(() async {
137 c.lockBackground = v;
138 await c.saveSettings();
139 }),
140 ),
141 const SizedBox(height: 16),
142 DropdownButtonFormField<int>(
143 initialValue: c.idleMinutes,
144 decoration: InputDecoration(labelText: context.tr('idleTimeout')),
145 items: [
146 1,
147 5,
148 15,
149 ].map((i) => DropdownMenuItem(value: i, child: Text('$i'))).toList(),
150 onChanged: (i) => action(() async {
151 c.idleMinutes = i!;
152 await c.saveSettings();
153 }),
154 ),
155 const SizedBox(height: 16),
156 DropdownButtonFormField<int>(
157 initialValue: c.monitorSeconds,
158 decoration: InputDecoration(labelText: context.tr('monitorInterval')),
159 items: [
160 5,
161 10,
162 30,
163 60,
164 ].map((i) => DropdownMenuItem(value: i, child: Text('$i'))).toList(),
165 onChanged: (i) => action(() async {
166 c.monitorSeconds = i!;
167 await c.saveSettings();
168 }),
169 ),
170 const SizedBox(height: 24),
171 OutlinedButton.icon(
172 onPressed: busy ? null : () => action(importProfiles),
173 icon: const Icon(Icons.file_open),
174 label: Text(context.tr('importProfiles')),
175 ),
176 OutlinedButton.icon(
177 onPressed: busy
178 ? null
179 : () => action(() async {
180 if (c.vault.profiles.isEmpty) {
181 throw const Failure('invalidImport');
182 }
183 final bytes = Uint8List.fromList(
184 ProfileTransfer.encode(c.vault.profiles),
185 );
186 await c.withSystemDialog(
187 () => FilePicker.saveFile(
188 fileName: 'chj-server-profiles.json',
189 bytes: bytes,
190 mimeType: 'application/json',
191 ),
192 );
193 }),
194 icon: const Icon(Icons.ios_share),
195 label: Text(context.tr('exportProfiles')),
196 ),
197 const SizedBox(height: 16),
198 ListTile(
199 title: Text(context.tr('license')),
200 leading: const Icon(Icons.description_outlined),
201 onTap: () => Navigator.push(
202 context,
203 MaterialPageRoute(
204 builder: (ctx) => Scaffold(
205 appBar: AppBar(title: Text(ctx.tr('license'))),
206 body: SingleChildScrollView(
207 padding: const EdgeInsets.all(20),
208 child: SelectableText(c.legal.license),
209 ),
210 ),
211 ),
212 ),
213 ),
214 ListTile(
215 title: Text(context.tr('guidance')),
216 leading: const Icon(Icons.shield_outlined),
217 onTap: () => Navigator.push(
218 context,
219 MaterialPageRoute(
220 builder: (ctx) => Scaffold(
221 appBar: AppBar(title: Text(ctx.tr('guidance'))),
222 body: SingleChildScrollView(
223 padding: const EdgeInsets.all(20),
224 child: SelectableText(
225 c.legal.guidance[Localizations.localeOf(
226 ctx,
227 ).languageCode] ??
228 c.legal.guidance['en']!,
229 ),
230 ),
231 ),
232 ),
233 ),
234 ),
235 ListTile(
236 title: Text(context.tr('componentLicenses')),
237 leading: const Icon(Icons.inventory_2_outlined),
238 onTap: () => showLicensePage(
239 context: context,
240 applicationName: 'SM Mobile',
241 applicationVersion: appVersion,
242 ),
243 ),
244 const SizedBox(height: 16),
245 FilledButton.icon(
246 onPressed: c.lock,
247 icon: const Icon(Icons.lock),
248 label: Text(context.tr('lock')),
249 ),
250 if (busy) const LinearProgressIndicator(),
251 ],
252 );
253 }
256class ImportPreview extends StatefulWidget {
257 final List<ServerProfile> profiles, existing;
258 const ImportPreview(this.profiles, this.existing, {super.key});
259 @override
260 State<ImportPreview> createState() => _ImportPreviewState();
263class _ImportPreviewState extends State<ImportPreview> {
264 String policy = 'skipDuplicates';
265 @override
266 Widget build(BuildContext context) {
267 final seen = widget.existing.map((p) => p.identity).toSet();
268 final duplicates = widget.profiles.map((p) {
269 final found = !seen.add(p.identity);
270 return found;
271 }).toList();
272 return Scaffold(
273 appBar: AppBar(title: Text(context.tr('importPreview'))),
274 body: SafeArea(
275 child: Column(
276 children: [
277 Padding(
278 padding: const EdgeInsets.all(16),
279 child: Text(context.tr('importAdvice')),
280 ),
281 Padding(
282 padding: const EdgeInsets.symmetric(horizontal: 16),
283 child: DropdownButtonFormField<String>(
284 initialValue: policy,
285 isExpanded: true,
286 items:
287 ['skipDuplicates', 'updateDuplicates', 'createDuplicates']
288 .map(
289 (v) => DropdownMenuItem(
290 value: v,
291 child: Text(
292 context.tr(v),
293 overflow: TextOverflow.ellipsis,
294 ),
295 ),
296 )
297 .toList(),
298 onChanged: (v) => setState(() => policy = v!),
299 ),
300 ),
301 Expanded(
302 child: ListView.builder(
303 itemCount: widget.profiles.length,
304 itemBuilder: (ctx, i) {
305 final p = widget.profiles[i];
306 return ListTile(
307 title: Text(p.name),
308 subtitle: Text('${p.username}@${p.host}:${p.port}'),
309 trailing: duplicates[i]
310 ? Tooltip(
311 message: ctx.tr('duplicate'),
312 child: const Icon(Icons.copy),
313 )
314 : null,
315 );
316 },
317 ),
318 ),
319 Padding(
320 padding: const EdgeInsets.all(16),
321 child: FilledButton(
322 onPressed: () => Navigator.pop(context, policy),
323 child: Text(context.tr('importAction')),
324 ),
325 ),
326 ],
327 ),
328 ),
329 );
330 }
SHA-2567c6deb628d3cbd56de5cf6a1810dac448dd50c14ee0491f38eb1d4b428fc1190

CH-J Proprietary Software License 1.14 · Avots ir sniegts saskaņā ar CH-J Proprietary Software License 1.14. Tā pieejamība nemaina licences noteikumus un nepiešķir papildu tiesības.

Arhīvs SHA-256c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65