CH-J SM Mobilepengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J SM Mobile

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / SM Mobile 175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a Ditambah: · Dibina:
workspace.dart Ditambah: Tarikh tidak direkodkan 12.4 KB · 357 baris
Kod Muat turun fail
1import 'dart:math' as math;
2import 'package:flutter/material.dart';
3import '../core/app_controller.dart';
4import '../l10n/strings.dart';
5import 'adaptive.dart';
6import 'diagnostics_page.dart';
7import 'editor/viewers.dart';
8import '../services/editor/sources.dart';
9import 'files_page.dart';
10import 'monitor_page.dart';
11import 'servers_page.dart';
12import 'settings_page.dart';
13import 'terminal_page.dart';
14import 'theme.dart';
16class Workspace extends StatefulWidget {
17 final AppController controller;
18 const Workspace(this.controller, {super.key});
19 @override
20 State<Workspace> createState() => _WorkspaceState();
23class _WorkspaceState extends State<Workspace> {
24 int index = 0;
25 // The tool shown next to the server list in two panes.
26 int lastTool = 1;
27 // Stable slots: a page keeps its state (SFTP directory, search, terminal
28 // focus) when a resize, rotation or fold moves it to another place.
29 final serversSlot = GlobalKey(debugLabel: 'servers');
30 final toolSlot = GlobalKey(debugLabel: 'tool');
31 final serverSearch = TextEditingController();
32 @override
33 void dispose() {
34 serverSearch.dispose();
35 super.dispose();
36 }
38 static const names = [
39 'servers',
40 'terminal',
41 'files',
42 'monitor',
43 'diagnostics',
44 'settings',
45 ];
46 static const icons = [
47 Icons.dns_outlined,
48 Icons.terminal,
49 Icons.folder_outlined,
50 Icons.monitor_heart_outlined,
51 Icons.network_check,
52 Icons.settings_outlined,
53 ];
55 bool _openingIncoming = false;
57 /// Opens files other apps handed to SM Mobile, one at a time.
58 void _openIncoming() {
59 final c = widget.controller;
60 if (_openingIncoming || c.pendingIncoming.isEmpty) return;
61 _openingIncoming = true;
62 WidgetsBinding.instance.addPostFrameCallback((_) async {
63 try {
64 while (mounted && c.vault.unlocked && c.pendingIncoming.isNotEmpty) {
65 final doc = c.pendingIncoming.removeAt(0);
66 await openSource(context, c, DeviceSource(c.documents, doc));
67 }
68 } finally {
69 _openingIncoming = false;
70 }
71 });
72 }
74 void select(int v) => setState(() {
75 index = v;
76 if (v != 0) lastTool = v;
77 });
79 @override
80 Widget build(BuildContext context) => ListenableBuilder(
81 listenable: Listenable.merge([widget.controller.ssh, widget.controller]),
82 builder: (context, _) {
83 final c = widget.controller;
84 _openIncoming();
85 final layout = AdaptiveLayout.of(context);
86 final twoPane = layout.twoPane;
87 // In two panes the server list is always visible, so it is not a
88 // destination of its own.
89 // The tool beside the list becomes the current page, so that folding
90 // back to one pane keeps showing it, with its state, instead of
91 // replacing it with the list.
92 if (twoPane && index == 0) index = lastTool;
93 final shown = index;
94 final destinations = [for (var i = twoPane ? 1 : 0; i < 6; i++) i];
95 final servers = KeyedSubtree(
96 key: serversSlot,
97 child: ServersPage(
98 c,
99 search: serverSearch,
100 onConnected: () {
101 if (mounted) select(1);
102 },
103 ),
104 );
105 final page = shown == 0
106 ? servers
107 : KeyedSubtree(
108 key: toolSlot,
109 child: switch (shown) {
110 1 => TerminalPage(
111 c,
112 key: ValueKey(c.ssh.connected ? c.ssh.client : null),
113 ),
114 2 => FilesPage(
115 c,
116 key: ValueKey(c.ssh.connected ? c.ssh.client : null),
117 ),
118 3 => MonitorPage(
119 c,
120 key: ValueKey(c.ssh.connected ? c.ssh.client : null),
121 ),
122 4 => DiagnosticsPage(c),
123 _ => SettingsPage(c),
124 },
125 );
126 final squeezed = keyboardSqueezed(context);
127 return Scaffold(
128 appBar: squeezed
129 ? null
130 : AppBar(
131 titleSpacing: 12,
132 leading: const Padding(
133 padding: EdgeInsets.only(left: 12),
134 child: Center(child: BrandMark()),
135 ),
136 leadingWidth: 48,
137 title: PageTitle('SM Mobile', context.tr(names[shown])),
138 actions: [
139 if (c.ssh.connecting)
140 IconButton(
141 onPressed: c.ssh.disconnect,
142 tooltip: context.tr('cancel'),
143 icon: const Icon(Icons.close),
144 ),
145 if (c.ssh.connected)
146 IconButton(
147 onPressed: c.ssh.disconnect,
148 tooltip: context.tr('disconnect'),
149 icon: const Icon(Icons.link_off),
150 ),
151 if (!c.ssh.connected &&
152 !c.ssh.connecting &&
153 c.ssh.profile != null)
154 IconButton(
155 onPressed: () =>
156 connectProfile(context, c, c.ssh.profile!),
157 tooltip: context.tr('reconnect'),
158 icon: const Icon(Icons.refresh),
159 ),
160 IconButton(
161 onPressed: c.lock,
162 tooltip: context.tr('lock'),
163 icon: const Icon(Icons.lock_outline),
164 ),
165 ],
166 ),
167 body: MinHeight(
168 minHeight: 120,
169 child: Column(
170 children: [
171 if (squeezed) SizedBox(height: MediaQuery.paddingOf(context).top),
172 if (c.ssh.connecting) const LinearProgressIndicator(),
173 if (c.ssh.error != null)
174 SafeArea(
175 top: false,
176 bottom: false,
177 child: MaterialBanner(
178 content: Text(context.tr(c.ssh.error!)),
179 actions: [
180 TextButton(
181 onPressed: () {
182 c.ssh.error = null;
183 c.changed();
184 },
185 child: Text(context.tr('done')),
186 ),
187 ],
188 ),
189 ),
190 if (c.ssh.connected && !squeezed)
191 SafeArea(
192 top: false,
193 bottom: false,
194 child: Padding(
195 padding: const EdgeInsets.symmetric(
196 horizontal: 16,
197 vertical: 4,
198 ),
199 child: Row(
200 children: [
201 const Icon(
202 Icons.circle,
203 color: Color(0xff43d6a0),
204 size: 8,
205 ),
206 const SizedBox(width: 8),
207 Expanded(
208 child: Text(
209 '${c.ssh.profile!.name} · ${context.tr('connected')}',
210 maxLines: 1,
211 overflow: TextOverflow.ellipsis,
212 ),
213 ),
214 ],
215 ),
216 ),
217 ),
218 Expanded(
219 child: Row(
220 crossAxisAlignment: CrossAxisAlignment.stretch,
221 children: [
222 if (layout.useRail)
223 SafeArea(
224 right: false,
225 child: _Rail(
226 selected: destinations.indexOf(shown),
227 onSelected: (i) => select(destinations[i]),
228 destinations: [
229 for (final i in destinations)
230 NavigationRailDestination(
231 icon: Icon(icons[i]),
232 label: Text(context.tr(names[i])),
233 ),
234 ],
235 ),
236 ),
237 Expanded(
238 child: SafeArea(
239 left: !layout.useRail,
240 bottom: layout.useRail,
241 child: FoldAwareSplit(
242 secondaryWidth:
243 layout.windowClass >= WindowClass.large
244 ? 400
245 : 340,
246 secondary: twoPane ? servers : null,
247 primary: page,
248 ),
249 ),
250 ),
251 ],
252 ),
253 ),
254 ],
255 ),
256 ),
257 bottomNavigationBar: layout.useRail
258 ? null
259 : clampNavigationText(
260 NavigationBar(
261 selectedIndex: math.min(destinations.indexOf(shown), 3),
262 onDestinationSelected: (v) async {
263 if (v < 3 || destinations.length == 4) {
264 select(destinations[v]);
265 return;
266 }
267 final chosen = await showModalBottomSheet<int>(
268 context: context,
269 isScrollControlled: true,
270 builder: (ctx) => SafeArea(
271 child: SingleChildScrollView(
272 child: Column(
273 mainAxisSize: MainAxisSize.min,
274 children: [
275 for (final i in destinations.skip(3))
276 ListTile(
277 leading: Icon(icons[i]),
278 title: Text(ctx.tr(names[i])),
279 selected: i == shown,
280 onTap: () => Navigator.pop(ctx, i),
281 ),
282 ],
283 ),
284 ),
285 ),
286 );
287 if (chosen != null && mounted) select(chosen);
288 },
289 destinations: [
290 for (final i in destinations.take(3))
291 NavigationDestination(
292 icon: Icon(icons[i]),
293 label: context.tr(names[i]),
294 ),
295 if (destinations.length == 4)
296 NavigationDestination(
297 icon: Icon(icons[destinations[3]]),
298 label: context.tr(names[destinations[3]]),
299 )
300 else
301 NavigationDestination(
302 icon: Icon(
303 destinations.indexOf(shown) >= 3
304 ? icons[shown]
305 : Icons.more_horiz,
306 ),
307 label: context.tr(
308 destinations.indexOf(shown) >= 3
309 ? names[shown]
310 : 'settings',
311 ),
312 ),
313 ],
314 ),
315 ),
316 );
317 },
318 );
321/// Navigation rail that keeps every destination reachable in short windows:
322/// labels are hidden when they do not fit, and the rail scrolls as a last
323/// resort.
324class _Rail extends StatelessWidget {
325 final int selected;
326 final ValueChanged<int> onSelected;
327 final List<NavigationRailDestination> destinations;
328 const _Rail({
329 required this.selected,
330 required this.onSelected,
331 required this.destinations,
332 });
333 @override
334 Widget build(BuildContext context) => clampNavigationText(
335 LayoutBuilder(
336 builder: (context, constraints) {
337 final scale = MediaQuery.textScalerOf(context).scale(1);
338 final withLabels = destinations.length * 72 * scale + 16;
339 final labels = constraints.maxHeight >= withLabels;
340 final needed = labels ? withLabels : destinations.length * 56.0 + 16;
341 return SingleChildScrollView(
342 child: SizedBox(
343 height: math.max(constraints.maxHeight, needed),
344 child: NavigationRail(
345 selectedIndex: selected < 0 ? null : selected,
346 onDestinationSelected: onSelected,
347 labelType: labels
348 ? NavigationRailLabelType.all
349 : NavigationRailLabelType.none,
350 destinations: destinations,
351 ),
352 ),
353 );
354 },
355 ),
356 );
SHA-256769650c68c22178ad9ff4a0732e76d806deaa0de949e53949a22c7a073197220

CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.

SHA-256 arkib46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Fail