Compare commits

..

No commits in common. "master" and "Custom-project-fix" have entirely different histories.

62 changed files with 3197 additions and 5259 deletions

View file

@ -27,8 +27,6 @@ public class FileTransferConnectionManager: ObservableObject {
@Published public var isConnectedOrReconnecting = false // Is any peripheral connected or trying to connect
@Published public var isAnyPeripheralConnecting = false
@Published public var isDisconnectingFromCurrent = false
// Parameters
public var userDefaults = UserDefaults.standard // Can be replaced if data saved needs to be shared

View file

@ -1,25 +0,0 @@
MIT License
Copyright (c) 2023 Adafruit Industries
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -11,14 +11,6 @@
D505B99E2756894300386E9F /* ViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D505B99D2756894300386E9F /* ViewModifier.swift */; };
D517F68126C5771D002996E8 /* FillerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D517F68026C5771D002996E8 /* FillerView.swift */; };
D5199A2F28DD16F100ACC34C /* BleContentTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5199A2E28DD16F100ACC34C /* BleContentTransfer.swift */; };
D51D1413293A53BD0028AEDD /* WifiCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */; };
D5267411292E902700D4C79E /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5267410292E902700D4C79E /* Networking.swift */; };
D5269C00291960A300C0CE4B /* WifiSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269BFF291960A300C0CE4B /* WifiSelection.swift */; };
D5269C02291997DE00C0CE4B /* WifiServiceCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269C01291997DE00C0CE4B /* WifiServiceCellView.swift */; };
D5269C042919985400C0CE4B /* WifiServiceCellSubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269C032919985400C0CE4B /* WifiServiceCellSubView.swift */; };
D5269C08291AB75800C0CE4B /* WifiPairingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5269C07291AB75800C0CE4B /* WifiPairingView.swift */; };
D52A926D29071DF400973B6B /* SelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A926C29071DF400973B6B /* SelectionView.swift */; };
D52A926F29078E0A00973B6B /* WifiServiceSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A926E29078E0A00973B6B /* WifiServiceSelectionView.swift */; };
D52BE7EE269DF36E00630900 /* DownloadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52BE7EA269DF36E00630900 /* DownloadViewModel.swift */; };
D52BE7F3269DF62100630900 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = D52BE7F2269DF62100630900 /* Zip */; };
D52BE82A26A0660200630900 /* KeyboardUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D38B82691E0D000FBFE47 /* KeyboardUtils.swift */; };
@ -30,8 +22,6 @@
D52F7E7B2672F4C500911D43 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D52F7E7A2672F4C500911D43 /* Preview Assets.xcassets */; };
D534F3FC280B59090053699C /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D534F3FB280B59090053699C /* ExampleView.swift */; };
D535E21628E1FA910096E548 /* ScrollRefreshableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D535E21528E1FA910096E548 /* ScrollRefreshableView.swift */; };
D5361098296F5E5400228E15 /* JSONDecoderHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5361097296F5E5400228E15 /* JSONDecoderHelper.swift */; };
D536109A296FB2BB00228E15 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5361099296FB2BB00228E15 /* DataStore.swift */; };
D544A1D2281B9BB70038D483 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D544A1D1281B9BB70038D483 /* Buttons.swift */; };
D544A24B281F92840038D483 /* ImageCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = D544A24A281F92840038D483 /* ImageCaching.swift */; };
D544A24F282046840038D483 /* OnAnimationComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = D544A24E282046840038D483 /* OnAnimationComplete.swift */; };
@ -53,8 +43,6 @@
D567E2BA28C1382E0009F768 /* WifiSubViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D567E2B928C1382E0009F768 /* WifiSubViewCell.swift */; };
D567E2BC28C1527F0009F768 /* WifiSubViewCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D567E2BB28C1527F0009F768 /* WifiSubViewCellModel.swift */; };
D567E2DF28C8D40C0009F768 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D567E2DE28C8D40C0009F768 /* SettingsView.swift */; };
D56B75D4294BAAB400D008E7 /* BLESettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56B75D3294BAAB400D008E7 /* BLESettingsView.swift */; };
D56B75D6294BAACE00D008E7 /* BLESettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56B75D5294BAACE00D008E7 /* BLESettingsViewModel.swift */; };
D56F640C270242CA000E5975 /* UIColor+LightAndDark.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56F6407270242CA000E5975 /* UIColor+LightAndDark.swift */; };
D56F640D270242CA000E5975 /* FileTransferPathUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56F640A270242CA000E5975 /* FileTransferPathUtils.swift */; };
D56F640E270242CA000E5975 /* String+DeletingPrefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56F6408270242CA000E5975 /* String+DeletingPrefix.swift */; };
@ -118,14 +106,6 @@
D505B99D2756894300386E9F /* ViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifier.swift; sourceTree = "<group>"; };
D517F68026C5771D002996E8 /* FillerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillerView.swift; sourceTree = "<group>"; };
D5199A2E28DD16F100ACC34C /* BleContentTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BleContentTransfer.swift; sourceTree = "<group>"; };
D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiCellViewModel.swift; sourceTree = "<group>"; };
D5267410292E902700D4C79E /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = "<group>"; };
D5269BFF291960A300C0CE4B /* WifiSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WifiSelection.swift; path = "PyLeap/Views/Unpaired View/WifiSelection.swift"; sourceTree = SOURCE_ROOT; };
D5269C01291997DE00C0CE4B /* WifiServiceCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WifiServiceCellView.swift; path = ../../../../../../Desktop/WifiServiceCellView.swift; sourceTree = "<group>"; };
D5269C032919985400C0CE4B /* WifiServiceCellSubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WifiServiceCellSubView.swift; path = ../../../../../../Desktop/WifiServiceCellSubView.swift; sourceTree = "<group>"; };
D5269C07291AB75800C0CE4B /* WifiPairingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiPairingView.swift; sourceTree = "<group>"; };
D52A926C29071DF400973B6B /* SelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionView.swift; sourceTree = "<group>"; };
D52A926E29078E0A00973B6B /* WifiServiceSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiServiceSelectionView.swift; sourceTree = "<group>"; };
D52BE7EA269DF36E00630900 /* DownloadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadViewModel.swift; sourceTree = "<group>"; };
D52BE85326A0E39100630900 /* BTConnectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTConnectionViewModel.swift; sourceTree = "<group>"; };
D52F7E702672F4C400911D43 /* PyLeap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PyLeap.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -135,8 +115,6 @@
D52F7E7C2672F4C500911D43 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D534F3FB280B59090053699C /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = "<group>"; };
D535E21528E1FA910096E548 /* ScrollRefreshableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollRefreshableView.swift; sourceTree = "<group>"; };
D5361097296F5E5400228E15 /* JSONDecoderHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONDecoderHelper.swift; sourceTree = "<group>"; };
D5361099296FB2BB00228E15 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
D544A1D1281B9BB70038D483 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
D544A24A281F92840038D483 /* ImageCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCaching.swift; sourceTree = "<group>"; };
D544A24E282046840038D483 /* OnAnimationComplete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnAnimationComplete.swift; sourceTree = "<group>"; };
@ -162,8 +140,6 @@
D567E2B928C1382E0009F768 /* WifiSubViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiSubViewCell.swift; sourceTree = "<group>"; };
D567E2BB28C1527F0009F768 /* WifiSubViewCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiSubViewCellModel.swift; sourceTree = "<group>"; };
D567E2DE28C8D40C0009F768 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
D56B75D3294BAAB400D008E7 /* BLESettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESettingsView.swift; sourceTree = "<group>"; };
D56B75D5294BAACE00D008E7 /* BLESettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESettingsViewModel.swift; sourceTree = "<group>"; };
D56F6407270242CA000E5975 /* UIColor+LightAndDark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+LightAndDark.swift"; sourceTree = "<group>"; };
D56F6408270242CA000E5975 /* String+DeletingPrefix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+DeletingPrefix.swift"; sourceTree = "<group>"; };
D56F640A270242CA000E5975 /* FileTransferPathUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTransferPathUtils.swift; sourceTree = "<group>"; };
@ -187,7 +163,6 @@
D59DFDC1268CFA36001737F6 /* OnboardingStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStepView.swift; sourceTree = "<group>"; };
D59DFDC3268CFAB4001737F6 /* OnboardingDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingDataModel.swift; sourceTree = "<group>"; };
D59E31A9281B8DD300D24211 /* DownloadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadState.swift; sourceTree = "<group>"; };
D5A3D4ED292575F000ECCEC9 /* ReadexPro-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "ReadexPro-SemiBold.ttf"; sourceTree = "<group>"; };
D5AA27F728CA785B001CCE25 /* CircuitPythonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircuitPythonType.swift; sourceTree = "<group>"; };
D5AA27F928CA8D46001CCE25 /* WifiStatusHeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiStatusHeaderBarView.swift; sourceTree = "<group>"; };
D5BA1F7928B52A490012FC62 /* WifiListDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiListDetailView.swift; sourceTree = "<group>"; };
@ -360,42 +335,25 @@
D567E2DD28C8D3E20009F768 /* SettingsView */ = {
isa = PBXGroup;
children = (
D56B75D2294BAA8900D008E7 /* BLESetttings */,
D567E2DE28C8D40C0009F768 /* SettingsView.swift */,
D5DD39AA28D234C3000FAEB8 /* SettingsViewModel.swift */,
);
path = SettingsView;
sourceTree = "<group>";
};
D56B75D2294BAA8900D008E7 /* BLESetttings */ = {
isa = PBXGroup;
children = (
D56B75D3294BAAB400D008E7 /* BLESettingsView.swift */,
D56B75D5294BAACE00D008E7 /* BLESettingsViewModel.swift */,
);
path = BLESetttings;
sourceTree = "<group>";
};
D58E1C8628A2B0DE00AB683E /* Wifi View */ = {
isa = PBXGroup;
children = (
D567E2DD28C8D3E20009F768 /* SettingsView */,
D58E1C8728A2B10B00AB683E /* WifiView.swift */,
D567E2B728C137880009F768 /* WifiCell.swift */,
D51D1412293A53BD0028AEDD /* WifiCellViewModel.swift */,
D567E2B928C1382E0009F768 /* WifiSubViewCell.swift */,
D58E1C8928A2B15E00AB683E /* WifiViewModel.swift */,
D5269C07291AB75800C0CE4B /* WifiPairingView.swift */,
D52A926E29078E0A00973B6B /* WifiServiceSelectionView.swift */,
D5269BFF291960A300C0CE4B /* WifiSelection.swift */,
D5269C01291997DE00C0CE4B /* WifiServiceCellView.swift */,
D5269C032919985400C0CE4B /* WifiServiceCellSubView.swift */,
D5BA1F7E28B66F280012FC62 /* WifiServiceManager.swift */,
D5DD39A628D11817000FAEB8 /* WifiFileTransfer.swift */,
D5BA1F7E28B66F280012FC62 /* WifiServiceManager.swift */,
D5DD39A828D11962000FAEB8 /* WifiTransferService.swift */,
D567E2B728C137880009F768 /* WifiCell.swift */,
D5AA27F728CA785B001CCE25 /* CircuitPythonType.swift */,
D5482F4A28E75053000B0C8E /* LocalNetworkAuth.swift */,
D5AA27F928CA8D46001CCE25 /* WifiStatusHeaderBarView.swift */,
D567E2B928C1382E0009F768 /* WifiSubViewCell.swift */,
D567E2BB28C1527F0009F768 /* WifiSubViewCellModel.swift */,
D567E2B528B81B730009F768 /* Queue.swift */,
D5BA1F7928B52A490012FC62 /* WifiListDetailView.swift */,
@ -412,6 +370,7 @@
D59DFDB2268CCEAC001737F6 /* Views */ = {
isa = PBXGroup;
children = (
D567E2DD28C8D3E20009F768 /* SettingsView */,
D58E1C8628A2B0DE00AB683E /* Wifi View */,
D5507ACE26C668BC00512BAA /* UI Components */,
D59DFDB3268CCEB9001737F6 /* Onboarding Views */,
@ -469,7 +428,6 @@
D5C474C627E39FC8002DD160 /* ReadexPro-Medium.ttf */,
D5C474C727E39FC8002DD160 /* ReadexPro-Regular.ttf */,
D5C474C527E39FC8002DD160 /* ReadexPro-Light.ttf */,
D5A3D4ED292575F000ECCEC9 /* ReadexPro-SemiBold.ttf */,
D5C474C227E39FAD002DD160 /* ReadexPro-Bold.ttf */,
);
path = ReadexPro;
@ -490,11 +448,9 @@
children = (
D505B99B2755323C00386E9F /* NetworkMonitor.swift */,
D544A24A281F92840038D483 /* ImageCaching.swift */,
D5267410292E902700D4C79E /* Networking.swift */,
D5D1F4A327EBA7E30040E2BF /* NetworkManager.swift */,
D58358ED27DA5C0F0069F7F5 /* NetworkError.swift */,
D595FC2D2812C23D00569D8C /* Image Extension.swift */,
D5361097296F5E5400228E15 /* JSONDecoderHelper.swift */,
);
path = Networking;
sourceTree = "<group>";
@ -503,7 +459,6 @@
isa = PBXGroup;
children = (
D5D1F4B127ECFF760040E2BF /* ProjectsModel.swift */,
D5361099296FB2BB00228E15 /* DataStore.swift */,
D5D7DF2C28B489C0008552D1 /* WebDirectoryModel.swift */,
);
path = Model;
@ -524,7 +479,6 @@
isa = PBXGroup;
children = (
D5640215271B54BF00AE1519 /* MainSelectionView.swift */,
D52A926C29071DF400973B6B /* SelectionView.swift */,
D5482F4828E63DB7000B0C8E /* MainSelectionViewModel.swift */,
);
path = "Unpaired View";
@ -663,7 +617,6 @@
D544A1D2281B9BB70038D483 /* Buttons.swift in Sources */,
D5D1F4AE27ECFDA10040E2BF /* GifImage.swift in Sources */,
D58E1C8A28A2B15E00AB683E /* WifiViewModel.swift in Sources */,
D5267411292E902700D4C79E /* Networking.swift in Sources */,
D5C474AC27E174A5002DD160 /* WebView Content.swift in Sources */,
D544A2512822D4730038D483 /* Spotlight Extension.swift in Sources */,
D56F640D270242CA000E5975 /* FileTransferPathUtils.swift in Sources */,
@ -678,7 +631,6 @@
D56F640C270242CA000E5975 /* UIColor+LightAndDark.swift in Sources */,
D5F53CED2694B7A9007634C2 /* OnboardingBackgroundView.swift in Sources */,
D5D1F4B227ECFF760040E2BF /* ProjectsModel.swift in Sources */,
D5269C02291997DE00C0CE4B /* WifiServiceCellView.swift in Sources */,
D5BA1F7A28B52A490012FC62 /* WifiListDetailView.swift in Sources */,
D52F7E742672F4C400911D43 /* PyLeapApp.swift in Sources */,
D567E2B628B81B730009F768 /* Queue.swift in Sources */,
@ -692,17 +644,13 @@
D59DFDB6268CD052001737F6 /* AppEnvironment.swift in Sources */,
D5CC6BB428173AE0008629FB /* HeaderView.swift in Sources */,
D52BE85626A0E5A700630900 /* PeripheralAutoConnect.swift in Sources */,
D52A926D29071DF400973B6B /* SelectionView.swift in Sources */,
D505B99E2756894300386E9F /* ViewModifier.swift in Sources */,
D5CC6BB628173B91008629FB /* SubHeaderView.swift in Sources */,
D51D1413293A53BD0028AEDD /* WifiCellViewModel.swift in Sources */,
D5269C08291AB75800C0CE4B /* WifiPairingView.swift in Sources */,
D544A24B281F92840038D483 /* ImageCaching.swift in Sources */,
D517F68126C5771D002996E8 /* FillerView.swift in Sources */,
D5DD39A728D11817000FAEB8 /* WifiFileTransfer.swift in Sources */,
D5AA27FA28CA8D46001CCE25 /* WifiStatusHeaderBarView.swift in Sources */,
D5F53CEB2694B524007634C2 /* Blinka Animation.swift in Sources */,
D5269C00291960A300C0CE4B /* WifiSelection.swift in Sources */,
D5597BF826A9E14B00DF17C0 /* AppDelegate.swift in Sources */,
D56F640E270242CA000E5975 /* String+DeletingPrefix.swift in Sources */,
D57858F328333CBC008E8BE4 /* TroubleshootView.swift in Sources */,
@ -711,16 +659,13 @@
D5482F4928E63DB7000B0C8E /* MainSelectionViewModel.swift in Sources */,
D5597C3B26B98E1E00DF17C0 /* NumbersOnly.swift in Sources */,
D5D1F4A427EBA7E30040E2BF /* NetworkManager.swift in Sources */,
D56B75D6294BAACE00D008E7 /* BLESettingsViewModel.swift in Sources */,
D5DD39AB28D234C3000FAEB8 /* SettingsViewModel.swift in Sources */,
D52A926F29078E0A00973B6B /* WifiServiceSelectionView.swift in Sources */,
D5BA1F8328B68ED40012FC62 /* NetworkPeripheral.swift in Sources */,
D59DFDBC268CE0EB001737F6 /* RootView.swift in Sources */,
D595FC2E2812C23D00569D8C /* Image Extension.swift in Sources */,
D567E2BA28C1382E0009F768 /* WifiSubViewCell.swift in Sources */,
D59DFD8F268A4A4D001737F6 /* BTConnectionView.swift in Sources */,
D58D887B26CC02B60085604A /* OnboardingViewPure.swift in Sources */,
D5361098296F5E5400228E15 /* JSONDecoderHelper.swift in Sources */,
D52BE85426A0E39100630900 /* BTConnectionViewModel.swift in Sources */,
D5DD39A928D11962000FAEB8 /* WifiTransferService.swift in Sources */,
D59DFDC2268CFA36001737F6 /* OnboardingStepView.swift in Sources */,
@ -728,7 +673,6 @@
D544A24F282046840038D483 /* OnAnimationComplete.swift in Sources */,
D5507AD126C668BC00512BAA /* SearchBarView.swift in Sources */,
D59DFDBA268CDEEC001737F6 /* RootViewModel.swift in Sources */,
D536109A296FB2BB00228E15 /* DataStore.swift in Sources */,
D5C74DF527EB93E300730505 /* DemoViewCell.swift in Sources */,
D567E2DF28C8D40C0009F768 /* SettingsView.swift in Sources */,
D5482F4B28E75053000B0C8E /* LocalNetworkAuth.swift in Sources */,
@ -740,7 +684,6 @@
D5D1F4B027ECFDE00040E2BF /* NavBarModifier.swift in Sources */,
D5597C0C26AF018800DF17C0 /* View+If.swift in Sources */,
D58E1C8D28A2B32C00AB683E /* Wifi_ifaddrs.m in Sources */,
D56B75D4294BAAB400D008E7 /* BLESettingsView.swift in Sources */,
D52BE7EE269DF36E00630900 /* DownloadViewModel.swift in Sources */,
D59E31AA281B8DD300D24211 /* DownloadState.swift in Sources */,
D5BA1F7F28B66F280012FC62 /* WifiServiceManager.swift in Sources */,
@ -749,7 +692,6 @@
D5199A2F28DD16F100ACC34C /* BleContentTransfer.swift in Sources */,
D535E21628E1FA910096E548 /* ScrollRefreshableView.swift in Sources */,
D5640216271B54BF00AE1519 /* MainSelectionView.swift in Sources */,
D5269C042919985400C0CE4B /* WifiServiceCellSubView.swift in Sources */,
D505B99C2755323C00386E9F /* NetworkMonitor.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -881,17 +823,17 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = PyLeap/PyLeap.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_ASSET_PATHS = "\"PyLeap/Preview Content\"";
DEVELOPMENT_TEAM = 2X94RM7457;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = PyLeap/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.adafruit.PyLeap;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = NO;
@ -911,17 +853,17 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = PyLeap/PyLeap.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0;
CURRENT_PROJECT_VERSION = 14;
DEVELOPMENT_ASSET_PATHS = "\"PyLeap/Preview Content\"";
DEVELOPMENT_TEAM = 2X94RM7457;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = PyLeap/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.1;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.adafruit.PyLeap;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = NO;

View file

@ -9,6 +9,8 @@ import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// UI
@ -18,7 +20,21 @@ class AppDelegate: NSObject, UIApplicationDelegate {
}
private func setupAppearances() {
// Alerts
print("Set Appearance")
// Navigation bar title
// UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: UIColor.white]
// UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white]
// Navigation bar background
// UINavigationBar.appearance().barTintColor = .clear
// UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
// List background
// UITableView.appearance().backgroundColor = UIColor.clear
// UITableView.appearance().separatorStyle = .none
// UITableViewCell.appearance().backgroundColor = .clear
//
// Alerts
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = .blue
}
}

Binary file not shown.

View file

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "150",
"green" : "100",
"red" : "74"
"blue" : "0.769",
"green" : "0.561",
"red" : "0.380"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "150",
"green" : "100",
"red" : "74"
"blue" : "0.773",
"green" : "0.561",
"red" : "0.380"
}
},
"idiom" : "universal"

View file

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "97",
"green" : "97",
"red" : "97"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "97",
"green" : "97",
"red" : "97"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "bluetoothLogo.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "196",
"green" : "143",
"red" : "97"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "255",
"green" : "255",
"red" : "255"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -11,17 +11,17 @@ import Zip
class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate {
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let cachesPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
static let shared = DownloadViewModel()
var bundleURL = String()
var bundleTitle = String()
@StateObject var globalString = GlobalString()
// Alert
@Published var alertMsg = ""
@Published var showAlert = false
var manager = FileManager.default
// Saving Download task reference for cancelling...
@Published var downloadTaskSession: URLSessionDownloadTask!
// Show Progress View
@Published var downloadProgress: CGFloat = 0
@ -31,232 +31,12 @@ class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate
@Published var isDownloading = false
// Saving Download task refernce for cancelling...
// @Published var downloadtaskSession : URLSessionDownloadTask!
@Published var downloadtaskSession : URLSessionDownloadTask!
@Published var attemptToSendBunle = false
@Published var state: DownloadState = .idle
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForResource = 5
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
func trueDownload(useProject link: String, projectName: String) {
let CPZipName = directoryPath.appendingPathComponent("\(projectName).zip")
let request = URLRequest(url: URL(string: link)!)
session.downloadTask(with: request).resume()
bundleURL = link
bundleTitle = projectName
}
// Saving Download task reference for cancelling...
@Published var downloadTaskSession: URLSessionDownloadTask!
func newZip(projectTitle: String, location: URL) {
let CPZipName = directoryPath.appendingPathComponent("\(projectTitle).zip")
print("\(#function) @Line: \(#line)")
print("Location 1: \(location)")
do {
let zipData = try Data(contentsOf: location)
try zipData.write(to: CPZipName)
// let unzipDirectory = try Zip.quickUnzipFile(CPZipName) // Unzip
try FileManager.default.removeItem(at: CPZipName)
} catch {
print("newZip - Zip ERROR")
print("Error: \(error)")
print("Location 2: \(location)")
self.state = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .idle
}
}
}
func unzipProjectFile(urlString: String, projectTitle: String) {
print("Times unzipProjectFile was called")
let CPZipName = directoryPath.appendingPathComponent("\(projectTitle).zip")
// _ = directoryPath.appendingPathComponent("PyLeap Folder")
if let zipFileUrl = URL(string: urlString) {
// Download from this site
URLSession.shared.downloadTask(with: zipFileUrl) { (tempFileUrl, response, error) in
if let zipTempFileUrl = tempFileUrl {
do {
print("Times do looped in unzipProjectFile")
let zipData = try Data(contentsOf: zipTempFileUrl)
try zipData.write(to: CPZipName)
let unzipDirectory = try Zip.quickUnzipFile(CPZipName) // Unzip
try FileManager.default.removeItem(at: CPZipName)
var projectResponse = [String: String]()
projectResponse["projectTitle"] = self.bundleTitle
projectResponse["projectLink"] = self.bundleURL
NotificationCenter.default.post(name: .didCompleteZip, object: nil, userInfo: projectResponse)
print("times wifiDownloadComplete was triggered")
NotificationCenter.default.post(name: .wifiDownloadComplete, object: nil, userInfo: projectResponse)
} catch {
print("unzipProjectFile - Zip ERROR")
print("Error: \(error)")
NotificationCenter.default.post(name: .downloadErrorDidOccur, object: nil, userInfo: nil)
return
DispatchQueue.main.async {
self.state = .failed
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .idle
}
}
} else {
self.state = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .idle
}
}
}.resume()
}
}
// func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// print("Download succeeded")
// }
/// Periodically informs the delegate about the downloads progress - Used for progress UI
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
// Getting Progress
let numeralProgress = CGFloat(totalBytesWritten) / CGFloat(totalBytesExpectedToWrite)
print("Progress: \(numeralProgress)")
// Since URL Session will be running in the background thread
// UI will be done on the main thread
DispatchQueue.main.async {
self.downloadProgress = numeralProgress
print("Recorded downloadProgress Progress: \(self.downloadProgress)")
print("Recorded numeralProgress Progress: \(numeralProgress)")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let error = error else { return }
print(error)
guard let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data else {
print("Download failed")
return
}
session.downloadTask(withResumeData: resumeData).resume()
}
/// Tells the delegate that a download task has finished downloading.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("urlSession completed")
guard let httpResponse = downloadTask.response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print ("server error")
NotificationCenter.default.post(name: .downloadErrorDidOccur, object: nil, userInfo: nil)
return
}
print("Location: \(location)")
guard let url = downloadTask.originalRequest?.url else {
self.reportError(error: "An error has occurred...")
return
}
// Creating a destination for storing files with a destination URL
let destinationURL = directoryPath.appendingPathComponent(url.lastPathComponent)
//if that file already exists, replace it.
try? FileManager.default.removeItem(at: destinationURL)
print(#function)
do {
// Copy temp file to directory.
try FileManager.default.copyItem(at: location, to: destinationURL)
DispatchQueue.main.async {
print("unzipProjectFile loop")
self.unzipProjectFile(urlString: self.bundleURL, projectTitle: self.bundleTitle)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.attemptToSendBunle.toggle()
}
NotificationCenter.default.post(name: .didCompleteTransfer, object: nil, userInfo: nil)
}
} catch {
print(error)
self.reportError(error: "Try again later")
isDownloading = false
self.didDownloadBundle = false
}
}
// Report Error Function...
func reportError(error: String){
alertMsg = error
showAlert.toggle()
}
// MARK:- Download
func startDownload(urlString: String, projectTitle: String) {
print("Starting Download...")
@ -274,19 +54,86 @@ class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate
downloadTaskSession = session.downloadTask(with: validURL)
downloadTaskSession.resume()
unzipProjectFile(urlString: urlString, projectTitle: projectTitle)
}
func testCallback(completion: ()->()) {
print("Do something")
func startDownload(urlString: String, projectTitle: String, compeletion: () -> ()) {
print("Starting Download...")
isDownloading = true
// Check for valid URL
guard let validURL = URL(string: urlString) else {
self.reportError(error: "Invalid URL!")
return
}
downloadProgress = 0
// Download Task...
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
downloadTaskSession = session.downloadTask(with: validURL)
downloadTaskSession.resume()
unzipProjectFile(urlString: urlString, projectTitle: projectTitle)
}
func makeFileDirectory() {
// Creating a File Manager Object
// Creating a folder
let pyleapProjectFolderURL = directoryPath.appendingPathComponent("PyLeap Project Folder")
do {
try FileManager.default.createDirectory(at: pyleapProjectFolderURL,
withIntermediateDirectories: true,
attributes: [:])
} catch {
print(error)
}
}
}
extension DownloadViewModel {
func unzipProjectFile(urlString: String, projectTitle: String) {
let CPZipName = directoryPath.appendingPathComponent("\(projectTitle).zip")
// _ = directoryPath.appendingPathComponent("PyLeap Folder")
if let zipFileUrl = URL(string: urlString) {
// Download from this site
URLSession.shared.downloadTask(with: zipFileUrl) { (tempFileUrl, response, error) in
/*
if let...
if you can let the new variable name equal the non-optional version of optionalName, do the following with it"
*/
if let zipTempFileUrl = tempFileUrl {
do {
let zipData = try Data(contentsOf: zipTempFileUrl)
try zipData.write(to: CPZipName)
let unzipDirectory = try Zip.quickUnzipFile(CPZipName) // Unzip
try FileManager.default.removeItem(at: CPZipName)
} catch {
print("Zip ERROR")
print("Error: \(error)")
self.state = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .idle
}
}
}
}.resume()
}
}
func createNewTextFile() {
@ -335,20 +182,89 @@ extension DownloadViewModel {
}
}
func makeFileDirectory() {
// Creating a File Manager Object
/// Periodically informs the delegate about the downloads progress - Used for progress UI
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
// Getting Progress
let numeralProgress = CGFloat(totalBytesWritten) / CGFloat(totalBytesExpectedToWrite)
print("Progress: \(numeralProgress)")
// Creating a folder
let pyleapProjectFolderURL = directoryPath.appendingPathComponent("PyLeap Project Folder")
do {
// Since URL Session will be running in the background thread
// UI will be done on the main thread
DispatchQueue.main.async {
try FileManager.default.createDirectory(at: pyleapProjectFolderURL,
withIntermediateDirectories: true,
attributes: [:])
} catch {
print(error)
self.downloadProgress = numeralProgress
print("Recorded downloadProgress Progress: \(self.downloadProgress)")
print("Recorded numeralProgress Progress: \(numeralProgress)")
}
}
/// Tells the delegate that a download task has finished downloading.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let url = downloadTask.originalRequest?.url else {
self.reportError(error: "An error has occurred...")
return
}
// Creating a destination for storing files with a destination URL
let destinationURL = directoryPath.appendingPathComponent(url.lastPathComponent)
//if that file already exists, replace it.
try? FileManager.default.removeItem(at: destinationURL)
print(#function)
do {
// Copy temp file to directory.
try FileManager.default.copyItem(at: location, to: destinationURL)
DispatchQueue.main.async {
// If Successful...
print("Successful Download")
self.isDownloading = false
print("Download Location: \(location)")
self.downloadProgress = 1.0
print("\(self.didDownloadBundle) CURRENT STATE")
self.didDownloadBundle = true
print("\(self.didDownloadBundle) CURRENT STATE")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.attemptToSendBunle.toggle()
}
}
} catch {
print(error)
self.reportError(error: "Try again later")
isDownloading = false
self.didDownloadBundle = false
}
}
// Report Error Function...
func reportError(error: String){
alertMsg = error
showAlert.toggle()
}
// cancel Task...
func cancelTask(){
if let task = downloadtaskSession,task.state == .running{
// cancelling...
downloadtaskSession.cancel()
// closing view...
// withAnimation{self.showDownlodProgress = false}
}
}
}

View file

@ -19,7 +19,7 @@ struct ExampleView: View {
//
// PageView(title: "Choose your Adventure!", subtitle: "Choose a project you would like to send over to your PyLeap compatible device.", imageName: "slide3", showDismissButton: false, shouldShowOnboarding: $shouldShowOnboarding)
//
PageView(title: "Send projects directly from the Adafruit Learning System to your Adafruit Device...", subtitle: "...without opening a code editor or connecting to a computer.", imageName: "slide4", showDismissButton: true, shouldShowOnboarding: $shouldShowOnboarding)
PageView(title: "Send projects directly from the Adafruit Learning System to your Bluefruit Compatible Device...", subtitle: "...without opening a code editor or connecting to a computer.", imageName: "slide4", showDismissButton: true, shouldShowOnboarding: $shouldShowOnboarding)
}
.tabViewStyle(PageTabViewStyle())
}

View file

@ -20,6 +20,11 @@
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSBonjourServices</key>
<array>
<string>_circuitpython._tcp</string>
<string>_bonjour._tcp</string>
</array>
<key>LSApplicationCategoryType</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
@ -28,11 +33,6 @@
<string>PyLeap requires access to Bluetooth to connect to Adafruit devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>PyLeap requires access to Bluetooth to connect to Adafruit devices</string>
<key>NSBonjourServices</key>
<array>
<string>_circuitpython._tcp</string>
<string>_bonjour._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>PyLeap uses the local network to communicate with your Adafruit device</string>
<key>UIAppFonts</key>
@ -64,6 +64,10 @@
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>

View file

@ -1,159 +0,0 @@
//
// DataStore.swift
// PyLeap
//
// Created by Trevor Beaton on 1/11/23.
//
import Foundation
/**
/// This is a DataStore class that is used for saving and loading data to/from the file system using the FileManager class.
*/
public class DataStore {
let fileManager = FileManager.default
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
init() {}
/**
- parameter content: This method takes an array of ResultItem objects.
- returns: completion
/// This method writes it to a file named "StandardPyLeapProjects.json" in the documents directory.
*/
func save(content: [ResultItem], completion: @escaping () -> Void) {
let encoder = JSONEncoder()
if let encodedProjectData = try? encoder.encode(content) {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsURL.appendingPathComponent("StandardPyLeapProjects.json")
try? encodedProjectData.write(to: fileURL)
completion()
}
}
/**
- parameter content: This method takes an array of ResultItem objects.
- returns: completion
/// This method reads the "StandardPyLeapProjects.json" file in the documents directory and decodes it as an array of ResultItem objects, and then appends the customProjects array to it and saves it back to the file.
*/
func save(customProjects: [ResultItem], completion: @escaping () -> Void) {
var temp = customProjects
let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
NotificationCenter.default.post(name: .didCollectCustomProject, object: nil, userInfo: nil)
temp.append(contentsOf: savedProjects)
save(content: temp) {
self.removeDuplicates(projectList: temp)
}
completion()
}
}
/**
/// This method reads the "StandardPyLeapProjects.json" file in the documents directory and decodes it as an array of ResultItem objects, and then calls the loadCustomProjectList(contents:) method with the decoded array as an argument
*/
func loadDefaultProjectList() {
let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
loadCustomProjectList(contents: savedProjects)
}
}
/**
/// : This method reads the "StandardPyLeapProjects.json" file in the documents directory, decodes it as an array of ResultItem objects, and returns it.
*/
func loadDefaultList() -> [ResultItem] {
var result = [ResultItem]()
let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
result = savedProjects
}
return result
}
/**
- parameter: This method takes an array of ResultItem objects
- returns: completion
/// This method reads the "CustomProjects.json" file in the documents directory, decodes it as an array of ResultItem objects, appends it to the input array, and then calls
*/
func loadCustomProjectList(contents: [ResultItem]) {
var temp = contents
let fileURL = documentsDirectory.appendingPathComponent("CustomProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
temp.append(contentsOf: savedProjects)
removeDuplicates(projectList: temp)
}
}
/**
- parameter: This method takes an array of ResultItem objects
- returns: completion
/// This method uses the reduce(into:_:) method to iterate over the array, and it builds a new array that only contains unique ResultItem objects based on their bundleLink property. It then calls the save(content:completion:) method to save the new array to "StandardPyLeapProjects.json" file.
*/
func removeDuplicates(projectList: [ResultItem]) {
let combinedLists = projectList.reduce(into: [ResultItem]()) { (result, projectList) in
if !result.contains(where: { $0.bundleLink == projectList.bundleLink }) {
result.append(projectList)
}
}
save(content: combinedLists) {}
}
func loadThirdPartyProjectsFromFileManager() {
let fileURL = documentsDirectory.appendingPathComponent("CustomProjects.json")
let savedData = try? Data(contentsOf: fileURL)
if let savedData = savedData,
let savedProjects = try? JSONDecoder().decode([ResultItem].self, from: savedData) {
for project in savedProjects {
print("CustomProjects name: \(project.projectName)")
}
}
}
}

View file

@ -30,5 +30,3 @@ struct ResultItem: Codable, Identifiable, Equatable {
let learnGuideLink: String
let compatibility: [String]
}

View file

@ -1,15 +0,0 @@
//
// JSONDecoderHelper.swift
// PyLeap
//
// Created by Trevor Beaton on 1/11/23.
//
import Foundation
class JSONDecoderHelper {
static func decode<T: Decodable>(data: Data) -> T? {
let decoder = JSONDecoder()
return try? decoder.decode(T.self, from: data)
}
}

View file

@ -14,9 +14,221 @@ import Foundation
import SwiftUI
class NetworkService: ObservableObject {
let dataStore = DataStore()
let thirdPartyBackgroundQueue = DispatchQueue(label: "com.PyLeap.thirdPartyBackgroundQueue", qos: .background, attributes: .concurrent)
static let shared = NetworkService()
@Published var pdemos : [ResultItem] = []
@State var storedURL = ""
let userDefaults = UserDefaults.standard
init(){
load()
loadCustProjects()
}
func loadCustProjects() {
if let savedProjects = userDefaults.object(forKey: "CustomProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
print("++++Custom Projects++++")
for i in loadedProjects {
print("\(i.projectName)")
}
}
}
}
func save(content: [ResultItem]) {
print("Saving JSON response...")
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(content) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "SavedProjects")
}
}
func save(customProjects: [ResultItem]) {
NotificationCenter.default.post(name: .didCollectCustomProject, object: nil, userInfo: nil)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(customProjects) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "CustomProjects")
}
}
func saveCustomProjects(content: [ResultItem]) {
NotificationCenter.default.post(name: .didCollectCustomProject, object: nil, userInfo: nil)
// if let newIncoming = content.contains()
var customList: [ResultItem] = []
if let savedProjects = userDefaults.object(forKey: "CustomProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
print("Loading previous list")
for i in loadedProjects {
print("\(i.projectName)")
}
customList = loadedProjects
print("Appending new content")
customList.append(contentsOf: content)
for i in customList {
print("\(i.projectName)")
}
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(customList) {
let defaults = UserDefaults.standard
print("Saved new custom list.")
defaults.set(encoded, forKey: "CustomProjects")
}
}
}
}
func verifyIncomingProject(json response: [ResultItem]){
if let savedProjects = userDefaults.object(forKey: "CustomProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
/// Does incoming project exist already?
/// Check with object's property URL
if loadedProjects.contains(where: { $0.bundleLink == response[0].bundleLink
}) {
print("does exist")
} else {
print("does not exist")
}
print(loadedProjects)
}
}
}
func loadCustomProjects() -> [ResultItem]{
print(#function)
var customList: [ResultItem] = []
if let savedProjects = userDefaults.object(forKey: "CustomProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
print("Load custom projectsxo")
// print(loadedProjects)
for i in loadedProjects {
print("\(i.projectName)")
}
customList = loadedProjects
}
}
return customList
}
func mergeProjects() {
print(#function)
var standardList: [ResultItem] = []
var customList: [ResultItem] = []
if let savedProjects = userDefaults.object(forKey: "SavedProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
print("Load saved projects")
// let check = loadedProjects.map { $0.bundleLink == "" }
pdemos = loadedProjects
print(loadedProjects)
}
}
if let savedProjects = userDefaults.object(forKey: "CustomProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
print("Load saved projects")
print(loadedProjects)
}
}
}
func load() {
if let savedProjects = userDefaults.object(forKey: "SavedProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
let mergedList = loadedProjects + loadCustomProjects()
// save(content: <#T##[ResultItem]#>)
pdemos = mergedList
print("----Standard Projects----")
for i in loadedProjects {
print("\(i.projectName)")
}
print("++++Custom Projects++++")
for i in loadCustomProjects() {
print("\(i.projectName)")
}
}
}
}
private var dataTask: URLSessionDataTask?
@ -26,16 +238,24 @@ class NetworkService: ObservableObject {
// Session Configuration & Caching Policy
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .returnCacheDataElseLoad
// configuration.requestCachePolicy = .useProtocolCachePolicy
return URLSession(configuration: configuration)
}()
@Published var projectInfo = Data()
func fetch(completion: @escaping() -> Void) {
print("Attempting Network Request")
let request = URLRequest(url: URL(string: AdafruitInfo.baseURL)!, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 60.0)
let task = session.dataTask(with: request) { data, response, error in
func fetch() {
let cache = URLCache.shared
let requestForCache = URLRequest(url: URL(string: AdafruitInfo.baseURL)!)
let request = URLRequest(url: URL(string: AdafruitInfo.baseURL)!, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
print("Making Network Request.")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("error: \(error)")
@ -47,82 +267,78 @@ class NetworkService: ObservableObject {
if let projectData = try? JSONDecoder().decode(RootResults.self, from: data) {
DispatchQueue.main.async {
self.dataStore.save(content: projectData.projects, completion: self.dataStore.loadDefaultProjectList)
completion()
self.save(content: projectData.projects)
self.load()
}
} else {
print("No data found")
}
} else {
print("Updating UIList with Cached data...")
DispatchQueue.main.async {
self.dataStore.loadDefaultProjectList()
completion()
self.load()
}
}
}
task.resume()
}
func fetchThirdPartyProject(urlString: String?) {
thirdPartyBackgroundQueue.async {
guard let urlString = urlString else {
print("\(#function) @Line: \(#line)")
NotificationCenter.default.post(name: .invalidCustomNetworkRequest, object: nil, userInfo: nil)
print("Error urlString")
return
}
if urlString.contains(" ") {
NotificationCenter.default.post(name: .invalidCustomNetworkRequest, object: nil, userInfo: nil)
return
}
guard let requestURL = URL(string: urlString) else {
print("\(#function) @Line: \(#line)")
print("Error requestURL")
NotificationCenter.default.post(name: .invalidCustomNetworkRequest, object: nil, userInfo: nil)
return
}
let request = URLRequest(url: requestURL, cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 0.0)
print("Making Network Request for Custom Project.")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Could not load project. Please check your URL Invalid URL: \(urlString)")
NotificationCenter.default.post(name: .invalidCustomNetworkRequest, object: nil, userInfo: nil)
return
}
if let data = data {
let projectData = JSONDecoderHelper.decode(data: data) as RootResults?
if let projects = projectData?.projects {
DispatchQueue.main.async {
self.dataStore.save(customProjects: projects, completion: self.dataStore.loadThirdPartyProjectsFromFileManager)
}
}
}
}
task.resume()
func fetchThirdParyProject(urlString: String?) {
let cache = URLCache.shared
guard let urlString = urlString else {
print("Error")
return
}
if urlString.contains(" ") {
NotificationCenter.default.post(name: .invalidCustomNetworkRequest, object: nil, userInfo: nil)
return
}
let request = URLRequest(url: URL(string: urlString)!, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
print("Making Network Request for Custom Project.")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Could not load project. Please check your URL Invalid URL: \(urlString)")
NotificationCenter.default.post(name: .invalidCustomNetworkRequest, object: nil, userInfo: nil)
return
}
if let data = data {
print("Updating UIList with new data...")
let projectData = try? JSONDecoder().decode(RootResults.self, from: data)
if let projects = projectData?.projects {
DispatchQueue.main.async {
print(#function)
for i in projects {
print("\(i.projectName)")
}
// self.pdemos.append(contentsOf: projects)
self.saveCustomProjects(content: projects)
}
}
}
}
task.resume()
}
}

View file

@ -1,41 +0,0 @@
//
// Networking.swift
// PyLeap
//
// Created by Trevor Beaton on 11/23/22.
//
import Foundation
enum HTTPMethod: String {
case delete = "DELETE"
case get = "GET"
case patch = "PATCH"
case post = "POST"
case put = "PUT"
case options = "OPTIONS"
}
enum HTTPScheme: String {
case http
case https
}
/// The API protocol allows us to separate the task of constructing a URL,
/// its parameters, and HTTP method from the act of executing the URL request
/// and parsing the response.
///
protocol API {
var scheme: HTTPScheme { get }
var baseURL: String { get }
var path: String { get }
// [URLQueryItem(name: "api_key", value: API_KEY)]
var parameters: [URLQueryItem] { get }
var method: HTTPMethod { get }
}

BIN
PyLeap/Views/.DS_Store vendored

Binary file not shown.

View file

@ -60,7 +60,7 @@ struct BLEPairingView: View {
//.padding(.top, 100)
.padding(.horizontal, 20)
BlinkaAnimationView(height: 250, width: 250)
BlinkaAnimationView()
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.onAppear(){

View file

@ -29,37 +29,44 @@ struct BTConnectionView: View {
VStack{
HStack {
Button {
rootViewModel.goToSelection()
self.rootViewModel.goToMain()
} label: {
Image(systemName: "arrow.backward")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.foregroundColor(.black)
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30, alignment: .center)
.foregroundColor(Color("pyleap_gray"))
}
.padding()
Spacer()
Image("bluetooth")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30, alignment: .center)
.offset(y: -5)
.onReceive(timer) { _ in
nextText = 1
timer.upstream.connect().cancel()
}
}
.padding(.top, 15)
.padding(.top, 50)
.padding(.horizontal, 30)
Image("pyleapLogo")
.resizable()
.aspectRatio(contentMode: .fit)
.minimumScaleFactor(0.1)
.padding(.top, 50)
.padding(.horizontal, 60)
.sheet(isPresented: $showConnectionErrorView) {
TroubleshootView()
}
@ -69,29 +76,19 @@ struct BTConnectionView: View {
Text("Bluetooth Connect")
.font(Font.custom("ReadexPro-Regular", size: 36))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
Text("Searching for PyLeap compatible device...")
.padding(.horizontal, 30)
.font(Font.custom("ReadexPro-Regular", size: 24))
.minimumScaleFactor(0.1)
.padding(.horizontal, 30)
Spacer()
BlinkaAnimationView(height: 150, width: 145)
.padding(.bottom, 20)
BlinkaAnimationView()
.minimumScaleFactor(0.1)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.onAppear() {
Animation.linear(duration: 1.0)
.repeatForever(autoreverses: false)
isAnimating = true
}
Spacer()
.onAppear() {
Animation.linear(duration: 1.0)
@ -101,9 +98,6 @@ struct BTConnectionView: View {
Text(detailText)
.font(Font.custom("ReadexPro-Regular", size: 24))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.1)
.lineLimit(2)
Button(action: {
nextText = 1
@ -228,7 +222,10 @@ struct BTConnectionView: View {
Text("Pair Device")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.padding(.horizontal, 60)
.frame(height: 50)
.background(Color("pyleap_pink"))
.clipShape(Capsule())
@ -278,7 +275,7 @@ struct BTConnectionView: View {
let text: String
switch model.connectionStatus {
case .scanning:
text = "Scanning for PyLeap compatible devices..."
text = "Scanning..."
case .restoringConnection:
text = "Restoring connection..."
case .connecting:
@ -295,8 +292,11 @@ struct BTConnectionView: View {
case .disconnected(let error):
if let error = error {
text = "Disconnected: \(error.localizedDescription)"
//self.showSheetView.toggle()
} else {
text = "Disconnected"
}
}
return text

View file

@ -86,7 +86,7 @@ struct TroubleshootView: View {
Button {
UIApplication.shared.open(URL(string: "App-Prefs:root=Bluetooth")!)
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
} label: {
ZStack {

View file

@ -18,15 +18,17 @@ struct FillerView: View {
Image("pyleapLogo")
.resizable()
.aspectRatio(contentMode: .fit)
.offset(y: -30)
.offset(y: -20)
ProgressView()
}
.preferredColorScheme(.light)
.padding(.horizontal, 30)
.padding(.horizontal, 20)
.edgesIgnoringSafeArea(.all)
.modifier(Alerts(activeAlert: $model.activeAlert, model: model))
.onAppear {
print("Filler View")
model.setupBluetooth()
}
.onChange(of: model.isStartupFinished) { isStartupFinished in

View file

@ -9,13 +9,12 @@ import SwiftUI
import FileTransferClient
class BleContentCommands: ObservableObject {
class BleContentCommands {
private weak var fileTransferClient: FileTransferClient?
@Published var transmissionProgress: TransmissionProgress?
@Published var isTransmiting = false
@Published var bootUpInfo = String()
@Published var counter = 0
enum ProjectViewError: LocalizedError {
case fileTransferUndefined
@ -104,8 +103,6 @@ class BleContentCommands: ObservableObject {
let str = String(decoding: data, as: UTF8.self)
print("Read: \(str)")
self.bootUpInfo = str
sharedBootinfo = str
case .failure(let error):
self.lastTransmit = TransmissionLog(type: .error(message: error.localizedDescription))
@ -237,10 +234,8 @@ class BleContentCommands: ObservableObject {
func writeFileCommand(path: String, data: Data, completion: ((Result<Date?, Error>) -> Void)?) {
guard let fileTransferClient = fileTransferClient else { completion?(.failure(ProjectViewError.fileTransferUndefined)); return }
DispatchQueue.main.async {
self.counter += 1
}
@ -257,11 +252,9 @@ class BleContentCommands: ObservableObject {
switch result {
case .success:
DLog("writeFile \(path) success. Size: \(data.count)")
print("\(#function) @Line: \(#line)")
case .failure(let error):
DLog("writeFile \(path) error: \(error)")
print("Deep Error")
print("\(#function) @Line: \(#line)")
}
}

File diff suppressed because it is too large Load diff

View file

@ -8,8 +8,8 @@
import SwiftUI
import FileTransferClient
class ExpandedBLECellState: ObservableObject {
@Published var currentCell = ""
class SpotlightCounter: ObservableObject {
@Published var counter = 0
}
struct BleModuleView: View {
@ -25,43 +25,52 @@ struct BleModuleView: View {
}
}
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var expandedState : ExpandedBLECellState
@ObservedObject var connectionManager = FileTransferConnectionManager.shared
let selectedPeripheral = FileTransferConnectionManager.shared.selectedPeripheral
@StateObject var viewModel = BleModuleViewModel()
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var viewModel = BleModuleViewModel()
@ObservedObject var networkServiceModel = NetworkService()
@StateObject var globalString = GlobalString()
@StateObject var btConnectionViewModel = BTConnectionViewModel()
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var downloadModel = DownloadViewModel()
@StateObject var spotlight = SpotlightCounter()
//clearKnownPeripheralUUIDs
@State private var isConnected = false
//@State private var switchedView = false
@State private var errorOccured = false
@State var notExpanded = false
@State var isExpanded = true
@State private var downloadState = DownloadState.idle
@State private var scrollViewID = UUID()
@State var currentHightlight: Int = 0
@State private var activeAlert: ActiveAlert?
@State private var internetAlert = false
@State private var showAlert1 = false
@State private var boardBootInfo = ""
@State private var inConnectedInSelectionView = true
@AppStorage("shouldShowOnboarding123") var switchedView: Bool = false
@State var subviewHeight : CGFloat = 0
func showConfirmationPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
connectionManager.isDisconnectingFromCurrent = true
} cancel: {
}
}
var body: some View {
var connectedPeripherals = connectionManager.peripherals.filter{$0.state == .connected }
let selectedPeripheral = FileTransferConnectionManager.shared.selectedPeripheral
VStack {
//Start
@ -69,7 +78,6 @@ struct BleModuleView: View {
HStack {
Spacer()
Image("bluetooth")
.resizable()
.aspectRatio(contentMode: .fit)
@ -170,139 +178,68 @@ struct BleModuleView: View {
HeaderView()
// Button {
// print("Disconnect")
//
// activeAlert = .confirmUnpair(blePeripheral: connectedPeripherals[0])
// connectedPeripherals = []
//
// connectionManager.isConnectedOrReconnecting = false
// FileTransferConnectionManager.shared
// connectionManager.selectedPeripheral = nil
// connectionManager.isAnyPeripheralConnecting = false
// connectionManager.isSelectedPeripheralReconnecting = false
// // connectionManager.clearAllPeripheralInfo()
// rootViewModel.goToMain()
//
// print("Destination: \(rootViewModel.destination)")
// } label: {
// Text("Disconnection")
// }
// Sub-Header
VStack {
if boardBootInfo == "circuitplayground_bluefruit" {
HStack {
Text("Connected to Circuit Playground Bluefruit")
.font(Font.custom("ReadexPro-Regular", size: 16))
Image("bluetoothLogo")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
Text("Circuit Playground Bluefruit.")
.font(Font.custom("ReadexPro-Regular", size: 14))
.minimumScaleFactor(0.1)
Button {
showConfirmationPrompt()
} label: {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
.minimumScaleFactor(0.1)
}
}
}
if boardBootInfo == "clue_nrf52840_express" {
VStack {
HStack {
Image("bluetoothLogo")
.resizable()
.scaledToFit()
.frame(width: 16, height: 16)
Text("Adafruit CLUE.")
.font(Font.custom("ReadexPro-Regular", size: 14))
Button {
showConfirmationPrompt()
} label: {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
//.minimumScaleFactor(0.1)
}
}
// Expandable
VStack {
Text("More Info")
}
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
.onPreferenceChange(ViewHeightKey.self) { subviewHeight = $0 }
.frame(height: isExpanded ? subviewHeight : 50, alignment: .top)
.clipped()
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom))
.onTapGesture {
withAnimation(.easeIn(duration: 0.5)) {
isExpanded.toggle()
}
}
Text("Connected to Adafruit CLUE")
.font(Font.custom("ReadexPro-Regular", size: 16))
}
}
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
.frame(maxHeight: 40)
.background(Color("adafruit_blue"))
.background(Color("pyleap_green"))
.foregroundColor(.white)
ScrollView(.vertical, showsIndicators: false) {
ScrollView(.vertical, showsIndicators: true) {
ScrollViewReader { scroll in
if boardBootInfo == "clue_nrf52840_express" {
MainSubHeaderView(device: "Adafruit CLUE")
MainSubHeaderView()
// .spotlight(enabled: spotlight.counter == 1, title: "1")
let check = networkServiceModel.pdemos.filter {
$0.compatibility[0] == boardBootInfo
}
if boardBootInfo == "circuitplayground_bluefruit" {
MainSubHeaderView(device: "Circuit Playground")
}
let check = viewModel.pdemos.filter {
$0.compatibility.contains(boardBootInfo)
}
ForEach(check) { demo in
if demo.bundleLink == expandedState.currentCell {
DemoViewCell(result: demo, isExpanded: true, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
})
.onAppear(){
print("Cell Appeared")
withAnimation {
scroll.scrollTo(demo.id)
}
DemoViewCell(result: demo, isConnected: $inConnectedInSelectionView, bootOne: $boardBootInfo, onViewGeometryChanged: {
withAnimation {
scroll.scrollTo(demo.id)
}
} else {
DemoViewCell(result: demo, isExpanded: false, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
})
}
}, stateBinder: $downloadState)
}
@ -310,38 +247,143 @@ struct BleModuleView: View {
.id(self.scrollViewID)
}
.environmentObject(expandedState)
}
.alert("Cannot Write To Device", isPresented: $errorOccured) {
Button("OK") {
// Handle acknowledgement.
print("OK")
errorOccured = false
}
} message: {
Text("""
Unplug device from computer and use external power source.
Then press RESET on device to continue.
""")
.multilineTextAlignment(.leading)
}
.onTapGesture {
print("\(networkServiceModel.pdemos.count)")
}
}
}
.background(Color.white)
.environmentObject(globalString)
.onChange(of: viewModel.isConnectedToInternet, perform: { newValue in
if newValue {
internetAlert = false
} else {
internetAlert = true
}
})
.modifier(Alerts(activeAlert: $activeAlert))
.onChange(of: viewModel.state, perform: { newValue in
print("State: \(newValue)")
downloadState = newValue
print("State Change: \(newValue )")
if newValue == .failed {
print("Failed Value")
errorOccured = true
}
})
.onChange(of: viewModel.writeError, perform: { newValue in
print("Change happened! \(newValue)")
globalString.bundleHasBeenDownloaded = newValue
})
.onChange(of: viewModel.sendingBundle, perform: { newValue in
globalString.isSendingG = newValue
if newValue {
print("Is transferring...")
} else {
print("Not transferring...")
}
})
.onChange(of: viewModel.numOfFiles, perform: { newValue in
globalString.numberOfFilesG = newValue
print("NumOfFiles: \(newValue)")
})
.onChange(of: viewModel.counter, perform: { newValue in
globalString.counterG = newValue
print("Change for counterG happened: Value should be \(newValue)")
})
.onChange(of: viewModel.bootUpInfo, perform: { newValue in
viewModel.readMyStatus()
print("newValue \(newValue)")
boardBootInfo = newValue
})
.onChange(of: globalString.projectString, perform: { newValue in
print("Start Transfer")
// viewModel.getProjectURL(nameOf: newValue)
})
.onChange(of: globalString.attemptToDownload, perform: { newValue in
print("Start Download Process\(globalString.downloadLinkString) - \(globalString.projectString)")
downloadModel.startDownload(urlString: globalString.downloadLinkString, projectTitle: globalString.projectString)
})
.onChange(of: globalString.attemptToSend, perform: { newValue in
viewModel.getProjectURL(nameOf: globalString.projectString)
})
.onChange(of: downloadModel.attemptToSendBunle, perform: { newValue in
print("Attempting transfer of: \(globalString.projectString)")
viewModel.getProjectURL(nameOf: globalString.projectString)
})
.onChange(of: connectionManager.selectedClient) { selectedClient in
viewModel.setup(fileTransferClient: selectedClient)
}
.onAppear(){
print("Opened BleModuleView")
// networkServiceModel.fetch()
viewModel.setup(fileTransferClient:connectionManager.selectedClient)
connectionManager.isSelectedPeripheralReconnecting = true
print("On Appear")
networkServiceModel.fetch()
viewModel.setup(fileTransferClient: connectionManager.selectedClient)
viewModel.readFile(filename: "boot_out.txt")
}
}
enum ButtonStatus: CaseIterable, Identifiable {
case download
case transfer
case complete
var id: String { return title }
var title: String {
switch self {
case .download: return "Download"
case .transfer: return "Transfer"
case .complete: return "Complete"
}
}
}
struct Alerts: ViewModifier {
@Binding var activeAlert: ActiveAlert?
@ -364,9 +406,4 @@ struct BleModuleView: View {
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}

View file

@ -10,29 +10,656 @@ import Zip
import FileTransferClient
class BleModuleViewModel: ObservableObject {
private weak var fileTransferClient: FileTransferClient?
@StateObject var contentTransfer = BleContentTransfer()
@StateObject var globalString = GlobalString()
private weak var fileTransferClient: FileTransferClient?
@Published var entries = [BlePeripheral.DirectoryEntry]()
@Published var isTransmiting = false
@Published var bootUpInfo = ""
let dataStore = DataStore()
@Published var pdemos : [ResultItem] = []
var projectDirectories: [URL] = []
@Published var sendingBundle = false
@Published var didCompleteTranfer = false
@Published var writeError = false
init() {
pdemos = dataStore.loadDefaultList()
}
@Published var counter = 0
@Published var numOfFiles = 0
@Published var fileArray: [ContentFile] = []
@Published var contentList: [URLData] = []
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
var networkMonitor = NetworkMonitor()
static let shared = BleModuleViewModel()
@Published var isConnectedToInternet = false
@Published var showAlert = false
var downloadPhases: String = ""
@Published var state: DownloadState = .idle
enum ProjectViewError: LocalizedError {
case fileTransferUndefined
}
func displayErrorMessage() {
DispatchQueue.main.async {
self.writeError = true
self.sendingBundle = false
}
}
func internetMonitoring() {
networkMonitor.startMonitoring()
networkMonitor.monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
print("Connected to internet.")
DispatchQueue.main.async {
self.showAlert = false
self.isConnectedToInternet = true
}
} else {
print("No connection.")
DispatchQueue.main.async {
self.showAlert = true
self.isConnectedToInternet = false
}
}
print("isExpensive: \(path.isExpensive)")
}
}
init() {
internetMonitoring()
}
/// Deletes all files and dic. on Bluefruit device *Except boot_out.txt*
func removeAllFiles(){
self.listDirectoryCommand(path: "") { result in
switch result {
case .success(let contents):
for i in contents! where i.name != "boot_out.txt" {
self.deleteFileCommand(path: i.name) { deletionResult in
switch deletionResult {
case .success:
print("Successfully Deleted")
case .failure:
print("Failed to delete.")
}
}
}
case .failure:
print("No content listed")
}
}
}
/*
- Find URL by name - send it to filesDownloaded
- Enumerate thru found URL
- Get collection of files and directories - then send URL to startFileTransfer
*/
func getProjectURL(nameOf project: String) {
print("getProjectURL called")
counter = 0
state = .transferring
if let enumerator = FileManager.default.enumerator(at: directoryPath, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
// for case condition: Only process URLs
for case let fileURL as URL in enumerator {
do {
print("Starting a loop...")
if fileURL.lastPathComponent == project {
print("Searching for... \(project)")
do {
print(#function)
print("Found \(project) project at this location...")
print("URL Path: \(fileURL.path)")
print("URL : \(fileURL)")
let newURL = URL(fileURLWithPath: fileURL.path, relativeTo: directoryPath)
print("URL: \(newURL)")
filesDownloaded(url: fileURL)
return
} catch { print(error, fileURL) }
} else {
print("Project was not found for...\(project)")
print("\(state)")
state = .idle
}
}
}
}
}
func filesDownloaded(url: URL) {
print("filesDownloaded was called")
//Cycles through files and directories in File Manager Document Directory
fileArray.removeAll()
var files = [URL]()
// Returns a directory enumerator object that can be used to perform a deep enumeration of the directory at the specified URL.
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
// for case condition: Only process URLs
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey, .addedToDirectoryDateKey,.isDirectoryKey])
print("INCOMING FILE: \(fileURL.path)")
if fileURL.path.contains("adafruit-circuitpython-bundle-7.x-mpy") {
print("Removing adafruit-circuitpython-bundle-7.x-mpy: \(fileURL.path)")
} else {
print("FILTERED INCOMING FILE: \(fileURL.path)")
contentList.append(.init(urlTitle: fileURL))
if fileAttributes.isRegularFile! {
files.append(fileURL)
let resources = try fileURL.resourceValues(forKeys:[.fileSizeKey])
let fileSize = resources.fileSize!
let addedFile = ContentFile(title: fileURL.lastPathComponent, fileSize: fileSize)
fileArray.append(addedFile)
}
let addedFile = ContentFile(title: fileURL.lastPathComponent, fileSize: 0 )
fileArray.append(addedFile)
}
} catch { print(error, fileURL) }
}
startFileTransfer(url: url)
numOfFiles = files.count
print("Contents in URL \(fileArray.count)")
print("Number of Files in URL \(files.count)")
for i in contentList {
print("CL: \(i.urlTitle.pathComponents)")
}
contentList.removeAll()
}
}
func startFileTransfer(url: URL) {
print("Project Location: \(url)")
let localFileManager = FileManager()
let resourceKeys = Set<URLResourceKey>([.nameKey, .isDirectoryKey])
var fileURLs: [URL] = []
let dirEnumerator = localFileManager.enumerator(at: url, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)!
for case let fileURL as URL in dirEnumerator {
guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys),
let isDirectory = resourceValues.isDirectory,
let name = resourceValues.name
else {
continue
}
if fileURL.path.contains("adafruit-circuitpython-bundle-7.x-mpy") {
print("Removing adafruit-circuitpython-bundle-7.x-mpy: \(fileURL.path)")
} else {
if isDirectory {
print("Directories Found")
print(fileURL.lastPathComponent)
if name == "_extras" {
dirEnumerator.skipDescendants()
}
//adafruit-circuitpython-bundle
if fileURL.lastPathComponent.contains("adafruit-circuitpython-bundle") {
print("We got one!")
print("Bad file - \(fileURL)")
} else {
if fileURL.pathComponents.count > 12 {
print("File Path component count: \(fileURL.pathComponents.count)")
projectDirectories.append(fileURL)
}
}
} else {
print("APPENDED: \(fileURL.path)")
fileURLs.append(fileURL)
}
}
}
print("List of Directories")
for i in projectDirectories {
print("Directory: \(i.path)")
}
print("List of Files")
for i in fileURLs {
print("Files: \(i.path)")
}
DispatchQueue.main.async {
self.sendingBundle = true
}
print("Current projectDirectories: \(projectDirectories[0])")
sortDirectory(dirList: projectDirectories, filesUrls: fileURLs)
}
func sortDirectory(dirList: [URL], filesUrls: [URL]) {
print(#function)
//Creates a sorted list of directories
var tempDirectory = dirList.sorted(by: { $1.pathComponents.count > $0.pathComponents.count} )
print("Evaluating: \(String(describing: tempDirectory.first?.lastPathComponent))")
print("With Path: \(String(describing: tempDirectory.first?.path))")
print("Sorted Directory")
for i in tempDirectory{
print(i.lastPathComponent)
}
// If directories are not found, start transferring files over to directories.
if dirList.isEmpty {
print("No directories left in queue")
projectDirectories.removeAll()
self.transferFiles(files: filesUrls)
} else {
guard let firstDirectory = tempDirectory.first else {
print("No directory exist here")
return
}
// If lib/ directory is found in the project bundle, make a lib directory on client.
if firstDirectory.lastPathComponent == "lib" {
mkLibDir(libDirectory: firstDirectory, copiedDirectory: tempDirectory, filesUrl: filesUrls)
} else {
mkSubLibDir(subdirectory: firstDirectory, copiedDirectory: tempDirectory, filesURL: filesUrls)
}
}
self.projectDirectories.removeAll()
}
//Make lib/ Directory
func mkLibDir(libDirectory: URL, copiedDirectory: [URL], filesUrl: [URL]) {
print(#function)
var temp = copiedDirectory
// print(temp)
print("mkLibDir list")
for i in temp {
print("\(i)")
}
listDirectoryCommand(path: "") { result in
switch result {
// Check that lib/ exist.
case .success(let contents):
print("ListDirCommand: \(String(describing: contents))")
if contents!.contains(where: { name in name.name == libDirectory.lastPathComponent}) {
print("lib directory exist")
temp.removeFirst()
self.sortDirectory(dirList: temp, filesUrls: filesUrl)
} else {
print("lib directory does not exist")
print("XXXX mkLibDir")
var tempURL = libDirectory.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
print("FIXED PATHxx:\(joined)")
self.makeDirectoryCommand(path: joined) { result in
switch result {
case .success:
print("Success")
temp.removeFirst()
self.sortDirectory(dirList: temp, filesUrls: filesUrl)
case .failure:
print("Failed to create directory \(joined)")
temp.removeAll()
self.projectDirectories.removeAll()
self.displayErrorMessage()
}
}
}
case .failure:
print("Failure - mkLibDir")
temp.removeAll()
self.projectDirectories.removeAll()
self.displayErrorMessage()
}
}
}
func mkSubLibDir(subdirectory: URL, copiedDirectory: [URL], filesURL: [URL]) {
print(#function)
var temp = copiedDirectory
print("List of Directories Currently in mkSubLibDir")
for i in temp {
print("\(i.path)")
}
var tempURL = subdirectory.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
print("Modified Path top: \(joined)")
var pathDirectoryForListCommand = tempURL
pathDirectoryForListCommand.removeLast()
let pathDirectoryForListCommandJoined = pathDirectoryForListCommand.joined(separator: "/")
print("pathDirectoryForListCommandJoined: \(pathDirectoryForListCommandJoined)")
print("How its taken: \(pathDirectoryForListCommandJoined)/")
listDirectoryCommand(path: "\(pathDirectoryForListCommandJoined)/") { result in
switch result {
case .success(let contents):
if contents!.contains(where: { name in name.name == subdirectory.lastPathComponent}) {
print("FULL PATH OF: \(subdirectory.lastPathComponent)")
print("\(subdirectory.path)")
// Skips the existing directory.
temp.removeFirst()
self.sortDirectory(dirList: temp, filesUrls: filesURL)
} else {
print("\(subdirectory.lastPathComponent) directory does not exist")
print("Here's the full path of \(subdirectory.lastPathComponent): \(subdirectory.path)")
print("XXXX mkSubLibDir")
var tempURL = subdirectory.pathComponents
print("Incoming URL: \(tempURL)")
tempURL.removeFirst(12)
print("Modified Path without seperators: \(tempURL)")
let joined = tempURL.joined(separator: "/")
print("Modified Path: \(joined)")
var pathDirectoryForListCommand = tempURL
pathDirectoryForListCommand.removeLast()
let pathDirectoryForListCommandJoined = pathDirectoryForListCommand.joined(separator: "/")
print("pathDirectoryForListCommandJoined: \(pathDirectoryForListCommandJoined)")
self.makeDirectoryCommand(path: joined) { result in
switch result {
case .success:
print("Success")
temp.removeFirst()
self.sortDirectory(dirList: temp, filesUrls: filesURL)
case .failure:
print("Failed to create directory - 2")
temp.removeAll()
self.projectDirectories.removeAll()
self.displayErrorMessage()
}
}
}
case .failure:
print("Fail in: \(#function)")
temp.removeAll()
self.projectDirectories.removeAll()
self.displayErrorMessage()
}
}
}
func completedTransfer() {
DispatchQueue.main.async {
self.didCompleteTranfer = true
self.numOfFiles = 0
self.counter = 0
self.state = .complete
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.didCompleteTranfer = false
self.state = .idle
}
}
func transferFiles(files: [URL]) {
print(#function)
var copiedFiles = files
print("Number of files in filesArray \(files.count)")
print(files)
if files.isEmpty {
print("Array of contents empty - Check other directories")
self.completedTransfer()
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
self.sendingBundle = false
self.counter = 0
self.numOfFiles = 0
self.contentList.removeAll()
}
} else {
guard let selectedUrl = files.first else {
print("No such file exist here")
return
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: selectedUrl.deletingPathExtension().lastPathComponent, relativeTo: selectedUrl).appendingPathExtension(selectedUrl.pathExtension)) else {
print("File not found")
return
}
if selectedUrl.deletingLastPathComponent().lastPathComponent == "CircuitPython 7.x"{
print("Selected Path: \(selectedUrl.path)")
var tempURL = selectedUrl.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
var newModPath = tempURL
newModPath.removeLast()
print("Test file path: \(tempURL)")
print("File transfer modified path xx: \(joined)")
self.writeFileCommand(path: joined, data: data) { result in
switch result {
case .success(_):
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
case .failure(_):
DispatchQueue.main.async {
print("Transfer Failure")
print("\(joined)")
self.state = .failed
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.state = .idle
}
}
self.displayErrorMessage()
}
}
}
else if selectedUrl.deletingLastPathComponent().lastPathComponent == "lib" {
var tempURL = selectedUrl.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
print("File transfer modified path 11:\(joined)")
print("Updated Path:\(joined)")
writeFileCommand(path: joined, data: data) { result in
switch result {
case .success(_):
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
case .failure(_):
print("Transfer Failure - 2")
self.state = .failed
self.displayErrorMessage()
}
}
} else {
if selectedUrl.lastPathComponent == "README.txt" {
print("Got one")
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
} else {
var tempURL = selectedUrl.pathComponents
tempURL.removeFirst(12)
let joined = tempURL.joined(separator: "/")
print("File transfer modified path: \(joined)")
print("Updated Path:\(joined)")
writeFileCommand(path: joined, data: data) { result in
switch result {
case .success(_):
copiedFiles.removeFirst()
self.transferFiles(files: copiedFiles)
case .failure(let error):
print("Failed: \(error): \(result)")
// self.displayErrorMessage()
}
}
}
}
}
DispatchQueue.main.async {
self.sendingBundle = true
}
}
func readMyStatus() {
// model.readFile(filename: "boot_out.txt")
print(#function)
print("BOOT INFO: \(bootUpInfo)")
@ -41,21 +668,29 @@ class BleModuleViewModel: ObservableObject {
case let str where str.contains("circuitplayground_bluefruit"):
print("Circuit Playground Bluefruit device")
bootUpInfo = "circuitplayground_bluefruit"
// DispatchQueue.main.async { [self] in
// self.globalString.compatibilityString = "circuitplayground_bluefruit"
// }
case let str where str.contains("clue_nrf52840_express"):
print("Clue device")
bootUpInfo = "clue_nrf52840_express"
// DispatchQueue.main.async { [self] in
// globalString.compatibilityString = "clue_nrf52840_express"
//
// }
default:
print("Unknown Device")
}
}
func readBoardForCircuitPythonVersion() {
}
// MARK: System
struct TransmissionProgress {
@ -70,10 +705,6 @@ class BleModuleViewModel: ObservableObject {
}
@Published var transmissionProgress: TransmissionProgress?
@Published var lastTransmit: TransmissionLog? = TransmissionLog(type: .write(size: 334))
@Published var activeAlert: ActiveAlert?
// Data
private let bleManager = BleManager.shared
struct TransmissionLog: Equatable {
enum TransmissionType: Equatable {
@ -100,7 +731,16 @@ class BleModuleViewModel: ObservableObject {
return modeText
}
}
@Published var lastTransmit: TransmissionLog? = TransmissionLog(type: .write(size: 334))
@Published var activeAlert: ActiveAlert?
// Data
private let bleManager = BleManager.shared
// MARK: - Setup
func onAppear() {
//registerNotifications(enabled: true)
@ -133,10 +773,8 @@ class BleModuleViewModel: ObservableObject {
case .success(let data):
self.lastTransmit = TransmissionLog(type: .read(data: data))
let str = String(decoding: data, as: UTF8.self)
print("Read: \(str)")
self.bootUpInfo = str
sharedBootinfo = str
case .failure(let error):
self.lastTransmit = TransmissionLog(type: .error(message: error.localizedDescription))
@ -268,7 +906,11 @@ class BleModuleViewModel: ObservableObject {
private func writeFileCommand(path: String, data: Data, completion: ((Result<Date?, Error>) -> Void)?) {
guard let fileTransferClient = fileTransferClient else { completion?(.failure(ProjectViewError.fileTransferUndefined)); return }
DispatchQueue.main.async {
self.counter += 1
}
DLog("start writeFile \(path)")
fileTransferClient.writeFile(path: path, data: data, progress: { [weak self] written, total in
DLog("writing progress: \( String(format: "%.1f%%", Float(written) * 100 / Float(total)) )")
@ -348,8 +990,6 @@ class BleModuleViewModel: ObservableObject {
}
public var sharedBootinfo = ""
enum ActiveAlert: Identifiable {
case error(error: Error)

View file

@ -8,83 +8,76 @@
import SwiftUI
import FileTransferClient
class GlobalString: ObservableObject {
@Published var projectString = ""
@Published var downloadLinkString = ""
@Published var compatibilityString = ""
@Published var counterG = 0
@Published var numberOfFilesG = 0
@Published var isSendingG = false
@Published var bundleHasBeenDownloaded = false
@Published var numberOfTimesDownloaded = 0
@Published var attemptToDownload = false
@Published var attemptToSend = false
}
struct DemoSubview: View {
@State var transferInProgress = false
@State var isDownloaded = false
@EnvironmentObject var globalString : GlobalString
@Binding var bindingString: String
let result: ResultItem
@Binding var downloadStateBinder: DownloadState
@State private var toggleView: Bool = false
let title: String
let image: String
let description: String
let learnGuideLink: URLRequest
let downloadLink: String
let compatibility: [String]
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var downloadModel = DownloadViewModel()
@StateObject var viewModel = SubCellViewModel()
@StateObject var selectionModel = BleModuleViewModel()
@StateObject var contentTransfer = BleContentTransfer()
@ObservedObject var connectionManager = FileTransferConnectionManager.shared
@Binding var isConnected : Bool
@State private var showWebViewPopover: Bool = false
func showAlertMessage() {
alertMessage(title: """
There's a problem with your internet connection.
Try again later.
""", exitTitle: "Ok") {
}
}
@State var errorOccured = false
@State private var presentAlert = false
@State var offlineWithoutProject = false
func showTransferErrorMessage() {
alertMessage(title: """
Transfer Failed
Disconnect device from the computer.
Press "Reset" on the device and use a battery source.
""", exitTitle: "Retry") {
contentTransfer.transferError = false
}
}
func showDownloadErrorMessage() {
alertMessage(title: """
Server Error
This project can not be downloaded at this time
Try again later
""", exitTitle: "Ok") {
contentTransfer.downloaderror = false
}
}
var body: some View {
VStack {
if viewModel.projectDownloaded {
// HStack {
// Spacer()
//
// Text("Downloaded")
// .foregroundColor(.green)
// .padding(.trailing, -15)
// Circle()
// .fill(.green)
// .frame(width: 15, height: 15)
// .padding()
// }
// .padding(.vertical, -8)
}
VStack(alignment: .leading, spacing: 0, content: {
ImageWithURL(result.projectImage)
ImageWithURL(image)
.scaledToFit()
.frame(maxWidth: .infinity)
.cornerRadius(14)
.padding(.top, 30)
Text(result.description)
Text(description)
.font(Font.custom("ReadexPro-Regular", size: 18))
.multilineTextAlignment(.leading)
.minimumScaleFactor(0.1)
@ -92,9 +85,9 @@ Try again later
Text("Compatible with:")
.font(Font.custom("ReadexPro-Bold", size: 18))
.padding(.top, 5)
ForEach(result.compatibility, id: \.self) { string in
ForEach(compatibility, id: \.self) { string in
if string == "circuitplayground_bluefruit" {
HStack {
@ -109,7 +102,7 @@ Try again later
.padding(.top, 10)
}
if string == "clue_nrf52840_express" {
@ -130,19 +123,14 @@ Try again later
.padding(.horizontal, 30)
Button(action: {
if !viewModel.isConnectedToInternet {
showAlertMessage()
} else {
showWebViewPopover = true
}
showWebViewPopover = true
}) {
LearnGuideButton()
.padding(.top, 20)
}
.sheet(isPresented: $showWebViewPopover, content: {
SwiftUIWebView(webAddress: result.learnGuideLink)
WebView(URLRequest(url: learnGuideLink.url!))
})
@ -150,72 +138,123 @@ Try again later
if isConnected {
if result.compatibility.contains(bindingString) {
if compatibility.contains(bindingString) {
Button {
contentTransfer.getProjectURL(nameOf: title)
} label: {
Text("XXX")
}
// Button {
// viewModel.deleteStoredFilesInFM()
// } label: {
// Text("Delete File Manager Contents")
// .bold()
// .padding(12)
// }
if contentTransfer.downloadState == .idle {
if downloadStateBinder == .idle {
Button(action: {
downloadStateBinder = .transferring
globalString.isSendingG = true
globalString.counterG = 0
globalString.numberOfFilesG = 1
/// Condition: Connected to the internet
///- If you're not connected to the internet, but you've downloaded the project...
/// - If you're not connected to the internet, and you're project is not downloaded...
/// *Show Alert*
if viewModel.projectDownloaded == false && viewModel.isConnectedToInternet == false {
showAlertMessage()
} else {
contentTransfer.testFileExistance(for: result.projectName, bundleLink: result.bundleLink)
globalString.downloadLinkString = downloadLink
globalString.projectString = title
globalString.attemptToDownload.toggle()
if selectionModel.isConnectedToInternet == false {
print("Going offline...")
downloadStateBinder = .transferring
globalString.projectString = title
globalString.attemptToSend.toggle()
}
if viewModel.projectDownloaded == false && selectionModel.isConnectedToInternet == false {
offlineWithoutProject = true
downloadStateBinder = .idle
}
if viewModel.projectDownloaded == false {
}
}) {
RunItButton()
.padding(.top, 20)
.padding(.top, 20)
}
}
if contentTransfer.downloadState == .failed {
if downloadStateBinder == .failed {
FailedButton()
.padding(.top, 20)
}
if contentTransfer.downloadState == .transferring {
DownloadingButton()
.padding(.top, 20)
.disabled(true)
if downloadStateBinder == .transferring {
VStack(alignment: .center, spacing: 5) {
ProgressView("", value: CGFloat(contentTransfer.counter), total: CGFloat(contentTransfer.numOfFiles) )
.padding(.horizontal, 90)
.padding(.top, -8)
.padding(.bottom, 10)
.accentColor(Color.gray)
.scaleEffect(x: 1, y: 2, anchor: .center)
.cornerRadius(10)
.frame(height: 10)
ProgressView()
Button(action: {
print("Project Selected: \(title) - DemoSubView")
globalString.projectString = title
globalString.numberOfTimesDownloaded += 1
}) {
DownloadingButton()
.padding(.top, 20)
}
.disabled(true)
if globalString.isSendingG {
VStack(alignment: .center, spacing: 0) {
ProgressView("", value: CGFloat(globalString.counterG), total: CGFloat(globalString.numberOfFilesG) )
.padding(.horizontal, 90)
.padding(.top, -8)
.padding(.bottom, 10)
.accentColor(Color.gray)
.scaleEffect(x: 1, y: 2, anchor: .center)
.cornerRadius(10)
.frame(height: 10)
ProgressView()
}
}
}
if contentTransfer.downloadState == .complete {
if downloadStateBinder == .complete {
CompleteButton()
.padding(.top, 20)
}
}
} else {
Button {
rootViewModel.goToSelection()
rootViewModel.goTobluetoothPairing()
} label: {
ConnectButton()
.padding(.top, 20)
@ -225,48 +264,58 @@ Try again later
}
Spacer()
.frame(height: 30)
.ignoresSafeArea(.all)
.ignoresSafeArea(.all)
.onAppear(){
print("On Appear")
contentTransfer.contentCommands.setup(fileTransferClient: connectionManager.selectedClient)
.alert("Project Not Found", isPresented: $offlineWithoutProject) {
Button("OK") {
// Handle acknowledgement.
print("OK")
offlineWithoutProject = false
downloadStateBinder = .idle
selectionModel.state = .idle
print("\(offlineWithoutProject)")
}
} message: {
Text("""
To use this project, connect to the internet.
""")
.multilineTextAlignment(.leading)
}
.onChange(of: downloadModel.isDownloading, perform: { newValue in
viewModel.getProjectForSubClass(nameOf: title)
})
.onChange(of: downloadModel.didDownloadBundle, perform: { newValue in
print("For project: \(title), project download is \(newValue)")
// viewModel.readFile(filename: "boot_out.txt")
}
.onChange(of: contentTransfer.transferError, perform: { newValue in
if newValue {
showTransferErrorMessage()
globalString.projectString = title
if newValue {
DispatchQueue.main.async {
print("Getting project from Subclass \(title)")
viewModel.getProjectForSubClass(nameOf: title)
isDownloaded = true
}
})
.onChange(of: contentTransfer.downloaderror, perform: { newValue in
if newValue {
showDownloadErrorMessage()
}
})
// .onChange(of: connectionManager.selectedClient) { selectedClient in
// viewModel.setup(fileTransferClient: selectedClient)
// }
.onAppear(perform: {
contentTransfer.readMyStatus()
viewModel.searchPathForProject(nameOf: result.projectName)
if viewModel.projectDownloaded {
viewModel.projectDownloaded = true
} else {
viewModel.projectDownloaded = false
}
}else {
print("Is not downloaded")
isDownloaded = false
}
)
.padding(.top, 8)
})
.onAppear(perform: {
viewModel.getProjectForSubClass(nameOf: title)
if viewModel.projectDownloaded {
isDownloaded = true
} else {
isDownloaded = false
}
print("is downloaded? \(isDownloaded)")
})
.padding(.top, 8)
}
}

View file

@ -9,21 +9,23 @@ import Foundation
struct DemoViewCell: View {
@EnvironmentObject var expandedState : ExpandedBLECellState
@StateObject var spotlight = SpotlightCounter()
let result : ResultItem
@State var isExpanded: Bool = false {
@State private var isExpanded: Bool = false {
didSet {
onViewGeometryChanged()
onViewGeometryChanged()
}
}
@Binding var isConnected: Bool
@Binding var deviceInfo: String
@Binding var bootOne: String
let onViewGeometryChanged: ()->Void
@Binding var stateBinder: DownloadState
var body: some View {
content
.frame(maxWidth: .infinity)
@ -33,10 +35,19 @@ struct DemoViewCell: View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
Group {
DemoSubview(bindingString: $deviceInfo, result: result, isConnected: $isConnected)
DemoSubview(bindingString: $bootOne, downloadStateBinder: $stateBinder, title: result.projectName,
image: result.projectImage,
description: result.description,
learnGuideLink: URLRequest(url: URL(string: result.learnGuideLink)!),
downloadLink: result.bundleLink,
compatibility: result.compatibility,
isConnected: $isConnected)
}
}
@ -64,10 +75,7 @@ struct DemoViewCell: View {
.padding(.leading)
.frame(maxWidth: .infinity)
.background(Color("pyleap_purple"))
.onTapGesture {
expandedState.currentCell = result.bundleLink
}
.onTapGesture { isExpanded.toggle() }
}
}

View file

@ -13,7 +13,7 @@ struct ScrollRefreshableView<Content: View>: View {
var onRefresh: ()->()
init(title: String, tintColor: Color, @ViewBuilder content: @escaping () -> Content, onRefresh: @escaping () -> ()) {
init(title: String, tintColor: Color, @ViewBuilder content: @escaping ()->Content, onRefresh: @escaping ()->()) {
self.content = content()
self.onRefresh = onRefresh

View file

@ -11,74 +11,37 @@ class SubCellViewModel: ObservableObject {
@Published var projectDownloaded = false
@Published var failedProjectLaunch = false
@Published var isConnectedToInternet = false
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
var manager = FileManager.default
var networkMonitor = NetworkMonitor()
init() {
internetMonitoring()
}
func deleteStoredFilesInFM () {
print("\(#function) @Line: \(#line)")
do {
try manager.removeItem(at: directoryPath)
func getProjectForSubClass(nameOf project: String) {
if let enumerator = FileManager.default.enumerator(at: directoryPath, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
// for case condition: Only process URLs
for case let fileURL as URL in enumerator {
} catch {
print(error)
}
}
func find(projectWith title: String) {
let nestedFolderURL = directoryPath.appendingPathComponent(title)
if manager.fileExists(atPath: nestedFolderURL.relativePath) {
print("\(title) - Exists")
projectDownloaded = true
} else {
print("\(title) - Does not exist.")
projectDownloaded = false
}
}
func internetMonitoring() {
networkMonitor.startMonitoring()
networkMonitor.monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
print("Connected to internet.")
if fileURL.lastPathComponent == project {
failedProjectLaunch = false
projectDownloaded = true
print(#function)
print("Searching for... \(project)")
print("URL Path: \(fileURL.path)")
print("URL : \(fileURL)")
return
} else {
failedProjectLaunch = true
projectDownloaded = false
print("Project was not found...")
}
DispatchQueue.main.async {
// self.showAlert = false
self.isConnectedToInternet = true
}
} else {
print("No connection.")
DispatchQueue.main.async {
// self.showAlert = true
self.isConnectedToInternet = false
}
}
print("isExpensive: \(path.isExpensive)")
}
}
func searchPathForProject(nameOf project: String) {
var manager = FileManager.default
let nestedFolderURL = directoryPath.appendingPathComponent(project)
if manager.fileExists(atPath: nestedFolderURL.relativePath) {
print("\(project) - Exist")
projectDownloaded = true
} else {
print("Does not exist - \(project)")
projectDownloaded = false
}
}
}

View file

@ -11,8 +11,6 @@ import FileTransferClient
struct RootView: View {
@StateObject private var model = RootViewModel()
@StateObject var currentCellID = ExpandedState()
@StateObject var currentBLECellID = ExpandedBLECellState()
@ObservedObject var connectionManager = FileTransferConnectionManager.shared
@AppStorage("onboarding") var onboardingSeen = true
@ -21,7 +19,7 @@ struct RootView: View {
var body: some View {
Group {
Group{
switch model.destination {
case .onboard :
@ -45,35 +43,15 @@ struct RootView: View {
case .fileTransfer:
BleModuleView()
case .wifiServiceSelection:
WifiServiceSelectionView()
case .wifi:
WifiView()
case .selection:
SelectionView()
case .wifiSelection:
WifiSelection()
case .wifiPairingTutorial:
WifiPairingView()
case .settings:
SettingsView()
case .bleSettings:
BLESettingsView()
default:
FillerView()
}
}
.environmentObject(currentCellID)
.environmentObject(currentBLECellID)
.onReceive(NotificationCenter.default.publisher(for: .didUpdateBleState)) { notification in
if !Config.isSimulatingBluetooth {
@ -93,24 +71,10 @@ struct RootView: View {
if isConnectedOrReconnecting, model.destination == .fileTransfer {
model.destination = .fileTransfer
isReconnecting = true
} else {
isReconnecting = false
}
}
.onChange(of: connectionManager.isDisconnectingFromCurrent) { isDisconnected in
if isDisconnected {
print("Is disconnected.")
isReconnecting = false
connectionManager.clearAllPeripheralInfo()
connectionManager.peripherals = []
connectionManager.isDisconnectingFromCurrent = false
model.destination = .selection
}
}
@ -118,7 +82,6 @@ struct RootView: View {
DLog("App moving to the foreground. Force reconnect")
FileTransferConnectionManager.shared.reconnect()
}
.environmentObject(model)
.environmentObject(connectionManager)
.background(Color.white)
@ -126,7 +89,6 @@ struct RootView: View {
.edgesIgnoringSafeArea(.all)
.ignoresSafeArea(.all)
.preferredColorScheme(.light)
.statusBar(hidden: true)
}
}

View file

@ -8,9 +8,7 @@
import Foundation
import FileTransferClient
public class RootViewModel: ObservableObject {
// public var shared = RootViewModel()
class RootViewModel: ObservableObject {
enum Destination {
//case splash
@ -22,13 +20,7 @@ public class RootViewModel: ObservableObject {
case fileTransfer
case wifi
case settings
case bleSettings
case mainSelection
case wifiSelection
case wifiPairingTutorial
case wifiServiceSelection
case selection
}
@Published var destination: Destination = AppEnvironment.isRunningTests ? .mainSelection : .startup
@ -38,18 +30,6 @@ public class RootViewModel: ObservableObject {
//destination = .test
}
func goToWiFiServiceSelection() {
destination = .wifiServiceSelection
}
func goToWifiPairingTutorial() {
destination = .wifiPairingTutorial
}
func goToWiFiSelection() {
destination = .wifiSelection
}
func goToWifiView() {
destination = .wifi
}
@ -58,10 +38,6 @@ public class RootViewModel: ObservableObject {
destination = .bluetoothPairing
}
func goToSelection(){
destination = .selection
}
func goToMainSelection(){
destination = .mainSelection
}
@ -76,10 +52,6 @@ public class RootViewModel: ObservableObject {
}
}
func backToMain() {
destination = .main
}
func goToStartup(){
destination = .startup
}
@ -92,14 +64,10 @@ public class RootViewModel: ObservableObject {
destination = .fileTransfer
}
func goToSettings(content: SettingState){
func goToSettings(){
destination = .settings
}
func goToBLESettings(){
destination = .bleSettings
}
func showWarningIfBluetoothStateIsNotReady() {
let bluetoothState = BleManager.shared.state
let shouldShowBluetoothDialog = bluetoothState == .poweredOff || bluetoothState == .unsupported || bluetoothState == .unauthorized

View file

@ -0,0 +1,317 @@
//
// SettingsView.swift
// PyLeap
//
// Created by Trevor Beaton on 9/7/22.
//
import SwiftUI
struct SettingsView: View {
@State private var jsonFileName: String = ""
@State private var pythonFileName: String = ""
@State private var presentJSONAlert = false
@State private var presentPythonAlert = false
@EnvironmentObject var rootViewModel: RootViewModel
@ObservedObject var networkModel = NetworkService()
@StateObject var viewModel = SettingsViewModel()
private let kPrefix = Bundle.main.bundleIdentifier!
let userDefaults = UserDefaults.standard
func showInvalidURLEntryAlert() {
alertMessage(title: "Invalid URL entry", exitTitle: "Ok") {
}
}
func showDownloadConfirmationAlert() {
alertMessage(title: "Added to Project List", exitTitle: "Ok") {
}
}
func showDisconnectionPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
viewModel.clearKnownIPAddress()
rootViewModel.goToWifiView()
} cancel: {
}
}
var body: some View {
VStack {
HStack {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Back")
}
.padding(.all,20)
Spacer()
}
//.frame(width: .infinity)
.frame(height: UIScreen.main.bounds.height / 19)
.background(Color(.systemGroupedBackground))
HStack {
Text("Settings")
.font(.largeTitle)
.padding(.all,20)
Spacer()
}
.background(Color(.systemGroupedBackground))
Form {
VStack() {
if viewModel.connectedToDevice {
Section() {
VStack(alignment: .leading, spacing: 8) {
Text("Host Name:")
.bold()
Text(viewModel.hostName)
Text("IP Address:")
.bold()
Text(viewModel.ipAddress)
Text("Device:")
.bold()
Text(viewModel.device)
}
.padding(.leading,0)
}
} else {
Section() {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Connect to Adafruit Device")
}
}
}
}
if viewModel.connectedToDevice {
Section() {
Button {
showDisconnectionPrompt()
} label: {
Text("Disconnect")
}
}
}
Section {
Toggle(isOn: .constant(false)) {
Text("Dark Mode")
}
}
header: {
Text("Display")
}
if viewModel.connectedToDevice {
Section {
Text("Enter project URL")
TextField("https://", text: $pythonFileName)
.keyboardType(.URL)
.textContentType(.URL)
.onSubmit {
NetworkService.shared.fetchThirdParyProject(urlString: pythonFileName)
print(pythonFileName)
pythonFileName = ""
}
} header: {
Text("Add Project")
}
.listRowSeparator(.hidden)
Section {
Button("Create Python File"){
presentPythonAlert = true
}
.alert("Create Python File", isPresented: $presentPythonAlert, actions: {
TextField("", text: $pythonFileName)
Button("Add", action: {})
Button("Cancel", role: .cancel, action: {
presentPythonAlert = false
})
}, message: {
Text("Please enter your username and password.")
})
Button("Create JSON File"){
presentJSONAlert = true
}
.alert("Create JSON File", isPresented: $presentJSONAlert, actions: {
TextField("", text: $jsonFileName)
Button("Add", action: {})
Button("Cancel", role: .cancel, action: {
presentJSONAlert = false
})
}, message: {
Text("Please enter your username and password.")
})
}
header: {
Text("Create")
}
}
Section{
Label("[Go to Adafruit.com](https://www.adafruit.com)", systemImage: "link")
}
.font(.system(size: 16, weight: .semibold))
}
.onChange(of: viewModel.invalidURL, perform: { newValue in
showInvalidURLEntryAlert()
viewModel.invalidURL = false
})
.onChange(of: viewModel.confirmDownload, perform: { newValue in
showDownloadConfirmationAlert()
viewModel.confirmDownload = false
})
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Back")
// .font(.system(size: 18, weight: .regular, design: .default))
.foregroundColor(.blue)
}
.padding(12)
}
}
}
//.padding(.all, 11.0)
.background(Color(UIColor.systemGroupedBackground))
// .safeAreaInset(edge: .top) {
// HStack {
// Button {
// rootViewModel.goToWifiView()
// } label: {
// Text("Back")
// }
//
// .padding(.all,20)
// Spacer()
// }
// // .background(Color(UIColor.systemGroupedBackground))
//
//
//
//
// }
}
}
extension View {
func comfirmationAlertMessage(title: String, exitTitle: String, primaryTitle: String,disconnect: @escaping() -> (),cancel: @escaping() -> ()){
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(.init(title: primaryTitle, style: .destructive, handler: { _ in
disconnect()
}))
alert.addAction(.init(title: exitTitle, style: .cancel, handler: { _ in
cancel()
}))
rootController().present(alert, animated: true, completion: nil)
}
func alertMessage(title: String, exitTitle: String, cancel: @escaping()->()){
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(.init(title: exitTitle, style: .cancel, handler: { _ in
cancel()
}))
rootController().present(alert, animated: true, completion: nil)
}
func alertTF(title: String, message: String, hintText: String, primaryTitle: String, secondaryTitle: String, primaryAction: @escaping(String)->(), secondaryAction: @escaping()->()) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addTextField { field in
field.keyboardType = .decimalPad
field.placeholder = hintText
}
alert.addAction(.init(title: secondaryTitle, style: .cancel, handler: { _ in
secondaryAction()
}))
alert.addAction(.init(title: primaryTitle, style: .default, handler: { _ in
if let text = alert.textFields?[0].text {
primaryAction(text)
} else {
primaryAction("")
}
}))
rootController().present(alert, animated: true, completion: nil)
}
func rootController()->UIViewController{
guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return .init()
}
guard let root = screen.windows.first?.rootViewController else {
return .init()
}
return root
}
}

View file

@ -9,7 +9,7 @@ import Foundation
import SwiftUI
class SettingsViewModel: ObservableObject {
private let kPrefix = Bundle.main.bundleIdentifier!
let userDefaults = UserDefaults.standard
@Published var hostName = ""
@ -19,7 +19,6 @@ class SettingsViewModel: ObservableObject {
@Published var invalidURL = false
@Published var confirmDownload = false
init() {
check()
registerNotifications(enabled: true)
@ -57,9 +56,10 @@ errorObserver = notificationCenter.addObserver(forName: .invalidCustomNetworkReq
func check() {
print(#function)
if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") == nil {
if userDefaults.object(forKey: kPrefix+".storedIP") == nil {
connectedToDevice = false
} else {
print("Stored: \(String(describing: userDefaults.object(forKey: kPrefix+".storedIP"))), @: \(kPrefix+".storedIP")")
connectedToDevice = true

View file

@ -16,16 +16,13 @@ struct BlinkaAnimationView: View {
.repeatForever(autoreverses: false)
}
var height: CGFloat
var width: CGFloat
var body: some View {
ZStack {
Image("BlinkaLoading")
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: width, height: height)
.frame(width: 300, height: 300)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.animation(self.isAnimating ? foreverAnimation : .default)
@ -37,6 +34,6 @@ struct BlinkaAnimationView: View {
struct ScanningView_Previews: PreviewProvider {
static var previews: some View {
BlinkaAnimationView(height: 300, width: 300)
BlinkaAnimationView()
}
}

View file

@ -17,7 +17,7 @@ struct RunItButton: View {
.cornerRadius(25)
.foregroundColor(Color("pyleap_pink"))
Text("Run")
Text("Run it!")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(height: 50)
@ -35,8 +35,8 @@ struct PairingTutorialButton: View {
Rectangle()
.frame(width: 270, height: 50, alignment: .center)
.cornerRadius(25)
.foregroundColor(Color("bluetooth_button_color"))
.foregroundColor(Color("pyleap_pink"))
Text("Pairing Tutorial")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)

View file

@ -10,44 +10,50 @@ import SwiftUI
struct HeaderView: View {
@State var showSheetView = false
@EnvironmentObject var rootViewModel: RootViewModel
var body: some View {
VStack {
HStack {
Button {
rootViewModel.goToWifiView()
} label: {
Image(systemName: "wifi.circle")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.offset(y: 15)
.foregroundColor(.white)
}
.padding()
HStack (alignment: .center, spacing: 0) {
Image(systemName: "gearshape")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.padding(.leading, CGFloat(20))
.foregroundColor(.clear)
Spacer()
Image("pyleap_logo_white")
.resizable()
.scaledToFit()
.frame(width: 125, height: 125)
.offset(y: 12)
Spacer()
Button {
rootViewModel.goToBLESettings()
} label: {
Image(systemName: "plus")
Spacer()
Image("pyleap_logo_white")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.padding(.trailing, CGFloat(20))
.foregroundColor(.white)
.scaledToFit()
.frame(width: 125, height: 125)
.offset(y: 12)
// .padding(.leading, 60)
Spacer()
Button {
self.showSheetView.toggle()
} label: {
Image(systemName: "info.circle")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.offset(y: 15)
.foregroundColor(.white)
}.sheet(isPresented: $showSheetView) {
CreditView(isPresented: $showSheetView)
}
.padding()
}
Spacer()
}
.frame(maxWidth: .infinity)
.frame(maxHeight: 120)
.background(Color("pyleap_gray"))
@ -101,7 +107,6 @@ struct MainHeaderView: View {
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
HeaderView()
}

View file

@ -11,7 +11,7 @@ struct SubHeaderView: View {
var body: some View {
HStack {
Text("Browse available WiFi PyLeap Projects")
Text("Browse available Wi-Fi PyLeap Projects")
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(/*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
.font(Font.custom("ReadexPro-Regular", size: 25))
@ -22,14 +22,10 @@ struct SubHeaderView: View {
}
struct MainSubHeaderView: View {
let device: String
var body: some View {
HStack {
Text("Pick a project to run on your \(device)")
Text("Browse available PyLeap Projects")
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(/*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
.font(Font.custom("ReadexPro-Regular", size: 25))

View file

@ -8,7 +8,6 @@
import SwiftUI
struct WifiHeaderView: View {
@Environment(\.presentationMode) var presentationMode
@State var showSheetView = false
@EnvironmentObject var rootViewModel: RootViewModel
@ -16,38 +15,41 @@ struct WifiHeaderView: View {
VStack {
HStack (alignment: .center, spacing: 0) {
Image(systemName: "gearshape")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.padding(.leading, CGFloat(20))
.foregroundColor(.clear)
HStack {
Button {
rootViewModel.goToMain()
} label: {
Image(systemName: "arrow.backward")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.offset(y: 15)
.foregroundColor(.white)
}
.padding()
Spacer()
Image("pyleap_logo_white")
.resizable()
.scaledToFit()
.frame(width: 125, height: 125)
.offset(y: 12)
// .padding(.leading, 60)
Spacer()
Button {
rootViewModel.goToSettings(content: .wifi)
rootViewModel.goToSettings()
} label: {
Image(systemName: "plus")
Image(systemName: "gearshape")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.frame(width: 30, height: 30, alignment: .center)
.offset(y: 15)
.padding(.trailing, CGFloat(20))
.foregroundColor(.white)
}
.padding()
}
Spacer()
}

View file

@ -3,7 +3,7 @@
// PyLeap
//
// Created by Trevor Beaton on 10/16/21.
//
//
import SwiftUI
import FileTransferClient
@ -14,54 +14,78 @@ enum AdafruitDevices {
case esp32s2
}
struct MainSelectionView: View {
struct MainSelectionView: View {
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject private var connectionManager: FileTransferConnectionManager
@State private var showWebViewPopover: Bool = false
@State private var inConnectedInSelectionView = true
@State private var boardBootInfo = ""
@EnvironmentObject var expandedState : ExpandedBLECellState
@ObservedObject var networkModel = NetworkService()
@ObservedObject var viewModel = MainSelectionViewModel()
@State private var isConnected = false
@State private var test = ""
@State private var nilBinder = DownloadState.idle
@EnvironmentObject var rootViewModel: RootViewModel
@AppStorage("shouldShowOnboarding") var shouldShowOnboarding: Bool = true
var body: some View {
VStack(alignment: .center, spacing: 0) {
MainHeaderView()
HStack(alignment: .center, spacing: 8, content: {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Connect to Wi-Fi Mode")
.font(Font.custom("ReadexPro-Regular", size: 16))
.underline()
}
})
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
.frame(maxHeight: 40)
.background(Color("pyleap_blue"))
.foregroundColor(.white)
HStack(alignment: .center, spacing: 8, content: {
Text("Not Connected to a Device.")
.font(Font.custom("ReadexPro-Regular", size: 16))
Button {
rootViewModel.goToSelection()
rootViewModel.goTobluetoothPairing()
} label: {
Text("Connect Now")
.font(Font.custom("ReadexPro-Regular", size: 16))
.underline()
}
})
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
.frame(maxHeight: 40)
.background(Color("pyleap_burg"))
.foregroundColor(.white)
ScrollView {
MainSubHeaderView(device: "Adafruit device")
if viewModel.pdemos.isEmpty {
MainSubHeaderView()
if networkModel.pdemos.isEmpty {
HStack{
Spacer()
ProgressView()
@ -69,48 +93,24 @@ enum AdafruitDevices {
Spacer()
}
.padding(0)
}
ScrollViewReader { scroll in
ForEach(viewModel.pdemos) { demo in
if demo.bundleLink == expandedState.currentCell {
DemoViewCell(result: demo, isExpanded: true, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
})
.onAppear(){
print("Cell Appeared")
withAnimation {
scroll.scrollTo(demo.id)
}
ForEach(networkModel.pdemos) { demo in
DemoViewCell(result: demo, isConnected: $isConnected, bootOne: $test, onViewGeometryChanged: {
withAnimation {
scroll.scrollTo(demo.id)
}
} else {
DemoViewCell(result: demo, isExpanded: false, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
})
}
}, stateBinder: $nilBinder)
}
}
}
}
.onDisappear() {
}
/// **Pull down to Refresh feature**
// ScrollRefreshableView(title: "Refresh", tintColor: .purple) {
// HStack{
@ -136,19 +136,17 @@ enum AdafruitDevices {
// }
//
// }
.onChange(of: viewModel.pdemos, perform: { newValue in
print("Update")
})
.onAppear() {
print("Opened MainSelectionView")
networkModel.fetch()
}
.fullScreenCover(isPresented: $shouldShowOnboarding, content: {
ExampleView(shouldShowOnboarding: $shouldShowOnboarding)
})
@ -156,91 +154,14 @@ enum AdafruitDevices {
.background(Color.white)
.navigationBarColor(UIColor(named: "pyleap_gray"))
.navigationBarTitleDisplayMode(.inline)
}
}
//struct MainSelectionView_Previews: PreviewProvider {
// static var previews: some View {
// MainSelectionView()
// }
//}
//
//struct MainSelectionView: View {
//
// @State private var showWebViewPopover: Bool = false
// @ObservedObject var networkModel = NetworkService()
// @ObservedObject var viewModel = MainSelectionViewModel()
// @State private var test = ""
// @State private var nilBinder = DownloadState.idle
// @EnvironmentObject var rootViewModel: RootViewModel
// @AppStorage("shouldShowOnboarding") var shouldShowOnboarding: Bool = true
//
// var body: some View {
// VStack(alignment: .center, spacing: 0) {
// MainHeaderView()
// connectionMessageView()
// ScrollView {
// MainSubHeaderView(device: "Adafruit device")
// if networkModel.pdemos.isEmpty {
// loadingIndicatorView()
// }
// ScrollViewReader { scroll in
// ForEach(networkModel.pdemos) { demo in
// demoViewCell(demo: demo, scroll: scroll)
// }
// }
// }
// }
// }
//
// private func connectionMessageView() -> some View {
// HStack(alignment: .center, spacing: 8) {
// Text("Not Connected to a Device.")
// .font(Font.custom("ReadexPro-Regular", size: 16))
// Button {
// rootViewModel.goToSelection()
// } label: {
// Text("Connect Now")
// .font(Font.custom("ReadexPro-Regular", size: 16))
// .underline()
// }
// }
// .padding(.all, 0.0)
// .frame(maxWidth: .infinity)
// .frame(maxHeight: 40)
// .background(Color("pyleap_burg"))
// .foregroundColor(.white)
// }
//
// private func loadingIndicatorView() -> some View {
// HStack{
// Spacer()
// ProgressView()
// .scaleEffect(2)
// Spacer()
// }
// .padding(0)
// }
//
// private func demoViewCell(demo: Demo, scroll: ScrollViewProxy) -> some View {
// if demo.bundleLink == expandedState.currentCell {
// DemoViewCell(result: demo, isExpanded: true, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
// })
// .onAppear(){
// print("Cell Appeared")
// withAnimation {
// scroll.scrollTo(demo.id)
// }
// }
// } else {
// DemoViewCell(result: demo, isExpanded: false, isConnected: $inConnectedInSelectionView, deviceInfo: $boardBootInfo, onViewGeometryChanged: {
// })
// }
// }
//}
struct MainSelectionView_Previews: PreviewProvider {
static var previews: some View {
MainSelectionView()
}
}

View file

@ -7,82 +7,44 @@
import Foundation
import SwiftUI
import Network
import Combine
class InternetConnectionManager: ObservableObject {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "InternetConnectionMonitor")
@Published var isConnected = false
init() {
startMonitoring(completion: {
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async {
let newIsConnected = path.status == .satisfied
if self.isConnected != newIsConnected {
self.isConnected = newIsConnected
print("net: \(path.status) \(self.isConnected)")
}
}
}
})
}
func startMonitoring(completion:()->Void) {
print("Start Monitoring Network")
monitor.start(queue: queue)
completion()
}
deinit {
print("Network Deinit")
monitor.cancel()
}
}
class MainSelectionViewModel: ObservableObject {
@ObservedObject var networkModel = NetworkService()
@ObservedObject var networkMonitor = InternetConnectionManager()
let fileManager = FileManager.default
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let dataStore = DataStore()
let userDefaults = UserDefaults.standard
@Published var pdemos : [ResultItem] = []
var networkMonitorCancellable: AnyCancellable?
init() {
let fileURL = documentsDirectory.appendingPathComponent("StandardPyLeapProjects.json")
networkMonitorCancellable = networkMonitor.$isConnected.sink { isConnected in
if isConnected {
print("The device is currently connected to the internet.")
// Perform some action when the device is connected to the internet.
self.networkModel.fetch {
self.pdemos = self.dataStore.loadDefaultList()
}
} else {
print("The device is not currently connected to the internet.")
// Perform some action when the device is not connected to the internet.
print("Loading cached remote data.")
self.pdemos = self.dataStore.loadDefaultList()
}
}
// load()
// self.pdemos = load()
}
func makeNetworkCall(){
networkModel.fetch()
}
func load() -> [ResultItem] {
if let savedProjects = userDefaults.object(forKey: "SavedProjects") as? Data {
let decoder = JSONDecoder()
if let loadedProjects = try? decoder.decode([ResultItem].self, from: savedProjects) {
print("Load saved projects")
print(loadedProjects)
// pdemos = loadedProjects
return loadedProjects
}
}
print("Returned Empty pdemos")
return []
}
}

View file

@ -1,121 +0,0 @@
//
// SelectionView.swift
// PyLeap
//
// Created by Trevor Beaton on 10/24/22.
//
import SwiftUI
import FileTransferClient
struct SelectionView: View {
@EnvironmentObject var rootViewModel: RootViewModel
@ObservedObject var connectionManager = FileTransferConnectionManager.shared
var body: some View {
VStack {
HStack {
Button {
// rootViewModel.goToSelection()
} label: {
Image(systemName: "arrow.backward")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.foregroundColor(.clear)
}
.padding()
Spacer()
}
.padding(.top, 15)
Image("pyleapLogo")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.top, 100)
.padding(.horizontal, 60)
Text("What type of device do you want to connect to?")
.font(Font.custom("ReadexPro-Regular", size: 36))
.minimumScaleFactor(0.01)
.multilineTextAlignment(.center)
.padding(.top, 100)
.padding(.horizontal, 30)
Spacer()
VStack {
Button {
rootViewModel.goToWiFiSelection()
} label: {
Text("Wifi")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("pyleap_pink"))
.clipShape(Capsule())
.padding(5)
}
Button {
rootViewModel.goTobluetoothPairing()
} label: {
Text("Bluetooth")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("bluetooth_button_color"))
.clipShape(Capsule())
.padding(5)
}
Button {
rootViewModel.backToMain()
} label: {
Text("I Don't Know")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.padding(.horizontal, 60)
.minimumScaleFactor(0.1)
.frame(width: 270, height: 50, alignment: .center)
.background(Color.gray)
.clipShape(Capsule())
.padding(5)
}
Button {
rootViewModel.goTobluetoothPairing()
} label: {
Text("Reconnect to a device")
.font(Font.custom("ReadexPro-Regular", size: 25))
.frame(width: 270, height: 50, alignment: .center)
.foregroundColor(.blue)
}
}
}
.padding(.bottom, 60)
}
}
struct SelectionView_Previews: PreviewProvider {
static var previews: some View {
SelectionView()
}
}

View file

@ -1,182 +0,0 @@
//
// WifiSelection.swift
// PyLeap
//
// Created by Trevor Beaton on 11/7/22.
//
import SwiftUI
struct WifiSelection: View {
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var viewModel = WifiViewModel()
let userDefaults = UserDefaults.standard
private let kPrefix = Bundle.main.bundleIdentifier!
func toggleViewModelIP() {
viewModel.isInvalidIP.toggle()
}
func storeResolvedAddress(service: ResolvedService) {
print("Storing resolved address")
userDefaults.set(service.ipAddress, forKey: kPrefix+".storeResolvedAddress.ipAddress" )
userDefaults.set(service.hostName, forKey: kPrefix+".storeResolvedAddress.hostName" )
userDefaults.set(service.device, forKey: kPrefix+".storeResolvedAddress.device" )
print("Stored UserDefaults")
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device"))
}
func showConfirmationPrompt(service: ResolvedService, hostName: String) {
comfirmationAlertMessage(title: "Would you like to connect to \(hostName)?", exitTitle: "Cancel", primaryTitle: "Connect") {
storeResolvedAddress(service: service)
viewModel.printStoredInfo()
rootViewModel.goToWifiView()
} cancel: {
}
}
func showValidationPrompt() {
alertTF(title: "Enter Device IP Address",
message: "PyLeap will use this IP address to search for Adafruit devices on your local network",
hintText: "IP Address...",
primaryTitle: "Done",
secondaryTitle: "Cancel") { text in
viewModel.checkServices(ip: text)
} secondaryAction: {
print("Cancel")
}
}
func showAlertMessage() {
alertMessage(title: "IP address Not Found", exitTitle: "Ok") {
showValidationPrompt()
}
}
var body: some View {
VStack {
HStack {
Button {
rootViewModel.goToSelection()
} label: {
Image(systemName: "arrow.backward")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.foregroundColor(.black)
}
.padding(.leading, 30)
Spacer()
}
.padding(.top, 15)
Image("pyleapLogo")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.top, 100)
.padding(.horizontal, 60)
Text("How do you want to connect to your WiFi enabled device?")
.font(Font.custom("ReadexPro-Regular", size: 36))
.minimumScaleFactor(0.01)
.multilineTextAlignment(.center)
.padding(.top, 100)
.padding(.horizontal, 30)
Spacer()
VStack {
Button {
rootViewModel.goToWiFiServiceSelection()
} label: {
Text("Scan for a Device")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("pyleap_pink"))
.clipShape(Capsule())
.padding(5)
}
Button {
showValidationPrompt()
} label: {
Text("Manually Connect")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("pyleap_purple"))
.clipShape(Capsule())
.padding(5)
}
Link("Learn More", destination: URL(string: "https://learn.adafruit.com/pyleap-app")!)
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.minimumScaleFactor(0.1)
.frame(width: 270, height: 50, alignment: .center)
.background(Color.gray)
.clipShape(Capsule())
.padding(5)
Button {
rootViewModel.goTobluetoothPairing()
} label: {
Text("Reconnect to a device")
.font(Font.custom("ReadexPro-Regular", size: 25))
.frame(width: 270, height: 50, alignment: .center)
.foregroundColor(.blue)
}
}
}
.padding(.bottom, 60)
.onChange(of: viewModel.ipInputValidation, perform: { newValue in
if newValue {
rootViewModel.goToWifiView()
viewModel.ipInputValidation.toggle()
}
})
.onChange(of: viewModel.isInvalidIP, perform: { newValue in
print("viewModel.isInvalidIP .onChange")
if newValue {
showAlertMessage()
toggleViewModelIP()
}
})
}
}
struct WifiSelection_Previews: PreviewProvider {
static var previews: some View {
WifiSelection()
}
}

View file

@ -1,175 +0,0 @@
//
// BLESettingsView.swift
// PyLeap
//
// Created by Trevor Beaton on 12/15/22.
//
import SwiftUI
struct BLESettingsView: View {
@State private var thirdPartyLink: String = ""
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var viewModel = SettingsViewModel()
@ObservedObject var networkModel = NetworkService()
func showInvalidURLEntryAlert() {
alertMessage(title: "Invalid URL entry", exitTitle: "Ok") {
}
}
func showDownloadConfirmationAlert() {
alertMessage(title: "Added to Project List", exitTitle: "Ok") {
}
}
func showDisconnectionPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
viewModel.clearKnownIPAddress()
rootViewModel.goToWifiView()
} cancel: {
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Add Custom Project")
.font(Font.custom("ReadexPro-SemiBold", size: 24))
.padding([.horizontal, .top])
Text("""
Please enter the URL link to your own project that you would like to add to the current list of projects in PyLeap.
""")
.font(Font.custom("ReadexPro-Regular", size: 18))
.font(.callout)
.padding()
TextField("https://", text: $thirdPartyLink)
.background(Color.white)
.cornerRadius(5)
.keyboardType(.URL)
.textContentType(.URL)
.onSubmit {
networkModel.fetchThirdPartyProject(urlString: thirdPartyLink)
thirdPartyLink = ""
}
.padding(.horizontal)
Form {
Section{
Label("[Go to GitHub](https://github.com/adafruit/pyleap.github.io)", systemImage: "link")
}
header: {
Text("""
Find more information on adding your own project here:
""")
}
Section{
Label("[Go to Adafruit.com](https://www.adafruit.com)", systemImage: "link")
}
.font(.system(size: 16, weight: .semibold))
}
.onChange(of: viewModel.invalidURL, perform: { newValue in
showInvalidURLEntryAlert()
viewModel.invalidURL = false
})
.onChange(of: viewModel.confirmDownload, perform: { newValue in
showDownloadConfirmationAlert()
viewModel.confirmDownload = false
})
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Back")
.foregroundColor(.blue)
}
.padding(12)
}
}
}
.background(Color(UIColor.systemGroupedBackground))
.safeAreaInset(edge: .top) {
VStack {
HStack {
Button {
rootViewModel.goToFileTransfer()
} label: {
Text("Back")
}
.padding(.leading, 20)
Spacer()
// switch appState {
//
// case .ble:
// Button {
// rootViewModel.goToFileTransfer()
// } label: {
// Text("Back")
// }
// .padding(.leading, 20)
// Spacer()
//
// case .wifi:
// Button {
// rootViewModel.goToWifiView()
// } label: {
// Text("Back")
// }
// .padding(.leading, 20)
// Spacer()
//
// case .none:
// Button {
// rootViewModel.goToMainSelection()
// } label: {
// Text("Back")
// }
// .padding(.leading, 20)
// Spacer()
// }
}
.frame(height: UIScreen.main.bounds.height / 19)
.background(Color(.systemGroupedBackground))
HStack {
Text("Settings")
.font(Font.custom("ReadexPro-Bold", size: 32))
.padding(.leading,20)
Spacer()
}
}
.padding(.top, 25)
}
}
}
struct BLESettingsView_Previews: PreviewProvider {
static var previews: some View {
BLESettingsView()
}
}

View file

@ -1,79 +0,0 @@
//
// BLESettingsViewModel.swift
// PyLeap
//
// Created by Trevor Beaton on 12/15/22.
//
import Foundation
import SwiftUI
class BLESettingsViewModel: ObservableObject {
private let kPrefix = Bundle.main.bundleIdentifier!
let userDefaults = UserDefaults.standard
@Published var hostName = ""
@Published var device = ""
@Published var ipAddress = ""
var connectedToDevice = false
@Published var invalidURL = false
@Published var confirmDownload = false
init() {
check()
registerNotifications(enabled: true)
}
private weak var errorObserver: NSObjectProtocol?
private weak var confirmDownloadObserver: NSObjectProtocol?
private weak var invalidIPObserver: NSObjectProtocol?
private func registerNotifications(enabled: Bool) {
let notificationCenter = NotificationCenter.default
if enabled {
errorObserver = notificationCenter.addObserver(forName: .invalidCustomNetworkRequest, object: nil, queue: .main, using: {[weak self] _ in self?.showError()})
confirmDownloadObserver = notificationCenter.addObserver(forName: .didCollectCustomProject, object: nil, queue: .main, using: {[weak self] _ in self?.showConfirmationAlert()})
} else {
if let testObserver = errorObserver {notificationCenter.removeObserver(testObserver)}
}
}
func showError() {
invalidURL = true
}
func showConfirmationAlert() {
confirmDownload = true
}
func check() {
print(#function)
if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") == nil {
connectedToDevice = false
} else {
connectedToDevice = true
ipAddress = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") as! String
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
device = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device") as! String
}
}
func clearKnownIPAddress() {
userDefaults.set(nil, forKey: kPrefix+".storedIP")
userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.ipAddress" )
userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.hostName" )
userDefaults.set(nil, forKey: kPrefix+".storeResolvedAddress.device" )
}
}

View file

@ -1,243 +0,0 @@
//
// SettingsView.swift
// PyLeap
//
// Created by Trevor Beaton on 9/7/22.
//
import SwiftUI
enum SettingState {
case ble
case wifi
case none
}
struct SettingsView: View {
// @State public var appState: SettingState = .none
@State private var thirdPartyLink: String = ""
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var viewModel = SettingsViewModel()
@ObservedObject var networkModel = NetworkService()
func showInvalidURLEntryAlert() {
alertMessage(title: "Invalid URL entry", exitTitle: "Ok") {
}
}
func showDownloadConfirmationAlert() {
alertMessage(title: "Added to Project List", exitTitle: "Ok") {
}
}
func showDisconnectionPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
viewModel.clearKnownIPAddress()
rootViewModel.goToWifiView()
} cancel: {
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Add Custom Project")
.font(Font.custom("ReadexPro-SemiBold", size: 24))
.padding([.horizontal, .top])
Text("""
Please enter the URL link to your own project that you would like to add to the current list of projects in PyLeap.
""")
.font(Font.custom("ReadexPro-Regular", size: 18))
.font(.callout)
.padding()
TextField("https://", text: $thirdPartyLink)
.background(Color.white)
.cornerRadius(5)
.keyboardType(.URL)
.textContentType(.URL)
.onSubmit {
networkModel.fetchThirdPartyProject(urlString: thirdPartyLink)
thirdPartyLink = ""
}
.padding(.horizontal)
Form {
Section{
Label("[Go to GitHub](https://github.com/adafruit/pyleap.github.io)", systemImage: "link")
}
header: {
Text("""
Find more information on adding your own project here:
""")
}
Section{
Label("[Go to Adafruit.com](https://www.adafruit.com)", systemImage: "link")
}
.font(.system(size: 16, weight: .semibold))
}
.onChange(of: viewModel.invalidURL, perform: { newValue in
showInvalidURLEntryAlert()
viewModel.invalidURL = false
})
.onChange(of: viewModel.confirmDownload, perform: { newValue in
showDownloadConfirmationAlert()
viewModel.confirmDownload = false
})
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Back")
.foregroundColor(.blue)
}
.padding(12)
}
}
}
.background(Color(UIColor.systemGroupedBackground))
.safeAreaInset(edge: .top) {
VStack {
HStack {
Button {
rootViewModel.goToWifiView()
} label: {
Text("Back")
}
.padding(.leading, 20)
Spacer()
// switch appState {
//
// case .ble:
// Button {
// rootViewModel.goToFileTransfer()
// } label: {
// Text("Back")
// }
// .padding(.leading, 20)
// Spacer()
//
// case .wifi:
// Button {
// rootViewModel.goToWifiView()
// } label: {
// Text("Back")
// }
// .padding(.leading, 20)
// Spacer()
//
// case .none:
// Button {
// rootViewModel.goToMainSelection()
// } label: {
// Text("Back")
// }
// .padding(.leading, 20)
// Spacer()
// }
}
.frame(height: UIScreen.main.bounds.height / 19)
.background(Color(.systemGroupedBackground))
HStack {
Text("Settings")
.font(Font.custom("ReadexPro-Bold", size: 32))
.padding(.leading,20)
Spacer()
}
}
.padding(.top, 25)
}
}
}
extension View {
func comfirmationAlertMessage(title: String, exitTitle: String, primaryTitle: String,disconnect: @escaping() -> (),cancel: @escaping() -> ()){
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(.init(title: primaryTitle, style: .destructive, handler: { _ in
disconnect()
}))
alert.addAction(.init(title: exitTitle, style: .cancel, handler: { _ in
cancel()
}))
rootController().present(alert, animated: true, completion: nil)
}
func alertMessage(title: String, exitTitle: String, cancel: @escaping()->()){
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
alert.addAction(.init(title: exitTitle, style: .cancel, handler: { _ in
cancel()
}))
rootController().present(alert, animated: true, completion: nil)
}
func alertTF(title: String, message: String, hintText: String, primaryTitle: String, secondaryTitle: String, primaryAction: @escaping(String)->(), secondaryAction: @escaping()->()) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addTextField { field in
field.keyboardType = .decimalPad
field.placeholder = hintText
}
alert.addAction(.init(title: secondaryTitle, style: .cancel, handler: { _ in
secondaryAction()
}))
alert.addAction(.init(title: primaryTitle, style: .default, handler: { _ in
if let text = alert.textFields?[0].text {
primaryAction(text)
} else {
primaryAction("")
}
}))
rootController().present(alert, animated: true, completion: nil)
}
func rootController()->UIViewController{
guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return .init()
}
guard let root = screen.windows.first?.rootViewController else {
return .init()
}
return root
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView()
}
}

View file

@ -8,37 +8,22 @@
import SwiftUI
import Foundation
class ExpandedState: ObservableObject {
@Published var currentCell = ""
}
struct WifiCell: View {
@EnvironmentObject var expandedState : ExpandedState
let result : ResultItem
@State var isExpanded: Bool = false {
@State private var isExpanded: Bool = false {
didSet {
onViewGeometryChanged()
onViewGeometryChanged()
}
}
@State var isExpandedTest: String = ""
@ObservedObject var viewModel = WifiCellViewModel()
@Binding var isConnected: Bool
@Binding var bootOne: String
@Binding var stateBinder: DownloadState
var showRunItButton = false
var projectName = String()
let onViewGeometryChanged: ()->Void
var body: some View {
@ -46,53 +31,48 @@ struct WifiCell: View {
.frame(maxWidth: .infinity)
}
var header: some View {
HStack {
Text(result.projectName)
.font(Font.custom("ReadexPro-Regular", size: 24))
.padding(8)
.foregroundColor(.white)
Spacer()
Image(systemName: "chevron.down")
.resizable()
.scaledToFit()
.frame(width: 30, height: 15, alignment: .center)
.foregroundColor(.white)
.padding(.trailing, 30)
}
.padding(.vertical, 5)
.padding(.leading)
.frame(maxWidth: .infinity)
.background(Color("pyleap_purple"))
.onTapGesture {
expandedState.currentCell = result.bundleLink
}
}
var content: some View {
private var content: some View {
VStack(alignment: .leading, spacing: 8) {
header
if isExpanded {
if isExpanded {
Group {
WifiSubViewCell(result: result, bindingString: $bootOne, downloadStateBinder: $stateBinder,isConnected: $isConnected)
WifiSubViewCell(bindingString: $bootOne, downloadStateBinder: $stateBinder, title: result.projectName,
image: result.projectImage,
description: result.description,
learnGuideLink: URLRequest(url: URL(string: result.learnGuideLink)!),
downloadLink: result.bundleLink,
compatibility: result.compatibility,
isConnected: $isConnected)
}
}
}
}
private var header: some View {
HStack {
Text(result.projectName)
.font(Font.custom("ReadexPro-Regular", size: 24))
.padding(8)
.foregroundColor(.white)
Spacer()
Image(systemName: "chevron.down")
.resizable()
.frame(width: 30, height: 15, alignment: .center)
.foregroundColor(.white)
.padding(.trailing, 30)
}
.padding(.vertical, 5)
.padding(.leading)
.frame(maxWidth: .infinity)
.background(Color("pyleap_purple"))
.onTapGesture { isExpanded.toggle() }
}
}

View file

@ -1,27 +0,0 @@
//
// WifiCellViewModel.swift
// PyLeap
//
// Created by Trevor Beaton on 12/2/22.
//
import Foundation
import SwiftUI
class WifiCellViewModel: ObservableObject {
@Published var isExpanded : Bool = false
init() {
print("Initialized")
print("\(#function) @Line: \(#line)")
}
deinit {
print("Deinitialized")
print("\(#function) @Line: \(#line)")
}
@Published var projectBundle = ""
}

File diff suppressed because it is too large Load diff

View file

@ -1,163 +0,0 @@
//
// WifiPairingView.swift
// PyLeap
//
// Created by Trevor Beaton on 11/8/22.
//
import SwiftUI
import WebKit
struct SwiftUIWebView: UIViewRepresentable {
typealias UIViewType = WKWebView
let webAddress: String
let webView: WKWebView
init(webAddress: String) {
self.webAddress = webAddress
webView = WKWebView(frame: .zero)
webView.load(URLRequest(url: URL(string: webAddress)!))
}
func makeUIView(context: Context) -> WKWebView {
webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct WifiPairingView: View {
@EnvironmentObject var rootViewModel: RootViewModel
@State private var showModal = false
@State private var showProgress = false
@State private var userWaitThreshold = false
@State private var nextText = 0
@State var showSheetView = false
@State var showConnectionErrorView = false
let tutorialAddress = "https://learn.adafruit.com/pyleap-app/wifi-pairing"
var body: some View {
VStack {
HStack {
Button {
self.rootViewModel.goToWiFiSelection()
} label: {
Image(systemName: "arrow.backward")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(Color("pyleap_gray"))
}
Spacer()
Image(systemName: "wifi")
.resizable()
.foregroundColor(.clear)
.aspectRatio(contentMode: .fit)
.frame(width: 25, height: 25, alignment: .center)
.offset(y: -5)
}
.padding(.top, 50)
.padding(.horizontal, 30)
//
//
//
// Image("pyleapLogo")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .padding(.top, 50)
// .padding(.horizontal, 60)
//
//
// if nextText == 0 {
//
//
//
// Text("""
// PyLeaps WiFi mode requires EPS32 devices to have WiFi credentials in an ./env file.
//
// If youre having trouble connecting, check this documentation:
//
// https://docs.circuitpython.org/en/latest/docs/workflows.html#web
//
// """)
// .font(Font.custom("ReadexPro-Regular", size: 24))
// .minimumScaleFactor(0.01)
// .multilineTextAlignment(.leading)
// .padding(.top, 100)
// .padding(.horizontal, 30)
// .padding(.bottom, 69)
//
//
// Spacer()
//
// Button(action: {
// rootViewModel.goToWiFiSelection()
// }) {
//
// Text("Back")
// .font(Font.custom("ReadexPro-Regular", size: 25))
// .foregroundColor(Color.white)
//
// .padding()
// .padding(.horizontal, 60)
//
// .frame(height: 50)
// .background(Color("pyleap_purple"))
// .clipShape(Capsule())
//
// }
// Spacer()
// .frame(height: 60)
//
// }
SwiftUIWebView(webAddress: tutorialAddress)
.padding(.vertical)
Button(action: {
rootViewModel.goToWiFiSelection()
}) {
Text("Back")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.padding()
.padding(.horizontal, 60)
.frame(height: 50)
.background(Color("pyleap_purple"))
.clipShape(Capsule())
}
.padding()
}
.edgesIgnoringSafeArea(.all)
}
}
struct WifiPairingView_Previews: PreviewProvider {
static var previews: some View {
WifiPairingView()
}
}

View file

@ -8,8 +8,7 @@
import Foundation
import SwiftUI
struct ResolvedService: Identifiable, Equatable {
var id = UUID()
struct ResolvedService {
var ipAddress: String
var hostName: String
var device: String
@ -38,58 +37,36 @@ class WifiServiceManager: NSObject, ObservableObject {
override init() {
super.init()
print("Wifi Module Used")
serviceManagerBrowser.delegate = self
findService()
}
deinit {
print("Wifi Module Removed")
self.serviceManagerBrowser.stop()
}
func findService() {
print("Start Scan")
// self.serviceManagerBrowser.searchForServices(ofType: CircuitPythonType.serviceType, inDomain: CircuitPythonType.serviceDomain)
resolvedServices.removeAll()
print("Current state of isSearching: \(isSearching)")
if isSearching == false {
print("Start Scanning")
startDiscovery()
}
}
func startDiscovery() {
print("Start Discovery")
DispatchQueue.main.async {
self.isSearching = true
}
print("\(#function) @Line: \(#line)")
print("Current state of isSearching: \(isSearching)")
// connectionStatus = .connected
print("Start Scan")
isSearching = true
self.serviceManagerBrowser.searchForServices(ofType: CircuitPythonType.serviceType, inDomain: CircuitPythonType.serviceDomain)
let timer = Timer.scheduledTimer(withTimeInterval: 12, repeats: false) { timer in
self.stopDiscoveryScan()
}
}
func stopDiscoveryScan() {
if isSearching {
isSearching = false
self.serviceManagerBrowser.stop()
print(resolvedServices)
}
}
}
extension WifiServiceManager: NetServiceBrowserDelegate, NetServiceDelegate {
@ -111,26 +88,11 @@ extension WifiServiceManager: NetServiceBrowserDelegate, NetServiceDelegate {
print("didFindDomain")
}
func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) {
print("netServiceBrowserDidStopSearch")
}
func netServiceWillResolve(_ sender: NetService) {
print("netServiceWillResolve")
}
func netServiceBrowser(_ browser: NetServiceBrowser, didRemoveDomain domainString: String, moreComing: Bool) {
"didRemoveDomain"
}
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
print(#function)
discoveredService = service
discoveredService?.delegate = self
discoveredService?.resolve(withTimeout: 7)
discoveredService?.resolve(withTimeout: 10)
if services.contains(service) {
print("All ready in service array")
@ -139,22 +101,13 @@ extension WifiServiceManager: NetServiceBrowserDelegate, NetServiceDelegate {
services.append(service)
}
if !moreComing {
serviceManagerBrowser.stop()
isSearching = false
if moreComing == false {
browser.stop()
}
print("Service: \(service)")
print("Service count: \(services.count)")
self.serviceManagerBrowser.remove(from: .main, forMode: .common)
}
func netServiceDidStop(_ sender: NetService) {
isSearching = false
print("isSearching: \(isSearching)")
@ -183,16 +136,7 @@ extension WifiServiceManager: NetServiceBrowserDelegate, NetServiceDelegate {
let resolvedService = ResolvedService(ipAddress: ipAddress, hostName: updatedHostName ?? "Unknown", device: sender.name)
if resolvedServices.contains(where: {$0.ipAddress == resolvedService.ipAddress}) {
// it exists, do nothing
print("\(resolvedService.ipAddress) for \(resolvedService.hostName) exists in network")
} else {
print("\(resolvedService.ipAddress) for \(resolvedService.hostName) Added to Network List")
resolvedServices.append(resolvedService)
}
resolvedServices.append(resolvedService)
print("resolvedServices count: \(resolvedServices.count)")
}
@ -204,7 +148,17 @@ extension WifiServiceManager: NetServiceBrowserDelegate, NetServiceDelegate {
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
print("didRemove")
//self.discovered.removeAll { $0.name == service.name }
}
// func netServiceDidResolveAddress(_ sender: NetService) {
// print("netServiceDidResolveAddress")
// if let data = sender.txtRecordData() {
// let dict = NetService.dictionary(fromTXTRecord: data)
// /// do stuff with txtRecord dict here and then add to discovered array.
// // discoveredService = nil
// }
// }
}

View file

@ -1,293 +0,0 @@
//
// WifiServiceSelectionView.swift
// PyLeap
//
// Created by Trevor Beaton on 10/24/22.
//
import SwiftUI
struct WifiServiceSelectionView: View {
@ObservedObject var wifiServiceViewModel = WifiServiceManager()
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var viewModel = WifiViewModel()
let userDefaults = UserDefaults.standard
private let kPrefix = Bundle.main.bundleIdentifier!
@State private var scrollViewID = UUID()
// For Blinka spinning animation
@State private var isAnimating = false
var foreverAnimation: Animation {
Animation.linear(duration: 2.0)
.repeatForever(autoreverses: false)
}
func storeResolvedAddress(service: ResolvedService) {
print("Storing resolved address")
userDefaults.set(service.ipAddress, forKey: kPrefix+".storeResolvedAddress.ipAddress" )
userDefaults.set(service.hostName, forKey: kPrefix+".storeResolvedAddress.hostName" )
userDefaults.set(service.device, forKey: kPrefix+".storeResolvedAddress.device" )
print("Stored UserDefaults")
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device"))
}
func showConfirmationPrompt(service: ResolvedService, hostName: String) {
comfirmationAlertMessage(title: "Would you like to connect to \(hostName)?", exitTitle: "Cancel", primaryTitle: "Connect") {
storeResolvedAddress(service: service)
viewModel.printStoredInfo()
rootViewModel.goToWifiView()
} cancel: {
}
}
func showValidationPrompt() {
alertTF(title: "Enter Device IP Address",
message: "PyLeap will use this IP address to search for Adafruit devices on your local network",
hintText: "IP Address...",
primaryTitle: "Done",
secondaryTitle: "Cancel") { text in
viewModel.checkServices(ip: text)
} secondaryAction: {
print("Cancel")
}
}
func showAlertMessage() {
alertMessage(title: "IP address Not Found", exitTitle: "Ok") {
showValidationPrompt()
}
}
func toggleViewModelIP() {
viewModel.isInvalidIP.toggle()
}
var body: some View {
VStack {
HStack {
Button {
rootViewModel.goToSelection()
} label: {
Image(systemName: "arrow.backward")
.resizable()
.frame(width: 25, height: 25, alignment: .center)
.offset(y: 15)
.foregroundColor(.black)
}
.padding(.leading, 30)
Spacer()
}
.padding(.top, 15)
Image("pyleapLogo")
.resizable()
.aspectRatio(contentMode: .fit)
.minimumScaleFactor(0.1)
.padding(.top, 50)
.padding(.horizontal, 60)
if wifiServiceViewModel.isSearching && wifiServiceViewModel.resolvedServices.isEmpty {
Text("WiFi Connect")
.font(Font.custom("ReadexPro-Regular", size: 36))
.multilineTextAlignment(.center)
.lineLimit(1)
.padding()
Spacer()
BlinkaAnimationView(height: 150, width: 145)
.padding(.bottom, 20)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0.0))
.onAppear() {
Animation.linear(duration: 1.0)
.repeatForever(autoreverses: false)
isAnimating = true
}
VStack {
Text("""
Scanning for PyLeap
compatible devices...
""")
.font(Font.custom("ReadexPro-Regular", size: 24))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.1)
.lineLimit(2)
Button {
rootViewModel.goToWifiPairingTutorial()
} label: {
Text("Pairing Tutorial")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.minimumScaleFactor(0.1)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("pyleap_pink"))
.clipShape(Capsule())
.padding(5)
}
}
.padding(.horizontal, 30)
.padding(.bottom, 60)
}
else {
Spacer()
VStack {
if !wifiServiceViewModel.resolvedServices.isEmpty {
Text("WiFi Devices Found")
.font(Font.custom("ReadexPro-Regular", size: 24))
.multilineTextAlignment(.center)
.lineLimit(1)
.minimumScaleFactor(0.1)
} else {
Text("No WiFi Devices Found")
.font(Font.custom("ReadexPro-Regular", size: 24))
.multilineTextAlignment(.center)
.lineLimit(1)
.minimumScaleFactor(0.1)
}
Button {
} label: {
HStack {
Image(systemName: "arrow.clockwise")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.white)
Button {
wifiServiceViewModel.findService()
} label: {
Text("Rescan")
}
}
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.frame(width: 146, height: 50, alignment: .center)
.background(Color("pyleap_purple"))
.clipShape(Capsule())
}
if wifiServiceViewModel.isSearching {
ProgressView()
}
}
.padding(.vertical, 40)
ScrollView(.vertical, showsIndicators: true) {
ScrollViewReader { scroll in
ForEach(wifiServiceViewModel.resolvedServices) { service in
WifiServiceCellView(resolvedService: service, onViewGeometryChanged: {
withAnimation {
scroll.scrollTo(service.id)
}
})
.onTapGesture {
// Save Cred to User Defaults
showConfirmationPrompt(service: service, hostName: service.hostName)
}
}
}
.id(self.scrollViewID)
}
.foregroundColor(.black)
}
if wifiServiceViewModel.resolvedServices.isEmpty && !wifiServiceViewModel.isSearching {
VStack {
Text("""
Unable to find any WiFi
compatible Adafruit devices
on your network
""")
.font(Font.custom("ReadexPro-Regular", size: 24))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.1)
.lineLimit(3)
Button {
rootViewModel.goToWifiPairingTutorial()
} label: {
Text("Pairing Tutorial")
.font(Font.custom("ReadexPro-Regular", size: 25))
.foregroundColor(Color.white)
.minimumScaleFactor(0.1)
.frame(width: 270, height: 50, alignment: .center)
.background(Color("pyleap_pink"))
.clipShape(Capsule())
.padding(5)
}
}
.padding(.horizontal, 30)
}
}
.padding(.bottom, 60)
.onChange(of: viewModel.ipInputValidation, perform: { newValue in
if newValue {
rootViewModel.goToWifiView()
viewModel.ipInputValidation.toggle()
}
})
.onChange(of: viewModel.isInvalidIP, perform: { newValue in
print("viewModel.isInvalidIP .onChange")
if newValue {
showAlertMessage()
toggleViewModelIP()
}
})
}
}
struct WifiServiceSelectionView_Previews: PreviewProvider {
static var previews: some View {
WifiServiceSelectionView()
}
}

View file

@ -12,39 +12,14 @@ struct WifiStatusConnectedView: View {
let userDefaults = UserDefaults.standard
private let kPrefix = Bundle.main.bundleIdentifier!
@EnvironmentObject var rootViewModel: RootViewModel
@Binding var hostName: String
func showConfirmationPrompt() {
comfirmationAlertMessage(title: "Are you sure you want to disconnect?", exitTitle: "Cancel", primaryTitle: "Disconnect") {
rootViewModel.goToSelection()
} cancel: {
}
}
var body: some View {
HStack(alignment: .center, spacing: 0, content: {
HStack(alignment: .center, spacing: 8, content: {
Image(systemName: "wifi")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(5)
Text("Connected to \(hostName). ")
.font(Font.custom("ReadexPro-Regular", size: 14))
Button {
showConfirmationPrompt()
} label: {
Text("Disconnect")
.font(Font.custom("ReadexPro-Bold", size: 14))
.underline()
.minimumScaleFactor(0.1)
}
Text("Connected To \(hostName)")
.font(Font.custom("ReadexPro-Regular", size: 16))
})
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
@ -59,7 +34,7 @@ struct WifiStatusNoConnectionView: View {
HStack(alignment: .center, spacing: 8, content: {
Text("No Device Detected")
.font(Font.custom("ReadexPro-SemiBold", size: 14))
.font(Font.custom("ReadexPro-Regular", size: 16))
})
.padding(.all, 0.0)
@ -75,7 +50,7 @@ struct WifiStatusConnectingView: View {
var body: some View {
HStack(alignment: .center, spacing: 8, content: {
Text("Searching for Adafruit Devices...")
.font(Font.custom("ReadexPro-Regular", size: 14))
.font(Font.custom("ReadexPro-Regular", size: 16))
})
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
@ -93,7 +68,7 @@ struct NetworkConnectionBanner: View {
var body: some View {
HStack(alignment: .center, spacing: 8, content: {
Text("Searching local network...")
.font(Font.custom("ReadexPro-Regular", size: 14))
.font(Font.custom("ReadexPro-Regular", size: 16))
// ProgressView()
//.resizable()

View file

@ -8,14 +8,14 @@
import SwiftUI
struct WifiSubViewCell: View {
@State var transferInProgress = false
@State var isDownloaded = false
@EnvironmentObject var globalString : GlobalString
@StateObject var wifiFileTransfer = WifiFileTransfer()
@StateObject var wifiTransferService = WifiTransferService()
let result : ResultItem
@Binding var bindingString: String
@ -23,17 +23,18 @@ struct WifiSubViewCell: View {
@State private var toggleView: Bool = false
let title: String
let image: String
let description: String
let learnGuideLink: URLRequest
let downloadLink: String
let compatibility: [String]
@EnvironmentObject var rootViewModel: RootViewModel
@StateObject var downloadModel = DownloadViewModel()
@ObservedObject var viewModel = WifiSubViewCellModel()
@Binding var isConnected : Bool
@State private var counter = 0
@State private var numOfFiles = 0
@State var downloadState: DownloadState = .idle
@State private var showWebViewPopover: Bool = false
@State var errorOccured = false
@State private var presentAlert = false
@ -41,100 +42,6 @@ struct WifiSubViewCell: View {
@State var offlineWithoutProject = false
func showTransferErrorMessage() {
alertMessage(title: """
Download Error
Unable to download project
Try again later
""", exitTitle: "Retry") {
wifiFileTransfer.transferError = false
}
}
func usbInUseErrorMessage() {
alertMessage(title: """
USB In Use
Files cannot be tranferred or moved while USB is in use.
Remove device from USB. Press "Reset" on the device.
""", exitTitle: "Retry") {
// wifiFileTransfer.transferError = false
}
}
func startTransferProcess() {
if isDownloaded {
print("Project found")
wifiFileTransfer.testFileExistance(for: result.projectName, bundleLink: result.bundleLink)
} else {
print("Project not found")
downloadModel.trueDownload(useProject: result.bundleLink, projectName: result.projectName)
}
}
func testOperation() {
let operationQueue = OperationQueue()
let operation1 = BlockOperation {
wifiTransferService.optionRequest(handler: { results in
switch results {
case .success(let contents):
if contents.contains("GET, OPTIONS, PUT, DELETE, MOVE") {
} else {
print("Connected to USB")
DispatchQueue.main.async {
usbInUseErrorMessage()
wifiFileTransfer.stopTransfer = true
}
}
case .failure:
print("Failure")
}
})
}
let operation2 = BlockOperation {
if isDownloaded {
print("Project found")
wifiFileTransfer.testFileExistance(for: result.projectName, bundleLink: result.bundleLink)
} else {
print("Project not found")
downloadModel.trueDownload(useProject: result.bundleLink, projectName: result.projectName)
}
}
// Add operations to the operation queue
operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)
// Block the current thread until all operations have finished executing
operationQueue.waitUntilAllOperationsAreFinished()
}
var body: some View {
@ -142,14 +49,14 @@ Remove device from USB. Press "Reset" on the device.
VStack(alignment: .leading, spacing: 0, content: {
ImageWithURL(result.projectImage)
ImageWithURL(image)
.scaledToFit()
.frame(maxWidth: .infinity)
.cornerRadius(14)
.padding(.top, 30)
Text(result.description)
Text(description)
.font(Font.custom("ReadexPro-Regular", size: 18))
.multilineTextAlignment(.leading)
.minimumScaleFactor(0.1)
@ -157,20 +64,9 @@ Remove device from USB. Press "Reset" on the device.
Text("Compatible with:")
.font(Font.custom("ReadexPro-Bold", size: 18))
.padding(.top, 5)
HStack {
Image(systemName: "checkmark")
.resizable()
.frame(width: 25, height: 22, alignment: .center)
.foregroundColor(.green)
Text("ESP32-S2")
.font(Font.custom("ReadexPro-Regular", size: 18))
.foregroundColor(.black)
}
.padding(.top, 10)
ForEach(result.compatibility, id: \.self) { string in
ForEach(compatibility, id: \.self) { string in
if string == "circuitplayground_bluefruit" {
HStack {
@ -185,7 +81,7 @@ Remove device from USB. Press "Reset" on the device.
.padding(.top, 10)
}
if string == "clue_nrf52840_express" {
@ -213,7 +109,7 @@ Remove device from USB. Press "Reset" on the device.
.padding(.top, 20)
}
.sheet(isPresented: $showWebViewPopover, content: {
SwiftUIWebView(webAddress: result.learnGuideLink)
WebView(URLRequest(url: learnGuideLink.url!))
})
@ -221,16 +117,18 @@ Remove device from USB. Press "Reset" on the device.
if isConnected {
if result.compatibility.contains(bindingString) {
if wifiFileTransfer.testIndex.downloadState == .idle {
if compatibility.contains(bindingString) {
Button {
// NotificationCenter.default.post(name: .didCompleteZip, object: nil, userInfo: projectResponse)
testOperation()
print("Wifi Project Attempt \(title)")
if wifiFileTransfer.projectDownloaded {
wifiFileTransfer.projectValidation(nameOf: title)
} else {
downloadModel.startDownload(urlString: downloadLink, projectTitle: title) {
print("DONE")
}
}
} label: {
RunItButton()
@ -239,40 +137,8 @@ Remove device from USB. Press "Reset" on the device.
}
if wifiFileTransfer.testIndex.downloadState == .transferring {
DownloadingButton()
.padding(.top, 20)
.disabled(true)
VStack(alignment: .center, spacing: 5) {
ProgressView("", value: CGFloat(counter), total: CGFloat(numOfFiles))
.padding(.horizontal, 90)
.padding(.top, -5)
.padding(.bottom, 10)
.accentColor(Color.gray)
.scaleEffect(x: 1, y: 2, anchor: .center)
.cornerRadius(10)
.frame(height: 10)
ProgressView()
}
}
if wifiFileTransfer.testIndex.downloadState == .complete {
CompleteButton()
.padding(.top, 20)
.disabled(true)
}
if wifiFileTransfer.testIndex.downloadState == .failed {
FailedButton()
.padding(.top, 20)
.disabled(true)
}
}
} else {
@ -285,87 +151,61 @@ Remove device from USB. Press "Reset" on the device.
}
}
Spacer()
.frame(height: 30)
.ignoresSafeArea(.all)
.ignoresSafeArea(.all)
.onAppear(perform: {
wifiFileTransfer.registerWifiNotification(enabled: true)
viewModel.searchPathForProject(nameOf: result.projectName)
if viewModel.projectDownloaded {
isDownloaded = true
} else {
isDownloaded = false
.alert("Project Not Found", isPresented: $offlineWithoutProject) {
Button("OK") {
// Handle acknowledgement.
print("OK")
offlineWithoutProject = false
downloadStateBinder = .idle
// selectionModel.state = .idle
print("\(offlineWithoutProject)")
}
} message: {
Text("""
To use this project, connect to the internet.
""")
.multilineTextAlignment(.leading)
}
print("is downloaded? \(viewModel.projectDownloaded)")
})
.onChange(of: downloadModel.isDownloading, perform: { newValue in
wifiFileTransfer.getProjectForSubClass(nameOf: title)
})
.padding(.top, 8)
.onChange(of: wifiFileTransfer.transferError, perform: { newValue in
if newValue {
showTransferErrorMessage()
}
})
.onChange(of: viewModel.showUsbInUseError) { newValue in
if newValue {
usbInUseErrorMessage()
}
}
.onChange(of: wifiFileTransfer.counter) { newValue in
print("New counter : \(newValue)")
counter = newValue
}
.onChange(of: wifiFileTransfer.numOfFiles) { newValue in
print("New numOfFiles : \(newValue)")
numOfFiles = newValue
}
.onChange(of: wifiFileTransfer.testIndex.count) { newValue in
print("New count index : \(newValue)")
}
.onChange(of: wifiFileTransfer.testIndex.numberOfFiles) { newValue in
print("New numberOfFiles index : \(newValue)")
}
.onChange(of: wifiFileTransfer.testIndex.downloadState) { newValue in
print("New download state : \(newValue)")
}
.onChange(of: downloadModel.didDownloadBundle, perform: { newValue in
print("For project: \(title), project download is \(newValue)")
.onChange(of: wifiFileTransfer.downloadState) { newValue in
switch newValue {
case .idle:
downloadState = .idle
print("idle")
case .transferring:
downloadState = .transferring
print("transferring")
case .complete:
downloadState = .complete
print("complete")
case .downloading:
downloadState = .downloading
print("downloading")
case .failed:
downloadState = .failed
print("failed")
// globalString.projectString = title
if newValue {
DispatchQueue.main.async {
print("Getting project from Subclass \(title)")
// viewModel.getProjectForSubClass(nameOf: title)
wifiFileTransfer.projectValidation(nameOf: title)
isDownloaded = true
}
}else {
print("Is not downloaded")
isDownloaded = false
}
})
.onAppear(perform: {
wifiFileTransfer.getProjectForSubClass(nameOf: title)
if wifiFileTransfer.projectDownloaded {
isDownloaded = true
} else {
isDownloaded = false
}
print("is downloaded? \(isDownloaded)")
})
.padding(.top, 8)
}
}

View file

@ -6,119 +6,40 @@
//
import Foundation
import SwiftUI
class WifiSubViewCellModel: ObservableObject {
@ObservedObject var wifiTransferService = WifiTransferService()
@ObservedObject var wifiFileTransfer = WifiFileTransfer()
@Published var downloadState: DownloadState = .idle
@Published var projectDownloaded = false
@Published var failedProjectLaunch = false
@Published var usbInUse = false
@Published var showUsbInUseError = false
init() {
registerForUSBInUseErrorNotification(enabled: true)
}
private weak var usbInUseErrorNotification: NSObjectProtocol?
private func registerForUSBInUseErrorNotification(enabled: Bool) {
print("\(#function) @Line: \(#line)")
let notificationCenter = NotificationCenter.default
if enabled {
// NotificationCenter.default.addObserver(self, selector: #selector(zipSuccess(_:)), name: .usbInUseErrorNotification,object: nil)
usbInUseErrorNotification = notificationCenter.addObserver(forName: .usbInUseErrorNotification, object: nil, queue: .main, using: {[weak self] _ in self?.zipSuccess()})
} else {
}
}
func zipSuccess() {
showUsbInUseError = true
}
func checkIfUSBInUse() {
wifiTransferService.optionRequest(handler: { result in
switch result {
case .success:
print("Success")
self.wifiTransferService.getRequest(read: "boot_out.txt") { result in
print(result)
}
case .failure:
print("Failure")
}
// if success.contains("GET, OPTIONS, PUT, DELETE, MOVE") {
//
// print("USB not in use.")
// DispatchQueue.main.async {
// self.usbInUse = false
// }
//
// // DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// // if wifiFileTransfer.projectDownloaded {
// //
// // wifiFileTransfer.testFileExistance(for: result.projectName, bundleLink: result.bundleLink)
// //
// // } else {
// // downloadModel.trueDownload(useProject: result.bundleLink, projectName: result.projectName)
// // }
// // }
//
// } else {
// DispatchQueue.main.async {
// self.usbInUse = true
// }
// print("USB in use - files cannot be tranferred or moved while USB is in use. Show Error")
// }
})
}
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
func searchPathForProject(nameOf project: String) {
var manager = FileManager.default
func getProjectForSubClass(nameOf project: String) {
let nestedFolderURL = directoryPath.appendingPathComponent(project)
if manager.fileExists(atPath: nestedFolderURL.relativePath) {
print("\(project) - Exist")
projectDownloaded = true
} else {
print("Does not exist - \(project)")
projectDownloaded = false
if let enumerator = FileManager.default.enumerator(at: directoryPath, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
// for case condition: Only process URLs
for case let fileURL as URL in enumerator {
if fileURL.lastPathComponent == project {
failedProjectLaunch = false
projectDownloaded = true
print(#function)
print("Searching for... \(project)")
print("URL Path: \(fileURL.path)")
print("URL : \(fileURL)")
return
} else {
failedProjectLaunch = true
projectDownloaded = false
print("Project was not found...")
}
}
}
}
}

View file

@ -16,19 +16,19 @@ protocol WifiTransferServiceDelegate: AnyObject {
class WifiTransferService: ObservableObject {
// weak var delegate: WifiTransferServiceDelegate?
weak var delegate: WifiTransferServiceDelegate?
let userDefaults = UserDefaults.standard
private let kPrefix = Bundle.main.bundleIdentifier!
@Published var counter = 0
@Published var webDirectoryInfo = [WebDirectoryModel]()
@Published var hostName = ""
func startup() {
print("\(#function) @Line: \(#line)")
print(#function)
print("Startup")
if (userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName")) != nil {
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String)
@ -43,7 +43,6 @@ class WifiTransferService: ObservableObject {
startup()
}
//*
func sendPutRequest(fileName: String,
body: Data,
then handler: @escaping(Result<Data, Error>) -> Void) {
@ -77,88 +76,30 @@ class WifiTransferService: ObservableObject {
print("Print curl:")
print(request.cURL(pretty: true))
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("File write error")
let task = urlSession.dataTask(
with: request,
completionHandler: { data, response, error in
// Validate response and call handler
handler(.failure(error))
if let error = error {
print("File write error")
handler(.failure(error))
}
if let data = data {
print("File write success!")
handler(.success(data))
}
}
if let data = data {
print("File write success!")
handler(.success(data))
}
}
)
task.resume()
}
func requestWithCheck() {
}
func optionRequest(handler: @escaping(Result<String, Error>) -> Void) {
print("HOST | \(hostName)")
let username = ""
let password = "passw0rd"
let loginString = "\(username):\(password)"
guard let loginData = loginString.data(using: String.Encoding.utf8) else {
return
}
let base64LoginString = loginData.base64EncodedString()
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
request.httpMethod = "OPTIONS"
//request.httpBody = try? JSONSerialization.data(withJSONObject: [:], options: [])
print("Print curl:")
print(request.cURL(pretty: true))
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
print("Response HTTP Status code: \(response.statusCode)")
print("Specific header: \(response.value(forHTTPHeaderField: "Access-Control-Allow-Methods") ?? "Header Not found")")
handler(.success(response.value(forHTTPHeaderField: "Access-Control-Allow-Methods") ?? "Header Not found"))
}
if let error = error {
print("File write error")
handler(.failure(error))
}
guard let data = data else {
print(String(describing: "Error Found: \(String(describing: error))"))
print("Failed! Option Request")
return
}
if let str = String(data: data, encoding: .utf8) {
print("Output: \(str)")
}
}
task.resume()
}
func getRequest() {
@ -175,11 +116,8 @@ class WifiTransferService: ObservableObject {
// var request = URLRequest(url: URL(string: "http://cpy-9cbe10.local/fs/")!,timeoutInterval: Double.infinity)
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
print("Print curl:")
@ -209,13 +147,8 @@ class WifiTransferService: ObservableObject {
task.resume()
}
//completion: @escaping (Bool) -> Void
typealias CompletionHandler = (_ success:String) -> Void
func getRequest(read: String, completionHandler: @escaping CompletionHandler) {
print("Second network call made!")
func getRequest(incoming: String) -> String {
var semaphore = DispatchSemaphore (value: 0)
let username = ""
@ -224,14 +157,13 @@ class WifiTransferService: ObservableObject {
var outgoingString = String()
let loginData = loginString.data(using: String.Encoding.utf8)
guard let loginData = loginString.data(using: String.Encoding.utf8) else {
return "Error"
}
let base64LoginString = loginData.base64EncodedString()
let base64LoginString = loginData!.base64EncodedString()
print("Host Name: \(hostName)")
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/\(read)")!,timeoutInterval: Double.infinity)
// var request = URLRequest(url: URL(string: "http://cpy-9cbe10.local/fs/")!,timeoutInterval: Double.infinity)
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/\(incoming)")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
@ -243,116 +175,17 @@ class WifiTransferService: ObservableObject {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: "Error Found: \(error)"))
return
}
do {
print("In do-catch loop of getRequest")
let wifiIncomingData = try JSONDecoder().decode([WebDirectoryModel].self, from: data)
DispatchQueue.main.async {
self.webDirectoryInfo = wifiIncomingData
}
} catch {
print(error.localizedDescription)
}
if let str = String(data: data, encoding: .utf8) {
print("Out-going getRequest data: \(str)")
outgoingString = str
print("\(#function) @Line: \(#line)")
completionHandler(outgoingString)
} else {
print("\(#function) @Line: \(#line)")
print("Error")
}
}
task.resume()
}
func myFunction() {
var a = 0
let group = DispatchGroup()
group.enter()
// avoid deadlocks by not using .main queue here
DispatchQueue.global(qos: .default).async {
a = 1
group.leave()
}
// wait ...
group.wait()
print(a) // you could also `return a` here
}
typealias CompletionHandlerForCheck = (_ success: [WebDirectoryModel]) -> Void
func getRequestForFileCheck(read: String, completionHandler: @escaping CompletionHandlerForCheck){
print("Incoming Read String: \(read)")
var semaphore = DispatchSemaphore (value: 0)
let username = ""
let password = "passw0rd"
let loginString = "\(username):\(password)"
var outgoingString = String()
let loginData = loginString.data(using: String.Encoding.utf8)
let base64LoginString = loginData!.base64EncodedString()
print("Host Name: \(hostName)")
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/\(read)")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
print("GET Request: \(String(describing: request.url?.absoluteURL))\n")
print("Print curl:")
print(request.cURL(pretty: true))
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: "Error Found: \(error)"))
semaphore.signal()
return
}
// print(String(data: data, encoding: .utf8)!)
semaphore.signal()
do {
let wifiIncomingData = try JSONDecoder().decode([WebDirectoryModel].self, from: data)
DispatchQueue.main.async {
self.webDirectoryInfo = wifiIncomingData
completionHandler(self.webDirectoryInfo)
print("On completion \(self.webDirectoryInfo)")
}
} catch {
print(error.localizedDescription)
@ -361,21 +194,16 @@ class WifiTransferService: ObservableObject {
if let str = String(data: data, encoding: .utf8) {
print(str)
outgoingString = str
print("\(#function) @Line: \(#line)")
} else {
print("\(#function) @Line: \(#line)")
print("Error")
}
}
task.resume()
semaphore.wait()
return outgoingString
}
// func putDirectory(directoryPath: String, completion: @escaping (Result<Data?, Error>) -> Void) {
func putRequest(fileName: String, fileContent: Data, completion: @escaping (Result<Data?, Error>) -> Void) {
print("Test Transfer")
let parameters = fileContent
let postData = parameters
@ -412,16 +240,15 @@ class WifiTransferService: ObservableObject {
print("File write success!")
completion(.success(data))
}
// print(String(data: data, encoding: .utf8)!)
}
task.resume()
}
// Make
func putDirectory(directoryPath: String, completion: @escaping (Result<Data?, Error>) -> Void) {
print("\(#function) @Line: \(#line)")
let username = ""
let password = "passw0rd"
@ -444,20 +271,12 @@ class WifiTransferService: ObservableObject {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let response = response as? HTTPURLResponse {
print("Response HTTP Status code: \(response.statusCode)")
}
if let error = error {
print("Write Directory Failure")
completion(.failure(error))
}
if let data = data {
print("Write Directory Success")
completion(.success(data))
}
@ -482,7 +301,7 @@ class WifiTransferService: ObservableObject {
let base64LoginString = loginData.base64EncodedString()
// var request = URLRequest(url: URL(string: "http://cpy-9cbe10.local/fs/")!,timeoutInterval: Double.infinity)
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/test.txt")!,timeoutInterval: Double.infinity)
var request = URLRequest(url: URL(string: "http://\(hostName).local/fs/testing.txt")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
request.addValue("text/plain", forHTTPHeaderField: "Content-Type")

View file

@ -5,6 +5,7 @@
// Created by Trevor Beaton on 8/9/22.
//
// My IP Address - 192.168.1.111
import SwiftUI
import Combine
@ -13,70 +14,32 @@ struct WifiView: View {
@StateObject var viewModel = WifiViewModel()
private let kPrefix = Bundle.main.bundleIdentifier!
@StateObject var wifiviewModel = WifiServiceManager()
// User Defaults
let userDefaults = UserDefaults.standard
@ObservedObject var networkModel = NetworkService()
@EnvironmentObject var rootViewModel: RootViewModel
@State private var downloadState = DownloadState.idle
@State private var scrollViewID = UUID()
@State private var inConnectedInWifiView = true
@State private var boardBootInfo = "esp32-s2"
@State var hostName = ""
@EnvironmentObject var test : ExpandedState
@State var falseTog = false
@State var trueTog = true
@State private var showPopover: Bool = false
func toggleViewModelIP() {
viewModel.isInvalidIP.toggle()
}
func scanNetworkWifi() {
viewModel.wifiServiceManager.findService()
}
func printArray(array: [Any]) {
for i in array {
print("\(i)")
}
}
func checkForStoredIPAddress() {
if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") == nil {
print("storeResolvedAddress - not stored")
} else {
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
viewModel.ipAddressStored = true
print("storeResolvedAddress - is stored")
viewModel.connectionStatus = .connected
}
}
func showValidationPrompt() {
alertTF(title: "Enter Device IP Address",
message: "PyLeap will use this IP address to search for Adafruit devices on your local network",
hintText: "IP Address...",
primaryTitle: "Done",
secondaryTitle: "Cancel") { text in
viewModel.checkServices(ip: text)
} secondaryAction: {
print("Cancel")
}
}
viewModel.checkServices(ip: text)
} secondaryAction: {
print("Cancel")
}
}
func showAlertMessage() {
alertMessage(title: "IP address Not Found", exitTitle: "Ok") {
@ -84,61 +47,106 @@ struct WifiView: View {
}
}
func initialIPStoreCheck() {
if userDefaults.object(forKey: kPrefix+".storedIP") == nil {
print("No IP address found.")
showValidationPrompt()
} else {
viewModel.connectionStatus = .connected
print("Stored: \(String(describing: userDefaults.object(forKey: kPrefix+".storedIP"))), @: \(kPrefix+".storedIP")")
}
}
var body: some View {
VStack(spacing: 0) {
WifiHeaderView()
Group{
switch viewModel.connectionStatus {
case .connected:
WifiStatusConnectedView(hostName: $hostName)
case .noConnection:
WifiStatusNoConnectionView()
case .connecting:
WifiStatusConnectingView()
}
if wifiviewModel.isSearching {
NetworkConnectionBanner()
} else {
}
ScrollView(.vertical, showsIndicators: false) {
Group{
switch viewModel.connectionStatus {
case .connected:
WifiStatusConnectedView(hostName: $hostName)
case .noConnection:
WifiStatusNoConnectionView()
case .connecting:
WifiStatusConnectingView()
}
}
if !viewModel.ipAddressStored {
HStack(alignment: .center, content: {
Button {
showValidationPrompt()
} label: {
Text("Enter IP address")
.font(Font.custom("ReadexPro-Regular", size: 16))
.foregroundColor(.white)
.background(.indigo)
// .cornerRadius(15)
.padding(5)
}
Button {
wifiviewModel.findService()
} label: {
Text("Scan Network")
.foregroundColor(.white)
.background(.indigo)
// .cornerRadius(15)
.padding(5)
}
Button {
rootViewModel.goTobluetoothPairing()
} label: {
Text("BLE Mode")
.foregroundColor(.white)
.background(.indigo)
// .cornerRadius(15)
.padding(5)
}
})
.padding(.all, 0.0)
.frame(maxWidth: .infinity)
.frame(maxHeight: 40)
.background(Color.clear)
.foregroundColor(.black)
} else {
}
ScrollView(.vertical, showsIndicators: true) {
ScrollViewReader { scroll in
SubHeaderView()
let check = viewModel.pdemos.filter {
let check = networkModel.pdemos.filter {
$0.compatibility.contains(boardBootInfo)
}
ForEach(check) { demo in
if demo.bundleLink == test.currentCell {
WifiCell(result: demo,isExpanded: trueTog, isConnected: $inConnectedInWifiView, bootOne: $boardBootInfo, stateBinder: $downloadState, onViewGeometryChanged: {
})
.onAppear(){
withAnimation {
scroll.scrollTo(demo.id)
}
WifiCell(result: demo, isConnected: $inConnectedInWifiView, bootOne: $boardBootInfo, stateBinder: $downloadState, onViewGeometryChanged: {
withAnimation {
scroll.scrollTo(demo.id)
}
} else {
WifiCell(result: demo, isExpanded: falseTog, isConnected: $inConnectedInWifiView, bootOne: $boardBootInfo, stateBinder: $downloadState, onViewGeometryChanged: {
withAnimation {
// scroll.scrollTo(demo.id)
}
})
}
})
}
}
@ -146,73 +154,52 @@ struct WifiView: View {
.id(self.scrollViewID)
}
.foregroundColor(.black)
.environmentObject(test)
}
.onChange(of: viewModel.connectionStatus, perform: { newValue in
if newValue == .connected {
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
}
})
.onChange(of: viewModel.wifiServiceManager.resolvedServices, perform: { newValue in
print("Credential Check!")
print(newValue)
if newValue.contains(where: { result in
result.hostName == userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
}) {
print("Matched")
} else {
print("Un-Matched")
}
})
.onChange(of: viewModel.isInvalidIP, perform: { newValue in
print("viewModel.isInvalidIP .onChange")
if newValue {
showAlertMessage()
toggleViewModelIP()
viewModel.isInvalidIP.toggle()
}
})
.onAppear(){
checkForStoredIPAddress()
viewModel.printStoredInfo()
viewModel.read()
print("On Appear")
networkModel.fetch()
// viewModel.checkStoredIP()
initialIPStoreCheck()
if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") == nil {
print("Nothing stored.")
} else {
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
}
}
}
}
struct WifiView_Previews: PreviewProvider {
static var previews: some View {
WifiView()
}
}
extension Notification.Name {
private static let kPrefix = Bundle.main.bundleIdentifier!
public static let testNotificationName = Notification.Name(kPrefix+".testNotificationName")
public static let didUpdateState = Notification.Name(kPrefix+".test")
public static let invalidIPNotif = Notification.Name(kPrefix+".invalidIPNotif")
public static let invalidCustomNetworkRequest = Notification.Name(kPrefix+".invalidCustomNetworkRequest")
public static let didCollectCustomProject = Notification.Name(kPrefix+".didCollectCustomProject")
public static let didEncounterZipError = Notification.Name(kPrefix+".didEncounterZipError")
public static let didCompleteZip = Notification.Name(kPrefix+".didCompleteZip")
public static let wifiDownloadComplete = Notification.Name(kPrefix+".wifiDownloadComplete")
public static let didCompleteTransfer = Notification.Name(kPrefix+".didCompleteTransfer")
public static let didEncounterTransferError = Notification.Name(kPrefix+".didEncounterTransferError")
public static let downloadErrorDidOccur = Notification.Name(kPrefix+".downloadErrorDidOccur")
public static let usbInUseErrorNotification = Notification.Name(kPrefix+".usbInUseErrorNotification")
}

View file

@ -7,6 +7,7 @@
import SwiftUI
import UIKit
import CoreLocation
import Network
@ -22,33 +23,21 @@ class WifiViewModel: ObservableObject {
private let kPrefix = Bundle.main.bundleIdentifier!
@Published var connectionStatus: ConnectionStatus = .noConnection
@Published var isInvalidIP = false
@Published var ipInputValidation = false
//Dependencies
var networkMonitor = NetworkMonitor()
var networkAuth = LocalNetworkAuthorization()
public var wifiNetworkService = WifiNetworkService()
@Published var wifiTransferService = WifiTransferService()
@Published var wifiServiceManager = WifiServiceManager()
var circuitPythonVersion = Int()
@Published var webDirectoryInfo = [WebDirectoryModel]()
@Published var hostName = ""
@Published var downloadState: DownloadState = .idle
let dataStore = DataStore()
@Published var pdemos : [ResultItem] = []
// File Manager Data
let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
@Published var fileArray: [ContentFile] = []
@ -56,47 +45,21 @@ class WifiViewModel: ObservableObject {
var projectDirectories: [URL] = []
var returnedArray = [[String]]()
var ipAddressStored = false
init() {
pdemos = dataStore.loadDefaultList()
checkIP()
registerNotifications(enabled: true)
wifiServiceManager.findService()
//read()
}
/// Makes a network call to populate our project list
func fetch() {
// networkModel.fetch()
}
@Published var pyleapProjects = [ResultItem]()
func read() {
// This method can't be used until the device has permission to communicate.
print("READING CP Vers.")
wifiTransferService.getRequest(read: "boot_out.txt") { result in
if result.contains("CircuitPython 7") {
WifiCPVersion.versionNumber = 7
print("WifiCPVersion.versionNumber set to: \(WifiCPVersion.versionNumber)")
}
if result.contains("CircuitPython 8") {
WifiCPVersion.versionNumber = 8
print("WifiCPVersion.versionNumber set to: \(WifiCPVersion.versionNumber)")
}
}
}
private weak var invalidIPObserver: NSObjectProtocol?
private weak var testObserver: NSObjectProtocol?
private weak var invalidIPObserver: NSObjectProtocol?
private func registerNotifications(enabled: Bool) {
let notificationCenter = NotificationCenter.default
@ -113,9 +76,6 @@ class WifiViewModel: ObservableObject {
}
func checkIP() {
print("Tiggered checkIP")
@ -124,7 +84,9 @@ class WifiViewModel: ObservableObject {
ipAddressStored = true
hostName = userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName") as! String
} else {
ipAddressStored = false
print(userDefaults.object(forKey: kPrefix+".storedIP"))
ipAddressStored = false
}
}
@ -148,18 +110,16 @@ class WifiViewModel: ObservableObject {
// @Published var connectionStatus: ConnectionStatus = AppEnvironment.isRunningTests ? .connected : .noConnection
func printStoredInfo() {
print("======Stored UserDefaults======")
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.hostName"))
print(userDefaults.object(forKey: kPrefix+".storeResolvedAddress.device"))
func printIPStorageAtLocation() {
print("Stored: \(String(describing: userDefaults.object(forKey: kPrefix+".storedIP"))), @: \(kPrefix+".storedIP")")
}
func storeIPAddress(ipAddress: String) {
userDefaults.set(ipAddress, forKey: kPrefix+".storedIP" )
printIPStorageAtLocation()
}
func storeResolvedAddress(service: ResolvedService) {
userDefaults.set(service.ipAddress, forKey: kPrefix+".storeResolvedAddress.ipAddress" )
@ -175,6 +135,7 @@ class WifiViewModel: ObservableObject {
func clearKnownIPAddress() {
userDefaults.set(nil, forKey: kPrefix+".storedIP")
printIPStorageAtLocation()
}
@ -188,11 +149,14 @@ class WifiViewModel: ObservableObject {
let resolvedService = wifiServiceManager.resolvedServices.filter { $0.ipAddress == ip }
// To store in UserDefaults
storeResolvedAddress(service: resolvedService[0])
storeIPAddress(ipAddress: ip)
connectionStatus = .connected
ipInputValidation = true
} else {
isInvalidIP = true
print("1 does not exists in the array")
@ -200,19 +164,16 @@ class WifiViewModel: ObservableObject {
}
func checkStoredIP() {
if userDefaults.object(forKey: kPrefix+".storeResolvedAddress.ipAddress") == nil {
if userDefaults.object(forKey: kPrefix+".storedIP") == nil {
print("Nothing stored.")
} else {
NotificationCenter.default.post(name: .invalidIPNotif, object: nil, userInfo: nil)
print("Stored: \(String(describing: userDefaults.object(forKey: kPrefix+".storedIP"))), @: \(kPrefix+".storedIP")")
}
}
public func internetMonitoring() {
networkMonitor.monitor.pathUpdateHandler = { path in