ซอร์สที่เผยแพร่
ดาวน์โหลด ZIP ของซอร์ส
CH-J SM Mobile
เรียกดูไดเรกทอรีและไฟล์ของรุ่นแอปพลิเคชันที่ระบุ
CH-J / SM Mobile
175 ไฟล์
sm-mobile-20261016T185235Z-8e93ecc1b59a
วันที่เพิ่ม: · วันที่บิลด์:
1
import 'dart:async';2
import 'dart:convert';3
import 'dart:io';4
import 'dart:math' as math;5
import 'package:cryptography/cryptography.dart';6
import 'package:flutter/material.dart';7
import 'package:flutter/services.dart';8
import 'package:flutter_svg/flutter_svg.dart';9
import 'package:intl/intl.dart' show DateFormat;10
import 'package:path_provider/path_provider.dart';11
import '../../core/app_controller.dart';12
import '../../core/failure.dart';13
import '../../l10n/strings.dart';14
import '../../models/server_profile.dart';15
import '../../services/editor/inspect.dart';16
import '../../services/editor/languages.dart';17
import '../../services/editor/sources.dart';18
import '../../services/metrics_format.dart';19
import '../../services/sftp_service.dart';20
import '../../services/ssh_service.dart';21
import '../../services/vault.dart';22
import '../common.dart';23
import '../export_sheet.dart';24
import 'editor_page.dart';25
import 'save_as.dart';27
/// Opens [source] with the right tool: the editor for text, a viewer for28
/// everything else. Nothing is executed: HTML and SVG are never run as29
/// pages, and documents are shown by the system viewer from a private30
/// temporary copy.31
Future<void> openSource(32
BuildContext context,33
AppController c,34
FileSource source, {35
Draft? draft,36
}) async {37
unawaited(recordRecent(c, source));38
final kind = draft != null ? FileKind.text : fileKind(source.name);39
final page = kind == FileKind.text40
? EditorPage(c, source, draft: draft)41
: ViewerPage(c, source, kind: kind);42
await Navigator.push(context, MaterialPageRoute(builder: (_) => page));43
}45
class ViewerPage extends StatefulWidget {46
final AppController controller;47
final FileSource source;48
final FileKind kind;49
const ViewerPage(50
this.controller,51
this.source, {52
required this.kind,53
super.key,54
});55
@override56
State<ViewerPage> createState() => _ViewerPageState();57
}59
class _ViewerPageState extends State<ViewerPage> {60
Uint8List? bytes;61
String? error;62
late FileKind kind = widget.kind;63
AppController get c => widget.controller;65
@override66
void initState() {67
super.initState();68
unawaited(_load());69
}71
Future<void> _load() async {72
try {73
final b = await widget.source.load(maxBytes: maxViewBytes);74
if (!mounted) return;75
setState(() {76
bytes = b;77
if (kind == FileKind.text) kind = fileKind(widget.source.name, head: b);78
});79
} catch (e) {80
if (mounted) setState(() => error = failureCode(e));81
}82
}84
void _asText() => Navigator.pushReplacement(85
context,86
MaterialPageRoute(87
builder: (_) => EditorPage(c, widget.source, bytes: bytes),88
),89
);91
Future<String> _tempCopy() async {92
final temp = await getTemporaryDirectory();93
final dir = await Directory('${temp.path}/sm-transfer-${newId()}').create();94
final file = File('${dir.path}/${safeName(widget.source.name)}');95
await file.writeAsBytes(bytes!, flush: true);96
return file.path;97
}99
/// The system viewer (Quick Look on iOS, an app the user picks on100
/// Android) shows a private temporary copy; the copy is removed when the101
/// Vault locks or the app restarts.102
Future<void> _system() async {103
try {104
final path = await _tempCopy();105
final shown = await c.withSystemDialog(106
() => c.documents.preview(path, mime: mimeOf(widget.source.name)),107
);108
if (!shown && mounted) {109
showFailure(context, const Failure('noViewer'));110
}111
} catch (e) {112
if (mounted) showFailure(context, e);113
}114
}116
Future<void> _share() async {117
final box = context.findRenderObject() as RenderBox?;118
final o = box?.localToGlobal(Offset.zero) ?? Offset.zero;119
try {120
await shareBytes(121
c,122
widget.source.name,123
bytes!,124
Rect.fromLTWH(o.dx, o.dy, 1, 1),125
);126
} catch (e) {127
if (mounted) showFailure(context, e);128
}129
}131
Future<void> _saveCopy() async {132
try {133
await saveAsDialog(context, c, name: widget.source.name, bytes: bytes!);134
} catch (e) {135
if (mounted) showFailure(context, e);136
}137
}139
@override140
Widget build(BuildContext context) {141
final b = bytes;142
return Scaffold(143
appBar: AppBar(144
title: Column(145
crossAxisAlignment: CrossAxisAlignment.start,146
children: [147
Text(148
widget.source.name,149
maxLines: 1,150
overflow: TextOverflow.ellipsis,151
),152
Text(153
widget.source.location,154
maxLines: 1,155
overflow: TextOverflow.ellipsis,156
style: Theme.of(context).textTheme.bodySmall,157
),158
],159
),160
actions: [161
if (b != null)162
PopupMenuButton<String>(163
onSelected: (v) => switch (v) {164
'text' => _asText(),165
'system' => _system(),166
'share' => _share(),167
'save' => _saveCopy(),168
_ => null,169
},170
itemBuilder: (ctx) => [171
if (b.length <= maxEditableBytes)172
PopupMenuItem(173
value: 'text',174
child: Text(ctx.tr('openAsText')),175
),176
PopupMenuItem(177
value: 'system',178
child: Text(ctx.tr('openInSystemViewer')),179
),180
PopupMenuItem(value: 'save', child: Text(ctx.tr('saveAs'))),181
PopupMenuItem(value: 'share', child: Text(ctx.tr('shareFile'))),182
],183
),184
],185
),186
body: SafeArea(187
child: error != null188
? PageMessage(error!, icon: Icons.error_outline)189
: b == null190
? const Center(child: CircularProgressIndicator())191
: switch (kind) {192
FileKind.image => _ImageView(b),193
FileKind.svg => _SvgView(b, onSource: _asText),194
FileKind.pdf when Platform.isAndroid => _PdfView(c, _tempCopy),195
FileKind.pdf ||196
FileKind.office ||197
FileKind.audio ||198
FileKind.video => _SystemView(kind, b.length, onOpen: _system),199
FileKind.archive => _ArchiveView(widget.source.name, b),200
FileKind.certificate => _CryptoView(b, onText: _asText),201
FileKind.notebook => _NotebookView(b, onJson: _asText),202
FileKind.binary ||203
FileKind.text => _HexView(b, onText: _asText),204
},205
),206
);207
}208
}210
class _Info extends StatelessWidget {211
final IconData icon;212
final String title;213
final String? detail;214
final List<Widget> actions;215
const _Info(this.icon, this.title, {this.detail, this.actions = const []});216
@override217
Widget build(BuildContext context) => Center(218
child: SingleChildScrollView(219
padding: const EdgeInsets.all(24),220
child: ConstrainedBox(221
constraints: const BoxConstraints(maxWidth: 520),222
child: Column(223
mainAxisSize: MainAxisSize.min,224
children: [225
Icon(icon, size: 56, color: Theme.of(context).colorScheme.primary),226
const SizedBox(height: 16),227
Text(228
title,229
textAlign: TextAlign.center,230
style: Theme.of(context).textTheme.titleMedium,231
),232
if (detail != null) ...[233
const SizedBox(height: 8),234
Text(detail!, textAlign: TextAlign.center),235
],236
const SizedBox(height: 16),237
Wrap(238
spacing: 8,239
runSpacing: 8,240
alignment: WrapAlignment.center,241
children: actions,242
),243
],244
),245
),246
),247
);248
}250
class _ImageView extends StatelessWidget {251
final Uint8List bytes;252
const _ImageView(this.bytes);253
@override254
Widget build(BuildContext context) => InteractiveViewer(255
maxScale: 12,256
child: Center(257
child: Image.memory(258
bytes,259
filterQuality: FilterQuality.medium,260
errorBuilder: (ctx, _, _) =>261
_Info(Icons.broken_image_outlined, ctx.tr('imageUnsupported')),262
),263
),264
);265
}267
/// SVG is drawn as a picture: no scripts, no animation, no external268
/// resources, no links.269
class _SvgView extends StatelessWidget {270
final Uint8List bytes;271
final VoidCallback onSource;272
const _SvgView(this.bytes, {required this.onSource});273
@override274
Widget build(BuildContext context) => Column(275
children: [276
MaterialBanner(277
content: Text(context.tr('svgSafe')),278
actions: [279
TextButton(280
onPressed: onSource,281
child: Text(context.tr('openAsText')),282
),283
],284
),285
Expanded(286
child: InteractiveViewer(287
maxScale: 12,288
child: Center(289
child: SvgPicture.memory(290
bytes,291
errorBuilder: (ctx, _, _) => _Info(292
Icons.broken_image_outlined,293
ctx.tr('imageUnsupported'),294
),295
),296
),297
),298
),299
],300
);301
}303
class _SystemView extends StatelessWidget {304
final FileKind kind;305
final int size;306
final VoidCallback onOpen;307
const _SystemView(this.kind, this.size, {required this.onOpen});308
@override309
Widget build(BuildContext context) => _Info(310
switch (kind) {311
FileKind.audio => Icons.audiotrack,312
FileKind.video => Icons.movie_outlined,313
FileKind.pdf => Icons.picture_as_pdf_outlined,314
_ => Icons.description_outlined,315
},316
context.tr('systemViewerTitle'),317
detail:318
'${context.tr(Platform.isIOS ? 'systemViewerIos' : 'systemViewerAndroid')}\n'319
'${context.tr('size')}: ${formatBytes(size, (v, d) => v.toStringAsFixed(d))}',320
actions: [321
FilledButton.icon(322
onPressed: onOpen,323
icon: const Icon(Icons.open_in_new),324
label: Text(context.tr('openInSystemViewer')),325
),326
],327
);328
}330
/// Android: PDF pages rendered by the system PdfRenderer.331
class _PdfView extends StatefulWidget {332
final AppController controller;333
final Future<String> Function() copy;334
const _PdfView(this.controller, this.copy);335
@override336
State<_PdfView> createState() => _PdfViewState();337
}339
class _PdfViewState extends State<_PdfView> {340
String? path, error;341
int pages = 0;342
final cache = <int, Future<Uint8List>>{};343
@override344
void initState() {345
super.initState();346
unawaited(() async {347
try {348
final p = await widget.copy();349
final n = await widget.controller.documents.pdfPageCount(p);350
if (mounted) {351
setState(() {352
path = p;353
pages = n;354
});355
}356
} catch (e) {357
if (mounted) setState(() => error = failureCode(e));358
}359
}());360
}362
@override363
Widget build(BuildContext context) {364
if (error != null) return PageMessage(error!, icon: Icons.error_outline);365
if (path == null) return const Center(child: CircularProgressIndicator());366
return LayoutBuilder(367
builder: (context, constraints) {368
final width =369
(constraints.maxWidth * MediaQuery.devicePixelRatioOf(context))370
.clamp(400, 2400)371
.round();372
return InteractiveViewer(373
maxScale: 6,374
child: ListView.builder(375
itemCount: pages,376
itemBuilder: (context, i) => Padding(377
padding: const EdgeInsets.all(8),378
child: FutureBuilder<Uint8List>(379
future: cache[i] ??= widget.controller.documents.pdfPage(380
path!,381
i,382
width,383
),384
builder: (context, snap) => snap.hasData385
? Image.memory(snap.data!)386
: AspectRatio(387
aspectRatio: 1 / 1.414,388
child: Center(389
child: snap.hasError390
? Text(context.tr('fileError'))391
: const CircularProgressIndicator(),392
),393
),394
),395
),396
),397
);398
},399
);400
}401
}403
class _ArchiveView extends StatelessWidget {404
final String name;405
final Uint8List bytes;406
const _ArchiveView(this.name, this.bytes);407
@override408
Widget build(BuildContext context) {409
final ArchiveListing listing;410
try {411
listing = listArchive(name, bytes);412
} on ArchiveUnsupported catch (e) {413
return _Info(414
Icons.archive_outlined,415
e.format == 'tooLarge'416
? context.tr('archiveTooLarge')417
: '${context.tr('archiveUnsupported')} (${e.format})',418
);419
} catch (_) {420
return _Info(Icons.archive_outlined, context.tr('archiveDamaged'));421
}422
String size(int v) => formatBytes(v, (x, d) => x.toStringAsFixed(d));423
final date = DateFormat.yMd(424
Localizations.localeOf(context).toString(),425
).add_Hm();426
final files = listing.entries.where((e) => !e.directory).toList();427
return Column(428
crossAxisAlignment: CrossAxisAlignment.stretch,429
children: [430
ListTile(431
title: Text(listing.format),432
subtitle: Text(433
'${files.length} ${context.tr('filesCount')} · '434
'${size(files.fold(0, (a, e) => a + e.size))}'435
'${listing.uncompressed == null ? '' : ' · ${context.tr('uncompressed')}: ${size(listing.uncompressed!)}'}'436
'\n${context.tr('archiveListOnly')}',437
),438
),439
const Divider(height: 1),440
Expanded(441
child: ListView.builder(442
itemCount: listing.entries.length,443
itemBuilder: (context, i) {444
final e = listing.entries[i];445
return ListTile(446
dense: true,447
leading: Icon(448
e.directory449
? Icons.folder_outlined450
: Icons.insert_drive_file_outlined,451
),452
title: Text(e.name),453
subtitle: e.directory454
? null455
: Text(456
[457
size(e.size),458
if (e.modified != null)459
date.format(e.modified!.toLocal()),460
].join(' · '),461
),462
);463
},464
),465
),466
],467
);468
}469
}471
class _CryptoView extends StatelessWidget {472
final Uint8List bytes;473
final VoidCallback onText;474
const _CryptoView(this.bytes, {required this.onText});475
@override476
Widget build(BuildContext context) => FutureBuilder<List<CryptoItem>>(477
future: inspectCrypto(bytes),478
builder: (context, snap) {479
if (!snap.hasData) {480
return snap.hasError481
? _Info(Icons.key_off, context.tr('cryptoUnreadable'))482
: const Center(child: CircularProgressIndicator());483
}484
final items = snap.data!;485
return ListView(486
padding: const EdgeInsets.all(16),487
children: [488
Text(489
context.tr('cryptoNote'),490
style: Theme.of(context).textTheme.bodySmall,491
),492
const SizedBox(height: 8),493
if (items.isEmpty) Text(context.tr('cryptoUnreadable')),494
for (final item in items)495
Card(496
child: Padding(497
padding: const EdgeInsets.all(16),498
child: Column(499
crossAxisAlignment: CrossAxisAlignment.start,500
children: [501
Row(502
children: [503
Icon(504
item.secret ? Icons.vpn_key : Icons.verified_outlined,505
),506
const SizedBox(width: 8),507
Expanded(508
child: Text(509
item.type,510
style: Theme.of(context).textTheme.titleMedium,511
),512
),513
],514
),515
if (item.secret)516
Padding(517
padding: const EdgeInsets.only(top: 8),518
child: Text(519
context.tr('privateKeyHidden'),520
style: TextStyle(521
color: Theme.of(context).colorScheme.error,522
),523
),524
),525
for (final MapEntry(:key, :value) in item.fields.entries)526
Padding(527
padding: const EdgeInsets.only(top: 6),528
child: Column(529
crossAxisAlignment: CrossAxisAlignment.start,530
children: [531
Text(532
key,533
style: Theme.of(context).textTheme.bodySmall,534
),535
SelectableText(value),536
],537
),538
),539
],540
),541
),542
),543
const SizedBox(height: 8),544
OutlinedButton(545
onPressed: onText,546
child: Text(context.tr('openAsText')),547
),548
],549
);550
},551
);552
}554
class _NotebookView extends StatelessWidget {555
final Uint8List bytes;556
final VoidCallback onJson;557
const _NotebookView(this.bytes, {required this.onJson});558
@override559
Widget build(BuildContext context) {560
final List<NotebookCell> cells;561
try {562
cells = parseNotebook(563
String.fromCharCodes(bytes).contains('Ã')564
? const Utf8Decoder(allowMalformed: true).convert(bytes)565
: const Utf8Decoder(allowMalformed: true).convert(bytes),566
);567
} catch (_) {568
return _Info(569
Icons.data_object,570
context.tr('notebookUnreadable'),571
actions: [572
FilledButton(573
onPressed: onJson,574
child: Text(context.tr('openAsText')),575
),576
],577
);578
}579
final mono = TextStyle(580
fontFamily: Platform.isIOS ? 'Menlo' : 'monospace',581
fontSize: 13,582
);583
return ListView(584
padding: const EdgeInsets.all(12),585
children: [586
Align(587
alignment: AlignmentDirectional.centerEnd,588
child: TextButton(589
onPressed: onJson,590
child: Text(context.tr('openAsText')),591
),592
),593
for (final cell in cells)594
Card(595
child: Padding(596
padding: const EdgeInsets.all(12),597
child: Column(598
crossAxisAlignment: CrossAxisAlignment.stretch,599
children: [600
Text(601
cell.type,602
style: Theme.of(context).textTheme.labelSmall,603
),604
const SizedBox(height: 4),605
SelectableText(606
cell.source,607
style: cell.type == 'code' ? mono : null,608
),609
for (final o in cell.outputs) ...[610
const Divider(),611
SelectableText(o, style: mono),612
],613
for (final img in cell.images) ...[614
const Divider(),615
Image.memory(616
img,617
errorBuilder: (_, _, _) => const SizedBox.shrink(),618
),619
],620
],621
),622
),623
),624
],625
);626
}627
}629
/// Size, SHA-256 and a hexadecimal view of any file.630
class _HexView extends StatefulWidget {631
final Uint8List bytes;632
final VoidCallback onText;633
const _HexView(this.bytes, {required this.onText});634
@override635
State<_HexView> createState() => _HexViewState();636
}638
class _HexViewState extends State<_HexView> {639
String? sha256;640
@override641
void initState() {642
super.initState();643
unawaited(() async {644
final h = await Sha256().hash(widget.bytes);645
if (mounted) {646
setState(647
() => sha256 = h.bytes648
.map((b) => b.toRadixString(16).padLeft(2, '0'))649
.join(),650
);651
}652
}());653
}655
@override656
Widget build(BuildContext context) {657
final b = widget.bytes;658
final rows = (b.length / 16).ceil();659
final mono = TextStyle(660
fontFamily: Platform.isIOS ? 'Menlo' : 'monospace',661
fontSize: 12,662
fontFeatures: const [FontFeature.tabularFigures()],663
);664
return Column(665
crossAxisAlignment: CrossAxisAlignment.stretch,666
children: [667
ListTile(668
title: Text(context.tr('binaryFile')),669
subtitle: SelectableText(670
'${context.tr('size')}: ${b.length} B\nSHA-256: ${sha256 ?? '…'}',671
),672
trailing: b.length <= maxEditableBytes673
? TextButton(674
onPressed: widget.onText,675
child: Text(context.tr('openAsText')),676
)677
: null,678
),679
const Divider(height: 1),680
Expanded(681
child: Scrollbar(682
child: SingleChildScrollView(683
scrollDirection: Axis.horizontal,684
child: SizedBox(685
width: 640,686
child: ListView.builder(687
itemCount: rows,688
itemExtent: 20,689
itemBuilder: (context, r) {690
final start = r * 16, end = math.min(start + 16, b.length);691
final hex = StringBuffer(), text = StringBuffer();692
for (var i = start; i < start + 16; i++) {693
if (i < end) {694
hex.write('${b[i].toRadixString(16).padLeft(2, '0')} ');695
text.writeCharCode(696
b[i] >= 0x20 && b[i] < 0x7f ? b[i] : 0x2e,697
);698
} else {699
hex.write(' ');700
}701
if (i == start + 7) hex.write(' ');702
}703
return Padding(704
padding: const EdgeInsets.symmetric(horizontal: 12),705
child: Text(706
'${start.toRadixString(16).padLeft(8, '0')} $hex $text',707
style: mono,708
),709
);710
},711
),712
),713
),714
),715
),716
],717
);718
}719
}SHA-256
1e400e9d6a27416e05b6b75805e362c14893e6a8c20230a39126834a65f77fe7CH-J Proprietary Software License 1.14 · ซอร์สโค้ดนี้ให้ไว้ภายใต้ CH-J Proprietary Software License 1.14 การเปิดให้เข้าถึงไม่ได้เปลี่ยนแปลงเงื่อนไขของสัญญาอนุญาตหรือให้สิทธิ์เพิ่มเติม
SHA-256 ของไฟล์บีบอัด
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655