diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index b2d70232..b03205bb 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -51,6 +51,10 @@ extension Defaults.Keys { static let trendingCountry = Key("trendingCountry", default: .us) static let visibleSections = Key>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists]) + + #if os(macOS) + static let enableBetaChannel = Key("enableBetaChannel", default: false) + #endif } enum ResolutionSetting: String, CaseIterable, Defaults.Serializable { diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index 0b1b999a..83130f1e 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -5,7 +5,7 @@ import SwiftUI struct SettingsView: View { #if os(macOS) private enum Tabs: Hashable { - case instances, browsing, playback, services + case instances, browsing, playback, services, updates } #endif @@ -57,6 +57,14 @@ struct SettingsView: View { Label("Services", systemImage: "puzzlepiece") } .tag(Tabs.services) + + Form { + UpdatesSettings() + } + .tabItem { + Label("Updates", systemImage: "gearshape.2") + } + .tag(Tabs.updates) } .padding(20) .frame(width: 400, height: 380) diff --git a/Shared/Yattee.entitlements b/Shared/Yattee.entitlements index ee95ab7e..d858aaec 100644 --- a/Shared/Yattee.entitlements +++ b/Shared/Yattee.entitlements @@ -4,6 +4,11 @@ com.apple.security.app-sandbox + com.apple.security.temporary-exception.mach-lookup.global-name + + $(PRODUCT_BUNDLE_IDENTIFIER)-spki + $(PRODUCT_BUNDLE_IDENTIFIER)-spks + com.apple.security.network.client diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index dfe0615f..9acc0291 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -5,6 +5,7 @@ import SwiftUI struct YatteeApp: App { #if os(macOS) @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @StateObject private var updater = UpdaterModel() #endif @StateObject private var menu = MenuModel() @@ -18,7 +19,16 @@ struct YatteeApp: App { .handlesExternalEvents(matching: Set(["*"])) .commands { SidebarCommands() + CommandGroup(replacing: .newItem, addition: {}) + + #if os(macOS) + CommandGroup(after: .appInfo) { + CheckForUpdatesView() + .environmentObject(updater) + } + #endif + MenuCommands(model: Binding(get: { menu }, set: { _ in })) } #endif @@ -28,6 +38,7 @@ struct YatteeApp: App { SettingsView() .environmentObject(AccountsModel()) .environmentObject(InstancesModel()) + .environmentObject(updater) } #endif } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 7e4eb9af..0a56bfa6 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -407,6 +407,7 @@ 37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */; }; 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; + 37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE7AF02760013C00DBECED /* UpdatesSettings.swift */; }; 37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; 37BF661D27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; 37BF661F27308884008CCFB0 /* DropFavoriteOutside.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */; }; @@ -442,6 +443,9 @@ 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3705B17F267B4DFB00704544 /* TrendingCountry.swift */; }; + 37C90AEF275F9CC00015EAF7 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 37C90AEE275F9CC00015EAF7 /* Sparkle */; }; + 37C90AF1275F9D450015EAF7 /* UpdaterModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */; }; + 37C90AF3275F9D5D0015EAF7 /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */; }; 37CB12792724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; 37CB127A2724C76D00213B45 /* VideoURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB12782724C76D00213B45 /* VideoURLParser.swift */; }; 37CB128B2724CC1F00213B45 /* VideoURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */; }; @@ -691,6 +695,7 @@ 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; + 37BE7AF02760013C00DBECED /* UpdatesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesSettings.swift; sourceTree = ""; }; 37BF661B27308859008CCFB0 /* DropFavorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavorite.swift; sourceTree = ""; }; 37BF661E27308884008CCFB0 /* DropFavoriteOutside.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropFavoriteOutside.swift; sourceTree = ""; }; 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = ""; }; @@ -704,6 +709,8 @@ 37C3A24C272360470087A57A /* ChannelPlaylist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChannelPlaylist+Fixtures.swift"; sourceTree = ""; }; 37C3A250272366440087A57A /* ChannelPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylistView.swift; sourceTree = ""; }; 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = ""; }; + 37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterModel.swift; sourceTree = ""; }; + 37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; 37CB12782724C76D00213B45 /* VideoURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParser.swift; sourceTree = ""; }; 37CB127B2724C79D00213B45 /* VideoURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoURLParserTests.swift; sourceTree = ""; }; 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItem.swift; sourceTree = ""; }; @@ -793,6 +800,7 @@ 37FB284F272209AB00A57617 /* SDWebImageSwiftUI in Frameworks */, 3765917A27237D07009F956E /* PINCache in Frameworks */, 37BD07BE2698AC96003EBB87 /* Defaults in Frameworks */, + 37C90AEF275F9CC00015EAF7 /* Sparkle in Frameworks */, 37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */, 377FC7ED267A0A0800A6BBAF /* SwiftyJSON in Frameworks */, 37BD07C02698AC97003EBB87 /* Siesta in Frameworks */, @@ -1101,6 +1109,7 @@ 37BE0BD826A214500092E2DB /* macOS */ = { isa = PBXGroup; children = ( + 37BE7AF227601DBF00DBECED /* Updates */, 374C0542272496E4009BDDBE /* AppDelegate.swift */, 37FD43DB270470B70073EE42 /* InstancesSettings.swift */, 374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */, @@ -1112,6 +1121,16 @@ path = macOS; sourceTree = ""; }; + 37BE7AF227601DBF00DBECED /* Updates */ = { + isa = PBXGroup; + children = ( + 37C90AF2275F9D5D0015EAF7 /* CheckForUpdatesView.swift */, + 37C90AF0275F9D450015EAF7 /* UpdaterModel.swift */, + 37BE7AF02760013C00DBECED /* UpdatesSettings.swift */, + ); + path = Updates; + sourceTree = ""; + }; 37C7A9022679058300E721B4 /* Extensions */ = { isa = PBXGroup; children = ( @@ -1376,6 +1395,7 @@ 37FB2850272209AB00A57617 /* SDWebImageWebPCoder */, 37FB285727220D9600A57617 /* SDWebImagePINPlugin */, 3765917927237D07009F956E /* PINCache */, + 37C90AEE275F9CC00015EAF7 /* Sparkle */, ); productName = "Yattee (macOS)"; productReference = 37D4B0CF2671614900C925CA /* Yattee.app */; @@ -1539,6 +1559,7 @@ 37FB2847272207F000A57617 /* XCRemoteSwiftPackageReference "SDWebImageWebPCoder" */, 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */, 3765917827237D07009F956E /* XCRemoteSwiftPackageReference "PINCache" */, + 37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */, ); productRefGroup = 37D4B0CA2671614900C925CA /* Products */; projectDirPath = ""; @@ -1942,6 +1963,7 @@ 3788AC2826F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 378AE93A274EDFAF006A4EE1 /* Badge+Backport.swift in Sources */, 37599F35272B44000087F250 /* FavoritesModel.swift in Sources */, + 37C90AF3275F9D5D0015EAF7 /* CheckForUpdatesView.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */, @@ -1954,6 +1976,7 @@ 377FC7E2267A084A00A6BBAF /* VideoCell.swift in Sources */, 37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */, 378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */, + 37BE7AF12760013C00DBECED /* UpdatesSettings.swift in Sources */, 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, 37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, 3765788A2685471400D4EA09 /* Playlist.swift in Sources */, @@ -1971,6 +1994,7 @@ 3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, 372915E72687E3B900F5A35B /* Defaults.swift in Sources */, 37C3A242272359900087A57A /* Double+Format.swift in Sources */, + 37C90AF1275F9D450015EAF7 /* UpdaterModel.swift in Sources */, 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 376578922685490700D4EA09 /* PlaylistsView.swift in Sources */, 37484C2626FC83E000287258 /* InstanceForm.swift in Sources */, @@ -3076,6 +3100,14 @@ minimumVersion = 0.1.3; }; }; + 37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + branch = 2.x; + kind = branch; + }; + }; 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON.git"; @@ -3226,6 +3258,11 @@ package = 37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; + 37C90AEE275F9CC00015EAF7 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 37C90AED275F9CC00015EAF7 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; 37D4B19C2671817900C925CA /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 37D4B19B2671817900C925CA /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6d2dbedf..8139743d 100644 --- a/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Yattee.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -91,6 +91,15 @@ "version": "1.5.2" } }, + { + "package": "Sparkle", + "repositoryURL": "https://github.com/sparkle-project/Sparkle", + "state": { + "branch": "2.x", + "revision": "831e9b4eb7e871a9c072469fb14049614fc92810", + "version": null + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", diff --git a/macOS/Info.plist b/macOS/Info.plist index e8105311..ff8e1b28 100644 --- a/macOS/Info.plist +++ b/macOS/Info.plist @@ -15,5 +15,11 @@ + SUEnableInstallerLauncherService + + SUFeedURL + https://repos.yattee.stream/appcast.xml + SUPublicEDKey + 73U5at3utQRS7F/z/7nztpjp3l1gw1Ih+ztOelRLSx4= diff --git a/macOS/Updates/CheckForUpdatesView.swift b/macOS/Updates/CheckForUpdatesView.swift new file mode 100644 index 00000000..db7ee99a --- /dev/null +++ b/macOS/Updates/CheckForUpdatesView.swift @@ -0,0 +1,10 @@ +import SwiftUI + +struct CheckForUpdatesView: View { + @EnvironmentObject private var updater + + var body: some View { + Button("Check For Updates…", action: updater.checkForUpdates) + .disabled(!updater.canCheckForUpdates) + } +} diff --git a/macOS/Updates/UpdaterModel.swift b/macOS/Updates/UpdaterModel.swift new file mode 100644 index 00000000..5fb17565 --- /dev/null +++ b/macOS/Updates/UpdaterModel.swift @@ -0,0 +1,41 @@ +import Defaults +import Sparkle +import SwiftUI + +final class UpdaterModel: ObservableObject { + @Published var canCheckForUpdates = false + + private let updaterController: SPUStandardUpdaterController + private let updaterDelegate = UpdaterDelegate() + + init() { + updaterController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: updaterDelegate, + userDriverDelegate: nil + ) + + updaterController.updater.publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } + + func checkForUpdates() { + updaterController.checkForUpdates(nil) + } + + var automaticallyChecksForUpdates: Bool { + updaterController.updater.automaticallyChecksForUpdates + } + + func setAutomaticallyChecksForUpdates(_ value: Bool) { + updaterController.updater.automaticallyChecksForUpdates = value + } +} + +final class UpdaterDelegate: NSObject, SPUUpdaterDelegate { + @Default(.enableBetaChannel) private var enableBetaChannel + + func allowedChannels(for _: SPUUpdater) -> Set { + Set(enableBetaChannel ? ["beta"] : []) + } +} diff --git a/macOS/Updates/UpdatesSettings.swift b/macOS/Updates/UpdatesSettings.swift new file mode 100644 index 00000000..9fd2baef --- /dev/null +++ b/macOS/Updates/UpdatesSettings.swift @@ -0,0 +1,32 @@ +import Defaults +import SwiftUI + +struct UpdatesSettings: View { + @EnvironmentObject private var updater + + @State private var automaticallyChecksForUpdates = false + @Default(.enableBetaChannel) private var enableBetaChannel + + var body: some View { + Section(header: SettingsHeader(text: "Updates")) { + Toggle("Check automatically", isOn: $automaticallyChecksForUpdates) + Toggle("Enable beta channel", isOn: $enableBetaChannel) + } + .onAppear { + automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates + } + .onChange(of: automaticallyChecksForUpdates) { _ in + updater.setAutomaticallyChecksForUpdates(automaticallyChecksForUpdates) + } + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + } +} + +struct UpdatesSettings_Previews: PreviewProvider { + static var previews: some View { + UpdatesSettings() + .injectFixtureEnvironmentObjects() + } +}