Sumber yang diterbitkan
Muat turun ZIP sumber
CH-J SM Mobile
Layari direktori dan fail untuk keluaran aplikasi tertentu.
CH-J / SM Mobile
175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a
Ditambah: · Dibina:
1
import 'dart:math' as math;2
import 'package:flutter/material.dart';3
import '../core/app_controller.dart';4
import '../l10n/strings.dart';5
import 'adaptive.dart';6
import 'diagnostics_page.dart';7
import 'editor/viewers.dart';8
import '../services/editor/sources.dart';9
import 'files_page.dart';10
import 'monitor_page.dart';11
import 'servers_page.dart';12
import 'settings_page.dart';13
import 'terminal_page.dart';14
import 'theme.dart';16
class Workspace extends StatefulWidget {17
final AppController controller;18
const Workspace(this.controller, {super.key});19
@override20
State<Workspace> createState() => _WorkspaceState();21
}23
class _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, terminal28
// 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
@override33
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
@override80
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 a88
// destination of its own.89
// The tool beside the list becomes the current page, so that folding90
// back to one pane keeps showing it, with its state, instead of91
// 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 == 0106
? servers107
: 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: squeezed129
? null130
: 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.large244
? 400245
: 340,246
secondary: twoPane ? servers : null,247
primary: page,248
),249
),250
),251
],252
),253
),254
],255
),256
),257
bottomNavigationBar: layout.useRail258
? null259
: 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
else301
NavigationDestination(302
icon: Icon(303
destinations.indexOf(shown) >= 3304
? icons[shown]305
: Icons.more_horiz,306
),307
label: context.tr(308
destinations.indexOf(shown) >= 3309
? names[shown]310
: 'settings',311
),312
),313
],314
),315
),316
);317
},318
);319
}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 last323
/// resort.324
class _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
@override334
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: labels348
? NavigationRailLabelType.all349
: NavigationRailLabelType.none,350
destinations: destinations,351
),352
),353
);354
},355
),356
);357
}SHA-256
769650c68c22178ad9ff4a0732e76d806deaa0de949e53949a22c7a073197220CH-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 arkib
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655