Fix main-thread blocking I/O

This commit is contained in:
世界
2026-02-05 02:37:02 +08:00
parent 4ae421cd04
commit 2e682d746c
17 changed files with 643 additions and 237 deletions
@@ -78,6 +78,20 @@ public struct ProfileCard: View {
QRCodeSheet(profileName: profile.name, remoteURL: remoteURL)
}
}
.sheet(
isPresented: $viewModel.showQRSShare,
onDismiss: {
viewModel.qrsShareData = nil
viewModel.qrsShareProfileName = nil
},
content: {
if let data = viewModel.qrsShareData, let name = viewModel.qrsShareProfileName {
QRSSheet(profileName: name, profileData: data)
} else {
ProgressView()
}
}
)
#else
.sheet(isPresented: $viewModel.showNewProfile, onDismiss: {
environments.profileUpdate.send()
@@ -98,11 +112,20 @@ public struct ProfileCard: View {
}
}
#endif
.sheet(isPresented: $viewModel.showQRSShare) {
if let profile = selectedProfile, let data = try? profile.origin.toContent().encode() {
QRSSheet(profileName: profile.name, profileData: data)
.sheet(
isPresented: $viewModel.showQRSShare,
onDismiss: {
viewModel.qrsShareData = nil
viewModel.qrsShareProfileName = nil
},
content: {
if let data = viewModel.qrsShareData, let name = viewModel.qrsShareProfileName {
QRSSheet(profileName: name, profileData: data)
} else {
ProgressView()
}
}
}
)
.fileExporter(
isPresented: $viewModel.showExporter,
document: viewModel.exportDocument,
@@ -253,12 +276,10 @@ public struct ProfileCard: View {
}
}
if let data = try? profile.origin.toContent().encode() {
FormNavigationLink {
QRSSheet(profileName: profile.name, profileData: data)
} label: {
Label("Share as QRS Code", systemImage: "qrcode")
}
Button {
prepareQRSShare(profile)
} label: {
Label("Share as QRS Code", systemImage: "qrcode")
}
} label: {
Image(systemName: "square.and.arrow.up")
@@ -301,7 +322,7 @@ public struct ProfileCard: View {
}
Button {
viewModel.showQRSShare = true
prepareQRSShare(profile)
} label: {
Label("Share as QRS Code", systemImage: "qrcode")
}
@@ -333,42 +354,52 @@ public struct ProfileCard: View {
#if !os(tvOS)
private func shareProfile(_ profile: ProfilePreview, type: ShareItemType) {
do {
let url: URL
switch type {
case .file:
url = try profile.origin.toContent().generateShareFile()
case .json:
url = try profile.origin.read().generateShareFile(name: "\(profile.name).json")
Task {
do {
let url: URL
switch type {
case .file:
url = try await profile.origin.generateShareFileAsync()
case .json:
url = try await profile.origin.generateJSONShareFileAsync(name: "\(profile.name).json")
}
#if os(iOS)
presentShareController(url)
#elseif os(macOS)
let anchorView = viewModel.shareButtonView ?? NSApp.keyWindow?.contentView ?? NSView()
NSSharingServicePicker(items: [url]).show(
relativeTo: .zero,
of: anchorView,
preferredEdge: .minY
)
#endif
} catch {
viewModel.alert = AlertState(error: error)
}
#if os(iOS)
presentShareController(url)
#elseif os(macOS)
let anchorView = viewModel.shareButtonView ?? NSApp.keyWindow?.contentView ?? NSView()
NSSharingServicePicker(items: [url]).show(
relativeTo: .zero,
of: anchorView,
preferredEdge: .minY
)
#endif
} catch {
viewModel.alert = AlertState(error: error)
}
}
private func exportProfile(_ profile: ProfilePreview, type: ExportItemType) {
do {
switch type {
case .file:
let doc = try ProfileExportDocument(content: profile.origin.toContent())
viewModel.exportDocument = ProfileAnyExportDocument(profile: doc)
case .json:
let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name)
viewModel.exportDocument = ProfileAnyExportDocument(json: doc)
Task {
do {
let document: ProfileAnyExportDocument
switch type {
case .file:
let data = try await profile.origin.encodedContentDataAsync()
document = ProfileAnyExportDocument(data: data, filename: "\(profile.name).bpf", contentType: .data)
case .json:
let content = try await profile.origin.readAsync()
document = ProfileAnyExportDocument(data: Data(content.utf8), filename: "\(profile.name).json", contentType: .json)
}
await MainActor.run {
viewModel.exportDocument = document
viewModel.showExporter = true
}
} catch {
await MainActor.run {
viewModel.alert = AlertState(error: error)
}
}
viewModel.showExporter = true
} catch {
viewModel.alert = AlertState(error: error)
}
}
@@ -391,6 +422,20 @@ public struct ProfileCard: View {
#endif
#endif
private func prepareQRSShare(_ profile: ProfilePreview) {
viewModel.qrsShareProfileName = profile.name
viewModel.qrsShareData = nil
viewModel.showQRSShare = true
Task {
do {
viewModel.qrsShareData = try await profile.origin.encodedContentDataAsync()
} catch {
viewModel.alert = AlertState(error: error)
viewModel.showQRSShare = false
}
}
}
@ViewBuilder
private func profileInfo(for profile: ProfilePreview) -> some View {
HStack(spacing: 8) {
@@ -474,6 +519,8 @@ extension ProfileCard {
@Published var showProfilePicker = false
@Published var showQRCode = false
@Published var showQRSShare = false
@Published var qrsShareData: Data?
@Published var qrsShareProfileName: String?
@Published var isUpdating = false
@Published var alert: AlertState?
@Published var profileToEdit: Profile?
@@ -529,6 +529,7 @@ private struct ProfilePickerRow: View {
@State private var isUpdating = false
@State private var showQRCode = false
@State private var showQRSShare = false
@State private var qrsShareData: Data?
#if os(macOS)
@State private var shareItemType: ShareItemType?
@State private var exportItemType: ExportItemType?
@@ -561,6 +562,19 @@ private struct ProfilePickerRow: View {
QRCodeSheet(profileName: profile.name, remoteURL: remoteURL)
}
}
.sheet(
isPresented: $showQRSShare,
onDismiss: {
qrsShareData = nil
},
content: {
if let data = qrsShareData {
QRSSheet(profileName: profile.name, profileData: data)
} else {
ProgressView()
}
}
)
}
private var tvOSNormalBody: some View {
@@ -618,12 +632,10 @@ private struct ProfilePickerRow: View {
}
}
if let data = try? profile.origin.toContent().encode() {
FormNavigationLink {
QRSSheet(profileName: profile.name, profileData: data)
} label: {
Label("Share as QRS Code", systemImage: "barcode")
}
Button {
prepareQRSShare()
} label: {
Label("Share as QRS Code", systemImage: "barcode")
}
} label: {
Label("Share", systemImage: "square.and.arrow.up")
@@ -710,11 +722,19 @@ private struct ProfilePickerRow: View {
QRCodeSheet(profileName: profile.name, remoteURL: remoteURL)
}
}
.sheet(isPresented: $showQRSShare) {
if let data = try? profile.origin.toContent().encode() {
QRSSheet(profileName: profile.name, profileData: data)
.sheet(
isPresented: $showQRSShare,
onDismiss: {
qrsShareData = nil
},
content: {
if let data = qrsShareData {
QRSSheet(profileName: profile.name, profileData: data)
} else {
ProgressView()
}
}
}
)
.fileExporter(
isPresented: $showExporter,
document: exportDocument,
@@ -749,11 +769,19 @@ private struct ProfilePickerRow: View {
QRCodeSheet(profileName: profile.name, remoteURL: remoteURL)
}
}
.sheet(isPresented: $showQRSShare) {
if let data = try? profile.origin.toContent().encode() {
QRSSheet(profileName: profile.name, profileData: data)
.sheet(
isPresented: $showQRSShare,
onDismiss: {
qrsShareData = nil
},
content: {
if let data = qrsShareData {
QRSSheet(profileName: profile.name, profileData: data)
} else {
ProgressView()
}
}
}
)
.fileExporter(
isPresented: $showExporter,
document: exportDocument,
@@ -881,6 +909,24 @@ private struct ProfilePickerRow: View {
#endif
}
private func prepareQRSShare() {
qrsShareData = nil
showQRSShare = true
Task {
do {
let data = try await profile.origin.encodedContentDataAsync()
await MainActor.run {
qrsShareData = data
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
showQRSShare = false
}
}
}
}
#if !os(tvOS)
@ViewBuilder
private var shareMenu: some View {
@@ -919,7 +965,7 @@ private struct ProfilePickerRow: View {
ShareButtonCompat($alert) {
Label("Share File", systemImage: "doc")
} itemURL: {
try profile.origin.toContent().generateShareFile()
try await profile.origin.generateShareFileAsync()
}
Button {
@@ -931,7 +977,7 @@ private struct ProfilePickerRow: View {
ShareButtonCompat($alert) {
Label("Share Content JSON File", systemImage: "curlybraces")
} itemURL: {
try profile.origin.read().generateShareFile(name: "\(profile.name).json")
try await profile.origin.generateJSONShareFileAsync(name: "\(profile.name).json")
}
#endif
@@ -944,7 +990,7 @@ private struct ProfilePickerRow: View {
}
Button {
showQRSShare = true
prepareQRSShare()
} label: {
Label("Share as QRS Code", systemImage: "barcode")
}
@@ -954,18 +1000,26 @@ private struct ProfilePickerRow: View {
}
private func exportProfile(type: ExportItemType) {
do {
switch type {
case .file:
let doc = try ProfileExportDocument(content: profile.origin.toContent())
exportDocument = ProfileAnyExportDocument(profile: doc)
case .json:
let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name)
exportDocument = ProfileAnyExportDocument(json: doc)
Task {
do {
let document: ProfileAnyExportDocument
switch type {
case .file:
let data = try await profile.origin.encodedContentDataAsync()
document = ProfileAnyExportDocument(data: data, filename: "\(profile.name).bpf", contentType: .data)
case .json:
let content = try await profile.origin.readAsync()
document = ProfileAnyExportDocument(data: Data(content.utf8), filename: "\(profile.name).json", contentType: .json)
}
await MainActor.run {
exportDocument = document
showExporter = true
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
}
}
showExporter = true
} catch {
alert = AlertState(error: error)
}
}
#endif
@@ -996,38 +1050,52 @@ private struct ProfilePickerRow: View {
#if os(macOS)
private func shareProfile(type: ShareItemType) {
do {
let url: URL
switch type {
case .file:
url = try profile.origin.toContent().generateShareFile()
case .json:
url = try profile.origin.read().generateShareFile(name: "\(profile.name).json")
Task {
do {
let url: URL
switch type {
case .file:
url = try await profile.origin.generateShareFileAsync()
case .json:
url = try await profile.origin.generateJSONShareFileAsync(name: "\(profile.name).json")
}
await MainActor.run {
let anchorView = menuAnchorView ?? NSApp.keyWindow?.contentView ?? NSView()
NSSharingServicePicker(items: [url]).show(
relativeTo: .zero,
of: anchorView,
preferredEdge: .minY
)
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
}
}
let anchorView = menuAnchorView ?? NSApp.keyWindow?.contentView ?? NSView()
NSSharingServicePicker(items: [url]).show(
relativeTo: .zero,
of: anchorView,
preferredEdge: .minY
)
} catch {
alert = AlertState(error: error)
}
}
private func exportProfileMacOS(type: ExportItemType) {
do {
switch type {
case .file:
let doc = try ProfileExportDocument(content: profile.origin.toContent())
exportDocument = ProfileAnyExportDocument(profile: doc)
case .json:
let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name)
exportDocument = ProfileAnyExportDocument(json: doc)
Task {
do {
let document: ProfileAnyExportDocument
switch type {
case .file:
let data = try await profile.origin.encodedContentDataAsync()
document = ProfileAnyExportDocument(data: data, filename: "\(profile.name).bpf", contentType: .data)
case .json:
let content = try await profile.origin.readAsync()
document = ProfileAnyExportDocument(data: Data(content.utf8), filename: "\(profile.name).json", contentType: .json)
}
await MainActor.run {
exportDocument = document
showExporter = true
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
}
}
showExporter = true
} catch {
alert = AlertState(error: error)
}
}
@@ -1135,6 +1203,7 @@ private struct ProfilePickerRow: View {
@State private var isUpdating = false
@State private var showQRCode = false
@State private var showQRSShare = false
@State private var qrsShareData: Data?
@State private var exportDocument: ProfileAnyExportDocument?
@State private var showExporter = false
@@ -1194,11 +1263,19 @@ private struct ProfilePickerRow: View {
QRCodeSheet(profileName: profile.name, remoteURL: remoteURL)
}
}
.sheet(isPresented: $showQRSShare) {
if let data = try? profile.origin.toContent().encode() {
QRSSheet(profileName: profile.name, profileData: data)
.sheet(
isPresented: $showQRSShare,
onDismiss: {
qrsShareData = nil
},
content: {
if let data = qrsShareData {
QRSSheet(profileName: profile.name, profileData: data)
} else {
ProgressView()
}
}
}
)
.fileExporter(
isPresented: $showExporter,
document: exportDocument,
@@ -1250,6 +1327,24 @@ private struct ProfilePickerRow: View {
.buttonStyle(.plain)
}
private func prepareQRSShare() {
qrsShareData = nil
showQRSShare = true
Task {
do {
let data = try await profile.origin.encodedContentDataAsync()
await MainActor.run {
qrsShareData = data
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
showQRSShare = false
}
}
}
}
@ViewBuilder
private var shareMenu: some View {
Menu {
@@ -1262,7 +1357,7 @@ private struct ProfilePickerRow: View {
ShareButtonCompat($alert) {
Label("Share File", systemImage: "doc")
} itemURL: {
try profile.origin.toContent().generateShareFile()
try await profile.origin.generateShareFileAsync()
}
Button {
@@ -1274,7 +1369,7 @@ private struct ProfilePickerRow: View {
ShareButtonCompat($alert) {
Label("Share Content JSON File", systemImage: "curlybraces")
} itemURL: {
try profile.origin.read().generateShareFile(name: "\(profile.name).json")
try await profile.origin.generateJSONShareFileAsync(name: "\(profile.name).json")
}
if profile.type == .remote {
@@ -1286,7 +1381,7 @@ private struct ProfilePickerRow: View {
}
Button {
showQRSShare = true
prepareQRSShare()
} label: {
Label("Share as QRS Code", systemImage: "barcode")
}
@@ -1296,18 +1391,26 @@ private struct ProfilePickerRow: View {
}
private func exportProfile(type: ExportItemType) {
do {
switch type {
case .file:
let doc = try ProfileExportDocument(content: profile.origin.toContent())
exportDocument = ProfileAnyExportDocument(profile: doc)
case .json:
let doc = try ProfileJSONExportDocument(jsonContent: profile.origin.read(), name: profile.name)
exportDocument = ProfileAnyExportDocument(json: doc)
Task {
do {
let document: ProfileAnyExportDocument
switch type {
case .file:
let data = try await profile.origin.encodedContentDataAsync()
document = ProfileAnyExportDocument(data: data, filename: "\(profile.name).bpf", contentType: .data)
case .json:
let content = try await profile.origin.readAsync()
document = ProfileAnyExportDocument(data: Data(content.utf8), filename: "\(profile.name).json", contentType: .json)
}
await MainActor.run {
exportDocument = document
showExporter = true
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
}
}
showExporter = true
} catch {
alert = AlertState(error: error)
}
}