ซอร์สที่เผยแพร่
ดาวน์โหลด ZIP ของซอร์ส
CH-J SM Mobile
เรียกดูไดเรกทอรีและไฟล์ของรุ่นแอปพลิเคชันที่ระบุ
CH-J / SM Mobile
175 ไฟล์
sm-mobile-20261016T185235Z-8e93ecc1b59a
วันที่เพิ่ม: · วันที่บิลด์:
1
import 'dart:convert';2
import 'dart:typed_data';3
import 'package:flutter/material.dart';4
import 'package:flutter_test/flutter_test.dart';5
import 'package:re_editor/re_editor.dart';6
import 'package:sm_mobile/core/app_controller.dart';7
import 'package:sm_mobile/core/failure.dart';8
import 'package:sm_mobile/l10n/strings.dart';9
import 'package:sm_mobile/services/editor/sources.dart';10
import 'package:sm_mobile/services/editor/text_codec.dart';11
import 'package:sm_mobile/ui/editor/editor_page.dart';12
import '../support.dart';14
/// A file kept in memory, with the same conflict rule as real sources.15
class MemorySource implements FileSource {16
@override17
final String name;18
Uint8List bytes;19
int version = 0, loadedVersion = 0, saves = 0;20
MemorySource(this.name, this.bytes);21
@override22
String get location => 'memory:$name';23
@override24
String get kind => 'device';25
@override26
String get key => location;27
@override28
bool get canSave => true;29
@override30
String? get readOnlyReason => null;31
@override32
Future<Uint8List> load({int maxBytes = maxEditableBytes}) async {33
if (bytes.length > maxBytes) throw const Failure('fileTooLarge');34
loadedVersion = version;35
return bytes;36
}38
@override39
Future<void> save(40
Uint8List next, {41
bool force = false,42
bool inPlace = false,43
}) async {44
if (!force && version != loadedVersion) {45
throw const Failure('documentChanged');46
}47
bytes = next;48
version++;49
loadedVersion = version;50
saves++;51
}53
@override54
Map<String, dynamic> toJson() => {'kind': 'new', 'name': name};55
}57
void main() {58
late AppController c;59
setUp(() async {60
c = AppController(MemoryStore());61
});63
Future<void> open(WidgetTester tester, MemorySource source) async {64
tester.view.physicalSize = const Size(1000, 1600);65
tester.view.devicePixelRatio = 1;66
addTearDown(tester.view.reset);67
await tester.runAsync(() async {68
await c.initialize();69
await c.acceptLicense();70
if (!c.vault.unlocked) {71
await c.openVault('editor test password', create: true);72
}73
});74
await tester.pumpWidget(75
MaterialApp(76
locale: const Locale('en'),77
localizationsDelegates: AppLocalizations.localizationsDelegates,78
supportedLocales: AppLocalizations.supportedLocales,79
home: EditorPage(c, source),80
),81
);82
await tester.runAsync(() async {83
for (var i = 0; i < 3000; i++) {84
await tester.pump(const Duration(milliseconds: 10));85
if (find.byType(CodeEditor).evaluate().isNotEmpty ||86
find.byIcon(Icons.error_outline).evaluate().isNotEmpty ||87
find.byIcon(Icons.memory).evaluate().isNotEmpty) {88
break;89
}90
await Future<void>.delayed(const Duration(milliseconds: 10));91
}92
});93
await tester.pump();94
}96
CodeLineEditingController editor(WidgetTester tester) =>97
tester.widget<CodeEditor>(find.byType(CodeEditor)).controller!;99
Future<void> save(WidgetTester tester) async {100
await tester.tap(find.byIcon(Icons.save));101
await tester.runAsync(() async {102
for (var i = 0; i < 300; i++) {103
await tester.pump(const Duration(milliseconds: 10));104
if (find.byType(CircularProgressIndicator).evaluate().isEmpty) break;105
await Future<void>.delayed(const Duration(milliseconds: 10));106
}107
});108
await tester.pump();109
}111
Future<void> close(WidgetTester tester) async {112
// Stop the cursor blink (a real timer in re_editor) and let snack bars113
// and debounce timers run out.114
FocusManager.instance.primaryFocus?.unfocus();115
await tester.runAsync(116
() => Future<void>.delayed(const Duration(milliseconds: 600)),117
);118
await tester.pump(const Duration(seconds: 10));119
await tester.pumpWidget(const SizedBox());120
await tester.pump(const Duration(seconds: 10));121
c.dispose();122
}124
final samples = <String, (String, String)>{125
'notes.txt': ('Hello\nworld\n', 'Text'),126
'index.html': (127
'<!DOCTYPE html>\n<html><body><p>Hi</p></body></html>\n',128
'HTML',129
),130
'index.php': ('<?php\necho "Ahoj";\n', 'PHP'),131
'data.json': ('{\n "a": 1\n}\n', 'JSON'),132
'compose.yaml': ('services:\n web:\n image: nginx\n', 'YAML'),133
'feed.xml': ('<?xml version="1.0"?>\n<a><b/></a>\n', 'XML'),134
'site.css': ('body {\n color: red;\n}\n', 'CSS'),135
'app.js': ('const a = 1;\nconsole.log(a);\n', 'JavaScript'),136
'tool.py': ('def main():\n print("x")\n', 'Python'),137
'deploy.sh': ('#!/bin/sh\necho ok\n', 'Shell (Bash/sh/zsh)'),138
'nginx.conf': ('server {\r\n listen 80;\r\n}\r\n', 'NGINX'),139
'web.service': (140
'[Unit]\nDescription=Web\n\n[Service]\nExecStart=/bin/true\n',141
'systemd unit',142
),143
'motd': ('Welcome\nto the server', 'Text'),144
};146
for (final MapEntry(key: name, value: (text, syntax)) in samples.entries) {147
testWidgets('open, edit and save $name', (tester) async {148
final source = MemorySource(name, Uint8List.fromList(utf8.encode(text)));149
await open(tester, source);150
expect(find.text(syntax), findsOneWidget, reason: 'syntax shown');151
final e = editor(tester);152
expect(e.text, text.replaceAll('\r\n', '\n').isEmpty ? '' : e.text);153
// Insert at the start of line 1.154
e.selection = const CodeLineSelection.collapsed(index: 0, offset: 0);155
e.replaceSelection('# edit\n');156
await tester.pump();157
expect(find.textContaining('● $name'), findsOneWidget, reason: 'dirty');158
await save(tester);159
final crlf = text.contains('\r\n');160
final expected = (crlf ? '# edit\r\n' : '# edit\n') + text;161
expect(utf8.decode(source.bytes), expected);162
expect(source.saves, 1);163
expect(find.textContaining('● '), findsNothing);164
await close(tester);165
});166
}168
testWidgets('Windows-1250 and UTF-16 stay in their encoding', (tester) async {169
final source = MemorySource(170
'legacy.txt',171
encodeText('Žluťoučký kůň\r\n', TextEncoding.windows1250),172
);173
await open(tester, source);174
// Unknown 8-bit encoding: the user is asked, nothing is guessed.175
expect(find.text('Unknown text encoding'), findsOneWidget);176
await tester.tap(find.text('Windows-1250'));177
await tester.runAsync(() async {178
for (var i = 0; i < 100; i++) {179
await tester.pump(const Duration(milliseconds: 10));180
if (find.byType(CodeEditor).evaluate().isNotEmpty) break;181
}182
});183
await tester.pump();184
final e = editor(tester);185
e.selection = CodeLineSelection.collapsed(186
index: 0,187
offset: e.codeLines[0].length,188
);189
e.replaceSelection('!');190
await tester.pump();191
await save(tester);192
expect(193
source.bytes,194
encodeText('Žluťoučký kůň!\r\n', TextEncoding.windows1250),195
);196
await close(tester);198
final wide = MemorySource(199
'w.txt',200
encodeText('a\nb\n', TextEncoding.utf16be),201
);202
c = AppController(MemoryStore());203
await open(tester, wide);204
expect(find.text('UTF-16 BE'), findsOneWidget);205
final w = editor(tester);206
w.selection = const CodeLineSelection.collapsed(index: 0, offset: 1);207
w.replaceSelection('Z');208
await tester.pump();209
await save(tester);210
expect(wide.bytes, encodeText('aZ\nb\n', TextEncoding.utf16be));211
await close(tester);212
});214
testWidgets('a change elsewhere is a conflict, not a silent overwrite', (215
tester,216
) async {217
final source = MemorySource(218
'a.txt',219
Uint8List.fromList(utf8.encode('one\n')),220
);221
await open(tester, source);222
source.version++; // changed by another app223
final e = editor(tester);224
e.selection = const CodeLineSelection.collapsed(index: 0, offset: 0);225
e.replaceSelection('x');226
await tester.pump();227
await save(tester);228
expect(find.text('The file on the device has changed'), findsOneWidget);229
expect(source.saves, 0);230
await tester.tap(find.text('Overwrite'));231
await tester.runAsync(() async {232
for (var i = 0; i < 100 && source.saves == 0; i++) {233
await tester.pump(const Duration(milliseconds: 10));234
await Future<void>.delayed(const Duration(milliseconds: 10));235
}236
});237
expect(utf8.decode(source.bytes), 'xone\n');238
await close(tester);239
});241
testWidgets('binary files are not opened as text without asking', (242
tester,243
) async {244
final source = MemorySource(245
'blob',246
// A PNG header followed by mixed binary data.247
Uint8List.fromList([248
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, //249
for (var i = 0; i < 512; i++) (i * 37) % 256,250
]),251
);252
await open(tester, source);253
expect(find.text('This looks like a binary file.'), findsOneWidget);254
expect(find.byType(CodeEditor), findsNothing);255
await close(tester);256
});258
testWidgets('unsaved text becomes a draft when the Vault locks', (259
tester,260
) async {261
final source = MemorySource(262
'd.txt',263
Uint8List.fromList(utf8.encode('x\n')),264
);265
await open(tester, source);266
final e = editor(tester);267
e.selection = const CodeLineSelection.collapsed(index: 0, offset: 1);268
e.replaceSelection('yz');269
await tester.pump();270
final draft = c.draftSources.single();271
expect(draft!.json['text'], 'xyz\n');272
expect(draft.json['encoding'], 'utf8');273
await close(tester);274
});276
for (final kb in [1, 100, 1024, 5 * 1024, 10 * 1024]) {277
testWidgets('$kb KB file opens, edits and saves intact', (tester) async {278
final line = 'line with some text, čeština a € ${'x' * 40}\n';279
final buffer = StringBuffer();280
while (buffer.length < kb * 1024) {281
buffer.write(line);282
}283
final bytes = Uint8List.fromList(utf8.encode(buffer.toString()));284
final source = MemorySource('big.log', bytes);285
final watch = Stopwatch()..start();286
await open(tester, source);287
final opened = watch.elapsedMilliseconds;288
final e = editor(tester);289
expect(e.lineCount, greaterThan(kb * 1024 ~/ line.length));290
e.selection = const CodeLineSelection.collapsed(index: 0, offset: 0);291
e.replaceSelection('>');292
await tester.pump();293
watch.reset();294
await save(tester);295
final saved = watch.elapsedMilliseconds;296
expect(source.bytes.length, bytes.length + 1);297
expect(source.bytes.sublist(1), bytes);298
// ignore: avoid_print299
print('editor $kb KB: open $opened ms, save $saved ms');300
await close(tester);301
}, timeout: const Timeout(Duration(minutes: 3)));302
}303
}SHA-256
68585353e30b5fe9cbb135708893164f12e4d7fecdada8e0d893033bb5c47bf8CH-J Proprietary Software License 1.14 · ซอร์สโค้ดนี้ให้ไว้ภายใต้ CH-J Proprietary Software License 1.14 การเปิดให้เข้าถึงไม่ได้เปลี่ยนแปลงเงื่อนไขของสัญญาอนุญาตหรือให้สิทธิ์เพิ่มเติม
SHA-256 ของไฟล์บีบอัด
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655