Nailathalang source
I-download ang source ZIP
CH-J SM Mobile
I-browse ang mga direktoryo at file ng isang partikular na release ng application.
CH-J / SM Mobile
175 file
sm-mobile-20261016T185235Z-8e93ecc1b59a
Idinagdag: · Binuo:
1
import 'dart:async';2
import 'package:flutter/material.dart';3
import 'package:flutter/services.dart';4
import 'package:re_editor/re_editor.dart';5
import 'package:re_highlight/styles/github-dark.dart';6
import 'package:re_highlight/styles/github.dart';7
import '../../core/app_controller.dart';8
import '../../core/failure.dart';9
import '../../l10n/strings.dart';10
import '../../services/editor/languages.dart';11
import '../../services/editor/sources.dart';12
import '../../services/editor/text_codec.dart';13
import '../../services/editor/validation.dart';14
import '../../services/ssh_service.dart';15
import '../../services/vault.dart';16
import '../common.dart';17
import 'find_panel.dart';18
import 'save_as.dart';20
/// Files above this size are edited without syntax colouring by default.21
const highlightLimit = 1024 * 1024;23
/// Folding by indentation for languages without braces (YAML, Python…):24
/// a block runs while the following lines are indented deeper.25
class IndentChunkAnalyzer implements CodeChunkAnalyzer {26
const IndentChunkAnalyzer();27
static int _indent(String s) {28
var n = 0;29
for (final c in s.codeUnits) {30
if (c == 0x20) {31
n++;32
} else if (c == 0x09) {33
n += 4;34
} else {35
break;36
}37
}38
return n;39
}41
@override42
List<CodeChunk> run(CodeLines codeLines) {43
final chunks = <CodeChunk>[];44
final n = codeLines.length;45
if (n > 200000) return chunks;46
final indents = List<int?>.generate(n, (i) {47
final t = codeLines[i].text;48
return t.trim().isEmpty ? null : _indent(t);49
});50
for (var i = 0; i < n; i++) {51
final base = indents[i];52
if (base == null) continue;53
var end = i, j = i + 1;54
while (j < n) {55
final d = indents[j];56
if (d != null) {57
if (d <= base) break;58
end = j;59
}60
j++;61
}62
if (end > i) chunks.add(CodeChunk(i, end + 1));63
}64
return chunks;65
}66
}68
/// What the editor holds besides the text.69
class _Format {70
TextEncoding encoding;71
LineBreak lineBreak;72
final bool mixed, exact;73
_Format(74
this.encoding,75
this.lineBreak, {76
this.mixed = false,77
this.exact = true,78
});79
}81
class EditorPage extends StatefulWidget {82
final AppController controller;83
final FileSource source;85
/// Already loaded content (viewers that offer "open as text").86
final Uint8List? bytes;88
/// A recovered draft: its text replaces the loaded content.89
final Draft? draft;90
const EditorPage(91
this.controller,92
this.source, {93
this.bytes,94
this.draft,95
super.key,96
});97
@override98
State<EditorPage> createState() => _EditorPageState();99
}101
class _EditorPageState extends State<EditorPage> with WidgetsBindingObserver {102
late FileSource source = widget.source;103
CodeLineEditingController? editing;104
CodeFindController? find;105
final scroll = CodeScrollController();106
final focus = FocusNode();107
Uint8List? original;108
_Format? format;109
Syntax syntax = plainText;110
bool loading = true, saving = false, dirty = false, highlight = true;111
String? error, notice;112
String? reloadBlocked;113
bool _registered = false;115
AppController get c => widget.controller;116
Map<String, dynamic> get prefs => c.editorPrefs;117
double get fontSize => (prefs['fontSize'] as num?)?.toDouble() ?? 14;118
bool get wrap => prefs['wrap'] == true;119
bool get tabs => prefs['tabs'] == true;120
int get indent => (prefs['indent'] as int?) ?? 2;121
bool get lineNumbers => prefs['lineNumbers'] != false;123
bool get readOnly => !source.canSave && source.kind != 'new';125
@override126
void initState() {127
super.initState();128
WidgetsBinding.instance.addObserver(this);129
c.draftSources.add(_draft);130
_registered = true;131
unawaited(_load());132
}134
@override135
void dispose() {136
// Stops the editor's cursor blink timer before its controller goes.137
focus.unfocus();138
WidgetsBinding.instance.removeObserver(this);139
if (_registered) c.draftSources.remove(_draft);140
editing?.dispose();141
find?.dispose();142
focus.dispose();143
super.dispose();144
}146
/// The unsaved buffer, kept encrypted across a lock.147
Draft? _draft() {148
final e = editing, f = format;149
if (!dirty || e == null || f == null) return null;150
return Draft({151
'source': source.toJson(),152
'name': source.name,153
'location': source.location,154
'text': e.text,155
'encoding': f.encoding.name,156
'lineBreak': f.lineBreak.name,157
'syntax': syntax.id,158
'savedAt': DateTime.now().toUtc().toIso8601String(),159
});160
}162
Future<void> _load({TextEncoding? encoding, bool asText = false}) async {163
setState(() {164
loading = true;165
error = null;166
});167
try {168
final restoring = widget.draft != null && editing == null;169
final Uint8List bytes;170
if (restoring) {171
// The draft text wins; the file is read only to know its current172
// state for the conflict check, and may be unreachable now.173
Uint8List? current;174
try {175
current = await source.load();176
} catch (_) {177
current = null;178
}179
bytes = current ?? Uint8List(0);180
encoding ??= TextEncoding.values181
.asNameMap()[widget.draft!.json['encoding']];182
if (current != null && looksBinary(current)) encoding = null;183
} else {184
bytes = original != null && encoding != null185
? original!186
: widget.bytes ?? await source.load();187
}188
if (bytes.length > maxEditableBytes) throw const Failure('fileTooLarge');189
if (!restoring && encoding == null && !asText && looksBinary(bytes)) {190
original = bytes;191
if (mounted) {192
setState(() {193
loading = false;194
error = 'binaryFile';195
});196
}197
return;198
}199
DecodedText decoded;200
try {201
decoded = await decodeInBackground(bytes, encoding: encoding);202
} on UnknownEncoding {203
if (restoring) {204
decoded = _empty(encoding);205
} else {206
original = bytes;207
if (!mounted) return;208
final chosen = await _chooseEncoding(unknown: true);209
if (chosen == null) {210
if (mounted) {211
setState(() {212
loading = false;213
error = 'unknownEncoding';214
});215
}216
return;217
}218
return await _load(encoding: chosen);219
}220
} on FormatException {221
if (restoring) {222
decoded = _empty(encoding);223
} else {224
original = bytes;225
if (mounted) {226
setState(() {227
loading = false;228
error = 'wrongEncoding';229
});230
}231
return;232
}233
}234
original = bytes;235
final draft = widget.draft;236
final text = draft != null && editing == null237
? draft.json['text'] as String238
: decoded.text;239
final f = _Format(240
draft == null || editing != null241
? decoded.encoding242
: TextEncoding.values.asNameMap()[draft.json['encoding']] ??243
decoded.encoding,244
draft == null || editing != null245
? decoded.lineBreaks.dominant246
: LineBreak.values.asNameMap()[draft.json['lineBreak']] ??247
decoded.lineBreaks.dominant,248
mixed: decoded.lineBreaks.mixed,249
exact: decoded.exact,250
);251
final lines = text.length > 256 * 1024252
? await text.codeLinesAsync253
: text.codeLines;254
if (!mounted) return;255
final previous = editing;256
final previousFind = find;257
final e = CodeLineEditingController(258
codeLines: lines,259
options: CodeLineOptions(260
lineBreak: _break(f.lineBreak),261
indentSize: indent,262
),263
);264
e.addListener(_changed);265
setState(() {266
editing = e;267
find = CodeFindController(e);268
format = f;269
syntax = draft != null && previous == null270
? syntaxById(draft.json['syntax'] as String? ?? '')271
: detectSyntax(272
source.kind == 'remote'273
? (source as RemoteSource).path274
: source.name,275
content: text.length > 4096 ? text.substring(0, 4096) : text,276
);277
highlight = bytes.length <= highlightLimit;278
dirty = draft != null && previous == null;279
loading = false;280
notice = f.mixed281
? 'mixedLineBreaks'282
: !f.exact283
? 'notExactRoundTrip'284
: null;285
});286
WidgetsBinding.instance.addPostFrameCallback((_) {287
previous?.dispose();288
previousFind?.dispose();289
});290
} catch (e) {291
if (mounted) {292
setState(() {293
loading = false;294
error = failureCode(e);295
});296
}297
}298
}300
static DecodedText _empty(TextEncoding? e) => DecodedText(301
'',302
e ?? TextEncoding.utf8,303
const LineBreakInfo(0, 0, 0),304
false,305
exact: true,306
);308
static TextLineBreak _break(LineBreak b) => switch (b) {309
LineBreak.lf => TextLineBreak.lf,310
LineBreak.crlf => TextLineBreak.crlf,311
LineBreak.cr => TextLineBreak.cr,312
};314
String? _lastText;315
void _changed() {316
final e = editing;317
if (e == null) return;318
// Selection moves notify too; only text changes make the file dirty.319
final lines = e.codeLines;320
final key = '${lines.length}:${identityHashCode(lines)}';321
if (key == _lastText) {322
setState(() {}); // cursor position in the status bar323
return;324
}325
final first = _lastText == null;326
_lastText = key;327
if (first) return;328
setState(() => dirty = true);329
}331
Future<TextEncoding?> _chooseEncoding({bool unknown = false}) =>332
showDialog<TextEncoding>(333
context: context,334
builder: (ctx) => SimpleDialog(335
title: Text(ctx.tr(unknown ? 'unknownEncoding' : 'reopenEncoding')),336
children: [337
if (unknown)338
Padding(339
padding: const EdgeInsets.symmetric(horizontal: 24),340
child: Text(ctx.tr('chooseEncodingAdvice')),341
),342
for (final e in TextEncoding.values)343
SimpleDialogOption(344
onPressed: () => Navigator.pop(ctx, e),345
child: Text(e.label),346
),347
],348
),349
);351
// ---------------------------------------------------------------------------352
// Saving354
Future<Uint8List?> _encoded() async {355
final e = editing!, f = format!;356
final text = e.text;357
try {358
return await encodeInBackground(text, f.encoding);359
} on UnencodableText catch (u) {360
if (!mounted) return null;361
final utf8 = await confirm(362
context,363
'unencodableTitle',364
detail:365
'${context.tr('unencodableBody')}\n'366
'${u.encoding.label}: "${u.sample}" ×${u.count}',367
);368
if (!utf8) return null;369
setState(() => f.encoding = TextEncoding.utf8);370
return encodeText(text, TextEncoding.utf8);371
}372
}374
Future<void> save({bool force = false, bool inPlace = false}) async {375
if (saving || editing == null) return;376
if (source.kind == 'new' || !source.canSave) return saveAs();377
setState(() => saving = true);378
try {379
final bytes = await _encoded();380
if (bytes == null) return;381
await source.save(bytes, force: force, inPlace: inPlace);382
original = bytes;383
if (!mounted) return;384
setState(() {385
dirty = false;386
notice = null;387
});388
ScaffoldMessenger.of(389
context,390
).showSnackBar(SnackBar(content: Text(context.tr('saved'))));391
} on Failure catch (e) {392
if (!mounted) return;393
switch (e.code) {394
case 'remoteChanged' || 'documentChanged' || 'remoteDeleted':395
await _conflict(e.code);396
case 'atomicNotPossible' || 'ownerWouldChange':397
final ok = await confirm(398
context,399
e.code,400
detail: context.tr('inPlaceAdvice'),401
);402
if (ok && mounted) {403
setState(() => saving = false);404
return save(force: true, inPlace: true);405
}406
case 'connectToSave' || 'disconnected':407
await _offline(e.code);408
default:409
showFailure(context, e);410
}411
} catch (e) {412
if (!mounted) return;413
final code = failureCode(e);414
if (code == 'networkError' || code == 'timeout') {415
await _offline(code);416
} else {417
showFailure(context, e);418
}419
} finally {420
if (mounted) setState(() => saving = false);421
}422
}424
/// The file changed elsewhere since it was opened.425
Future<void> _conflict(String code) async {426
final choice = await showDialog<String>(427
context: context,428
builder: (ctx) => AlertDialog(429
title: Text(ctx.tr(code)),430
content: Text(ctx.tr('conflictAdvice')),431
actions: [432
TextButton(433
onPressed: () => Navigator.pop(ctx),434
child: Text(ctx.tr('cancel')),435
),436
TextButton(437
onPressed: () => Navigator.pop(ctx, 'saveAs'),438
child: Text(ctx.tr('saveAs')),439
),440
TextButton(441
onPressed: () => Navigator.pop(ctx, 'reload'),442
child: Text(ctx.tr('reloadDiscard')),443
),444
FilledButton(445
onPressed: () => Navigator.pop(ctx, 'overwrite'),446
child: Text(ctx.tr('overwrite')),447
),448
],449
),450
);451
if (!mounted) return;452
switch (choice) {453
case 'overwrite':454
setState(() => saving = false);455
await save(force: true);456
case 'reload':457
await _reload(confirmDiscard: false);458
case 'saveAs':459
await saveAs();460
}461
}463
/// The server is not reachable: the text stays here, to retry later or464
/// to keep a copy on the device.465
Future<void> _offline(String code) async {466
final choice = await showDialog<String>(467
context: context,468
builder: (ctx) => AlertDialog(469
title: Text(ctx.tr(code)),470
content: Text(ctx.tr('offlineAdvice')),471
actions: [472
TextButton(473
onPressed: () => Navigator.pop(ctx),474
child: Text(ctx.tr('done')),475
),476
FilledButton(477
onPressed: () => Navigator.pop(ctx, 'device'),478
child: Text(ctx.tr('saveCopyToDevice')),479
),480
],481
),482
);483
if (choice == 'device' && mounted) await saveAs(target: 'device');484
}486
Future<void> saveAs({String? target}) async {487
final e = editing;488
if (e == null) return;489
final bytes = await _encoded();490
if (bytes == null || !mounted) return;491
final next = await saveAsDialog(492
context,493
c,494
name: source.name,495
bytes: bytes,496
target: target,497
);498
if (next == null || !mounted) return;499
setState(() {500
source = next;501
dirty = false;502
original = bytes;503
});504
await recordRecent(c, next);505
if (mounted) {506
ScaffoldMessenger.of(507
context,508
).showSnackBar(SnackBar(content: Text(context.tr('saved'))));509
}510
}512
Future<void> _reload({bool confirmDiscard = true}) async {513
if (confirmDiscard && dirty) {514
final ok = await confirm(context, 'discardChanges');515
if (!ok) return;516
}517
original = null;518
_lastText = null;519
final e = editing;520
editing = null;521
e?.removeListener(_changed);522
await _load();523
}525
Future<void> _reopenWith() async {526
if (dirty) {527
final ok = await confirm(context, 'discardChanges');528
if (!ok || !mounted) return;529
}530
final chosen = await _chooseEncoding();531
if (chosen == null) return;532
_lastText = null;533
editing?.removeListener(_changed);534
await _load(encoding: chosen);535
}537
Future<void> _saveWith() async {538
final chosen = await showDialog<TextEncoding>(539
context: context,540
builder: (ctx) => SimpleDialog(541
title: Text(ctx.tr('saveEncoding')),542
children: [543
for (final e in TextEncoding.values)544
SimpleDialogOption(545
onPressed: () => Navigator.pop(ctx, e),546
child: Text(e.label),547
),548
],549
),550
);551
if (chosen == null || chosen == format!.encoding) return;552
setState(() {553
format!.encoding = chosen;554
dirty = true;555
});556
}558
Future<void> _lineBreak() async {559
final chosen = await showDialog<LineBreak>(560
context: context,561
builder: (ctx) => SimpleDialog(562
title: Text(ctx.tr('lineBreaks')),563
children: [564
for (final b in LineBreak.values)565
SimpleDialogOption(566
onPressed: () => Navigator.pop(ctx, b),567
child: Text(b.label),568
),569
],570
),571
);572
if (chosen == null || chosen == format!.lineBreak) return;573
final e = editing!;574
final value = e.value;575
final next = CodeLineEditingController(576
codeLines: value.codeLines,577
options: CodeLineOptions(lineBreak: _break(chosen), indentSize: indent),578
);579
next.selection = value.selection;580
e.removeListener(_changed);581
next.addListener(_changed);582
final oldFind = find;583
setState(() {584
editing = next;585
find = CodeFindController(next);586
format!.lineBreak = chosen;587
dirty = true;588
notice = null;589
});590
WidgetsBinding.instance.addPostFrameCallback((_) {591
e.dispose();592
oldFind?.dispose();593
});594
}596
// ---------------------------------------------------------------------------597
// Tools599
Future<void> _goTo() async {600
final e = editing!;601
final field = TextEditingController();602
final result = await showDialog<String>(603
context: context,604
builder: (ctx) => AlertDialog(605
title: Text(ctx.tr('goToLine')),606
content: TextField(607
controller: field,608
autofocus: true,609
keyboardType: TextInputType.text,610
decoration: InputDecoration(611
hintText: '${ctx.tr('line')}[:${ctx.tr('column')}]',612
helperText: '1–${e.lineCount}',613
),614
onSubmitted: (v) => Navigator.pop(ctx, v),615
),616
actions: [617
TextButton(618
onPressed: () => Navigator.pop(ctx),619
child: Text(ctx.tr('cancel')),620
),621
FilledButton(622
onPressed: () => Navigator.pop(ctx, field.text),623
child: Text(ctx.tr('goToLine')),624
),625
],626
),627
);628
Future.delayed(const Duration(seconds: 1), field.dispose);629
if (result == null) return;630
final parts = result.trim().split(RegExp(r'[:,\s]+'));631
final line = int.tryParse(parts.first);632
if (line == null) return;633
final column = parts.length > 1 ? int.tryParse(parts[1]) ?? 1 : 1;634
_moveTo(line, column);635
}637
void _moveTo(int line, int column) {638
final e = editing!;639
final index = (line - 1).clamp(0, e.lineCount - 1);640
final text = e.codeLines[index].text;641
final offset = (column - 1).clamp(0, text.length);642
e.selection = CodeLineSelection.collapsed(index: index, offset: offset);643
e.makeCursorCenterIfInvisible();644
focus.requestFocus();645
}647
Future<void> _validate() async {648
final problem = validate(syntax.id, editing!.text);649
if (!mounted) return;650
if (problem == null) {651
ScaffoldMessenger.of(652
context,653
).showSnackBar(SnackBar(content: Text(context.tr('validOk'))));654
return;655
}656
final go = await showDialog<bool>(657
context: context,658
builder: (ctx) => AlertDialog(659
title: Text(ctx.tr('validationProblem')),660
content: SelectableText(661
'${problem.line == null ? '' : '${ctx.tr('line')} ${problem.line}, ${ctx.tr('column')} ${problem.column}\n'}'662
'${problem.message}',663
),664
actions: [665
TextButton(666
onPressed: () => Navigator.pop(ctx, false),667
child: Text(ctx.tr('done')),668
),669
if (problem.line != null)670
FilledButton(671
onPressed: () => Navigator.pop(ctx, true),672
child: Text(ctx.tr('goToLine')),673
),674
],675
),676
);677
if (go == true) _moveTo(problem.line!, problem.column ?? 1);678
}680
Future<void> _format() async {681
final e = editing!;682
final formatted = formatText(syntax.id, e.text, indent: indent);683
if (formatted == null) {684
if (mounted) showFailure(context, const Failure('formatFailed'));685
return;686
}687
final ok = await confirm(688
context,689
'formatDocument',690
detail: context.tr('formatAdvice'),691
);692
if (!ok) return;693
// Undoable, like any edit.694
e.selectAll();695
e.replaceSelection(formatted);696
}698
void _setPref(String key, Object value) {699
setState(() => prefs[key] = value);700
unawaited(c.saveSettings().catchError((Object _) {}));701
}703
Future<void> _settings() async {704
await showModalBottomSheet<void>(705
context: context,706
isScrollControlled: true,707
builder: (ctx) => StatefulBuilder(708
builder: (ctx, refresh) {709
void set(String k, Object v) {710
_setPref(k, v);711
refresh(() {});712
}714
return SafeArea(715
child: SingleChildScrollView(716
padding: const EdgeInsets.all(16),717
child: Column(718
crossAxisAlignment: CrossAxisAlignment.stretch,719
children: [720
Text(721
ctx.tr('editorSettings'),722
style: Theme.of(ctx).textTheme.titleMedium,723
),724
ListTile(725
contentPadding: EdgeInsets.zero,726
title: Text(ctx.tr('fontSize')),727
subtitle: Slider(728
value: fontSize,729
min: 9,730
max: 28,731
divisions: 19,732
label: fontSize.round().toString(),733
onChanged: (v) => set('fontSize', v.roundToDouble()),734
),735
),736
SwitchListTile.adaptive(737
contentPadding: EdgeInsets.zero,738
title: Text(ctx.tr('wordWrap')),739
value: wrap,740
onChanged: (v) => set('wrap', v),741
),742
SwitchListTile.adaptive(743
contentPadding: EdgeInsets.zero,744
title: Text(ctx.tr('lineNumbers')),745
value: lineNumbers,746
onChanged: (v) => set('lineNumbers', v),747
),748
SwitchListTile.adaptive(749
contentPadding: EdgeInsets.zero,750
title: Text(ctx.tr('indentWithTabs')),751
value: tabs,752
onChanged: (v) => set('tabs', v),753
),754
ListTile(755
contentPadding: EdgeInsets.zero,756
title: Text(ctx.tr('indentWidth')),757
trailing: SegmentedButton<int>(758
showSelectedIcon: false,759
segments: const [760
ButtonSegment(value: 2, label: Text('2')),761
ButtonSegment(value: 4, label: Text('4')),762
ButtonSegment(value: 8, label: Text('8')),763
],764
selected: {indent},765
onSelectionChanged: (v) {766
set('indent', v.first);767
_applyIndent();768
},769
),770
),771
SwitchListTile.adaptive(772
contentPadding: EdgeInsets.zero,773
title: Text(ctx.tr('syntaxHighlight')),774
subtitle:775
highlight || (original?.length ?? 0) <= highlightLimit776
? null777
: Text(ctx.tr('largeFileHighlight')),778
value: highlight,779
onChanged: (v) {780
setState(() => highlight = v);781
refresh(() {});782
},783
),784
],785
),786
),787
);788
},789
),790
);791
}793
void _applyIndent() {794
final e = editing;795
if (e == null) return;796
final next = CodeLineEditingController(797
codeLines: e.codeLines,798
options: CodeLineOptions(799
lineBreak: e.options.lineBreak,800
indentSize: indent,801
),802
);803
next.selection = e.selection;804
e.removeListener(_changed);805
next.addListener(_changed);806
final oldFind = find;807
setState(() {808
editing = next;809
find = CodeFindController(next);810
});811
WidgetsBinding.instance.addPostFrameCallback((_) {812
e.dispose();813
oldFind?.dispose();814
});815
}817
Future<void> _chooseSyntax() async {818
final chosen = await showModalBottomSheet<Syntax>(819
context: context,820
isScrollControlled: true,821
builder: (ctx) => SafeArea(822
child: DraggableScrollableSheet(823
expand: false,824
initialChildSize: .6,825
builder: (ctx, scrollController) => ListView(826
controller: scrollController,827
children: [828
ListTile(title: Text(ctx.tr('syntax'))),829
for (final s in syntaxes)830
ListTile(831
title: Text(s.label),832
subtitle: s.highlighted ? null : Text(ctx.tr('noGrammar')),833
selected: s.id == syntax.id,834
onTap: () => Navigator.pop(ctx, s),835
),836
],837
),838
),839
),840
);841
if (chosen != null) setState(() => syntax = chosen);842
}844
Future<bool> _confirmLeave() async {845
if (!dirty) return true;846
final choice = await showDialog<String>(847
context: context,848
builder: (ctx) => AlertDialog(849
title: Text(ctx.tr('unsavedChanges')),850
content: Text(source.location),851
actions: [852
TextButton(853
onPressed: () => Navigator.pop(ctx),854
child: Text(ctx.tr('cancel')),855
),856
TextButton(857
onPressed: () => Navigator.pop(ctx, 'discard'),858
child: Text(ctx.tr('discard')),859
),860
FilledButton(861
onPressed: () => Navigator.pop(ctx, 'save'),862
child: Text(ctx.tr('save')),863
),864
],865
),866
);867
if (choice == 'discard') return true;868
if (choice == 'save') {869
await save();870
return !dirty;871
}872
return false;873
}875
// ---------------------------------------------------------------------------876
// UI878
CodeEditorStyle _style(BuildContext context) {879
final scheme = Theme.of(context).colorScheme;880
final dark = Theme.of(context).brightness == Brightness.dark;881
final grammar = syntax.grammar;882
return CodeEditorStyle(883
fontSize: fontSize,884
fontFamily:885
Theme.of(context).platform == TargetPlatform.iOS ||886
Theme.of(context).platform == TargetPlatform.macOS887
? 'Menlo'888
: 'monospace',889
fontFamilyFallback: const ['Courier New', 'monospace'],890
textColor: scheme.onSurface,891
backgroundColor: dark ? const Color(0xff0b1622) : const Color(0xfffcfdff),892
selectionColor: scheme.primary.withValues(alpha: .3),893
cursorColor: scheme.primary,894
cursorLineColor: scheme.primary.withValues(alpha: dark ? .10 : .07),895
chunkIndicatorColor: scheme.onSurfaceVariant,896
codeTheme: highlight && grammar != null897
? CodeHighlightTheme(898
languages: {899
grammar: CodeHighlightThemeMode(900
mode: grammars[grammar]!,901
maxSize: maxEditableBytes,902
maxLineLength: 10000,903
),904
},905
theme: dark ? githubDarkTheme : githubTheme,906
)907
: null,908
);909
}911
Widget _statusBar(BuildContext context) {912
final e = editing!, f = format!;913
final sel = e.selection;914
final muted = TextStyle(915
fontSize: 12,916
color: Theme.of(context).colorScheme.onSurfaceVariant,917
);918
Widget chip(String text, VoidCallback? onTap, {String? tooltip}) => Tooltip(919
message: tooltip ?? text,920
child: InkWell(921
onTap: onTap,922
child: Padding(923
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),924
child: Text(text, style: muted),925
),926
),927
);928
return Material(929
color: Theme.of(context).colorScheme.surfaceContainer,930
child: SafeArea(931
top: false,932
child: SingleChildScrollView(933
scrollDirection: Axis.horizontal,934
child: Row(935
children: [936
chip(937
'${context.tr('lineShort')} ${sel.extentIndex + 1}, '938
'${context.tr('columnShort')} ${sel.extentOffset + 1}',939
_goTo,940
tooltip: context.tr('goToLine'),941
),942
chip(syntax.label, _chooseSyntax, tooltip: context.tr('syntax')),943
chip(944
f.encoding.label,945
readOnly ? null : _saveWith,946
tooltip: context.tr('saveEncoding'),947
),948
chip(949
f.lineBreak.label,950
readOnly ? null : _lineBreak,951
tooltip: context.tr('lineBreaks'),952
),953
chip(954
tabs955
? '${context.tr('tabs')} $indent'956
: '${context.tr('spaces')} $indent',957
_settings,958
),959
if (readOnly)960
chip(context.tr('readOnly'), null)961
else if (dirty)962
chip('● ${context.tr('modifiedState')}', null),963
],964
),965
),966
),967
);968
}970
Map<Type, Action<Intent>> get _overrides => {971
CodeShortcutSaveIntent: CallbackAction<CodeShortcutSaveIntent>(972
onInvoke: (_) {973
if (!readOnly) unawaited(save());974
return null;975
},976
),977
if (tabs)978
CodeShortcutIndentIntent: CallbackAction<CodeShortcutIndentIntent>(979
onInvoke: (_) {980
final e = editing!;981
if (e.selection.isSameLine) {982
e.replaceSelection('\t');983
} else {984
e.applyIndent();985
}986
return null;987
},988
),989
};991
@override992
Widget build(BuildContext context) {993
final e = editing;994
final wide = MediaQuery.sizeOf(context).width >= 700;995
final menu = <(String, IconData, VoidCallback?)>[996
('saveAs', Icons.save_as, e == null ? null : saveAs),997
('reload', Icons.refresh, () => _reload()),998
('goToLine', Icons.format_list_numbered, e == null ? null : _goTo),999
('selectAll', Icons.select_all, e == null ? null : () => e.selectAll()),1000
if (validatorFor(syntax.id) != null) ...[1001
('validate', Icons.rule, e == null ? null : _validate),1002
if (!readOnly && syntax.id != 'yaml' && syntax.id != 'jsonl')1003
('formatDocument', Icons.auto_fix_high, e == null ? null : _format),1004
],1005
(1006
'reopenEncoding',1007
Icons.translate,1008
original == null ? null : _reopenWith,1009
),1010
('syntax', Icons.code, _chooseSyntax),1011
('editorSettings', Icons.tune, _settings),1012
];1013
return PopScope(1014
canPop: !dirty,1015
onPopInvokedWithResult: (popped, _) async {1016
if (popped) return;1017
if (await _confirmLeave() && context.mounted) {1018
setState(() => dirty = false);1019
Navigator.pop(context);1020
}1021
},1022
child: Scaffold(1023
appBar: AppBar(1024
title: Column(1025
crossAxisAlignment: CrossAxisAlignment.start,1026
children: [1027
Text(1028
'${dirty ? '● ' : ''}${source.name}',1029
maxLines: 1,1030
overflow: TextOverflow.ellipsis,1031
),1032
Text(1033
source.location,1034
maxLines: 1,1035
overflow: TextOverflow.ellipsis,1036
style: Theme.of(context).textTheme.bodySmall,1037
),1038
],1039
),1040
actions: [1041
if (e != null && !readOnly)1042
IconButton(1043
tooltip: context.tr('save'),1044
onPressed: saving ? null : () => save(),1045
icon: saving1046
? const SizedBox.square(1047
dimension: 18,1048
child: CircularProgressIndicator(strokeWidth: 2),1049
)1050
: const Icon(Icons.save),1051
),1052
if (e != null && !readOnly) ...[1053
IconButton(1054
tooltip: context.tr('undo'),1055
onPressed: e.canUndo ? e.undo : null,1056
icon: const Icon(Icons.undo),1057
),1058
IconButton(1059
tooltip: context.tr('redo'),1060
onPressed: e.canRedo ? e.redo : null,1061
icon: const Icon(Icons.redo),1062
),1063
],1064
if (e != null)1065
IconButton(1066
tooltip: context.tr('search'),1067
onPressed: () => find?.findMode(),1068
icon: const Icon(Icons.search),1069
),1070
if (wide && e != null)1071
IconButton(1072
tooltip: context.tr('wordWrap'),1073
onPressed: () => _setPref('wrap', !wrap),1074
icon: Icon(wrap ? Icons.wrap_text : Icons.notes),1075
),1076
PopupMenuButton<int>(1077
onSelected: (i) => menu[i].$3?.call(),1078
itemBuilder: (ctx) => [1079
for (var i = 0; i < menu.length; i++)1080
PopupMenuItem(1081
value: i,1082
enabled: menu[i].$3 != null,1083
child: ListTile(1084
dense: true,1085
contentPadding: EdgeInsets.zero,1086
leading: Icon(menu[i].$2),1087
title: Text(ctx.tr(menu[i].$1)),1088
),1089
),1090
],1091
),1092
],1093
),1094
body: SafeArea(1095
bottom: false,1096
child: loading1097
? const Center(child: CircularProgressIndicator())1098
: error != null1099
? _errorView(context)1100
: Column(1101
children: [1102
if (notice != null)1103
MaterialBanner(1104
content: Text(context.tr(notice!)),1105
actions: [1106
TextButton(1107
onPressed: () => setState(() => notice = null),1108
child: Text(context.tr('done')),1109
),1110
],1111
),1112
if (readOnly)1113
MaterialBanner(1114
content: Text(1115
context.tr(source.readOnlyReason ?? 'readOnly'),1116
),1117
actions: [1118
TextButton(1119
onPressed: saveAs,1120
child: Text(context.tr('saveAs')),1121
),1122
],1123
),1124
Expanded(1125
child: CodeEditor(1126
controller: e,1127
findController: find,1128
scrollController: scroll,1129
focusNode: focus,1130
readOnly: readOnly,1131
wordWrap: wrap,1132
style: _style(context),1133
padding: const EdgeInsets.fromLTRB(4, 8, 8, 64),1134
chunkAnalyzer: switch (syntax.id) {1135
'yaml' ||1136
'python' ||1137
'markdown' => const IndentChunkAnalyzer(),1138
'text' ||1139
'log' ||1140
'csv' ||1141
'pem' => const NonCodeChunkAnalyzer(),1142
_ => const DefaultCodeChunkAnalyzer(),1143
},1144
indicatorBuilder: lineNumbers1145
? (context, editing, chunks, notifier) => Row(1146
children: [1147
DefaultCodeLineNumber(1148
controller: editing,1149
notifier: notifier,1150
),1151
DefaultCodeChunkIndicator(1152
width: 18,1153
controller: chunks,1154
notifier: notifier,1155
),1156
],1157
)1158
: null,1159
findBuilder: (context, controller, readOnly) =>1160
FindPanel(1161
controller: controller,1162
readOnly: readOnly,1163
),1164
toolbarController: MobileSelectionToolbarController(1165
builder:1166
({1167
required context,1168
required anchors,1169
required controller,1170
required onDismiss,1171
required onRefresh,1172
}) => AdaptiveTextSelectionToolbar.buttonItems(1173
anchors: anchors,1174
buttonItems: [1175
if (!readOnly &&1176
!controller.selection.isCollapsed)1177
ContextMenuButtonItem(1178
type: ContextMenuButtonType.cut,1179
onPressed: () {1180
controller.cut();1181
onDismiss();1182
},1183
),1184
if (!controller.selection.isCollapsed)1185
ContextMenuButtonItem(1186
type: ContextMenuButtonType.copy,1187
onPressed: () {1188
controller.copy();1189
onDismiss();1190
},1191
),1192
if (!readOnly)1193
ContextMenuButtonItem(1194
type: ContextMenuButtonType.paste,1195
onPressed: () {1196
controller.paste();1197
onDismiss();1198
},1199
),1200
ContextMenuButtonItem(1201
type: ContextMenuButtonType.selectAll,1202
onPressed: () {1203
controller.selectAll();1204
onRefresh();1205
},1206
),1207
],1208
),1209
),1210
shortcutOverrideActions: _overrides,1211
),1212
),1213
_statusBar(context),1214
],1215
),1216
),1217
),1218
);1219
}1221
Widget _errorView(BuildContext context) => Center(1222
child: SingleChildScrollView(1223
padding: const EdgeInsets.all(24),1224
child: Column(1225
mainAxisSize: MainAxisSize.min,1226
children: [1227
Icon(1228
error == 'binaryFile' ? Icons.memory : Icons.error_outline,1229
size: 48,1230
color: Theme.of(context).colorScheme.primary,1231
),1232
const SizedBox(height: 16),1233
Text(context.tr(error!), textAlign: TextAlign.center),1234
const SizedBox(height: 16),1235
Wrap(1236
spacing: 8,1237
runSpacing: 8,1238
alignment: WrapAlignment.center,1239
children: [1240
if (error == 'binaryFile' ||1241
error == 'wrongEncoding' ||1242
error == 'unknownEncoding')1243
OutlinedButton(1244
onPressed: () async {1245
final chosen = await _chooseEncoding();1246
if (chosen != null) await _load(encoding: chosen);1247
},1248
child: Text(context.tr('openAsTextWith')),1249
),1250
FilledButton(1251
onPressed: () => _reload(confirmDiscard: false),1252
child: Text(context.tr('reload')),1253
),1254
],1255
),1256
],1257
),1258
),1259
);1260
}SHA-256
7ab773041f3320031261c3e64c42dab56ccd77157b0ed971cc0197f7cfe2bc45CH-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 archive
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655