Foinse foilsithe
Íosluchtaigh foinse ZIP
CH-J SM Mobile
Brabhsáil comhadlanna agus comhaid chun feidhmchlár áirithe a scaoileadh.
CH-J / SM Mobile
134 Comhaid
sm-mobile-20261016T050549Z-3678f2a05fa9
Suimigh: · Tógtha:
1
import 'dart:typed_data';2
import 'package:file_picker/file_picker.dart';3
import 'package:flutter/material.dart';4
import '../core/app_controller.dart';5
import '../core/build_info.dart';6
import '../core/failure.dart';7
import '../l10n/strings.dart';8
import '../models/server_profile.dart';9
import '../services/local_files.dart';10
import '../services/profile_transfer.dart';11
import 'common.dart';12
import 'gates.dart';14
class SettingsPage extends StatefulWidget {15
final AppController controller;16
const SettingsPage(this.controller, {super.key});17
@override18
State<SettingsPage> createState() => _SettingsPageState();19
}21
class _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
@override94
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.values113
.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: busy128
? null129
: (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: busy178
? null179
: () => 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
}254
}256
class ImportPreview extends StatefulWidget {257
final List<ServerProfile> profiles, existing;258
const ImportPreview(this.profiles, this.existing, {super.key});259
@override260
State<ImportPreview> createState() => _ImportPreviewState();261
}263
class _ImportPreviewState extends State<ImportPreview> {264
String policy = 'skipDuplicates';265
@override266
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
}331
}SHA-256
7c6deb628d3cbd56de5cf6a1810dac448dd50c14ee0491f38eb1d4b428fc1190CH-J Proprietary Software License 1.14 · Tá Foinse ar fáil faoin CH-J Proprietary Software License 1.14. Ní chuireann a infhaighteacht a athrú ar na téarmaí ceadúnais nó cearta breise a dheonú.
Cartlann SHA-256
c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65