From 05e94902b80c5e22f1953cd44282079e3cbd855d Mon Sep 17 00:00:00 2001 From: Chris Rittenhouse Date: Fri, 28 Apr 2023 15:26:57 -0400 Subject: [PATCH 1/3] [Experimental Feature] Show Status Bar (#241) * Adds @Feature for Show Status Bar * Shows the Status Bar if the @Feature is enabled * Forces light content on Status Bar --- Delta/Emulation/GameViewController.swift | 8 ++++++++ Delta/Experimental Features/ExperimentalFeatures.swift | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index 2d417e4..da88100 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -180,6 +180,14 @@ class GameViewController: DeltaCore.GameViewController return .all } + override var prefersStatusBarHidden: Bool { + return !ExperimentalFeatures.shared.showStatusBar.isEnabled + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + required init() { super.init() diff --git a/Delta/Experimental Features/ExperimentalFeatures.swift b/Delta/Experimental Features/ExperimentalFeatures.swift index 3a6125a..40e2ac9 100644 --- a/Delta/Experimental Features/ExperimentalFeatures.swift +++ b/Delta/Experimental Features/ExperimentalFeatures.swift @@ -17,6 +17,10 @@ struct ExperimentalFeatures: FeatureContainer options: VariableFastForwardOptions()) var variableFastForward + @Feature(name: "Show Status Bar", + description: "Enable to show the Status Bar during gameplay.") + var showStatusBar + private init() { self.prepareFeatures() From 6bdc05f640e5e762d06ef0d3bca2bb696f62a961 Mon Sep 17 00:00:00 2001 From: Chris Rittenhouse Date: Fri, 28 Apr 2023 15:35:26 -0400 Subject: [PATCH 2/3] [Experimental Feature] Game Screenshots (#242) * Adds extension to handle interactions with Photos app - isAuthorized to check/ask for authorization - saveUIImage to save a UIImage to Photos * Adds a Pause Menu button for taking game screenshots * Adds @Feature and @Options for Game Screenshots * Implements Game Screenshots feature in GameViewController * Updates project settings * Passes call to save to Photos as a closure into authorization prompt - Ensures that the screenshot is saved when the user is first prompted for access to Photos - More elegant extension code --------- Co-authored-by: Riley Testut --- Delta.xcodeproj/project.pbxproj | 8 ++ Delta/Emulation/GameViewController.swift | 71 ++++++++++++++++++ .../ExperimentalFeatures.swift | 5 ++ .../Features/GameScreenshots.swift | 54 +++++++++++++ .../PHPhotoLibrary+Authorization.swift | 46 ++++++++++++ Delta/Pause Menu/PauseViewController.swift | 9 ++- .../Screenshot.imageset/Contents.json | 26 +++++++ .../Screenshot.imageset/screenshot.png | Bin 0 -> 1279 bytes .../Screenshot.imageset/screenshot@2x.png | Bin 0 -> 2157 bytes .../Screenshot.imageset/screenshot@3x.png | Bin 0 -> 2934 bytes 10 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 Delta/Experimental Features/Features/GameScreenshots.swift create mode 100644 Delta/Extensions/PHPhotoLibrary+Authorization.swift create mode 100644 Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/Contents.json create mode 100644 Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot.png create mode 100644 Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot@2x.png create mode 100644 Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot@3x.png diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 286b92e..92871dc 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ /* Begin PBXBuildFile section */ 1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; }; 87343D7B985519A5890A61C6 /* libPods-DeltaPreviews.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E3E5A45AB20C8A87754453B /* libPods-DeltaPreviews.a */; }; + ACF7E30D29F73D03000FE071 /* GameScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */; }; + ACF7E30F29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */; }; BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF00BEA525B758AA00C8607D /* SystemBIOS.swift */; }; BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */; }; BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF04E6FE1DB8625C000F35D3 /* ControllerSkinsViewController.swift */; }; @@ -257,6 +259,8 @@ 8ECE6641DE30D01EA30FE7F6 /* Pods-DeltaPreviews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.release.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.release.xcconfig"; sourceTree = ""; }; A01281C7023C0041B25963BE /* Pods-DeltaPreviews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.debug.xcconfig"; sourceTree = ""; }; A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = ""; }; + ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScreenshots.swift; sourceTree = ""; }; + ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Authorization.swift"; sourceTree = ""; }; BF00BEA525B758AA00C8607D /* SystemBIOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemBIOS.swift; sourceTree = ""; }; BF02D5D91DDEBB3000A5E131 /* openvgdb.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = openvgdb.sqlite; sourceTree = ""; }; BF0418131D01E93400E85BCF /* GBADeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBADeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -526,6 +530,7 @@ D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */, D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */, D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */, + ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */, ); path = Extensions; sourceTree = ""; @@ -1068,6 +1073,7 @@ isa = PBXGroup; children = ( D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */, + ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */, ); path = Features; sourceTree = ""; @@ -1554,6 +1560,8 @@ D5A9C00329DDED6D00A8D610 /* ExperimentalFeaturesView.swift in Sources */, BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */, BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */, + ACF7E30F29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift in Sources */, + ACF7E30D29F73D03000FE071 /* GameScreenshots.swift in Sources */, BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */, BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */, diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index da88100..955948c 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import Photos import DeltaCore import GBADeltaCore @@ -412,6 +413,9 @@ extension GameViewController pauseViewController.fastForwardItem?.action = { [unowned self] item in self.performFastForwardAction(activate: item.isSelected) } + pauseViewController.screenshotItem?.action = { [unowned self] item in + self.performScreenshotAction() + } pauseViewController.sustainButtonsItem?.isSelected = gameController.sustainedInputs.count > 0 pauseViewController.sustainButtonsItem?.action = { [unowned self, unowned pauseViewController] item in @@ -1078,6 +1082,73 @@ extension GameViewController emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound } } + + func performScreenshotAction() + { + guard let snapshot = self.emulatorCore?.videoManager.snapshot() else { return } + + let imageScale = ExperimentalFeatures.shared.gameScreenshots.size?.rawValue ?? 1.0 + let imageSize = CGSize(width: snapshot.size.width * imageScale, height: snapshot.size.height * imageScale) + + let format = UIGraphicsImageRendererFormat() + format.scale = 1 + let renderer = UIGraphicsImageRenderer(size: imageSize, format: format) + + let scaledSnapshot = renderer.image { (context) in + context.cgContext.interpolationQuality = .none + snapshot.draw(in: CGRect(origin: .zero, size: imageSize)) + } + + if ExperimentalFeatures.shared.gameScreenshots.saveToPhotos + { + PHPhotoLibrary.runIfAuthorized + { + PHPhotoLibrary.saveUIImage(image: scaledSnapshot) + } + } + + if ExperimentalFeatures.shared.gameScreenshots.saveToFiles + { + guard let data = scaledSnapshot.pngData() else { return } + + let screenshotsDirectory = FileManager.default.documentsDirectory.appendingPathComponent("Screenshots") + + do + { + try FileManager.default.createDirectory(at: screenshotsDirectory, withIntermediateDirectories: true, attributes: nil) + } + catch + { + print(error) + } + + let date = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" + + let fileName: URL + if let game = self.game as? Game + { + let filename = game.name + "_" + dateFormatter.string(from: date) + ".png" + fileName = screenshotsDirectory.appendingPathComponent(filename) + } + else + { + fileName = screenshotsDirectory.appendingPathComponent(dateFormatter.string(from: date) + ".png") + } + + do + { + try data.write(to: fileName) + } + catch + { + print(error) + } + } + + self.pauseViewController?.screenshotItem?.isSelected = false + } } //MARK: - GameViewControllerDelegate - diff --git a/Delta/Experimental Features/ExperimentalFeatures.swift b/Delta/Experimental Features/ExperimentalFeatures.swift index 40e2ac9..2d13cba 100644 --- a/Delta/Experimental Features/ExperimentalFeatures.swift +++ b/Delta/Experimental Features/ExperimentalFeatures.swift @@ -21,6 +21,11 @@ struct ExperimentalFeatures: FeatureContainer description: "Enable to show the Status Bar during gameplay.") var showStatusBar + @Feature(name: "Game Screenshots", + description: "When enabled, a Screenshot button will appear in the Pause Menu, allowing you to save a screenshot of your game. You can choose to save the screenshot to Photos or Files.", + options: GameScreenshotsOptions()) + var gameScreenshots + private init() { self.prepareFeatures() diff --git a/Delta/Experimental Features/Features/GameScreenshots.swift b/Delta/Experimental Features/Features/GameScreenshots.swift new file mode 100644 index 0000000..dfaa49f --- /dev/null +++ b/Delta/Experimental Features/Features/GameScreenshots.swift @@ -0,0 +1,54 @@ +// +// GameScreenshots.swift +// Delta +// +// Created by Chris Rittenhouse on 4/24/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI + +import DeltaFeatures + +enum ScreenshotSize: Double, CaseIterable, CustomStringConvertible +{ + case x5 = 5 + case x4 = 4 + case x3 = 3 + case x2 = 2 + + var description: String { + if #available(iOS 15, *) + { + let formattedText = self.rawValue.formatted(.number.decimalSeparator(strategy: .automatic)) + return "\(formattedText)x Size" + } + else + { + return "\(self.rawValue)x Size" + } + } +} + +extension ScreenshotSize: LocalizedOptionValue +{ + var localizedDescription: Text { + Text(self.description) + } + + static var localizedNilDescription: Text { + Text("Original Size") + } +} + +struct GameScreenshotsOptions +{ + @Option(name: "Save to Files", description: "Save the screenshot to the app's directory in Files.") + var saveToFiles: Bool = true + + @Option(name: "Save to Photos", description: "Save the screenshot to the Photo Library.") + var saveToPhotos: Bool = false + + @Option(name: "Image Size", description: "Choose the size of screenshots. This only increases the export size, it does not increase the quality.", values: ScreenshotSize.allCases) + var size: ScreenshotSize? +} diff --git a/Delta/Extensions/PHPhotoLibrary+Authorization.swift b/Delta/Extensions/PHPhotoLibrary+Authorization.swift new file mode 100644 index 0000000..f9f90be --- /dev/null +++ b/Delta/Extensions/PHPhotoLibrary+Authorization.swift @@ -0,0 +1,46 @@ +// +// PHPhotoLibrary+Authorization.swift +// Delta +// +// Created by Chris Rittenhouse on 4/24/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import UIKit +import Photos + +extension PHPhotoLibrary +{ + static func runIfAuthorized(code: @escaping () -> Void) + { + PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { success in + switch success + { + case .authorized: + code() + + case .denied, .restricted, .notDetermined, .limited: + break + } + }) + } + + static func saveUIImage(image: UIImage) + { + // Save the image to the Photos app + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAsset(from: image) + }, completionHandler: { success, error in + if success + { + // Image saved successfully + print("Image saved to Photos app.") + } + else + { + // Error saving image + print("Error saving image: \(error?.localizedDescription ?? "Unknown error")") + } + }) + } +} diff --git a/Delta/Pause Menu/PauseViewController.swift b/Delta/Pause Menu/PauseViewController.swift index 9a8df82..69d6fa4 100644 --- a/Delta/Pause Menu/PauseViewController.swift +++ b/Delta/Pause Menu/PauseViewController.swift @@ -19,7 +19,7 @@ class PauseViewController: UIViewController, PauseInfoProviding } var pauseItems: [MenuItem] { - return [self.saveStateItem, self.loadStateItem, self.cheatCodesItem, self.fastForwardItem, self.sustainButtonsItem].compactMap { $0 } + return [self.saveStateItem, self.loadStateItem, self.cheatCodesItem, self.fastForwardItem, self.sustainButtonsItem, self.screenshotItem].compactMap { $0 } } /// Pause Items @@ -28,6 +28,7 @@ class PauseViewController: UIViewController, PauseInfoProviding var cheatCodesItem: MenuItem? var fastForwardItem: MenuItem? var sustainButtonsItem: MenuItem? + var screenshotItem: MenuItem? /// PauseInfoProviding var pauseText: String? @@ -160,6 +161,7 @@ private extension PauseViewController self.cheatCodesItem = nil self.sustainButtonsItem = nil self.fastForwardItem = nil + self.screenshotItem = nil guard self.emulatorCore != nil else { return } @@ -179,6 +181,11 @@ private extension PauseViewController self.fastForwardItem = MenuItem(text: NSLocalizedString("Fast Forward", comment: ""), image: #imageLiteral(resourceName: "FastForward"), action: { _ in }) self.sustainButtonsItem = MenuItem(text: NSLocalizedString("Hold Buttons", comment: ""), image: #imageLiteral(resourceName: "SustainButtons"), action: { _ in }) + + if ExperimentalFeatures.shared.gameScreenshots.isEnabled + { + self.screenshotItem = MenuItem(text: NSLocalizedString("Screenshot", comment: ""), image: #imageLiteral(resourceName: "Screenshot"), action: { _ in }) + } } func updateSafeAreaInsets() diff --git a/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/Contents.json b/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/Contents.json new file mode 100644 index 0000000..78687fb --- /dev/null +++ b/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "screenshot.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "screenshot@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "screenshot@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot.png b/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb9952bdffd80686fb1f003a9f3198431c2d774 GIT binary patch literal 1279 zcmVj(c~6ZQZ=GhX>gWEo=15dmpr%21Tv4@* z@RYEBJ)@euBiyAVLJ{FJVTUFc6uvX{aEV_{j+gk`WGxu*f~8&99z~PRdQ&o z5+^|hhp1Vp9+emqJt`wf&g}o+zUr}&;ZpS$57a#y#(O{1Z$lw(7+*(*Q8)(c6I`at z`D=$T`XPG0to0p$Z4=I~%363AF1DciQ4MLKqGT1f#A5Jv3ayLKc>_($rJnunbMgSe z4f(2T*xQEzId5C}0Wi#G%?*6wuK)l532;bRa{vGi!~g&e!~vBn4jTXf10+dAK~z|U z)!9poR7Dg9;IEsG7(k5)iU=k!>V|-VAS5G+=vHxOT)HyBwULl0Aq00C65Z(n<5uHB z5@Lcpq5=yPN8$q)_$I~-V{~MO=Rmut8m8&FeQ))>wA1q^m9D<$+;je_TaR<9YNlf@ z9>N;jmLcDdZ!(^Dg9q_6>TS=@<7=FZ^-R?8BF50nDZ?2Ky2TF#J}zP_=46rlpiTJ# zM%v0*yj77T>=YW@ZhyRu!vQo}UwolOxF4&tTJt}kjurT@h0nH0VFRP4T2S?~-VD_6 zzQE}c?toRM*sq^4YSS@bir1#PFuUQZFx&bCKjZIMUsK2Y_~)|F&HaKV&c*tIei)BQ zm^DNj#VSd-9VL4G+TAV3i`vz_U_z`cGFjT`n`P*R= zA4lD8u>-$ib?VED@x1B1whkK;Z5+iHZO?}B9{xzQk&BmbMRBi{EfaXTD#=@gKqS@a zIF7{99$@5^k?5;5o=S9d92?xYip&wVW=2<ts1@OU+!fy`g(^hPU}+cn_u==l&Z-byC1jHm3Az(FC8Xh{ zP=)BDLRxzuLHD4L)vl}HFBe72(-T-GMB<%v#;_cl3T*Xk*25nB6-j)TA^$$g&f?uF zj;aRE%WznTtk&Xj;q%t;B|a6x$iq>$XIL+7pT2}QgoXXyST^JJC`+-ip@_@?ev%zH z*YH)Mqbg-cc3`~&zX>NdwnXLe4hr)d8ep2P#VyMs?%S!y)q^msxT&PWolTh#9_Ym?XPJe#q8zbFObr75KO)oSt0&s#&|yTp@aY p#PkcXCO*;I3$Y>LT;M{h;6H~soN&<9KAZpm002ovPDHLkV1hV3W0n8_ literal 0 HcmV?d00001 diff --git a/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot@2x.png b/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..51f5d0e14f9665d49004d133cc0d120e8f68f42c GIT binary patch literal 2157 zcmV-z2$J`SP)j(c~6ZQZ=GhX>gWEo=15dmpr%21Tv4@* z@RYEBJ)@euBiyAVLJ{FJVTUFc6uvX{aEV_{j+gk`WGxu*f~8&99z~PRdQ&o z5+^|hhp1Vp9+emqJt`wf&g}o+zUr}&;ZpS$57a#y#(O{1Z$lw(7+*(*Q8)(c6I`at z`D=$T`XPG0to0p$Z4=I~%363AF1DciQ4MLKqGT1f#A5Jv3ayLKc>_($rJnunbMgSe z4f(2T*xQEzId5C}0Wi#G%?*6wuK)l532;bRa{vGxhX4Q_hXIe}@nrx229ilcK~#90 z?VV|i6h#!rf3v%|L1bNt5QIgEfQqh)93PAq;(;b8=r@faYK%vs@r+TyC`RLrModh= z5S4($FM>n~2yu}_U1g&ZkEjrYg+*2&?5a^=W&H4F?3$XM+3K$9?rG?M@-jU=Rj*$E zr@GEpRgl3ca)x2f0cHV{fDyn@pw1v}AMhUV0`PM<6P;lQa4&EjaI|5Zy+9Z6EwBRk z1bL>=I1}gq@|Nf!T;rh5*P2b&QNq$H)kEjEqpn$Ov_ehNKP9 zFa@|UV4RJ>=MmUqh0T<<`hSf%{W{m!`7s(c8u%T1LjMF#j?wO0we%OOKQ;p|0lSbP{S#s`DhI5`p3I`O z5#Osn2zVCgM2f4?VCLu$6m`fS(%*xP1D*uVb)0@XFcl&Ok|x5H^Z}>X5MtX? zn(9DDC2U2eW;&6|1EjJWg*a@Diy+N<;9}s%2y`3aHQeWtg}@n9Z-L~1@e!rJ$bt`R z_)YqUl}_OD5H?&2YD{Hl5i-*d>TJz0?);>l8!9S8yI5QnW7k@@me z!$l6gOVil`tTu?Z1|eO=9S-bC)H&Fe&^35vWe%84`FwXZ=>cxkbbMfGQ)&tAp)`lh zx6t*9qGu$*{*=Rp(3=!Jmssd2idvz~w$PJGLO0;OrcJo1r8|LR3p#B^v8)J9YEEe> zVm!bTz=;lYwg68RuJcIWJa!~S=qly<3Ws`CU90f^wAhdYp*!)ql1;dEHy;BWSGev* z+N!Z42|{-(*ApYquj=fIKz~&vL1?#feO3heRmayo5$LaqBnTa>Tz5vGU)9+Zf&QvU zg3uAl^`;2)t2#$i3>6WQAaoSo5|o{k76G={hqvmv5m{y#E0Q3z5$PF$G;7)&>MpKP z1sQ}MSS4&ENU9xrjdDE~vvs5Wp%0j&e4ph&XY2^u^*lgn4L>KO9#tMw^tB=j*N&LJ zd>;4`Z`!tja{sCz(t)>xasYOlCZw`24=@3Eq;TB_Tn)S*LeFa8D!gLjA)@Ip!~)v^ z=7W?zd=42fHt{~`1#YHvGH(L*)7q(h<%`kQRo zb|J1XO9RBIH7rNEt3k$ReK~XG+q4h30k{X*aBw{08d%FXgj9YVM6fO^>4jfugzv(y zY^|qbWQ00KMyO+CggQn>sAFV=I!2Muymr4#g%OLl5Y;Ww1Km zogu)M!62T*ew$iuWM4>n!?Ci%+E<>4v|kXg^b zXhmGCZ4FovCUM7j8vEziMnBWcrUz6_K%8VJBF+ZGFh8JbC2~O3c7yOyVhB>Xm>#Z@ j$Nzw;f1@%mgAe`#pyd2}c%-7^00000NkvXXu0mjfn=kj& literal 0 HcmV?d00001 diff --git a/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot@3x.png b/Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/screenshot@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..229a7045612161097255cde4631cec8719680b14 GIT binary patch literal 2934 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 literal 0 HcmV?d00001 From 7fceccc114f3e74e8c50f719554ce095c6b7f1eb Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 28 Apr 2023 15:32:13 -0500 Subject: [PATCH 3/3] [Experimental Feature] Toast Notifications (#244) commit c340cf842fbf5fea476a6637efe4928dbd734eba Author: Chris Rittenhouse Date: Thu Apr 27 17:24:30 2023 -0400 Addresses Riley's requested changes - Minor code structure change in extension - Minor changes to text and phrasing commit 2a928dfa637dfb503e861dc863f6f85f5240941a Author: Chris Rittenhouse Date: Wed Apr 26 20:11:38 2023 -0400 Adds implementation for Toast Notifications Experimental Feature commit 4fa7d139669994eff888c41bf7af9ac0b6cd2a75 Author: Chris Rittenhouse Date: Wed Apr 26 20:11:04 2023 -0400 Adds @Feature and @Options for Toast Notifications Experimental Feature Co-authored-by: Chris Rittenhouse --- Delta.xcodeproj/project.pbxproj | 8 ++++ Delta/Emulation/GameViewController.swift | 26 +++++++++++++ .../ExperimentalFeatures.swift | 5 +++ .../Features/ToastNotificationOptions.swift | 38 +++++++++++++++++++ ...ameViewController+ExperimentalToasts.swift | 25 ++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 Delta/Experimental Features/Features/ToastNotificationOptions.swift create mode 100644 Delta/Extensions/GameViewController+ExperimentalToasts.swift diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 92871dc..9f4cb76 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ /* Begin PBXBuildFile section */ 1FA4ABA79AB72914FE414A61 /* libPods-Delta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */; }; 87343D7B985519A5890A61C6 /* libPods-DeltaPreviews.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E3E5A45AB20C8A87754453B /* libPods-DeltaPreviews.a */; }; + AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */; }; + AC1C992729F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */; }; ACF7E30D29F73D03000FE071 /* GameScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */; }; ACF7E30F29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */; }; BF00BEA625B758AA00C8607D /* SystemBIOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF00BEA525B758AA00C8607D /* SystemBIOS.swift */; }; @@ -259,6 +261,8 @@ 8ECE6641DE30D01EA30FE7F6 /* Pods-DeltaPreviews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.release.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.release.xcconfig"; sourceTree = ""; }; A01281C7023C0041B25963BE /* Pods-DeltaPreviews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DeltaPreviews.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DeltaPreviews/Pods-DeltaPreviews.debug.xcconfig"; sourceTree = ""; }; A19FF50F55441BC2B2248241 /* Pods-Delta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.release.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.release.xcconfig"; sourceTree = ""; }; + AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastNotificationOptions.swift; sourceTree = ""; }; + AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameViewController+ExperimentalToasts.swift"; sourceTree = ""; }; ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScreenshots.swift; sourceTree = ""; }; ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHPhotoLibrary+Authorization.swift"; sourceTree = ""; }; BF00BEA525B758AA00C8607D /* SystemBIOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemBIOS.swift; sourceTree = ""; }; @@ -531,6 +535,7 @@ D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */, D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */, ACF7E30E29F743A3000FE071 /* PHPhotoLibrary+Authorization.swift */, + AC1C992629F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift */, ); path = Extensions; sourceTree = ""; @@ -1074,6 +1079,7 @@ children = ( D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */, ACF7E30C29F73D03000FE071 /* GameScreenshots.swift */, + AC1C990F29F8B8C30020E6E4 /* ToastNotificationOptions.swift */, ); path = Features; sourceTree = ""; @@ -1490,6 +1496,7 @@ BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */, BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */, BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */, + AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */, BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */, BF59427E1E09BC830051894B /* Game.swift in Sources */, D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */, @@ -1567,6 +1574,7 @@ BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */, D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */, BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */, + AC1C992729F9F1CF0020E6E4 /* GameViewController+ExperimentalToasts.swift in Sources */, BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */, BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */, BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */, diff --git a/Delta/Emulation/GameViewController.swift b/Delta/Emulation/GameViewController.swift index 955948c..c9118d7 100644 --- a/Delta/Emulation/GameViewController.swift +++ b/Delta/Emulation/GameViewController.swift @@ -726,6 +726,11 @@ private extension GameViewController try context.save() try game.gameSaveURL.setExtendedAttribute(name: "com.rileytestut.delta.sha1Hash", value: hash) + + if ExperimentalFeatures.shared.toastNotifications.gameSaveEnabled + { + self.presentExperimentalToastView(NSLocalizedString("Game Data Saved", comment: "")) + } } catch CocoaError.fileNoSuchFile { @@ -844,6 +849,12 @@ extension GameViewController: SaveStatesViewControllerDelegate saveState.modifiedDate = Date() saveState.coreIdentifier = self.emulatorCore?.deltaCore.identifier + if ExperimentalFeatures.shared.toastNotifications.stateSaveEnabled, + saveState.type != .auto + { + self.presentExperimentalToastView(NSLocalizedString("Saved Save State", comment: "")) + } + if isRunning { self.resumeEmulation() @@ -891,6 +902,11 @@ extension GameViewController: SaveStatesViewControllerDelegate { try self.emulatorCore?.load(saveState) } + + if ExperimentalFeatures.shared.toastNotifications.stateLoadEnabled + { + self.presentExperimentalToastView(NSLocalizedString("Loaded Save State", comment: "")) + } } catch EmulatorCore.SaveStateError.doesNotExist { @@ -1076,10 +1092,20 @@ extension GameViewController if activate { emulatorCore.rate = emulatorCore.deltaCore.supportedRates.upperBound + + if ExperimentalFeatures.shared.toastNotifications.fastForwardEnabled + { + self.presentExperimentalToastView(NSLocalizedString("Fast Forward Enabled", comment: "")) + } } else { emulatorCore.rate = emulatorCore.deltaCore.supportedRates.lowerBound + + if ExperimentalFeatures.shared.toastNotifications.fastForwardEnabled + { + self.presentExperimentalToastView(NSLocalizedString("Fast Forward Disabled", comment: "")) + } } } diff --git a/Delta/Experimental Features/ExperimentalFeatures.swift b/Delta/Experimental Features/ExperimentalFeatures.swift index 2d13cba..8cf75b2 100644 --- a/Delta/Experimental Features/ExperimentalFeatures.swift +++ b/Delta/Experimental Features/ExperimentalFeatures.swift @@ -26,6 +26,11 @@ struct ExperimentalFeatures: FeatureContainer options: GameScreenshotsOptions()) var gameScreenshots + @Feature(name: "Toast Notifications", + description: "Show toast notifications as a confirmation for various actions, such as saving your game or loading a save state.", + options: ToastNotificationOptions()) + var toastNotifications + private init() { self.prepareFeatures() diff --git a/Delta/Experimental Features/Features/ToastNotificationOptions.swift b/Delta/Experimental Features/Features/ToastNotificationOptions.swift new file mode 100644 index 0000000..e025b59 --- /dev/null +++ b/Delta/Experimental Features/Features/ToastNotificationOptions.swift @@ -0,0 +1,38 @@ +// +// ToastNotificationOptions.swift +// Delta +// +// Created by Chris Rittenhouse on 4/25/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import SwiftUI + +import DeltaFeatures + +struct ToastNotificationOptions +{ + @Option(name: "Duration", description: "Change how long toasts should be shown.", detailView: { duration in + HStack { + Text("Duration: \(duration.wrappedValue, specifier: "%.1f")s") + Slider(value: duration, in: 1...5, step: 0.5).displayInline() + } + }) + var duration: Double = 1.5 + + @Option(name: "Game Data Saved", + description: "Show toasts when performing an in game save.") + var gameSaveEnabled: Bool = true + + @Option(name: "Saved Save State", + description: "Show toasts when saving a save state.") + var stateSaveEnabled: Bool = true + + @Option(name: "Loaded Save State", + description: "Show toasts when loading a save state.") + var stateLoadEnabled: Bool = true + + @Option(name: "Fast Forward Toggled", + description: "Show toasts when toggling fast forward.") + var fastForwardEnabled: Bool = true +} diff --git a/Delta/Extensions/GameViewController+ExperimentalToasts.swift b/Delta/Extensions/GameViewController+ExperimentalToasts.swift new file mode 100644 index 0000000..28c6c0b --- /dev/null +++ b/Delta/Extensions/GameViewController+ExperimentalToasts.swift @@ -0,0 +1,25 @@ +// +// GameViewController+ExperimentalToasts.swift +// Delta +// +// Created by Chris Rittenhouse on 4/26/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Roxas + +extension GameViewController +{ + func presentExperimentalToastView(_ text: String) + { + guard ExperimentalFeatures.shared.toastNotifications.isEnabled else { return } + + DispatchQueue.main.async { + let toastView = RSTToastView(text: text, detailText: nil) + toastView.edgeOffset.vertical = 8 + toastView.textLabel.textAlignment = .center + toastView.presentationEdge = .top + toastView.show(in: self.view, duration: ExperimentalFeatures.shared.toastNotifications.duration) + } + } +}