CH-J SM Mobilepamamahala ng server sa pamamagitan ng SSH
Menu
Nailathalang source

CH-J SM Mobile

I-browse ang mga direktoryo at file ng isang partikular na release ng application.

I-download ang source ZIP
CH-J / SM Mobile 175 file
sm-mobile-20261016T185235Z-8e93ecc1b59a Idinagdag: · Binuo:
servers_page.dart Idinagdag: Hindi naitala ang petsa 12.1 KB · 316 linya
Code I-download ang file
1import 'package:flutter/material.dart';
2import '../core/app_controller.dart';
3import '../l10n/strings.dart';
4import '../models/server_profile.dart';
5import 'adaptive.dart';
6import 'common.dart';
7import 'profile_editor.dart';
8import 'profile_io.dart';
10Future<void> connectProfile(
11 BuildContext context,
12 AppController c,
13 ServerProfile profile,
14) async {
15 final password = TextEditingController(),
16 passphrase = TextEditingController();
17 try {
18 final stored = c.vault.secret(
19 profile.id,
20 profile.auth == AuthMethod.password ? 'password' : 'privateKey',
21 );
22 final allowed = await showDialog<bool>(
23 context: context,
24 builder: (ctx) => AlertDialog(
25 title: Text('${ctx.tr('connect')} · ${profile.name}'),
26 content: SingleChildScrollView(
27 child: Column(
28 mainAxisSize: MainAxisSize.min,
29 children: [
30 SelectableText(
31 '${profile.username}@${profile.host}:${profile.port}',
32 ),
33 const SizedBox(height: 16),
34 if (profile.auth == AuthMethod.password)
35 TextField(
36 controller: password,
37 obscureText: true,
38 autocorrect: false,
39 enableSuggestions: false,
40 decoration: InputDecoration(
41 labelText: ctx.tr('password'),
42 hintText: stored == null ? null : '••••••••',
43 ),
44 ),
45 if (profile.auth == AuthMethod.privateKey)
46 TextField(
47 controller: passphrase,
48 obscureText: true,
49 autocorrect: false,
50 enableSuggestions: false,
51 decoration: InputDecoration(labelText: ctx.tr('passphrase')),
52 ),
53 ],
54 ),
55 ),
56 actions: [
57 TextButton(
58 onPressed: () => Navigator.pop(ctx, false),
59 child: Text(ctx.tr('cancel')),
60 ),
61 FilledButton(
62 onPressed: () => Navigator.pop(ctx, true),
63 child: Text(ctx.tr('connect')),
64 ),
65 ],
66 ),
67 );
68 if (allowed != true || !c.vault.unlocked || !context.mounted) return;
69 final credential = profile.auth == AuthMethod.password
70 ? (password.text.isEmpty ? stored ?? '' : password.text)
71 : stored ?? '';
72 await c.ssh.connect(
73 profile,
74 credential: credential,
75 passphrase: passphrase.text.isEmpty ? null : passphrase.text,
76 prompt: (endpoint, type, fingerprint) async {
77 if (!context.mounted || !c.vault.unlocked) return false;
78 return await showDialog<bool>(
79 context: context,
80 barrierDismissible: false,
81 builder: (ctx) => AlertDialog(
82 title: Text(ctx.tr('hostTrustTitle')),
83 content: SingleChildScrollView(
84 child: Column(
85 mainAxisSize: MainAxisSize.min,
86 crossAxisAlignment: CrossAxisAlignment.start,
87 children: [
88 Text(ctx.tr('hostTrustBody')),
89 const SizedBox(height: 16),
90 SelectableText('$endpoint\n$type\n$fingerprint'),
91 ],
92 ),
93 ),
94 actions: [
95 TextButton(
96 onPressed: () => Navigator.pop(ctx, false),
97 child: Text(ctx.tr('cancel')),
98 ),
99 FilledButton(
100 onPressed: () => Navigator.pop(ctx, true),
101 child: Text(ctx.tr('trust')),
102 ),
103 ],
104 ),
105 ) ??
106 false;
107 },
108 );
109 } catch (e) {
110 if (context.mounted) showFailure(context, e);
111 } finally {
112 password.clear();
113 passphrase.clear();
114 Future.delayed(const Duration(seconds: 1), () {
115 password.dispose();
116 passphrase.dispose();
117 });
118 }
121class ServersPage extends StatefulWidget {
122 final AppController controller;
123 final VoidCallback onConnected;
124 // Owned by the caller when the search must outlive this page, for example
125 // when a fold hides the list and shows it again.
126 final TextEditingController? search;
127 const ServersPage(
128 this.controller, {
129 required this.onConnected,
130 this.search,
131 super.key,
132 });
133 @override
134 State<ServersPage> createState() => _ServersPageState();
137class _ServersPageState extends State<ServersPage> {
138 late final search = widget.search ?? TextEditingController();
139 @override
140 void dispose() {
141 if (widget.search == null) search.dispose();
142 super.dispose();
143 }
145 @override
146 Widget build(BuildContext context) {
147 final c = widget.controller;
148 final query = search.text;
149 final profiles = c.vault.profiles
150 .where(
151 (p) => '${p.name} ${p.host} ${p.username}'.toLowerCase().contains(
152 query.toLowerCase(),
153 ),
154 )
155 .toList();
156 return LayoutBuilder(
157 builder: (context, constraints) {
158 final pad = readablePadding(constraints, maxWidth: 900, gutter: 16);
159 return MinHeight(
160 minHeight: 200,
161 child: Column(
162 children: [
163 Padding(
164 padding: pad,
165 child: Row(
166 children: [
167 Expanded(
168 child: TextField(
169 controller: search,
170 decoration: InputDecoration(
171 labelText: context.tr('search'),
172 prefixIcon: const Icon(Icons.search),
173 ),
174 onChanged: (_) => setState(() {}),
175 ),
176 ),
177 const SizedBox(width: 10),
178 IconButton.filled(
179 onPressed: () => Navigator.push(
180 context,
181 MaterialPageRoute(builder: (_) => ProfileEditor(c)),
182 ),
183 tooltip: context.tr('addServer'),
184 icon: const Icon(Icons.add),
185 ),
186 // Desktop-compatible JSON, also available in Settings.
187 PopupMenuButton<String>(
188 tooltip: context.tr('profileTransfer'),
189 icon: const Icon(Icons.import_export),
190 onSelected: (action) async {
191 try {
192 if (action == 'importProfiles') {
193 await importProfiles(context, c);
194 } else {
195 await exportProfiles(c);
196 }
197 } catch (e) {
198 if (context.mounted) showFailure(context, e);
199 }
200 },
201 itemBuilder: (ctx) => [
202 for (final (value, icon) in [
203 ('importProfiles', Icons.file_open),
204 ('exportProfiles', Icons.ios_share),
205 ])
206 PopupMenuItem(
207 value: value,
208 child: ListTile(
209 leading: Icon(icon),
210 title: Text(ctx.tr(value)),
211 contentPadding: EdgeInsets.zero,
212 ),
213 ),
214 ],
215 ),
216 ],
217 ),
218 ),
219 Expanded(
220 child: profiles.isEmpty
221 ? const PageMessage('emptyServers')
222 : ListView.builder(
223 padding: pad.copyWith(top: 0),
224 itemCount: profiles.length,
225 itemBuilder: (context, i) {
226 final p = profiles[i];
227 final active =
228 c.ssh.profile?.id == p.id &&
229 (c.ssh.connected || c.ssh.connecting);
230 return Card(
231 child: ListTile(
232 leading: Icon(
233 Icons.dns_outlined,
234 color: active
235 ? const Color(0xff43d6a0)
236 : Theme.of(context).colorScheme.primary,
237 ),
238 title: Text(p.name),
239 subtitle: Text(
240 '${p.username}@${p.host}:${p.port}\n${context.tr(active
241 ? c.ssh.connecting
242 ? 'connecting'
243 : 'connected'
244 : 'disconnected')}',
245 ),
246 isThreeLine: true,
247 onTap: c.ssh.connecting
248 ? null
249 : () async {
250 await connectProfile(context, c, p);
251 if (c.ssh.connected) widget.onConnected();
252 },
253 trailing: PopupMenuButton<String>(
254 onSelected: (action) async {
255 try {
256 if (action == 'edit') {
257 await Navigator.push(
258 context,
259 MaterialPageRoute(
260 builder: (_) =>
261 ProfileEditor(c, profile: p),
262 ),
263 );
264 }
265 if (action == 'duplicate') {
266 await c.vault.save(
267 ServerProfile(
268 name: p.name,
269 host: p.host,
270 port: p.port,
271 username: p.username,
272 ),
273 );
274 c.changed();
275 }
276 if (action == 'delete' &&
277 context.mounted &&
278 await confirm(
279 context,
280 'confirmDelete',
281 detail: p.name,
282 )) {
283 if (c.ssh.profile?.id == p.id) {
284 c.ssh.disconnect(clear: true);
285 }
286 await c.vault.deleteProfile(p.id);
287 c.changed();
288 }
289 } catch (e) {
290 if (context.mounted) {
291 showFailure(context, e);
292 }
293 }
294 },
295 itemBuilder: (ctx) =>
296 ['edit', 'duplicate', 'delete']
297 .map(
298 (v) => PopupMenuItem(
299 value: v,
300 child: Text(ctx.tr(v)),
301 ),
302 )
303 .toList(),
304 ),
305 ),
306 );
307 },
308 ),
309 ),
310 ],
311 ),
312 );
313 },
314 );
315 }
SHA-256e61002750211c94a3d74cce5c282893840b9fbd2d703d7350f6c673fe004d682

CH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.

SHA-256 ng archive46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Mga file