diff --git a/app/Shared/Localization/zh-Hans.lproj/Localizable.strings b/app/Shared/Localization/zh-Hans.lproj/Localizable.strings index 51973861..8bbf0996 100644 --- a/app/Shared/Localization/zh-Hans.lproj/Localizable.strings +++ b/app/Shared/Localization/zh-Hans.lproj/Localizable.strings @@ -383,3 +383,10 @@ "Pink" = "粉色"; "Brown" = "棕色"; "Gray" = "灰色"; + +"Quick Save Image on Long Press" = "长按图片快速保存"; +"Save to Photos" = "保存到相册"; +"View Image" = "查看图片"; +"Saved" = "已保存"; +"Save Failed" = "保存失败"; +"No Photo Library Permission" = "没有相册权限"; diff --git a/app/Shared/Storage/PreferencesStorage.swift b/app/Shared/Storage/PreferencesStorage.swift index 115a019f..21d0696f 100644 --- a/app/Shared/Storage/PreferencesStorage.swift +++ b/app/Shared/Storage/PreferencesStorage.swift @@ -59,6 +59,7 @@ class PreferencesStorage: ObservableObject { @AppStorage("autoOpenInBrowserWhenBannedNew") var autoOpenInBrowserWhenBanned = false @AppStorage("topicDetailsWebApiStrategyNew") var topicDetailsWebApiStrategy = TopicDetailsRequest.WebApiStrategy.secondary @AppStorage("alwaysShareImageAsFile") var alwaysShareImageAsFile = false + @AppStorage("quickSaveImage") var quickSaveImage = false // MARK: - Debug diff --git a/app/Shared/Views/ContentImageView.swift b/app/Shared/Views/ContentImageView.swift index 02ae7e35..3a283dcc 100644 --- a/app/Shared/Views/ContentImageView.swift +++ b/app/Shared/Views/ContentImageView.swift @@ -6,6 +6,7 @@ // import Foundation +import Photos import SDWebImageSwiftUI import SwiftUI import SwiftUIX @@ -65,6 +66,7 @@ struct ContentImageView: View { } @State var frameWidth: CGFloat? = nil + @State private var quickSaveDialogPresented = false var options: SDWebImageOptions { if inSnapshot { @@ -104,6 +106,19 @@ struct ContentImageView: View { .colorMultiply(shouldDimImage ? Color(white: 0.7) : Color.white) .clipShape(RoundedRectangle(cornerRadius: 8)) .onTapGesture(perform: showImage) + // Use a high-priority long press so the image can present its own quick + // actions instead of being swallowed by the post-level `.contextMenu`. + // The gesture mask disables it when the preference is off, leaving the + // post-level context menu intact. Avoid `.if()` here to keep WebImage state. + .highPriorityGesture( + LongPressGesture(minimumDuration: 0.4) + .onEnded { _ in quickSaveDialogPresented = true }, + including: prefs.quickSaveImage && inRealPost ? .all : .none, + ) + .confirmationDialog("Image", isPresented: $quickSaveDialogPresented, titleVisibility: .hidden) { + Button("Save to Photos") { saveImageToPhotos() } + Button("View Image") { showImage() } + } } } } @@ -118,6 +133,41 @@ struct ContentImageView: View { } } + func saveImageToPhotos() { + SDWebImageManager.shared.loadImage(with: url, options: [], progress: nil) { image, data, _, _, _, _ in + guard let image else { + ToastModel.showAuto(.error("Save Failed")) + return + } + PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in + guard status == .authorized || status == .limited else { + DispatchQueue.main.async { + ToastModel.showAuto(.error("No Photo Library Permission")) + } + return + } + PHPhotoLibrary.shared().performChanges { + if let data { + let options = PHAssetResourceCreationOptions() + options.originalFilename = url.lastPathComponent + let request = PHAssetCreationRequest.forAsset() + request.addResource(with: .photo, data: data, options: options) + } else { + PHAssetChangeRequest.creationRequestForAsset(from: image) + } + } completionHandler: { success, _ in + DispatchQueue.main.async { + if success { + ToastModel.showAuto(.success("Saved")) + } else { + ToastModel.showAuto(.error("Save Failed")) + } + } + } + } + } + } + private var shouldDimImage: Bool { colorScheme == .dark && PreferencesStorage.shared.postRowDimImagesInDarkMode } diff --git a/app/Shared/Views/PreferencesView.swift b/app/Shared/Views/PreferencesView.swift index 7c53692b..e9049ecd 100644 --- a/app/Shared/Views/PreferencesView.swift +++ b/app/Shared/Views/PreferencesView.swift @@ -79,6 +79,9 @@ private struct PostRowAppearanceView: View { Label("Dim Images in Dark Mode", systemImage: "moon.fill") } .disableWithPlusCheck(.customAppearance) + Toggle(isOn: $pref.quickSaveImage.animation()) { + Label("Quick Save Image on Long Press", systemImage: "square.and.arrow.down") + } } }.pickerStyle(.menu)