Opublikowane źródło
Pobierz źródło ZIP
CH-J SM Mobile
Przeglądaj katalogi i pliki dla konkretnego wydania aplikacji.
CH-J / SM Mobile
134 pliki
sm-mobile-20261016T050549Z-3678f2a05fa9
Dodano: · Built:
1
import 'package:flutter/material.dart';2
import '../core/app_controller.dart';3
import '../l10n/strings.dart';4
import '../models/server_profile.dart';5
import 'common.dart';6
import 'profile_editor.dart';8
Future<void> connectProfile(9
BuildContext context,10
AppController c,11
ServerProfile profile,12
) async {13
final password = TextEditingController(),14
passphrase = TextEditingController();15
try {16
final stored = c.vault.secret(17
profile.id,18
profile.auth == AuthMethod.password ? 'password' : 'privateKey',19
);20
final allowed = await showDialog<bool>(21
context: context,22
builder: (ctx) => AlertDialog(23
title: Text('${ctx.tr('connect')} · ${profile.name}'),24
content: SingleChildScrollView(25
child: Column(26
mainAxisSize: MainAxisSize.min,27
children: [28
SelectableText(29
'${profile.username}@${profile.host}:${profile.port}',30
),31
const SizedBox(height: 16),32
if (profile.auth == AuthMethod.password)33
TextField(34
controller: password,35
obscureText: true,36
autocorrect: false,37
enableSuggestions: false,38
decoration: InputDecoration(39
labelText: ctx.tr('password'),40
hintText: stored == null ? null : '••••••••',41
),42
),43
if (profile.auth == AuthMethod.privateKey)44
TextField(45
controller: passphrase,46
obscureText: true,47
autocorrect: false,48
enableSuggestions: false,49
decoration: InputDecoration(labelText: ctx.tr('passphrase')),50
),51
],52
),53
),54
actions: [55
TextButton(56
onPressed: () => Navigator.pop(ctx, false),57
child: Text(ctx.tr('cancel')),58
),59
FilledButton(60
onPressed: () => Navigator.pop(ctx, true),61
child: Text(ctx.tr('connect')),62
),63
],64
),65
);66
if (allowed != true || !c.vault.unlocked || !context.mounted) return;67
final credential = profile.auth == AuthMethod.password68
? (password.text.isEmpty ? stored ?? '' : password.text)69
: stored ?? '';70
await c.ssh.connect(71
profile,72
credential: credential,73
passphrase: passphrase.text.isEmpty ? null : passphrase.text,74
prompt: (endpoint, type, fingerprint) async {75
if (!context.mounted || !c.vault.unlocked) return false;76
return await showDialog<bool>(77
context: context,78
barrierDismissible: false,79
builder: (ctx) => AlertDialog(80
title: Text(ctx.tr('hostTrustTitle')),81
content: SingleChildScrollView(82
child: Column(83
mainAxisSize: MainAxisSize.min,84
crossAxisAlignment: CrossAxisAlignment.start,85
children: [86
Text(ctx.tr('hostTrustBody')),87
const SizedBox(height: 16),88
SelectableText('$endpoint\n$type\n$fingerprint'),89
],90
),91
),92
actions: [93
TextButton(94
onPressed: () => Navigator.pop(ctx, false),95
child: Text(ctx.tr('cancel')),96
),97
FilledButton(98
onPressed: () => Navigator.pop(ctx, true),99
child: Text(ctx.tr('trust')),100
),101
],102
),103
) ??104
false;105
},106
);107
} catch (e) {108
if (context.mounted) showFailure(context, e);109
} finally {110
password.clear();111
passphrase.clear();112
Future.delayed(const Duration(seconds: 1), () {113
password.dispose();114
passphrase.dispose();115
});116
}117
}119
class ServersPage extends StatefulWidget {120
final AppController controller;121
final VoidCallback onConnected;122
const ServersPage(this.controller, {required this.onConnected, super.key});123
@override124
State<ServersPage> createState() => _ServersPageState();125
}127
class _ServersPageState extends State<ServersPage> {128
String query = '';129
@override130
Widget build(BuildContext context) {131
final c = widget.controller;132
final profiles = c.vault.profiles133
.where(134
(p) => '${p.name} ${p.host} ${p.username}'.toLowerCase().contains(135
query.toLowerCase(),136
),137
)138
.toList();139
return Column(140
children: [141
Padding(142
padding: const EdgeInsets.all(16),143
child: Row(144
children: [145
Expanded(146
child: TextField(147
decoration: InputDecoration(148
labelText: context.tr('search'),149
prefixIcon: const Icon(Icons.search),150
),151
onChanged: (v) => setState(() => query = v),152
),153
),154
const SizedBox(width: 10),155
IconButton.filled(156
onPressed: () => Navigator.push(157
context,158
MaterialPageRoute(builder: (_) => ProfileEditor(c)),159
),160
tooltip: context.tr('addServer'),161
icon: const Icon(Icons.add),162
),163
],164
),165
),166
Expanded(167
child: profiles.isEmpty168
? const PageMessage('emptyServers')169
: ListView.builder(170
padding: const EdgeInsets.symmetric(horizontal: 16),171
itemCount: profiles.length,172
itemBuilder: (context, i) {173
final p = profiles[i];174
final active =175
c.ssh.profile?.id == p.id &&176
(c.ssh.connected || c.ssh.connecting);177
return Card(178
child: ListTile(179
leading: Icon(180
Icons.dns_outlined,181
color: active182
? const Color(0xff43d6a0)183
: Theme.of(context).colorScheme.primary,184
),185
title: Text(p.name),186
subtitle: Text(187
'${p.username}@${p.host}:${p.port}\n${context.tr(active188
? c.ssh.connecting189
? 'connecting'190
: 'connected'191
: 'disconnected')}',192
),193
isThreeLine: true,194
onTap: c.ssh.connecting195
? null196
: () async {197
await connectProfile(context, c, p);198
if (c.ssh.connected) widget.onConnected();199
},200
trailing: PopupMenuButton<String>(201
onSelected: (action) async {202
try {203
if (action == 'edit') {204
await Navigator.push(205
context,206
MaterialPageRoute(207
builder: (_) =>208
ProfileEditor(c, profile: p),209
),210
);211
}212
if (action == 'duplicate') {213
await c.vault.save(214
ServerProfile(215
name: p.name,216
host: p.host,217
port: p.port,218
username: p.username,219
),220
);221
c.changed();222
}223
if (action == 'delete' &&224
context.mounted &&225
await confirm(226
context,227
'confirmDelete',228
detail: p.name,229
)) {230
if (c.ssh.profile?.id == p.id) {231
c.ssh.disconnect(clear: true);232
}233
await c.vault.deleteProfile(p.id);234
c.changed();235
}236
} catch (e) {237
if (context.mounted) showFailure(context, e);238
}239
},240
itemBuilder: (ctx) => ['edit', 'duplicate', 'delete']241
.map(242
(v) => PopupMenuItem(243
value: v,244
child: Text(ctx.tr(v)),245
),246
)247
.toList(),248
),249
),250
);251
},252
),253
),254
],255
);256
}257
}SHA-256
c1899099e1f227e3b95fc5e482550809429a74237f49ea62c77c7060c91e6bf4CH-J Proprietary Software License 1.14 · Źródło jest dostarczane pod CH-J Proprietary Software License 1.14. Jego dostępność nie zmienia warunków licencji ani nie przyznaje dodatkowych praw.
SHA-256
c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65