mirror of
https://github.com/ProtonDriveApps/mac-drive.git
synced 2026-05-15 09:50:33 +00:00
2.10.2
This commit is contained in:
@@ -29,8 +29,8 @@ public enum ExternalFeatureFlag: String, CaseIterable, Codable {
|
||||
case driveDDKIntelEnabled = "DriveDDKIntelEnabled"
|
||||
case driveDDKDisabled = "DriveDDKDisabled"
|
||||
case driveMacSyncRecoveryDisabled = "DriveMacSyncRecoveryDisabled"
|
||||
case driveMacKeepDownloadedDisabled = "DriveMacKeepDownloadedDisabled"
|
||||
case driveMacPromoBannerDisabled = "DriveMacPromoBannerDisabled"
|
||||
case driveMacGradualRolloutChannelEnabled = "DriveMacGradualRolloutChannelEnabled"
|
||||
|
||||
// Sharing
|
||||
case driveSharingMigration = "DriveSharingMigration"
|
||||
|
||||
@@ -149,8 +149,8 @@ class ExternalFeatureFlagsRepository: FeatureFlagsRepository {
|
||||
case .driveDDKIntelEnabled: return .driveDDKIntelEnabled
|
||||
case .driveDDKDisabled: return .driveDDKDisabled
|
||||
case .driveMacSyncRecoveryDisabled: return .driveMacSyncRecoveryDisabled
|
||||
case .driveMacKeepDownloadedDisabled: return .driveMacKeepDownloadedDisabled
|
||||
case .driveMacPromoBannerDisabled: return .driveMacPromoBannerDisabled
|
||||
case .driveMacGradualRolloutChannelEnabled: return .driveMacGradualRolloutChannelEnabled
|
||||
// Sharing
|
||||
case .driveSharingMigration: return .driveSharingMigration
|
||||
case .driveSharingInvitations: return .driveSharingInvitations
|
||||
|
||||
@@ -38,8 +38,8 @@ extension LocalSettings: ExternalFeatureFlagsStore {
|
||||
case .driveDDKIntelEnabled: driveDDKIntelEnabled = value
|
||||
case .driveDDKDisabled: driveDDKDisabled = value
|
||||
case .driveMacSyncRecoveryDisabled: driveMacSyncRecoveryDisabled = value
|
||||
case .driveMacKeepDownloadedDisabled: driveMacKeepDownloadedDisabled = value
|
||||
case .driveMacPromoBannerDisabled: driveMacPromoBannerDisabled = value
|
||||
case .driveMacGradualRolloutChannelEnabled: driveMacGradualRolloutChannelEnabled = value
|
||||
// Sharing
|
||||
case .driveSharingMigration: driveSharingMigration = value
|
||||
case .driveSharingInvitations: driveSharingInvitations = value
|
||||
@@ -103,8 +103,8 @@ extension LocalSettings: ExternalFeatureFlagsStore {
|
||||
case .driveDDKIntelEnabled: return driveDDKIntelEnabled
|
||||
case .driveDDKDisabled: return driveDDKDisabled
|
||||
case .driveMacSyncRecoveryDisabled: return driveMacSyncRecoveryDisabled
|
||||
case .driveMacKeepDownloadedDisabled: return driveMacKeepDownloadedDisabled
|
||||
case .driveMacPromoBannerDisabled: return driveMacPromoBannerDisabled
|
||||
case .driveMacGradualRolloutChannelEnabled: return driveMacGradualRolloutChannelEnabled
|
||||
// Sharing
|
||||
case .driveSharingMigration: return driveSharingMigration
|
||||
case .driveSharingInvitations: return driveSharingInvitations
|
||||
|
||||
@@ -32,8 +32,8 @@ public enum FeatureAvailabilityFlag: CaseIterable {
|
||||
case driveDDKIntelEnabled
|
||||
case driveDDKDisabled
|
||||
case driveMacSyncRecoveryDisabled
|
||||
case driveMacKeepDownloadedDisabled
|
||||
case driveMacPromoBannerDisabled
|
||||
case driveMacGradualRolloutChannelEnabled
|
||||
|
||||
// Sharing
|
||||
case driveSharingMigration
|
||||
|
||||
@@ -60,13 +60,12 @@ public class LocalSettings: NSObject {
|
||||
@SettingsStorage("DriveDDKIntelEnabled") public var driveDDKIntelEnabledValue: Bool?
|
||||
@SettingsStorage("DriveDDKDisabled") public var driveDDKDisabledValue: Bool?
|
||||
@SettingsStorage("DriveMacSyncRecoveryDisabled") public var driveMacSyncRecoveryDisabledValue: Bool?
|
||||
@SettingsStorage("DriveMacKeepDownloadedDisabled") public var driveMacKeepDownloadedDisabledValue: Bool?
|
||||
@SettingsStorage("DriveMacPromoBannerDisabled") public var driveMacPromoBannerDisabledValue: Bool?
|
||||
@SettingsStorage("DriveMacGradualRolloutChannelEnabled") public var driveMacGradualRolloutChannelEnabledValue: Bool?
|
||||
@SettingsStorage("DriveAlbumsDisabled") public var driveAlbumsDisabledValue: Bool?
|
||||
@SettingsStorage("DriveCopyDisabled") public var driveCopyDisabledValue: Bool?
|
||||
@SettingsStorage("photoVolumeMigrationLastShownDate") public var photoVolumeMigrationLastShownDate: Date?
|
||||
@SettingsStorage("QuotaState") public var quotaStateValue: Int?
|
||||
@SettingsStorage("domainVersion") public var domainVersionValue: Data?
|
||||
@SettingsStorage("DrivePhotosTagsMigration") public var drivePhotosTagsMigrationValue: Bool?
|
||||
@SettingsStorage("DrivePhotosTagsMigrationDisabled") public var drivePhotosTagsMigrationDisabledValue: Bool?
|
||||
|
||||
@@ -184,15 +183,14 @@ public class LocalSettings: NSObject {
|
||||
self._driveDDKIntelEnabledValue.configure(with: suite)
|
||||
self._driveDDKDisabledValue.configure(with: suite)
|
||||
self._driveMacSyncRecoveryDisabledValue.configure(with: suite)
|
||||
self._driveMacKeepDownloadedDisabledValue.configure(with: suite)
|
||||
self._driveMacPromoBannerDisabledValue.configure(with: suite)
|
||||
self._driveMacGradualRolloutChannelEnabledValue.configure(with: suite)
|
||||
self._didFetchFeatureFlags.configure(with: suite)
|
||||
self._promotedNewFeaturesValue.configure(with: suite)
|
||||
self._driveAlbumsDisabledValue.configure(with: suite)
|
||||
self._driveCopyDisabledValue.configure(with: suite)
|
||||
self._photoVolumeMigrationLastShownDate.configure(with: suite)
|
||||
self._quotaStateValue.configure(with: suite)
|
||||
self._domainVersionValue.configure(with: suite)
|
||||
self._drivePhotosTagsMigrationValue.configure(with: suite)
|
||||
self._drivePhotosTagsMigrationDisabledValue.configure(with: suite)
|
||||
self._tagsMigrationFinishedValue.configure(with: suite)
|
||||
@@ -305,7 +303,6 @@ public class LocalSettings: NSObject {
|
||||
driveDDKIntelEnabled = driveDDKIntelEnabledValue ?? false
|
||||
driveDDKDisabled = driveDDKDisabledValue ?? false
|
||||
driveMacSyncRecoveryDisabled = driveMacSyncRecoveryDisabledValue ?? false
|
||||
driveMacKeepDownloadedDisabled = driveMacKeepDownloadedDisabledValue ?? false
|
||||
didEnableComputers = didEnableComputersValue ?? false
|
||||
driveiOSComputers = driveiOSComputersValue ?? false
|
||||
driveiOSComputersDisabled = driveiOSComputersDisabledValue ?? false
|
||||
@@ -344,7 +341,6 @@ public class LocalSettings: NSObject {
|
||||
self.optOutFromCrashReports = nil
|
||||
self.userId = nil
|
||||
self.didFetchFeatureFlags = nil
|
||||
self.domainVersionValue = nil
|
||||
// self.isOnboardedValue needs no clean up - we only show it for first login ever
|
||||
// self.isUpsellShownValue needs no clean up - we only show it once
|
||||
// self.isPhotoUpsellShownValue needs no clean up - we only show it once
|
||||
@@ -365,7 +361,6 @@ public class LocalSettings: NSObject {
|
||||
self.driveDDKIntelEnabledValue = nil
|
||||
self.driveDDKDisabledValue = nil
|
||||
self.driveMacSyncRecoveryDisabledValue = nil
|
||||
self.driveMacKeepDownloadedDisabledValue = nil
|
||||
self.pushNotificationIsEnabledValue = nil
|
||||
self.keepScreenAwakeBannerHasDismissed = nil
|
||||
self.didShowPhotosNotification = nil
|
||||
@@ -660,16 +655,16 @@ public class LocalSettings: NSObject {
|
||||
set { driveMacSyncRecoveryDisabledValue = newValue }
|
||||
}
|
||||
|
||||
public var driveMacKeepDownloadedDisabled: Bool {
|
||||
get { driveMacKeepDownloadedDisabledValue ?? false }
|
||||
set { driveMacKeepDownloadedDisabledValue = newValue }
|
||||
}
|
||||
|
||||
public var driveMacPromoBannerDisabled: Bool {
|
||||
get { driveMacPromoBannerDisabledValue ?? false }
|
||||
set { driveMacPromoBannerDisabledValue = newValue }
|
||||
}
|
||||
|
||||
public var driveMacGradualRolloutChannelEnabled: Bool {
|
||||
get { driveMacGradualRolloutChannelEnabledValue ?? false }
|
||||
set { driveMacGradualRolloutChannelEnabledValue = newValue }
|
||||
}
|
||||
|
||||
public var ratingIOSDrive: Bool {
|
||||
get { ratingIOSDriveValue ?? false }
|
||||
set { ratingIOSDriveValue = newValue }
|
||||
|
||||
@@ -48,6 +48,7 @@ public protocol FeatureFlagsControllerProtocol {
|
||||
var hasSDKDownloadMain: Bool { get }
|
||||
var hasSDKDownloadPhoto: Bool { get }
|
||||
var hasIOSBlackFriday2025: Bool { get }
|
||||
var hasGradualRolloutChannel: Bool { get }
|
||||
/// Makes current value publisher for the specific FF
|
||||
func makePublisher(keyPath: KeyPath<FeatureFlagsControllerProtocol, Bool>) -> AnyPublisher<Bool, Never>
|
||||
}
|
||||
@@ -181,6 +182,10 @@ public final class FeatureFlagsController: FeatureFlagsControllerProtocol {
|
||||
featureFlagsStore.isFeatureEnabled(.driveIOSBlackFriday2025)
|
||||
}
|
||||
|
||||
public var hasGradualRolloutChannel: Bool {
|
||||
featureFlagsStore.isFeatureEnabled(.driveMacGradualRolloutChannelEnabled)
|
||||
}
|
||||
|
||||
public func makePublisher(keyPath: KeyPath<FeatureFlagsControllerProtocol, Bool>) -> AnyPublisher<Bool, Never> {
|
||||
let currentValue = self[keyPath: keyPath]
|
||||
let updatePublisher = updatePublisher
|
||||
|
||||
@@ -33,7 +33,7 @@ let package = Package(
|
||||
/// Step 2 - Use the new version
|
||||
/// a. Update the version number below
|
||||
/// b. Rebuild the app
|
||||
.package(url: "https://gitlab.protontech.ch/drive/sdk-swift.git", branch: "0.0.16-ddk"),
|
||||
.package(url: "https://gitlab.protontech.ch/drive/sdk-swift.git", branch: "0.0.17-ddk"),
|
||||
|
||||
/// To use a local build of the DDK during development:
|
||||
/// 1. In the DDK repo run `./scripts/build_framework.sh`
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2025 Proton AG
|
||||
//
|
||||
// This file is part of Proton Drive.
|
||||
//
|
||||
// Proton Drive is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Drive is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import FileProvider
|
||||
import PDCore
|
||||
|
||||
public protocol DomainSettings {
|
||||
var domainVersion: NSFileProviderDomainVersion { get }
|
||||
func bumpDomainVersion()
|
||||
}
|
||||
|
||||
extension LocalSettings: DomainSettings {
|
||||
public var domainVersion: NSFileProviderDomainVersion {
|
||||
if let data = self.domainVersionValue {
|
||||
if let version = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSFileProviderDomainVersion.self, from: data) {
|
||||
return version
|
||||
}
|
||||
}
|
||||
|
||||
return NSFileProviderDomainVersion()
|
||||
}
|
||||
|
||||
public func bumpDomainVersion() {
|
||||
let version = self.domainVersion
|
||||
self.domainVersionValue = try? NSKeyedArchiver.archivedData(withRootObject: version.next(), requiringSecureCoding: true)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ public extension UserDefaults {
|
||||
case workingSetEnumerationInProgressKey = "workingSetEnumerationInProgress"
|
||||
case pathsMarkedAsKeepDownloadedKey = "pathsMarkedAsKeepDownloaded"
|
||||
case pathsMarkedAsOnlineOnlyKey = "pathsMarkedAsOnlineOnly"
|
||||
case isKeepDownloadedEnabledKey = "isKeepDownloadedEnabled"
|
||||
case openItemsInBrowserKey = "openItemsInBrowser"
|
||||
case extensionPathKey = "fileProviderExtensionPath"
|
||||
}
|
||||
|
||||
@@ -1825,7 +1825,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 2.10.1;
|
||||
MARKETING_VERSION = 2.10.2;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ch.protonmail.drive;
|
||||
PRODUCT_MODULE_NAME = ProtonDriveMac;
|
||||
@@ -1912,7 +1912,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 2.10.1;
|
||||
MARKETING_VERSION = 2.10.2;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = ch.protonmail.drive;
|
||||
PRODUCT_MODULE_NAME = ProtonDriveMac;
|
||||
@@ -1983,7 +1983,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 2.10.1;
|
||||
MARKETING_VERSION = 2.10.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_CODE_SIGN_FLAGS = "";
|
||||
|
||||
@@ -32,7 +32,6 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
||||
@SettingsStorage(UserDefaults.FileProvider.workingSetEnumerationInProgressKey.rawValue) var workingSetEnumerationInProgress: Bool?
|
||||
@SettingsStorage(UserDefaults.FileProvider.shouldReenumerateItemsKey.rawValue) var shouldReenumerateItems: Bool?
|
||||
@SettingsStorage("domainDisconnectedReasonCacheReset") public var cacheReset: Bool?
|
||||
@SettingsStorage(UserDefaults.FileProvider.isKeepDownloadedEnabledKey.rawValue) var isKeepDownloadedEnabledAccordingToExtension: Bool?
|
||||
@SettingsStorage(UserDefaults.FileProvider.pathsMarkedAsKeepDownloadedKey.rawValue) var pathsMarkedAsKeepDownloaded: String?
|
||||
@SettingsStorage(UserDefaults.FileProvider.pathsMarkedAsOnlineOnlyKey.rawValue) var pathsMarkedAsOnlineOnly: String?
|
||||
@SettingsStorage(UserDefaults.FileProvider.openItemsInBrowserKey.rawValue) var openItemsInBrowser: String?
|
||||
@@ -82,12 +81,6 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
||||
return true
|
||||
}
|
||||
|
||||
private var isKeepDownloadedEnabled: Bool {
|
||||
tower.featureFlags.isEnabled(flag: .driveMacKeepDownloadedDisabled) != true
|
||||
}
|
||||
|
||||
private var domainSettings: DomainSettings
|
||||
|
||||
private var isForceRefreshing: Bool = false
|
||||
|
||||
private let domain: NSFileProviderDomain // use domain to support multiple accounts
|
||||
@@ -188,9 +181,6 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
||||
_cacheReset.configure(with: Constants.appGroup)
|
||||
_fileProviderExtensionPath.configure(with: Constants.appGroup)
|
||||
updateLastLineBeforeHanging()
|
||||
|
||||
domainSettings = LocalSettings.shared
|
||||
updateLastLineBeforeHanging()
|
||||
|
||||
self.observationCenter = UserDefaultsObservationCenter(userDefaults: Constants.appGroup.userDefaults)
|
||||
updateLastLineBeforeHanging()
|
||||
@@ -246,10 +236,8 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
||||
|
||||
self.setUpKeepDownloadedObservers()
|
||||
updateLastLineBeforeHanging()
|
||||
let hasKeepDownloadedStateChanged = handleKeepDownloadedStateChange()
|
||||
updateLastLineBeforeHanging()
|
||||
|
||||
self.reenumerateIfNecessary(hasKeepDownloadedStateChanged: hasKeepDownloadedStateChanged)
|
||||
|
||||
self.reenumerateIfNecessary()
|
||||
updateLastLineBeforeHanging()
|
||||
|
||||
postExtensionLaunchNotification()
|
||||
@@ -371,8 +359,8 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
||||
}
|
||||
}
|
||||
|
||||
private func reenumerateIfNecessary(hasKeepDownloadedStateChanged: Bool) {
|
||||
if shouldReenumerateItems == true || workingSetEnumerationInProgress == true || hasKeepDownloadedStateChanged == true {
|
||||
private func reenumerateIfNecessary() {
|
||||
if shouldReenumerateItems == true || workingSetEnumerationInProgress == true {
|
||||
manager.signalEnumerator(for: .workingSet) { [weak self] error in
|
||||
guard let error else { return }
|
||||
let sei = self?.shouldReenumerateItems.map(\.description) ?? "nil"
|
||||
@@ -383,20 +371,6 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleKeepDownloadedStateChange() -> Bool {
|
||||
let oldState = isKeepDownloadedEnabledAccordingToExtension ?? false
|
||||
let newState = tower.featureFlags.isEnabled(flag: .driveMacKeepDownloadedDisabled) != true
|
||||
|
||||
if oldState != newState {
|
||||
isKeepDownloadedEnabledAccordingToExtension = newState
|
||||
initialServices.localSettings.bumpDomainVersion()
|
||||
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
observationCenter.removeObserver(self)
|
||||
Log.info("FileProviderExtension deinit: \(instanceIdentifier.uuidString)", domain: .syncing)
|
||||
@@ -922,14 +896,3 @@ extension FileProviderExtension: NSFileProviderCustomAction {
|
||||
}
|
||||
|
||||
// swiftlint:enable function_parameter_count
|
||||
|
||||
extension FileProviderExtension: NSFileProviderDomainState {
|
||||
public var domainVersion: NSFileProviderDomainVersion {
|
||||
domainSettings.domainVersion
|
||||
}
|
||||
|
||||
// Used to enable/disable actions defined in `info.plist`
|
||||
public var userInfo: [AnyHashable: Any] {
|
||||
return ["keepDownloadedEnabled": isKeepDownloadedEnabled]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,13 @@
|
||||
<true/>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderDocumentGroup</key>
|
||||
<string>$(TeamIdentifierPrefix)ch.protonmail.protondrive</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict/>
|
||||
<key>NSExtensionFileProviderActions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderActionActivationRule</key>
|
||||
<string>domainUserInfo.keepDownloadedEnabled == 1 && SUBQUERY ( fileproviderItems, $item, $item.itemIdentifier != "NSFileProviderRootContainerItemIdentifier" && $item.userInfo.keep_downloaded != YES && $item.userInfo.inherit_keep_downloaded != YES ).@count > 0</string>
|
||||
<string>SUBQUERY ( fileproviderItems, $item, $item.itemIdentifier != "NSFileProviderRootContainerItemIdentifier" && $item.userInfo.keep_downloaded != YES && $item.userInfo.inherit_keep_downloaded != YES ).@count > 0</string>
|
||||
<key>NSExtensionFileProviderActionIdentifier</key>
|
||||
<string>ch.protonmail.drive.fileprovider.action.keep_downloaded</string>
|
||||
<key>NSExtensionFileProviderActionName</key>
|
||||
@@ -46,7 +44,7 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderActionActivationRule</key>
|
||||
<string>SUBQUERY ( fileproviderItems, $item, ($item.itemIdentifier != "NSFileProviderRootContainerItemIdentifier" && $item.isUploaded == YES && ($item.isDownloaded == YES || $item.isDownloading == YES) ) ).@count > 0</string>
|
||||
<string>SUBQUERY ( fileproviderItems, $item, ($item.itemIdentifier != "NSFileProviderRootContainerItemIdentifier" && $item.isUploaded == YES && ($item.isDownloaded == YES || $item.isDownloading == YES) ) ).@count > 0</string>
|
||||
<key>NSExtensionFileProviderActionIdentifier</key>
|
||||
<string>ch.protonmail.drive.fileprovider.action.remove_download</string>
|
||||
<key>NSExtensionFileProviderActionName</key>
|
||||
@@ -54,7 +52,7 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderActionActivationRule</key>
|
||||
<string>SUBQUERY ( fileproviderItems, $item, ($item.itemIdentifier != "NSFileProviderRootContainerItemIdentifier" ) ).@count > 0</string>
|
||||
<string>SUBQUERY ( fileproviderItems, $item, ($item.itemIdentifier != "NSFileProviderRootContainerItemIdentifier" ) ).@count > 0</string>
|
||||
<key>NSExtensionFileProviderActionIdentifier</key>
|
||||
<string>ch.protonmail.drive.fileprovider.action.open_in_browser</string>
|
||||
<key>NSExtensionFileProviderActionName</key>
|
||||
@@ -69,6 +67,24 @@
|
||||
<string>Refresh</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSExtensionFileProviderAllowsContextualMenuDownloadEntry</key>
|
||||
<false/>
|
||||
<key>NSExtensionFileProviderAllowsUserControlledEviction</key>
|
||||
<false/>
|
||||
<key>NSExtensionFileProviderDocumentGroup</key>
|
||||
<string>$(TeamIdentifierPrefix)ch.protonmail.protondrive</string>
|
||||
<key>NSExtensionFileProviderDownloadPipelineDepth</key>
|
||||
<integer>8</integer>
|
||||
<key>NSExtensionFileProviderSupportsEnumeration</key>
|
||||
<true/>
|
||||
<key>NSExtensionFileProviderUploadPipelineDepth</key>
|
||||
<integer>8</integer>
|
||||
<key>NSExtensionFileProviderWantsFlattenedPackages</key>
|
||||
<true/>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.fileprovider-nonui</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).FileProviderExtension</string>
|
||||
<key>NSFileProviderDecorations</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -112,22 +128,6 @@
|
||||
<string></string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSExtensionFileProviderAllowsContextualMenuDownloadEntry</key>
|
||||
<false/>
|
||||
<key>NSExtensionFileProviderAllowsUserControlledEviction</key>
|
||||
<false/>
|
||||
<key>NSExtensionFileProviderSupportsEnumeration</key>
|
||||
<true/>
|
||||
<key>NSExtensionFileProviderWantsFlattenedPackages</key>
|
||||
<true/>
|
||||
<key>NSExtensionFileProviderDownloadPipelineDepth</key>
|
||||
<integer>8</integer>
|
||||
<key>NSExtensionFileProviderUploadPipelineDepth</key>
|
||||
<integer>8</integer>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.fileprovider-nonui</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).FileProviderExtension</string>
|
||||
</dict>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
|
||||
@@ -173,7 +173,10 @@ class AppCoordinator: NSObject, ObservableObject {
|
||||
let promoCampaignInteractor = PromoCampaignInteractor.shared
|
||||
|
||||
#if HAS_BUILTIN_UPDATER
|
||||
let appUpdateService = SparkleAppUpdateService()
|
||||
let featureFlagsStore = initialServices.localSettings
|
||||
let appUpdateService = SparkleAppUpdateService(
|
||||
gradualRolloutEnabled: featureFlagsStore.isFeatureEnabled(.driveMacGradualRolloutChannelEnabled)
|
||||
)
|
||||
#else
|
||||
let appUpdateService: AppUpdateServiceProtocol? = nil
|
||||
#endif
|
||||
|
||||
@@ -198,6 +198,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
||||
coordinator?.toggleStatusWindow(onlyOpen: true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: UNUserNotificationCenterDelegate {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "status-signed-out-update-available.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 7.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
11.607819 0.000000 m
|
||||
14.093100 0.000000 16.107819 2.014719 16.107819 4.500000 c
|
||||
16.107819 6.985281 14.093100 9.000000 11.607819 9.000000 c
|
||||
9.122538 9.000000 7.107819 6.985281 7.107819 4.500000 c
|
||||
7.107819 2.014719 9.122538 0.000000 11.607819 0.000000 c
|
||||
h
|
||||
12.056848 6.554014 m
|
||||
12.056848 6.815659 11.868741 7.000000 11.601750 7.000000 c
|
||||
11.340828 7.000000 11.146654 6.815659 11.146654 6.554014 c
|
||||
11.146654 4.580773 l
|
||||
11.195197 3.326066 l
|
||||
10.588401 4.039643 l
|
||||
9.866314 4.753221 l
|
||||
9.781363 4.836472 9.672139 4.884044 9.544712 4.884044 c
|
||||
9.295925 4.884044 9.107819 4.699703 9.107819 4.455897 c
|
||||
9.107819 4.331021 9.144226 4.223984 9.223110 4.146680 c
|
||||
11.249809 2.172448 l
|
||||
11.371168 2.053518 11.474323 2.000000 11.601750 2.000000 c
|
||||
11.735246 2.000000 11.844469 2.059465 11.959761 2.172448 c
|
||||
13.980392 4.146680 l
|
||||
14.059275 4.223984 14.107819 4.331021 14.107819 4.455897 c
|
||||
14.107819 4.699703 13.913644 4.884044 13.664858 4.884044 c
|
||||
13.531363 4.884044 13.422139 4.842418 13.343256 4.753221 c
|
||||
12.627236 4.039643 l
|
||||
12.008305 3.320119 l
|
||||
12.056848 4.580773 l
|
||||
12.056848 6.554014 l
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 5.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
13.001020 -3.281252 m
|
||||
13.001020 1.208868 l
|
||||
12.678776 1.117203 12.344465 1.054240 12.001020 1.022916 c
|
||||
12.001020 -3.281252 l
|
||||
12.001020 -3.678205 11.679226 -4.000000 11.282270 -4.000000 c
|
||||
10.996668 -4.000000 l
|
||||
10.996668 1.022415 l
|
||||
9.494219 1.157925 8.166166 1.898870 7.257843 2.999666 c
|
||||
4.676876 2.999666 l
|
||||
4.378308 2.999666 4.091987 3.118351 3.880975 3.329577 c
|
||||
3.834098 3.376502 l
|
||||
3.435521 3.775486 2.894691 3.999667 2.330730 3.999667 c
|
||||
1.005683 3.999667 l
|
||||
1.005903 4.281811 l
|
||||
1.006213 4.678546 1.327917 5.000000 1.724653 5.000000 c
|
||||
3.113554 5.000000 l
|
||||
3.279167 5.000000 3.441723 4.955386 3.584134 4.870847 c
|
||||
4.509006 4.321819 l
|
||||
4.805897 4.145577 5.144784 4.052567 5.490046 4.052567 c
|
||||
6.574296 4.052567 l
|
||||
6.416604 4.369721 6.288605 4.704270 6.193940 5.052567 c
|
||||
5.490046 5.052567 l
|
||||
5.324432 5.052567 5.161877 5.097182 5.019465 5.181721 c
|
||||
4.094594 5.730749 l
|
||||
3.797702 5.906991 3.458816 6.000000 3.113554 6.000000 c
|
||||
1.724653 6.000000 l
|
||||
0.775937 6.000000 0.006644 5.231306 0.005903 4.282591 c
|
||||
0.000001 -3.279908 l
|
||||
-0.000740 -4.229671 0.768986 -5.000000 1.718749 -5.000000 c
|
||||
11.282270 -5.000000 l
|
||||
12.231509 -5.000000 13.001020 -4.230492 13.001020 -3.281252 c
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
2446
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 16.107819 16.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002536 00000 n
|
||||
0000002559 00000 n
|
||||
0000002732 00000 n
|
||||
0000002806 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2865
|
||||
%%EOF
|
||||
@@ -101,8 +101,6 @@
|
||||
<array>
|
||||
<string>com.apple.icon-decoration.badge</string>
|
||||
</array>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>me.proton.drive.badge.checkmark</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Keep Downloaded</string>
|
||||
<key>UTTypeIcons</key>
|
||||
@@ -110,6 +108,8 @@
|
||||
<key>UTTypeIconName</key>
|
||||
<string>badge-checkmark</string>
|
||||
</dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>me.proton.drive.badge.checkmark</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
|
||||
@@ -21,7 +21,7 @@ import SwiftUI
|
||||
import PDCore
|
||||
import Combine
|
||||
|
||||
struct PromoCampaignConfiguration {
|
||||
struct PromoCampaignConfiguration: Comparable {
|
||||
enum BannerIcon {
|
||||
case drivePlus
|
||||
case discount
|
||||
@@ -36,40 +36,24 @@ struct PromoCampaignConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
enum TimeRange {
|
||||
enum TimeRange: Comparable {
|
||||
/// Campaign should only be active while start < date < end
|
||||
case limitedTime(start: Date, end: Date)
|
||||
/// Campaign should be active after the given date
|
||||
case indefinite(after: Date)
|
||||
/// Campaign should always be active (useful for testing!)
|
||||
case always
|
||||
|
||||
static func < (lhs: TimeRange, rhs: TimeRange) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.limitedTime(let lStart, _), .limitedTime(let rStart, _)),
|
||||
(.limitedTime(let lStart, _), .indefinite(let rStart)),
|
||||
(.indefinite(let lStart), .limitedTime(let rStart, _)),
|
||||
(.indefinite(let lStart), .indefinite(let rStart)):
|
||||
return lStart < rStart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate static let activeCampaigns: [PromoCampaignConfiguration] = [
|
||||
PromoCampaignConfiguration(
|
||||
campaignId: "bf-25-stage-1",
|
||||
timeRange: .limitedTime(
|
||||
start: Date(timeIntervalSinceReferenceDate: 783860400), // 2025-11-03 12:00 CET
|
||||
end: Date(timeIntervalSinceReferenceDate: 785156400) // 2025-11-18 12:00 CET
|
||||
),
|
||||
backgroundColor: Color(hex: "#D8FF00"),
|
||||
tintColor: Color(hex: "#291C5D"),
|
||||
icon: .discount,
|
||||
text: "Black Friday: 50% off",
|
||||
resetsPreviousDismissal: false
|
||||
),
|
||||
PromoCampaignConfiguration(
|
||||
campaignId: "bf-25-stage-2",
|
||||
timeRange: .limitedTime(
|
||||
start: Date(timeIntervalSinceReferenceDate: 785156400), // 2025-11-18 12:00 CET
|
||||
end: Date(timeIntervalSinceReferenceDate: 786452400) // 2025-12-03 12:00 CET
|
||||
),
|
||||
backgroundColor: Color(hex: "#D8FF00"),
|
||||
tintColor: Color(hex: "#291C5D"),
|
||||
icon: .discount,
|
||||
text: "Black Friday: 80% off",
|
||||
resetsPreviousDismissal: true
|
||||
),
|
||||
PromoCampaignConfiguration(
|
||||
campaignId: "upgrade-drive-plus",
|
||||
timeRange: .indefinite(
|
||||
@@ -79,7 +63,8 @@ struct PromoCampaignConfiguration {
|
||||
tintColor: ColorProvider.White,
|
||||
icon: .drivePlus,
|
||||
text: "Upgrade to Drive Plus",
|
||||
resetsPreviousDismissal: false
|
||||
resetsPreviousDismissal: false,
|
||||
displaysOnStatusBar: false
|
||||
)
|
||||
]
|
||||
|
||||
@@ -90,6 +75,11 @@ struct PromoCampaignConfiguration {
|
||||
let icon: BannerIcon
|
||||
let text: String
|
||||
let resetsPreviousDismissal: Bool
|
||||
let displaysOnStatusBar: Bool
|
||||
|
||||
static func < (lhs: PromoCampaignConfiguration, rhs: PromoCampaignConfiguration) -> Bool {
|
||||
lhs.timeRange < rhs.timeRange
|
||||
}
|
||||
}
|
||||
|
||||
protocol PromoCampaignInteractorProtocol {
|
||||
@@ -106,14 +96,16 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol {
|
||||
@SettingsStorage(UserDefaults.PromoCampaign.hasDismissedBanner.rawValue) private var hasDismissedBanner: Bool?
|
||||
@SettingsStorage(UserDefaults.PromoCampaign.lastSeenCampaignId.rawValue) private var lastSeenCampaign: String?
|
||||
|
||||
private let activeCampaigns: [PromoCampaignConfiguration]
|
||||
private var currentlyActiveCampaign = CurrentValueSubject<PromoCampaignConfiguration?, Never>(nil)
|
||||
|
||||
private let dateResource: DateResource
|
||||
|
||||
static let shared = PromoCampaignInteractor()
|
||||
|
||||
init(dateResource: DateResource) {
|
||||
init(dateResource: DateResource, activeCampaigns: [PromoCampaignConfiguration]) {
|
||||
self.dateResource = dateResource
|
||||
self.activeCampaigns = activeCampaigns
|
||||
|
||||
_hasDismissedBanner.configure(with: Constants.appGroup)
|
||||
_lastSeenCampaign.configure(with: Constants.appGroup)
|
||||
@@ -122,7 +114,10 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol {
|
||||
}
|
||||
|
||||
private convenience init() {
|
||||
self.init(dateResource: PromoCampaignDateResource())
|
||||
self.init(
|
||||
dateResource: PromoCampaignDateResource(),
|
||||
activeCampaigns: PromoCampaignConfiguration.activeCampaigns
|
||||
)
|
||||
}
|
||||
|
||||
func refreshCampaign(forceResetBannerDismissal: Bool = false) {
|
||||
@@ -146,7 +141,7 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol {
|
||||
}
|
||||
|
||||
private func getActiveCampaign() -> PromoCampaignConfiguration? {
|
||||
PromoCampaignConfiguration.activeCampaigns.first { campaign in
|
||||
activeCampaigns.sorted().first { campaign in
|
||||
let currentDate = dateResource.getDate()
|
||||
|
||||
switch campaign.timeRange {
|
||||
@@ -154,8 +149,6 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol {
|
||||
return start <= currentDate && currentDate < end
|
||||
case let .indefinite(start):
|
||||
return start <= currentDate
|
||||
case .always:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,15 +263,39 @@ struct QASettingsView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle())
|
||||
Picker(selection: $vm.updateChannel) {
|
||||
ForEach(AppUpdateChannel.allCases.map(\.rawValue), id: \.self) {
|
||||
Text($0)
|
||||
}
|
||||
} label: {
|
||||
Text("Select update channel")
|
||||
|
||||
HStack {
|
||||
TextField("Feed URL", text: $vm.providedFeedURL)
|
||||
Button("Set") { vm.setUpdateFeedURLAndQuit() }
|
||||
Button("Default") { vm.useDefaultUpdateFeedURL() }
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Select update channels (multi-select):")
|
||||
.font(.system(size: 13))
|
||||
|
||||
ForEach(AppUpdateChannel.allCases, id: \.self) { channel in
|
||||
HStack {
|
||||
Button {
|
||||
vm.toggleChannelSelection(channel)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: vm.selectedUpdateChannels.contains(channel) ? "checkmark.square.fill" : "square")
|
||||
.foregroundColor(vm.selectedUpdateChannels.contains(channel) ? .blue : .gray)
|
||||
Text(channel.rawValue)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
Text("Selected: \(vm.selectedUpdateChannels.map(\.rawValue).sorted().joined(separator: ", "))")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(vm.updateMessage)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
@@ -28,7 +28,8 @@ import ProtonCoreServices
|
||||
struct QASettingsConstants {
|
||||
static let shouldUpdateEvenOnDebugBuild = "shouldUpdateEvenOnDebugBuild"
|
||||
static let shouldUpdateEvenOnTestFlight = "shouldUpdateEvenOnTestFlight"
|
||||
static let updateChannel = "updateChannel"
|
||||
static let updateChannels = "updateChannels"
|
||||
static let updateFeedURL = "updateFeedURL"
|
||||
static let shouldObfuscateDumpsStorage = "shouldObfuscateDumpsStorage"
|
||||
static let disconnectDomainOnSignOut = "disconnectDomainOnSignOut"
|
||||
static let driveDDKEnabledInQASettings = "driveDDKEnabledInQASettings"
|
||||
@@ -63,13 +64,15 @@ class QASettingsViewModel: ObservableObject {
|
||||
@Published var shouldUpdateEvenOnTestFlight: Bool = false {
|
||||
didSet { shouldUpdateEvenOnTestFlightStorage = shouldUpdateEvenOnTestFlight }
|
||||
}
|
||||
@Published var updateChannel: String = AppUpdateChannel.stable.rawValue {
|
||||
didSet { updateChannelStorage = updateChannel }
|
||||
@Published var selectedUpdateChannels: Set<AppUpdateChannel> = [AppUpdateChannel.stable] {
|
||||
didSet { updateChannelsStorage = Array(selectedUpdateChannels) }
|
||||
}
|
||||
@Published var updateMessage: String = ""
|
||||
@Published var providedFeedURL: String = ""
|
||||
@SettingsStorage(QASettingsConstants.shouldUpdateEvenOnDebugBuild) var shouldUpdateEvenOnDebugBuildStorage: Bool?
|
||||
@SettingsStorage(QASettingsConstants.shouldUpdateEvenOnTestFlight) var shouldUpdateEvenOnTestFlightStorage: Bool?
|
||||
@SettingsStorage(QASettingsConstants.updateChannel) var updateChannelStorage: String?
|
||||
@SettingsStorage(QASettingsConstants.updateFeedURL) var updateFeedURL: String?
|
||||
@SettingsCodableProperty(QASettingsConstants.updateChannels) var updateChannelsStorage: [AppUpdateChannel] = []
|
||||
#endif
|
||||
|
||||
@Published var shouldFetchEvents: Bool = true {
|
||||
@@ -226,7 +229,8 @@ class QASettingsViewModel: ObservableObject {
|
||||
#if HAS_BUILTIN_UPDATER
|
||||
self.shouldUpdateEvenOnDebugBuild = shouldUpdateEvenOnDebugBuildStorage ?? false
|
||||
self.shouldUpdateEvenOnTestFlight = shouldUpdateEvenOnTestFlightStorage ?? false
|
||||
self.updateChannel = updateChannelStorage ?? AppUpdateChannel.stable.rawValue
|
||||
self.providedFeedURL = updateFeedURL ?? "https://proton.me/download/drive/macos/appcast.xml"
|
||||
self.selectedUpdateChannels = Set(updateChannelsStorage ?? [AppUpdateChannel.stable])
|
||||
if let appUpdateService {
|
||||
self.updateMessage = """
|
||||
Last update check: \(appUpdateService.updater.lastUpdateCheckDate.map(String.init) ?? "never")
|
||||
@@ -241,11 +245,31 @@ class QASettingsViewModel: ObservableObject {
|
||||
guard let self else { return }
|
||||
Constants.appGroup.userDefaults.set(self.environment, forKey: Constants.SettingsBundleKeys.host.rawValue)
|
||||
await self.signoutManager?.signOutAsync()
|
||||
_ = await MainActor.run {
|
||||
exit(0)
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
#if HAS_BUILTIN_UPDATER
|
||||
func toggleChannelSelection(_ channel: AppUpdateChannel) {
|
||||
if selectedUpdateChannels.contains(channel) {
|
||||
// Don't allow deselecting all channels - at least one must be selected
|
||||
if selectedUpdateChannels.count > 1 {
|
||||
selectedUpdateChannels.remove(channel)
|
||||
}
|
||||
} else {
|
||||
selectedUpdateChannels.insert(channel)
|
||||
}
|
||||
}
|
||||
|
||||
func setUpdateFeedURLAndQuit() {
|
||||
self.updateFeedURL = providedFeedURL
|
||||
}
|
||||
|
||||
func useDefaultUpdateFeedURL() {
|
||||
self.providedFeedURL = "https://proton.me/download/drive/macos/appcast.xml"
|
||||
self.updateFeedURL = nil
|
||||
}
|
||||
#endif
|
||||
|
||||
func jail() {
|
||||
Task { [weak self] in
|
||||
|
||||
@@ -45,33 +45,28 @@ enum UpdateAvailabilityStatus: Equatable {
|
||||
case errored(userFacingMessage: String)
|
||||
}
|
||||
|
||||
enum AppUpdateChannel: String, CaseIterable {
|
||||
enum AppUpdateChannel: String, CaseIterable, Codable {
|
||||
case stable
|
||||
case beta
|
||||
case alpha
|
||||
#if HAS_QA_FEATURES
|
||||
// special channels for testing variou update scenarios
|
||||
case testNoUpdate = "test-no-update"
|
||||
case testUpdateAvailable = "test-update-available"
|
||||
case testInvalidUpdate = "test-invalid"
|
||||
case testKeyRotation = "test-key-rotation"
|
||||
#endif
|
||||
case gradualRollout = "gradual-rollout"
|
||||
}
|
||||
|
||||
final class SparkleAppUpdateService: NSObject, AppUpdateServiceProtocol, SPUUpdaterDelegate, SPUStandardUserDriverDelegate {
|
||||
|
||||
|
||||
@Published private(set) var updateAvailability: UpdateAvailabilityStatus
|
||||
var updateAvailabilityPublisher: AnyPublisher<UpdateAvailabilityStatus, Never> {
|
||||
self.$updateAvailability.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
private static let shortUpdateCheckInterval: TimeInterval = 60 * 60 // one hour, minumum possible in Spark
|
||||
private static let longUpdateCheckInterval: TimeInterval = 24 * 60 * 60 // one day
|
||||
|
||||
|
||||
private let gradualRolloutEnabled: () -> Bool
|
||||
|
||||
#if HAS_QA_FEATURES
|
||||
@SettingsStorage(QASettingsConstants.shouldUpdateEvenOnDebugBuild) private var shouldUpdateEvenOnDebugBuild: Bool?
|
||||
@SettingsStorage(QASettingsConstants.shouldUpdateEvenOnTestFlight) private var shouldUpdateEvenOnTestFlight: Bool?
|
||||
@SettingsStorage(QASettingsConstants.updateChannel) private var updateChannel: String?
|
||||
@SettingsStorage(QASettingsConstants.updateFeedURL) private var updateFeedURL: String?
|
||||
@SettingsCodableProperty(QASettingsConstants.updateChannels) private var qaOverrideUpdateChannels: [AppUpdateChannel] = []
|
||||
#endif
|
||||
|
||||
private var debugBuild: Bool {
|
||||
@@ -89,6 +84,9 @@ final class SparkleAppUpdateService: NSObject, AppUpdateServiceProtocol, SPUUpda
|
||||
|
||||
private var isUpdateMechanismOn: Bool {
|
||||
#if HAS_QA_FEATURES
|
||||
// ensure no update happens for UI tests
|
||||
if Constants.isInUITests { return false }
|
||||
|
||||
let shouldUpdateEvenOnTestFlight = self.shouldUpdateEvenOnTestFlight ?? false
|
||||
let shouldUpdateEvenOnDebugBuild = self.shouldUpdateEvenOnDebugBuild ?? false
|
||||
#else
|
||||
@@ -113,7 +111,8 @@ final class SparkleAppUpdateService: NSObject, AppUpdateServiceProtocol, SPUUpda
|
||||
private var updaterController: SPUStandardUpdaterController!
|
||||
#endif
|
||||
|
||||
init(updaterController: SPUStandardUpdaterController? = nil) {
|
||||
init(gradualRolloutEnabled: @autoclosure @escaping () -> Bool, updaterController: SPUStandardUpdaterController? = nil) {
|
||||
self.gradualRolloutEnabled = gradualRolloutEnabled
|
||||
self.updateAvailability = .upToDate(version: Constants.versionDigits)
|
||||
super.init()
|
||||
if let updaterController {
|
||||
@@ -160,6 +159,14 @@ final class SparkleAppUpdateService: NSObject, AppUpdateServiceProtocol, SPUUpda
|
||||
|
||||
// configuration
|
||||
extension SparkleAppUpdateService {
|
||||
|
||||
func feedURLString(for updater: SPUUpdater) -> String? {
|
||||
#if HAS_QA_FEATURES
|
||||
return updateFeedURL
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
var supportsGentleScheduledUpdateReminders: Bool {
|
||||
return true
|
||||
@@ -167,11 +174,24 @@ extension SparkleAppUpdateService {
|
||||
|
||||
func allowedChannels(for updater: SPUUpdater) -> Set<String> {
|
||||
#if HAS_QA_FEATURES
|
||||
updateChannel.map { [$0] } ?? []
|
||||
#else
|
||||
// we only allow the stable channel in non-QA builds
|
||||
[AppUpdateChannel.stable.rawValue]
|
||||
// ensure no update happens for UI tests
|
||||
if Constants.isInUITests { return [] }
|
||||
// In QA builds, use the selected channels from settings
|
||||
// If no channels are selected in the QA settings, default to the usual logic
|
||||
guard qaOverrideUpdateChannels.isEmpty else {
|
||||
return Set(qaOverrideUpdateChannels.map(\.rawValue))
|
||||
}
|
||||
#endif
|
||||
|
||||
// Always include the stable channel
|
||||
var channels: Set<String> = [AppUpdateChannel.stable.rawValue]
|
||||
|
||||
// If the gradual rollout feature flag is enabled, also include the gradual rollout channel
|
||||
if gradualRolloutEnabled() {
|
||||
channels.insert(AppUpdateChannel.gradualRollout.rawValue)
|
||||
}
|
||||
|
||||
return channels
|
||||
}
|
||||
|
||||
func updaterShouldRelaunchApplication(_ updater: SPUUpdater) -> Bool {
|
||||
|
||||
@@ -163,7 +163,11 @@ class ApplicationState: ObservableObject {
|
||||
return .fullResyncInProgress
|
||||
}
|
||||
if accountInfo == nil {
|
||||
return .signedOut
|
||||
if isUpdateAvailable {
|
||||
return .signedOutAndUpdateAvailable
|
||||
} else {
|
||||
return .signedOut
|
||||
}
|
||||
}
|
||||
if isPaused {
|
||||
return .paused
|
||||
|
||||
@@ -42,6 +42,7 @@ enum ApplicationSyncStatus: Sendable, Equatable {
|
||||
case syncing
|
||||
// Source: AppUpdateService
|
||||
case updateAvailable
|
||||
case signedOutAndUpdateAvailable
|
||||
// Source: FileProvider
|
||||
case errored(Int)
|
||||
// Source: FileProvider
|
||||
@@ -53,7 +54,7 @@ enum ApplicationSyncStatus: Sendable, Equatable {
|
||||
var displayLabel: String {
|
||||
switch self {
|
||||
case .launching: Localization.menu_status_sync_launching
|
||||
case .signedOut: Localization.menu_status_signed_out
|
||||
case .signedOut, .signedOutAndUpdateAvailable: Localization.menu_status_signed_out
|
||||
case .paused: Localization.menu_status_sync_paused
|
||||
case .offline: Localization.menu_status_offline
|
||||
case .enumerating(let itemEnumerationDescription): itemEnumerationDescription
|
||||
|
||||
+3
-4
@@ -163,10 +163,9 @@ final class MainWindowCoordinator: NSObject, NSWindowDelegate {
|
||||
let idealX = buttonRect.midX - MainWindow.size.width / 2
|
||||
let requiredX = screenFrame.origin.x + screenFrame.width - MainWindow.size.width
|
||||
let requiredY = screenFrame.origin.y + screenFrame.height - MainWindow.size.height
|
||||
window.setFrameOrigin(NSPoint(
|
||||
x: min(idealX, requiredX),
|
||||
y: requiredY
|
||||
))
|
||||
let wouldBeOffScreenOnLeft = idealX < 0
|
||||
let x = wouldBeOffScreenOnLeft ? requiredX : min(idealX, requiredX)
|
||||
window.setFrameOrigin(NSPoint(x: x, y: requiredY))
|
||||
}
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
if #available(macOS 14.0, *) {
|
||||
|
||||
@@ -67,6 +67,7 @@ struct SyncStateView: View {
|
||||
.tint(ColorProvider.SignalDanger)
|
||||
case .synced,
|
||||
.signedOut,
|
||||
.signedOutAndUpdateAvailable,
|
||||
.updateAvailable,
|
||||
.fullResyncCompleted:
|
||||
Image("synced")
|
||||
|
||||
@@ -104,6 +104,8 @@ final class MenuBarCoordinator: NSObject, ObservableObject, NSMenuDelegate {
|
||||
switch status {
|
||||
case .signedOut:
|
||||
return "status-signed-out"
|
||||
case .signedOutAndUpdateAvailable:
|
||||
return "status-signed-out-update-available"
|
||||
case .paused:
|
||||
return "status-paused"
|
||||
case .offline:
|
||||
@@ -115,7 +117,7 @@ final class MenuBarCoordinator: NSObject, ObservableObject, NSMenuDelegate {
|
||||
case .updateAvailable:
|
||||
return "status-update-available"
|
||||
case .synced, .fullResyncCompleted:
|
||||
if state.visibleCampaign != nil && !state.items.isEmpty {
|
||||
if let campaign = state.visibleCampaign, campaign.displaysOnStatusBar, !state.items.isEmpty {
|
||||
return "status-promo"
|
||||
} else {
|
||||
return "status-synced"
|
||||
|
||||
@@ -45,7 +45,8 @@ class DeleteAlerter {
|
||||
guard deleteAlertShown != true else { return }
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Deleted cloud-only files can be recovered from Trash on the web"
|
||||
alert.icon = NSApp.applicationIconImage
|
||||
alert.messageText = "Deleted cloud-only files can be recovered from Proton Drive Trash on the web"
|
||||
alert.informativeText = "Files deleted from your Mac that are cloud-only will be permanently removed from your computer but can still be restored from Proton Drive Trash on the web. Files stored locally will be moved to your Mac's Trash."
|
||||
|
||||
alert.addButton(withTitle: "OK")
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<div>
|
||||
<h1>2.10.2</h1>
|
||||
|
||||
<p>
|
||||
- Improves update behavior for future releases<br>
|
||||
</p>
|
||||
|
||||
<h1>2.10.1</h1>
|
||||
|
||||
<p>
|
||||
|
||||
Reference in New Issue
Block a user