المصدر المنشور
تنزيل الشيفرة المصدرية بصيغة ZIP
CH-J SM Mobile
تصفّح الأدلة والملفات الخاصة بإصدار محدد من التطبيق.
CH-J / SM Mobile
175 ملفات
sm-mobile-20261016T185235Z-8e93ecc1b59a
تاريخ الإضافة: · تاريخ البناء:
1
import Flutter2
import QuickLook3
import UIKit4
import UniformTypeIdentifiers6
@main7
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {8
override func application(9
_ application: UIApplication,10
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?11
) -> Bool {12
return super.application(application, didFinishLaunchingWithOptions: launchOptions)13
}15
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {16
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)17
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "SMDocuments") {18
Documents.shared.attach(registrar.messenger())19
}20
}21
}23
/// Files on the device through the system document picker (Files app and24
/// file providers). Access exists only for what the user picks: documents25
/// are kept as security-scoped bookmarks and read/written with26
/// NSFileCoordinator, inside the iOS sandbox.27
final class Documents: NSObject, UIDocumentPickerDelegate, QLPreviewControllerDataSource,28
QLPreviewControllerDelegate29
{30
static let shared = Documents()31
private var channel: FlutterMethodChannel?32
private var pending: (([URL]?) -> Void)?33
private var incoming: [[String: Any?]] = []34
private var previewURL: URL?35
private var previewResult: FlutterResult?36
private let queue = DispatchQueue(label: "eu.chj.sm_mobile.documents")38
func attach(_ messenger: FlutterBinaryMessenger) {39
let channel = FlutterMethodChannel(name: "eu.chj.sm_mobile/documents", binaryMessenger: messenger)40
channel.setMethodCallHandler { [weak self] call, result in self?.handle(call, result) }41
self.channel = channel42
}44
// MARK: identifiers46
/// "b:<bookmark>" for a picked item; "b:<folder bookmark>\n<relative path>"47
/// for an item inside a picked folder.48
private func id(bookmarkOf url: URL) throws -> String {49
let scoped = url.startAccessingSecurityScopedResource()50
defer { if scoped { url.stopAccessingSecurityScopedResource() } }51
let data = try url.bookmarkData(options: [], includingResourceValuesForKeys: nil, relativeTo: nil)52
return "b:" + data.base64EncodedString()53
}55
private func resolve(_ id: String) throws -> (url: URL, scope: URL) {56
let parts = id.split(separator: "\n", maxSplits: 1).map(String.init)57
guard let first = parts.first, first.hasPrefix("b:"),58
let data = Data(base64Encoded: String(first.dropFirst(2)))59
else { throw DocError.code("notFound") }60
var stale = false61
let scope: URL62
do {63
scope = try URL(resolvingBookmarkData: data, options: [], relativeTo: nil, bookmarkDataIsStale: &stale)64
} catch {65
throw DocError.code("stale")66
}67
if parts.count == 2 {68
let relative = parts[1]69
if relative.split(separator: "/").contains("..") { throw DocError.code("denied") }70
return (scope.appendingPathComponent(relative), scope)71
}72
return (scope, scope)73
}75
enum DocError: Error { case code(String) }77
private func access<T>(_ id: String, _ work: (URL) throws -> T) throws -> T {78
let (url, scope) = try resolve(id)79
let scoped = scope.startAccessingSecurityScopedResource()80
defer { if scoped { scope.stopAccessingSecurityScopedResource() } }81
return try work(url)82
}84
private func describe(_ url: URL, id: String, location: String? = nil) -> [String: Any?] {85
let keys: Set<URLResourceKey> = [86
.nameKey, .fileSizeKey, .contentModificationDateKey, .isDirectoryKey, .isWritableKey,87
.contentTypeKey,88
]89
let v = try? url.resourceValues(forKeys: keys)90
let directory = v?.isDirectory ?? false91
return [92
"id": id,93
"name": v?.name ?? url.lastPathComponent,94
"size": v?.fileSize,95
"modified": v?.contentModificationDate.map { Int($0.timeIntervalSince1970 * 1000) },96
"directory": directory,97
"writable": !directory && (v?.isWritable ?? FileManager.default.isWritableFile(atPath: url.path)),98
"mime": v?.contentType?.preferredMIMEType,99
"location": location ?? url.deletingLastPathComponent().lastPathComponent,100
]101
}103
private func coordinatedRead(_ url: URL) throws -> Data {104
var error: NSError?105
var result: Result<Data, Error> = .failure(DocError.code("io"))106
NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &error) { u in107
result = Result { try Data(contentsOf: u) }108
}109
if let error { throw error }110
return try result.get()111
}113
private func coordinatedWrite(_ url: URL, _ data: Data) throws {114
var error: NSError?115
var result: Result<Void, Error> = .success(())116
NSFileCoordinator().coordinate(writingItemAt: url, options: .forReplacing, error: &error) { u in117
result = Result { try data.write(to: u, options: .atomic) }118
}119
if let error { throw error }120
try result.get()121
}123
// MARK: presentation125
private var top: UIViewController? {126
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }127
var vc = scenes.flatMap { $0.windows }.first { $0.isKeyWindow }?.rootViewController128
while let presented = vc?.presentedViewController { vc = presented }129
return vc130
}132
private func present(_ picker: UIDocumentPickerViewController, _ done: @escaping ([URL]?) -> Void) -> Bool {133
guard pending == nil, let top else { return false }134
pending = done135
picker.delegate = self136
top.present(picker, animated: true)137
return true138
}140
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {141
let done = pending142
pending = nil143
done?(urls)144
}146
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {147
let done = pending148
pending = nil149
done?(nil)150
}152
func numberOfPreviewItems(in controller: QLPreviewController) -> Int { previewURL == nil ? 0 : 1 }153
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {154
previewURL! as QLPreviewItem155
}156
func previewControllerDidDismiss(_ controller: QLPreviewController) {157
previewResult?(true)158
previewResult = nil159
previewURL = nil160
}162
// MARK: calls164
private func fail(_ result: @escaping FlutterResult, _ error: Error) {165
let code: String166
switch error {167
case DocError.code(let c): code = c168
case let e as NSError where e.domain == NSCocoaErrorDomain:169
switch e.code {170
case NSFileWriteNoPermissionError, NSFileReadNoPermissionError: code = "denied"171
case NSFileWriteOutOfSpaceError: code = "noSpace"172
case NSFileNoSuchFileError, NSFileReadNoSuchFileError: code = "notFound"173
case NSFileWriteVolumeReadOnlyError: code = "readOnly"174
default: code = "io"175
}176
default: code = "io"177
}178
DispatchQueue.main.async { result(FlutterError(code: code, message: "\(error)", details: nil)) }179
}181
private func background(_ result: @escaping FlutterResult, _ work: @escaping () throws -> Any?) {182
queue.async {183
do {184
let value = try work()185
DispatchQueue.main.async { result(value) }186
} catch {187
self.fail(result, error)188
}189
}190
}192
private func handle(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {193
let args = call.arguments as? [String: Any] ?? [:]194
switch call.method {195
case "pick":196
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: false)197
picker.allowsMultipleSelection = args["multiple"] as? Bool ?? false198
let shown = present(picker) { urls in199
guard let urls else { return result([]) }200
self.background(result) {201
try urls.map { url in202
let id = try self.id(bookmarkOf: url)203
return try self.access(id) { self.describe($0, id: id) }204
}205
}206
}207
if !shown { result(FlutterError(code: "busy", message: nil, details: nil)) }208
case "pickFolder":209
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder], asCopy: false)210
let shown = present(picker) { urls in211
guard let url = urls?.first else { return result(nil) }212
self.background(result) {213
let id = try self.id(bookmarkOf: url)214
return try self.access(id) { self.describe($0, id: id) }215
}216
}217
if !shown { result(FlutterError(code: "busy", message: nil, details: nil)) }218
case "list":219
let folder = args["id"] as! String220
background(result) {221
try self.access(folder) { url in222
let (_, scope) = try self.resolve(folder)223
let base = folder.split(separator: "\n", maxSplits: 1).first.map(String.init)!224
let items = try FileManager.default.contentsOfDirectory(225
at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])226
return items.map { item -> [String: Any?] in227
let relative = String(item.standardizedFileURL.path.dropFirst(scope.standardizedFileURL.path.count))228
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))229
return self.describe(item, id: base + "\n" + relative, location: url.lastPathComponent)230
}231
}232
}233
case "stat":234
let id = args["id"] as! String235
background(result) { try self.access(id) { self.describe($0, id: id) } }236
case "read":237
let id = args["id"] as! String238
background(result) { try self.access(id) { FlutterStandardTypedData(bytes: try self.coordinatedRead($0)) } }239
case "write":240
let id = args["id"] as! String241
let bytes = (args["bytes"] as! FlutterStandardTypedData).data242
background(result) {243
try self.access(id) { url in244
if !FileManager.default.isWritableFile(atPath: url.path) { throw DocError.code("readOnly") }245
try self.coordinatedWrite(url, bytes)246
return self.describe(url, id: id)247
}248
}249
case "create":250
let folder = args["folder"] as! String251
let name = args["name"] as! String252
let bytes = (args["bytes"] as! FlutterStandardTypedData).data253
background(result) {254
if name.contains("/") || name == ".." { throw DocError.code("denied") }255
return try self.access(folder) { url in256
let target = url.appendingPathComponent(name)257
if FileManager.default.fileExists(atPath: target.path) { throw DocError.code("exists") }258
try self.coordinatedWrite(target, bytes)259
let base = folder.split(separator: "\n", maxSplits: 1).map(String.init)260
let relative = (base.count == 2 ? base[1] + "/" : "") + name261
return self.describe(target, id: base[0] + "\n" + relative)262
}263
}264
case "saveAs":265
let name = args["name"] as! String266
let bytes = (args["bytes"] as! FlutterStandardTypedData).data267
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("sm-transfer-export-\(UUID().uuidString)")268
do {269
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)270
let temp = dir.appendingPathComponent((name as NSString).lastPathComponent)271
try bytes.write(to: temp, options: .atomic)272
let picker = UIDocumentPickerViewController(forExporting: [temp], asCopy: true)273
let shown = present(picker) { urls in274
try? FileManager.default.removeItem(at: dir)275
guard let url = urls?.first else { return result(nil) }276
self.background(result) {277
if let id = try? self.id(bookmarkOf: url) {278
return try self.access(id) { self.describe($0, id: id) }279
}280
// Saved, but iOS gave no lasting access to the new file.281
var info = self.describe(url, id: "")282
info["writable"] = false283
return info284
}285
}286
if !shown {287
try? FileManager.default.removeItem(at: dir)288
result(FlutterError(code: "busy", message: nil, details: nil))289
}290
} catch {291
fail(result, error)292
}293
case "preview":294
let url = URL(fileURLWithPath: args["path"] as! String)295
guard url.standardizedFileURL.path.hasPrefix(FileManager.default.temporaryDirectory.standardizedFileURL.path),296
QLPreviewController.canPreview(url as QLPreviewItem), let top297
else { return result(false) }298
previewURL = url299
previewResult = result300
let ql = QLPreviewController()301
ql.dataSource = self302
ql.delegate = self303
top.present(ql, animated: true)304
case "takeIncoming":305
result(incoming)306
incoming.removeAll()307
case "forget":308
result(nil)309
default:310
result(FlutterMethodNotImplemented)311
}312
}314
/// Files opened in SM Mobile from other apps ("Open in", Files).315
func receive(_ urls: [URL]) {316
queue.async {317
let docs: [[String: Any?]] = urls.compactMap { url in318
guard let id = try? self.id(bookmarkOf: url) else { return nil }319
return try? self.access(id) { self.describe($0, id: id) }320
}321
DispatchQueue.main.async {322
guard !docs.isEmpty else { return }323
if let channel = self.channel {324
channel.invokeMethod("incoming", arguments: self.incoming + docs)325
self.incoming.removeAll()326
} else {327
self.incoming.append(contentsOf: docs)328
}329
}330
}331
}332
}SHA-256
6b9c72191ccf5175b2993f8c283bc8102e47b7a20a81c8d7fea2cf3f2b0978ceCH-J Proprietary Software License 1.14 · تُتاح الشيفرة المصدرية بموجب CH-J Proprietary Software License 1.14. وإتاحتها لا تغيّر شروط الترخيص ولا تمنح حقوقًا إضافية.
SHA-256 للأرشيف
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655