CH-J SM MobilePalvelimen hallinta yli SSH
Valikko
Julkaistu lähde

CH-J SM Mobile

Selaile hakemistoja ja tiedostoja tietyn sovelluksen julkaisua varten.

Lataa lähde ZIP
CH-J / SM Mobile 134 tiedostot
sm-mobile-20261016T050549Z-3678f2a05fa9 Lisätty: · Rakennettu:
Tiedostot
servers_page.dart Lisätty: Kirjaamaton päivä 9,3 KB · 257 rivit
Lataa tiedosto
1import 'package:flutter/material.dart';
2import '../core/app_controller.dart';
3import '../l10n/strings.dart';
4import '../models/server_profile.dart';
5import 'common.dart';
6import 'profile_editor.dart';
8Future<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.password
68 ? (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 }
119class ServersPage extends StatefulWidget {
120 final AppController controller;
121 final VoidCallback onConnected;
122 const ServersPage(this.controller, {required this.onConnected, super.key});
123 @override
124 State<ServersPage> createState() => _ServersPageState();
127class _ServersPageState extends State<ServersPage> {
128 String query = '';
129 @override
130 Widget build(BuildContext context) {
131 final c = widget.controller;
132 final profiles = c.vault.profiles
133 .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.isEmpty
168 ? 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: active
182 ? 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(active
188 ? c.ssh.connecting
189 ? 'connecting'
190 : 'connected'
191 : 'disconnected')}',
192 ),
193 isThreeLine: true,
194 onTap: c.ssh.connecting
195 ? null
196 : () 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 }
SHA-256c1899099e1f227e3b95fc5e482550809429a74237f49ea62c77c7060c91e6bf4

CH-J Proprietary Software License 1.14 · Lähde ilmoitetaan CH-J Proprietary Software License 1.14-koodilla. Sen saatavuus ei muuta lisenssiehtoja eikä myönnä lisäoikeuksia.

Arkisto SHA-256c486a807e4828998b9c57ab0f2e760f7980b31965fe118e89d3e1a5a2b65be65