From fbb150277a3a1223cc4a9db9c610dd19b3034b0d Mon Sep 17 00:00:00 2001 From: Michal Polanski Date: Thu, 23 May 2024 21:52:00 +0200 Subject: [PATCH] Issue #380: Support curtain --- SUPLA.xcodeproj/project.pbxproj | 106 +++++++++++- SUPLA/ChannelCell.m | 6 + SUPLA/Core/Extensions/CGRect+Ext.swift | 27 +++ SUPLA/Core/Extensions/Float+Ext.swift | 6 +- SUPLA/Core/Extensions/UIView+Ext.swift | 22 +++ SUPLA/Core/UI/Details/StandardDetailVC.swift | 14 ++ .../UI/TableView/BaseTableViewModel.swift | 3 +- .../ChannelBaseTableViewController.swift | 3 +- .../Core/UI/Views/ThermostatControlView.swift | 6 +- .../Features/ChannelList/Cells/IconCell.swift | 6 +- .../WindowDetail/Base/BaseWindowVC.swift | 158 +++--------------- .../WindowDetail/Base/BaseWindowVM.swift | 4 + .../Base/Model/CurtainWindowState.swift | 33 ++++ .../Base/UI/Controls/ControlButtonType.swift | 72 ++++++++ .../LeftMiddleRightControlButton.swift | 124 ++++++++++++++ .../UI/Controls/LeftRightControlButton.swift | 110 ++++++++++++ .../UI/Controls/UpDownControllButton.swift | 107 ++++++++++++ .../WindowControlButton.swift} | 128 +------------- .../Base/UI/Controls/WindowControls.swift | 33 ++++ .../Controls/WindowHorizontalControls.swift | 105 ++++++++++++ .../UI/Controls/WindowVerticalControls.swift | 117 +++++++++++++ .../Base/UI/Curtain/CurtainView.swift | 157 +++++++++++++++++ .../UI/FacadeBlinds/FacadeBlindsView.swift | 140 +++++++++------- .../UI/RollerShutter/RollerShutterView.swift | 65 +++++-- .../UI/TerraceAwning/TerraceAwningView.swift | 30 +--- ...imens.swift => BaseWallWindowDimens.swift} | 75 +++------ .../UI/WindowView/BaseWallWindowView.swift | 132 +++++---------- .../Base/UI/WindowView/BaseWindowView.swift | 11 +- .../UI/WindowView/DefaultWindowDimens.swift | 25 +-- .../Base/UI/WindowView/MoveTimeView.swift | 95 +++++++++++ .../WindowDetail/Curtain/CurtainVC.swift | 34 ++++ .../WindowDetail/Curtain/CurtainVM.swift | 96 +++++++++++ .../Extensions/SAChannelBase+Ext.swift | 4 +- SUPLA/Resources/Default.strings | 1 + SUPLA/Resources/Extensions/String+Icons.swift | 4 + .../Resources/Extensions/UIImage+Supla.swift | 4 + .../fnc_curtain-closed.imageset/Contents.json | 22 +++ .../curtain - dark mode-1.svg | 9 + .../fnc_curtain-closed.imageset/curtain-1.svg | 9 + .../fnc_curtain-open.imageset/Contents.json | 22 +++ .../curtain - dark mode.svg | 8 + .../fnc_curtain-open.imageset/curtain.svg | 8 + .../Contents.json | 4 +- ...e_blind-closed.svg => facade blinders.svg} | 2 +- ...d-nm.svg => facade blinders_dark mode.svg} | 2 +- .../Contents.json | 4 +- .../facade blinders-1.svg | 12 ++ .../facade blinders-2.svg | 12 ++ .../facade_blind-open-nm.svg | 9 - .../facade_blind-open.svg | 9 - .../Contents.json | 4 +- ...creenclosed.svg => projector screen-1.svg} | 4 +- ...d.svg => projector screen_dark mode-1.svg} | 4 +- .../Contents.json | 4 +- ... screenopened.svg => projector screen.svg} | 6 +- ...ned.svg => projector screen_dark mode.svg} | 6 +- .../Contents.json | 4 +- ...nc_terrase_awning-open.svg => awnings.svg} | 4 +- ...ning-open-nm.svg => awnings_dark mode.svg} | 4 +- .../Contents.json | 4 +- ...errase_awning-closed.svg => awnings-1.svg} | 4 +- ...-closed-nm.svg => awnings_dark mode-1.svg} | 4 +- .../Contents.json | 15 ++ .../Frame 391.svg | 35 ++++ .../Contents.json | 15 ++ .../Frame 394.svg | 13 ++ .../Contents.json | 15 ++ .../Frame 392.svg | 35 ++++ .../Contents.json | 15 ++ .../Frame 393.svg | 13 ++ SUPLA/Resources/Strings.swift | 1 + SUPLA/Resources/de.lproj/Localizable.strings | 1 + SUPLA/Resources/pl.lproj/Localizable.strings | 1 + .../GetChannelBaseDefaultCaptionUseCase.swift | 2 + .../GetChannelBaseStateUseCase.swift | 4 +- .../Detail/ProvideDetailTypeUseCase.swift | 3 + ...gSystemGroupActivePercentageProvider.swift | 5 +- .../UpdateChannelGroupTotalValueUseCase.swift | 3 +- .../Icon/GetDefaultIconNameUseCase.swift | 1 + .../WindowDetail/Curtain/CurtainVMTests.swift | 123 ++++++++++++++ ...hannelBaseDefaultCaptionUseCaseTests.swift | 2 + .../GetChannelBaseStateUseCaseTests.swift | 42 +++++ .../ProvideDetailTypeUseCaseTests.swift | 18 ++ ...teChannelGroupTotalValueUseCaseTests.swift | 52 +++++- .../Icon/GetDefaultIconNameUseCaseTests.swift | 76 +++++++++ 85 files changed, 2150 insertions(+), 587 deletions(-) create mode 100644 SUPLA/Core/Extensions/CGRect+Ext.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/Model/CurtainWindowState.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/ControlButtonType.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftMiddleRightControlButton.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftRightControlButton.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/UpDownControllButton.swift rename SUPLA/Features/Details/WindowDetail/Base/UI/{UpDownControllButton.swift => Controls/WindowControlButton.swift} (66%) create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControls.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowHorizontalControls.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowVerticalControls.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/Curtain/CurtainView.swift rename SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/{RuntimeWindowDimens.swift => BaseWallWindowDimens.swift} (54%) create mode 100644 SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/MoveTimeView.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Curtain/CurtainVC.swift create mode 100644 SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain - dark mode-1.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain-1.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain - dark mode.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain.svg rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/{facade_blind-closed.svg => facade blinders.svg} (97%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/{facade_blind-closed-nm.svg => facade blinders_dark mode.svg} (97%) create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-1.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-2.svg delete mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open-nm.svg delete mode 100644 SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open.svg rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/{projector screenclosed.svg => projector screen-1.svg} (77%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/{projector screen_dark modeclosed.svg => projector screen_dark mode-1.svg} (77%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/{projector screenopened.svg => projector screen.svg} (97%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/{projector screen_dark modeopened.svg => projector screen_dark mode.svg} (97%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/{fnc_terrase_awning-open.svg => awnings.svg} (80%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/{fnc_terrase_awning-open-nm.svg => awnings_dark mode.svg} (80%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/{fnc_terrase_awning-closed.svg => awnings-1.svg} (73%) rename SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/{fnc_terrase_awning-closed-nm.svg => awnings_dark mode-1.svg} (73%) create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Frame 391.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Frame 394.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Frame 392.svg create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Contents.json create mode 100644 SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Frame 393.svg create mode 100644 SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift diff --git a/SUPLA.xcodeproj/project.pbxproj b/SUPLA.xcodeproj/project.pbxproj index 359daee4..7d50d1fb 100644 --- a/SUPLA.xcodeproj/project.pbxproj +++ b/SUPLA.xcodeproj/project.pbxproj @@ -306,7 +306,7 @@ A5074BC02BCE61C20081B6B1 /* RoofWindowVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BBF2BCE61C20081B6B1 /* RoofWindowVM.swift */; }; A5074BC22BCE61E60081B6B1 /* RoofWindowVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BC12BCE61E60081B6B1 /* RoofWindowVC.swift */; }; A5074BC52BCE66070081B6B1 /* DefaultWindowDimens.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BC42BCE66070081B6B1 /* DefaultWindowDimens.swift */; }; - A5074BC72BCE66930081B6B1 /* RuntimeWindowDimens.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BC62BCE66930081B6B1 /* RuntimeWindowDimens.swift */; }; + A5074BC72BCE66930081B6B1 /* BaseWallWindowDimens.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BC62BCE66930081B6B1 /* BaseWallWindowDimens.swift */; }; A5074BC92BCE682C0081B6B1 /* BaseWallWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BC82BCE682C0081B6B1 /* BaseWallWindowView.swift */; }; A5074BCC2BCE6F690081B6B1 /* FacadeBlindsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BCB2BCE6F690081B6B1 /* FacadeBlindsView.swift */; }; A5074BCF2BCE72250081B6B1 /* FacadeBlindsVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5074BCE2BCE72250081B6B1 /* FacadeBlindsVM.swift */; }; @@ -367,9 +367,14 @@ A50CD3D62A4D99E60012DD9B /* UpdateTokenTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50CD3D52A4D99E60012DD9B /* UpdateTokenTaskTests.swift */; }; A50CD3D82A4D9A040012DD9B /* SingleCallMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50CD3D72A4D9A040012DD9B /* SingleCallMock.swift */; }; A50CD3DA2A4D9AFC0012DD9B /* DateProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50CD3D92A4D9AFC0012DD9B /* DateProviderMock.swift */; }; - A5112F212BFD30CB0081EBC3 /* GetGroupActivePercentageUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5112F202BFD30CB0081EBC3 /* GetGroupActivePercentageUseCaseTests.swift */; }; + A50E5D702BFF4CAF00303BAE /* ChannelBaseActionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D6F2BFF4CAF00303BAE /* ChannelBaseActionUseCase.swift */; }; + A50E5D732BFF543500303BAE /* CurtainVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D722BFF543500303BAE /* CurtainVM.swift */; }; + A50E5D752BFF543D00303BAE /* CurtainVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D742BFF543D00303BAE /* CurtainVC.swift */; }; + A50E5D772BFF548200303BAE /* CurtainWindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D762BFF548200303BAE /* CurtainWindowState.swift */; }; + A50E5D7A2BFF552C00303BAE /* CurtainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D792BFF552C00303BAE /* CurtainView.swift */; }; A50E5D7C2BFFD67D00303BAE /* ChannelBaseActionUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D7B2BFFD67D00303BAE /* ChannelBaseActionUseCase.swift */; }; A50E5D7E2BFFDA7800303BAE /* ChannelBaseActionUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50E5D7D2BFFDA7800303BAE /* ChannelBaseActionUseCaseTests.swift */; }; + A5112F212BFD30CB0081EBC3 /* GetGroupActivePercentageUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5112F202BFD30CB0081EBC3 /* GetGroupActivePercentageUseCaseTests.swift */; }; A51BE8E72AA705AD00718F2F /* StandardDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8E52AA705AD00718F2F /* StandardDetailVM.swift */; }; A51BE8E82AA705AD00718F2F /* StandardDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8E62AA705AD00718F2F /* StandardDetailVC.swift */; }; A51BE8EB2AA7136000718F2F /* ThermostatDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BE8EA2AA7136000718F2F /* ThermostatDetailVC.swift */; }; @@ -776,6 +781,16 @@ A5ABE5CB2ABCD80300FFA50B /* DelayedThermostatActionSubjectMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5ABE5CA2ABCD80300FFA50B /* DelayedThermostatActionSubjectMock.swift */; }; A5ABE5CD2ABCD89400FFA50B /* LoadingTimoutManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5ABE5CC2ABCD89400FFA50B /* LoadingTimoutManagerMock.swift */; }; A5ABE5CF2ABD696900FFA50B /* DataMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5ABE5CE2ABD696900FFA50B /* DataMocks.swift */; }; + A5AD70182C00766400A36318 /* CGRect+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD70172C00766400A36318 /* CGRect+Ext.swift */; }; + A5AD701B2C00B1D700A36318 /* ControlButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD701A2C00B1D700A36318 /* ControlButtonType.swift */; }; + A5AD701D2C00B2CC00A36318 /* LeftRightControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD701C2C00B2CC00A36318 /* LeftRightControlButton.swift */; }; + A5AD701F2C00B45E00A36318 /* WindowControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD701E2C00B45E00A36318 /* WindowControlButton.swift */; }; + A5AD70212C00B6AF00A36318 /* WindowVerticalControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD70202C00B6AF00A36318 /* WindowVerticalControls.swift */; }; + A5AD70232C00C71200A36318 /* WindowHorizontalControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD70222C00C71200A36318 /* WindowHorizontalControls.swift */; }; + A5AD70252C012F5300A36318 /* WindowControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD70242C012F5300A36318 /* WindowControls.swift */; }; + A5AD70272C04656700A36318 /* LeftMiddleRightControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD70262C04656700A36318 /* LeftMiddleRightControlButton.swift */; }; + A5AD70292C04691100A36318 /* MoveTimeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD70282C04691100A36318 /* MoveTimeView.swift */; }; + A5AD702C2C04773F00A36318 /* CurtainVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD702B2C04773F00A36318 /* CurtainVMTests.swift */; }; A5AE7A7F2A3998260097FA8B /* NewGestureInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AE7A7E2A3998260097FA8B /* NewGestureInfoView.swift */; }; A5AE7A812A39AF410097FA8B /* Default.strings in Resources */ = {isa = PBXBuildFile; fileRef = A5AE7A802A39AF410097FA8B /* Default.strings */; }; A5AE7A852A3AC7020097FA8B /* BaseSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AE7A842A3AC7020097FA8B /* BaseSettingsCell.swift */; }; @@ -1615,7 +1630,7 @@ A5074BBF2BCE61C20081B6B1 /* RoofWindowVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoofWindowVM.swift; sourceTree = ""; }; A5074BC12BCE61E60081B6B1 /* RoofWindowVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoofWindowVC.swift; sourceTree = ""; }; A5074BC42BCE66070081B6B1 /* DefaultWindowDimens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWindowDimens.swift; sourceTree = ""; }; - A5074BC62BCE66930081B6B1 /* RuntimeWindowDimens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeWindowDimens.swift; sourceTree = ""; }; + A5074BC62BCE66930081B6B1 /* BaseWallWindowDimens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseWallWindowDimens.swift; sourceTree = ""; }; A5074BC82BCE682C0081B6B1 /* BaseWallWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseWallWindowView.swift; sourceTree = ""; }; A5074BCB2BCE6F690081B6B1 /* FacadeBlindsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacadeBlindsView.swift; sourceTree = ""; }; A5074BCE2BCE72250081B6B1 /* FacadeBlindsVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacadeBlindsVM.swift; sourceTree = ""; }; @@ -1676,9 +1691,14 @@ A50CD3D52A4D99E60012DD9B /* UpdateTokenTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTokenTaskTests.swift; sourceTree = ""; }; A50CD3D72A4D9A040012DD9B /* SingleCallMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleCallMock.swift; sourceTree = ""; }; A50CD3D92A4D9AFC0012DD9B /* DateProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProviderMock.swift; sourceTree = ""; }; - A5112F202BFD30CB0081EBC3 /* GetGroupActivePercentageUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetGroupActivePercentageUseCaseTests.swift; sourceTree = ""; }; + A50E5D6F2BFF4CAF00303BAE /* ChannelBaseActionUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelBaseActionUseCase.swift; sourceTree = ""; }; + A50E5D722BFF543500303BAE /* CurtainVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurtainVM.swift; sourceTree = ""; }; + A50E5D742BFF543D00303BAE /* CurtainVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurtainVC.swift; sourceTree = ""; }; + A50E5D762BFF548200303BAE /* CurtainWindowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurtainWindowState.swift; sourceTree = ""; }; + A50E5D792BFF552C00303BAE /* CurtainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurtainView.swift; sourceTree = ""; }; A50E5D7B2BFFD67D00303BAE /* ChannelBaseActionUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBaseActionUseCase.swift; sourceTree = ""; }; A50E5D7D2BFFDA7800303BAE /* ChannelBaseActionUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelBaseActionUseCaseTests.swift; sourceTree = ""; }; + A5112F202BFD30CB0081EBC3 /* GetGroupActivePercentageUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetGroupActivePercentageUseCaseTests.swift; sourceTree = ""; }; A51BE8E52AA705AD00718F2F /* StandardDetailVM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardDetailVM.swift; sourceTree = ""; }; A51BE8E62AA705AD00718F2F /* StandardDetailVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardDetailVC.swift; sourceTree = ""; }; A51BE8EA2AA7136000718F2F /* ThermostatDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatDetailVC.swift; sourceTree = ""; }; @@ -2092,6 +2112,16 @@ A5ABE5CA2ABCD80300FFA50B /* DelayedThermostatActionSubjectMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedThermostatActionSubjectMock.swift; sourceTree = ""; }; A5ABE5CC2ABCD89400FFA50B /* LoadingTimoutManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTimoutManagerMock.swift; sourceTree = ""; }; A5ABE5CE2ABD696900FFA50B /* DataMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMocks.swift; sourceTree = ""; }; + A5AD70172C00766400A36318 /* CGRect+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Ext.swift"; sourceTree = ""; }; + A5AD701A2C00B1D700A36318 /* ControlButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlButtonType.swift; sourceTree = ""; }; + A5AD701C2C00B2CC00A36318 /* LeftRightControlButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftRightControlButton.swift; sourceTree = ""; }; + A5AD701E2C00B45E00A36318 /* WindowControlButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControlButton.swift; sourceTree = ""; }; + A5AD70202C00B6AF00A36318 /* WindowVerticalControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowVerticalControls.swift; sourceTree = ""; }; + A5AD70222C00C71200A36318 /* WindowHorizontalControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowHorizontalControls.swift; sourceTree = ""; }; + A5AD70242C012F5300A36318 /* WindowControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControls.swift; sourceTree = ""; }; + A5AD70262C04656700A36318 /* LeftMiddleRightControlButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftMiddleRightControlButton.swift; sourceTree = ""; }; + A5AD70282C04691100A36318 /* MoveTimeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveTimeView.swift; sourceTree = ""; }; + A5AD702B2C04773F00A36318 /* CurtainVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurtainVMTests.swift; sourceTree = ""; }; A5AE7A7E2A3998260097FA8B /* NewGestureInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewGestureInfoView.swift; sourceTree = ""; }; A5AE7A802A39AF410097FA8B /* Default.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Default.strings; sourceTree = ""; }; A5AE7A842A3AC7020097FA8B /* BaseSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSettingsCell.swift; sourceTree = ""; }; @@ -3058,13 +3088,14 @@ A5074BBB2BCE5CBF0081B6B1 /* UI */ = { isa = PBXGroup; children = ( + A5AD70192C00B1C300A36318 /* Controls */, + A50E5D782BFF551B00303BAE /* Curtain */, A50B5D3B2BFB6E9100918D18 /* ProjectorScreen */, A50B5D2E2BF4A02B00918D18 /* TerraceAwning */, A5074BCA2BCE6F2C0081B6B1 /* FacadeBlinds */, A5074BC32BCE64A30081B6B1 /* WindowView */, A5B3A4B72BB57B4D0001D006 /* RoofWindow */, A5B3A4B62BB57B410001D006 /* RollerShutter */, - A55A8D832BAAD06C00C540D4 /* UpDownControllButton.swift */, A5B3A4BA2BB5B8F20001D006 /* WindowType.swift */, A50B5D012BEA41D700918D18 /* WindowGroupedValue.swift */, ); @@ -3086,8 +3117,9 @@ A55A8D812BA9B6B400C540D4 /* WindowColors.swift */, A5B3A4B22BB552590001D006 /* BaseWindowView.swift */, A5074BC42BCE66070081B6B1 /* DefaultWindowDimens.swift */, - A5074BC62BCE66930081B6B1 /* RuntimeWindowDimens.swift */, + A5074BC62BCE66930081B6B1 /* BaseWallWindowDimens.swift */, A5074BC82BCE682C0081B6B1 /* BaseWallWindowView.swift */, + A5AD70282C04691100A36318 /* MoveTimeView.swift */, ); path = WindowView; sourceTree = ""; @@ -3171,6 +3203,7 @@ A50B5D102BECC58800918D18 /* WindowDetail */ = { isa = PBXGroup; children = ( + A5AD702A2C04771500A36318 /* Curtain */, A50B5D312BF54FE600918D18 /* TerraceAwning */, A50B5D1E2BECE8B100918D18 /* FacadeBlinds */, A50B5D1B2BECDAA100918D18 /* RollerShutter */, @@ -3295,6 +3328,23 @@ path = UpdateToken; sourceTree = ""; }; + A50E5D712BFF540700303BAE /* Curtain */ = { + isa = PBXGroup; + children = ( + A50E5D722BFF543500303BAE /* CurtainVM.swift */, + A50E5D742BFF543D00303BAE /* CurtainVC.swift */, + ); + path = Curtain; + sourceTree = ""; + }; + A50E5D782BFF551B00303BAE /* Curtain */ = { + isa = PBXGroup; + children = ( + A50E5D792BFF552C00303BAE /* CurtainView.swift */, + ); + path = Curtain; + sourceTree = ""; + }; A51BE8E42AA7054F00718F2F /* Details */ = { isa = PBXGroup; children = ( @@ -3556,6 +3606,7 @@ A530EE3A2A5801AB00F8DAEE /* GetChannelBaseIconUseCase.swift */, A5A14A352B611863004B1598 /* GetChannelBaseDefaultCaptionUseCase.swift */, A5A14A372B61220C004B1598 /* GetChannelBaseCaptionUseCase.swift */, + A50E5D6F2BFF4CAF00303BAE /* ChannelBaseActionUseCase.swift */, ); path = ChannelBase; sourceTree = ""; @@ -3892,6 +3943,7 @@ A55A8D6C2BA831AD00C540D4 /* WindowDetail */ = { isa = PBXGroup; children = ( + A50E5D712BFF540700303BAE /* Curtain */, A50B5D342BFB6BDB00918D18 /* ProjectorScreen */, A50B5D272BF49F0D00918D18 /* TerraceAwning */, A5074BB62BCE58B30081B6B1 /* Base */, @@ -3963,6 +4015,7 @@ A50B5D092BEA4EC700918D18 /* FacadeBlindMarker.swift */, A50B5D2C2BF49F7700918D18 /* TerraceAwningWindowState.swift */, A50B5D372BFB6D1400918D18 /* ProjectorScreenState.swift */, + A50E5D762BFF548200303BAE /* CurtainWindowState.swift */, ); path = Model; sourceTree = ""; @@ -4670,6 +4723,29 @@ path = ThermostatGeneral; sourceTree = ""; }; + A5AD70192C00B1C300A36318 /* Controls */ = { + isa = PBXGroup; + children = ( + A55A8D832BAAD06C00C540D4 /* UpDownControllButton.swift */, + A5AD701A2C00B1D700A36318 /* ControlButtonType.swift */, + A5AD701C2C00B2CC00A36318 /* LeftRightControlButton.swift */, + A5AD701E2C00B45E00A36318 /* WindowControlButton.swift */, + A5AD70202C00B6AF00A36318 /* WindowVerticalControls.swift */, + A5AD70222C00C71200A36318 /* WindowHorizontalControls.swift */, + A5AD70242C012F5300A36318 /* WindowControls.swift */, + A5AD70262C04656700A36318 /* LeftMiddleRightControlButton.swift */, + ); + path = Controls; + sourceTree = ""; + }; + A5AD702A2C04771500A36318 /* Curtain */ = { + isa = PBXGroup; + children = ( + A5AD702B2C04773F00A36318 /* CurtainVMTests.swift */, + ); + path = Curtain; + sourceTree = ""; + }; A5AE7A822A39E6C20097FA8B /* AppSettings */ = { isa = PBXGroup; children = ( @@ -5074,6 +5150,7 @@ A55A8D7C2BA9A15200C540D4 /* UIImageView+Ext.swift */, A55A8D972BAB28CC00C540D4 /* ScopeFunctions.swift */, A50B5D232BEE4CE600918D18 /* UIViewController+Toast.swift */, + A5AD70172C00766400A36318 /* CGRect+Ext.swift */, ); path = Extensions; sourceTree = ""; @@ -5697,6 +5774,7 @@ AE1874C2290C581D00437146 /* SceneCaptionEditor.swift in Sources */, A50B5D562BFCC28200918D18 /* ShadingSystemGroupActivePercentageProvider.swift in Sources */, A530EE162A56F82A00F8DAEE /* GateIconNameProducer.swift in Sources */, + A50E5D772BFF548200303BAE /* CurtainWindowState.swift in Sources */, A5074BF12BDA73250081B6B1 /* GroupTotalValue.swift in Sources */, A51BE8FF2AA73A5900718F2F /* MeasurementValue.swift in Sources */, A56234012ABACEAF001CB948 /* SAChannel+Ext.swift in Sources */, @@ -5708,6 +5786,7 @@ 010714782656C605009C119F /* SAZWaveWakeupSettingsReport.m in Sources */, 40C7BA5420C008DB00ACEE42 /* SAChannelGroupRelation+CoreDataClass.m in Sources */, A5F29BB62A24CAAB00ED700A /* UpdateChannelValueUseCase.swift in Sources */, + A50E5D752BFF543D00303BAE /* CurtainVC.swift in Sources */, A55A8D792BA850B800C540D4 /* SAChannelGroup+Ext.swift in Sources */, A56233D82AB054EA001CB948 /* SeparatorView.swift in Sources */, A5477DB42AA1EF0600220B4A /* MarkChannelRelationsAsRemovableUseCase.swift in Sources */, @@ -5718,6 +5797,7 @@ A530EE472A5BEBFA00F8DAEE /* SwitchTimerConfigurationView.swift in Sources */, A57668FB2AEB999F0025509D /* SuplaChartMarkerView.swift in Sources */, A5074BB12BC957370081B6B1 /* DeviceCatalogVC.swift in Sources */, + A5AD701B2C00B1D700A36318 /* ControlButtonType.swift in Sources */, A530EE2A2A57E0BA00F8DAEE /* ElelectricityMeterIconNameProducer.swift in Sources */, A530EE3B2A5801AB00F8DAEE /* GetChannelBaseIconUseCase.swift in Sources */, A5F29B672A1E268E00ED700A /* GroupListVC.swift in Sources */, @@ -5759,6 +5839,7 @@ A55A8D752BA84D5400C540D4 /* RollerShutterVM.swift in Sources */, A530EE1A2A56FBBC00F8DAEE /* PowerSwitchIconNameProducer.swift in Sources */, A5F29B962A20B97B00ED700A /* CreateProfileChannelsListUseCase.swift in Sources */, + A5AD70232C00C71200A36318 /* WindowHorizontalControls.swift in Sources */, A5F14B7229DED06400682FA6 /* AccountRemovalVM.swift in Sources */, A5A14A362B611863004B1598 /* GetChannelBaseDefaultCaptionUseCase.swift in Sources */, A5756F8229DC102800C32A1B /* AccountRemovalVC.swift in Sources */, @@ -5998,7 +6079,7 @@ A52BFEE02B109F5A00A2F64C /* ThermostatTimerConfigurationView.swift in Sources */, A55A8DB92BADB29A00C540D4 /* GetGroupOnlineSummaryUseCase.swift in Sources */, A503ABB82B6BB082008CDA1F /* ChannelValueFormatter.swift in Sources */, - A5074BC72BCE66930081B6B1 /* RuntimeWindowDimens.swift in Sources */, + A5074BC72BCE66930081B6B1 /* BaseWallWindowDimens.swift in Sources */, A50B5CFD2BEA147100918D18 /* SuplaHeatpolThermostatFlag.swift in Sources */, A530EE042A555AFA00F8DAEE /* ReadChannelByRemoteIdUseCase.swift in Sources */, 01E3966E25A78B8B00A0DB18 /* SADimmerCalibrationTool.m in Sources */, @@ -6043,6 +6124,7 @@ AE535356276927690077BFFB /* LocationOrderingVC.swift in Sources */, A5F29B632A1E18FA00ED700A /* ChannelListVM.swift in Sources */, A5B3CBFB2B62693F00F95AC3 /* DepthValueStringProvider.swift in Sources */, + A5AD701D2C00B2CC00A36318 /* LeftRightControlButton.swift in Sources */, AEF79D8D2712FF8D00D7554B /* AppSettingsVC.swift in Sources */, A55A8D9F2BAC263000C540D4 /* ExecuteRollerShutterActionUseCase.swift in Sources */, A5F14B7429DED41000682FA6 /* BaseViewModel.swift in Sources */, @@ -6064,6 +6146,7 @@ 408034C51BC83D1A007666E7 /* MGSwipeTableCell.m in Sources */, A55A8DB32BAC65E500C540D4 /* SADialogNegativeButton.swift in Sources */, AEF79D8B2712FE1800D7554B /* AppSettingsVM.swift in Sources */, + A5AD70182C00766400A36318 /* CGRect+Ext.swift in Sources */, A5F29BCC2A2622BD00ED700A /* DeleteAllProfileDataUseCase.swift in Sources */, 010C8780249A4D55002FE526 /* SAInfoVC.m in Sources */, 01F8857522E6167100D18373 /* SADownloadUserIcons.m in Sources */, @@ -6077,6 +6160,7 @@ A51BE90B2AAAF29D00718F2F /* SetpointType.swift in Sources */, A56233E12AB0F5B1001CB948 /* EditQuartersDialogVM.swift in Sources */, A5074BB82BCE58CA0081B6B1 /* BaseWindowVM.swift in Sources */, + A5AD701F2C00B45E00A36318 /* WindowControlButton.swift in Sources */, A50E5D7C2BFFD67D00303BAE /* ChannelBaseActionUseCase.swift in Sources */, A5D837D22AF1061C002A420D /* ThermometerHistoryDetailVM.swift in Sources */, 408034C41BC83D1A007666E7 /* MGSwipeButton.m in Sources */, @@ -6092,6 +6176,7 @@ A5F29B6F2A1E305500ED700A /* Repository.swift in Sources */, AE929A8B2761487700B75715 /* NavigationCoordinator.swift in Sources */, 01BDAC7522C518DE00915646 /* SARestApiClientTask.m in Sources */, + A5AD70272C04656700A36318 /* LeftMiddleRightControlButton.swift in Sources */, 401CA1BF1BA067DB00117AF4 /* main.m in Sources */, A5F29BFE2A2DC19500ED700A /* CoreDataMigrationVersion.swift in Sources */, 018CFD2623281AF900888CB7 /* SAThermostatHPExtendedValue.m in Sources */, @@ -6167,6 +6252,7 @@ A52BFEB12B06163B00A2F64C /* SADateTimePicker.swift in Sources */, A57668F92AEAB12D0025509D /* HideableValue.swift in Sources */, A58317002B645814006113F8 /* ThermometerAndHumidityValueStringProvider.swift in Sources */, + A50E5D702BFF4CAF00303BAE /* ChannelBaseActionUseCase.swift in Sources */, A5477DCB2AA5EC4000220B4A /* CreateChannelWithChildrenUseCase.swift in Sources */, A5E4904D2A3C6250006801FE /* TemperatureUnit.swift in Sources */, A5F29BB02A24C12500ED700A /* UpdateLocationUseCase.swift in Sources */, @@ -6176,6 +6262,7 @@ A5F29BC62A24E5DE00ED700A /* ChangeChannelGroupRelationsVisibilityUseCase.swift in Sources */, A530EE532A5D437C00F8DAEE /* TimerProgressView.swift in Sources */, A55A8D962BAB14A700C540D4 /* SuplaRollerShutterFlag.swift in Sources */, + A50E5D7A2BFF552C00303BAE /* CurtainView.swift in Sources */, A530EE082A557EE400F8DAEE /* CircleControlButtonView.swift in Sources */, A57C88302ACAA445000B0F10 /* ValuesFormatter.swift in Sources */, A52BFEB92B065BF800A2F64C /* SuplaConfigResult.swift in Sources */, @@ -6221,6 +6308,7 @@ A5F29BDA2A268DDE00ED700A /* DisposeBagContainer.swift in Sources */, A5B3CBFE2B6274B800F95AC3 /* BaseDistanceValueStringProvider.swift in Sources */, A5F29BD62A2623E500ED700A /* UserIconRepository.swift in Sources */, + A5AD70252C012F5300A36318 /* WindowControls.swift in Sources */, A56D5ACD2A4C304C004F45DA /* SingleCall.swift in Sources */, A5A14A442B6143AC004B1598 /* SuplaChannelGeneralPurposeMeterConfig.swift in Sources */, AED6395E270CA3A800E5105B /* RoundedButton.swift in Sources */, @@ -6286,6 +6374,7 @@ 01E517C622C6AD7D000FE77A /* SADownloadMeasurements.m in Sources */, A58316FE2B6452AB006113F8 /* ThermometerAndHumidityValueProvider.swift in Sources */, A5B3CBF62B625E2400F95AC3 /* HumidityValueProvider.swift in Sources */, + A50E5D732BFF543500303BAE /* CurtainVM.swift in Sources */, A56F15F12A2E698400C2E21B /* DeleteEntitiesMigrationPolicy.swift in Sources */, A56233E32AB1903C001CB948 /* Float+Ext.swift in Sources */, A5477DB62AA1F17600220B4A /* DeleteRemovableChannelRelationsUseCase.swift in Sources */, @@ -6311,6 +6400,7 @@ A5074BC92BCE682C0081B6B1 /* BaseWallWindowView.swift in Sources */, 01CEABFA25B9920A009C3BFD /* SADigiglassDetailView.m in Sources */, 013F7EC123EB33E30061A497 /* SAChannelStateExtendedValue.m in Sources */, + A5AD70292C04691100A36318 /* MoveTimeView.swift in Sources */, A52BFEB52B0659FD00A2F64C /* BaseConfigEventsManager.swift in Sources */, A50B5D4A2BFCBCDF00918D18 /* DimmerGroupActivePercentageProvider.swift in Sources */, A573B39229DEB25900EBAFC4 /* ViewState.swift in Sources */, @@ -6329,6 +6419,7 @@ A55A8D912BAB13B300C540D4 /* RollerShutterValue.swift in Sources */, A530EDFD2A54153000F8DAEE /* SwitchDetailNavigationCoordinator.swift in Sources */, AEBCD8FC26E4D247001904F3 /* TemperaturePresenter.swift in Sources */, + A5AD70212C00B6AF00A36318 /* WindowVerticalControls.swift in Sources */, A51BE91B2AAB0DF900718F2F /* SuplaConfigIntegrator.m in Sources */, A503ABBC2B6BB7FF008CDA1F /* ThermometerValueFormatter.swift in Sources */, 407D4AFF1BC6E491009A5505 /* StatusVC.m in Sources */, @@ -6404,6 +6495,7 @@ A54A065B2AF4E3C300C03DBC /* SAOAuthToken+Mock.swift in Sources */, A59AB8B52A30795B00D91F1F /* SceneUseCasesMocks.swift in Sources */, A5DA31072AC16B21008179DB /* InsertChannelRelationForProfileUseCaseTests.swift in Sources */, + A5AD702C2C04773F00A36318 /* CurtainVMTests.swift in Sources */, A54A068C2AF97E0000C03DBC /* LoadChannelWithChildrenMeasurementsDateRangeUseCaseTests.swift in Sources */, A5074BA42BC90C800081B6B1 /* LoadActiveProfileUrlUseCaseTests.swift in Sources */, A57638C629E5D4C9003E15A3 /* XCTestCaseExtensions.swift in Sources */, diff --git a/SUPLA/ChannelCell.m b/SUPLA/ChannelCell.m index 1dcb9925..83e13ffc 100644 --- a/SUPLA/ChannelCell.m +++ b/SUPLA/ChannelCell.m @@ -298,6 +298,9 @@ -(void) updateCellView { case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER: case SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW: case SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND: + case SUPLA_CHANNELFNC_TERRACE_AWNING: + case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: + case SUPLA_CHANNELFNC_CURTAIN: self.left_OnlineStatus.hidden = YES; self.right_OnlineStatus.hidden = NO; break; @@ -386,6 +389,9 @@ -(void) updateCellView { case SUPLA_CHANNELFNC_VALVE_OPENCLOSE: case SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW: case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER: + case SUPLA_CHANNELFNC_TERRACE_AWNING: + case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: + case SUPLA_CHANNELFNC_CURTAIN: br = [MGSwipeButton buttonWithTitle:NSLocalizedString(@"Open", nil) icon:nil backgroundColor:[UIColor blackColor]]; bl = [MGSwipeButton buttonWithTitle:NSLocalizedString(@"Close", nil) icon:nil backgroundColor:[UIColor blackColor]]; break; diff --git a/SUPLA/Core/Extensions/CGRect+Ext.swift b/SUPLA/Core/Extensions/CGRect+Ext.swift new file mode 100644 index 00000000..083f1e29 --- /dev/null +++ b/SUPLA/Core/Extensions/CGRect+Ext.swift @@ -0,0 +1,27 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +extension CGRect { + func narrowToLeft(by: CGFloat) -> CGRect { + return CGRect(origin: origin, size: CGSize(width: width - by, height: height)) + } + + func narrowToRight(by: CGFloat) -> CGRect { + return CGRect(x: minX + by, y: minY, width: width - by, height: height) + } +} diff --git a/SUPLA/Core/Extensions/Float+Ext.swift b/SUPLA/Core/Extensions/Float+Ext.swift index cf536ecd..acd84339 100644 --- a/SUPLA/Core/Extensions/Float+Ext.swift +++ b/SUPLA/Core/Extensions/Float+Ext.swift @@ -44,11 +44,11 @@ extension CGFloat: ScopeFunctions { get { Float(self) } } - func toPercentage(max: CGFloat = 1) -> CGFloat { + func limit(min: CGFloat = 0, max: CGFloat = 1) -> CGFloat { if (self > max) { return max - } else if (self < 0) { - return 0 + } else if (self < min) { + return min } else { return self } diff --git a/SUPLA/Core/Extensions/UIView+Ext.swift b/SUPLA/Core/Extensions/UIView+Ext.swift index a96bb278..5ba8d233 100644 --- a/SUPLA/Core/Extensions/UIView+Ext.swift +++ b/SUPLA/Core/Extensions/UIView+Ext.swift @@ -57,4 +57,26 @@ extension UIView { context.drawPath(using: .stroke) } } + + func drawGlass(_ context: CGContext, _ glassRect: CGRect, _ colors: [CGColor]) { + context.saveGState() + + context.beginPath() + context.addRect(glassRect) + context.closePath() + context.clip() + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colorLocations: [CGFloat] = [0.0, 1.0] + + let startPoint = CGPoint(x: 0, y: glassRect.minY) + let endPoint = CGPoint(x: 0, y: glassRect.maxY) + + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)! + + context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) + context.fillPath() + + context.restoreGState() + } } diff --git a/SUPLA/Core/UI/Details/StandardDetailVC.swift b/SUPLA/Core/UI/Details/StandardDetailVC.swift index 206916f4..20d34574 100644 --- a/SUPLA/Core/UI/Details/StandardDetailVC.swift +++ b/SUPLA/Core/UI/Details/StandardDetailVC.swift @@ -93,6 +93,9 @@ class StandardDetailVC viewControllers.append(terraceAwningDetail()) case .projectorScreen: viewControllers.append(projectorScreenDetail()) + case .curtain: + viewControllers.append(curtainDetail()) + } } @@ -263,6 +266,17 @@ class StandardDetailVC ) return vc } + + private func curtainDetail() -> CurtainVC { + let vc = CurtainVC(itemBundle: item) + vc.navigationCoordinator = navigationCoordinator + vc.tabBarItem = UITabBarItem( + title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, + image: .iconGeneral, + tag: DetailTabTag.Window.rawValue + ) + return vc + } } protocol NavigationItemProvider: AnyObject { diff --git a/SUPLA/Core/UI/TableView/BaseTableViewModel.swift b/SUPLA/Core/UI/TableView/BaseTableViewModel.swift index 41bf47e5..495cb159 100644 --- a/SUPLA/Core/UI/TableView/BaseTableViewModel.swift +++ b/SUPLA/Core/UI/TableView/BaseTableViewModel.swift @@ -62,7 +62,8 @@ class BaseTableViewModel: BaseViewModel { SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, SUPLA_CHANNELFNC_TERRACE_AWNING, - SUPLA_CHANNELFNC_PROJECTOR_SCREEN: + SUPLA_CHANNELFNC_PROJECTOR_SCREEN, + SUPLA_CHANNELFNC_CURTAIN: return true case SUPLA_CHANNELFNC_LIGHTSWITCH, SUPLA_CHANNELFNC_POWERSWITCH, diff --git a/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift b/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift index f8e13736..59872405 100644 --- a/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift +++ b/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift @@ -195,7 +195,8 @@ class ChannelBaseTableViewController { SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, SUPLA_CHANNELFNC_TERRACE_AWNING, - SUPLA_CHANNELFNC_PROJECTOR_SCREEN: true + SUPLA_CHANNELFNC_PROJECTOR_SCREEN, + SUPLA_CHANNELFNC_CURTAIN: true default: false } } @@ -116,7 +117,8 @@ final class IconCell: BaseCell { SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, SUPLA_CHANNELFNC_TERRACE_AWNING, - SUPLA_CHANNELFNC_PROJECTOR_SCREEN: true + SUPLA_CHANNELFNC_PROJECTOR_SCREEN, + SUPLA_CHANNELFNC_CURTAIN: true default: false } } diff --git a/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVC.swift b/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVC.swift index 582bcbc6..7e67942d 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVC.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVC.swift @@ -24,38 +24,12 @@ private let TOP_VIEW_HEIGHT: CGFloat = 80 class BaseWindowVC, S: BaseWindowViewState, VM: BaseWindowVM>: BaseViewControllerVM { let itemBundle: ItemBundle + var windowControls: WindowControls + lazy var topView: BlindsTopView = .init() lazy var windowView: WV = getWindowView() - private let buttonsPositionGuide: UILayoutGuide = .init() - - private lazy var leftControlButton: UpDownControlButton = { - let button = UpDownControlButton() - button.upIcon = .iconArrowUp - button.downIcon = .iconArrowDown - return button - }() - - private lazy var rightControlButton: UpDownControlButton = { - let button = UpDownControlButton() - button.upIcon = .iconArrowOpen - button.downIcon = .iconArrowClose - return button - }() - - private lazy var stopControlButton: CircleControlButtonView = { - let button = CircleControlButtonView(size: 64) - button.translatesAutoresizingMaskIntoConstraints = false - button.icon = .suplaIcon(icon: .iconStop) - return button - }() - - private lazy var moveTimeView: MoveTimeView = { - let view = MoveTimeView() - return view - }() - private lazy var issuesView: IssuesView = { let view = IssuesView() return view @@ -69,8 +43,9 @@ class BaseWindowVC, S: BaseWindowViewSta private lazy var dynamicConstraints: [NSLayoutConstraint] = [] - init(itemBundle: ItemBundle, viewModel: VM) { + init(itemBundle: ItemBundle, viewModel: VM, windowControls: WindowControls = WindowVerticalControls()) { self.itemBundle = itemBundle + self.windowControls = windowControls super.init(nibName: nil, bundle: nil) self.viewModel = viewModel } @@ -146,16 +121,11 @@ class BaseWindowVC, S: BaseWindowViewSta topView.valueBottom = nil } - leftControlButton.isEnabled = !state.offline - rightControlButton.isEnabled = !state.offline - stopControlButton.isEnabled = !state.offline + windowControls.isEnabled = !state.offline topView.offline = state.offline windowView.isEnabled = !state.offline - moveTimeView.isHidden = state.touchTime == nil - if let touchTime = state.touchTime { - moveTimeView.value = String(format: "%.1fs", touchTime) - } + windowControls.moveTime = state.touchTime updateDynamicConstraints() } @@ -163,12 +133,8 @@ class BaseWindowVC, S: BaseWindowViewSta private func setupView() { view.addSubview(topView) view.addSubview(windowView) - view.addSubview(leftControlButton) - view.addSubview(rightControlButton) - view.addSubview(stopControlButton) - view.addSubview(moveTimeView) + view.addSubview(windowControls) view.addSubview(issuesView) - view.addLayoutGuide(buttonsPositionGuide) view.addSubview(slatTiltSlider) setupWindowGesturesObservers() @@ -176,21 +142,9 @@ class BaseWindowVC, S: BaseWindowViewSta windowView.rxPosition .subscribe(onNext: { [weak self] in self?.topView.valueTop = self?.viewModel.positionToString($0) }) .disposed(by: self) - viewModel.bind(leftControlButton.rx.touchDown) { [weak self] type in - guard let bundle = self?.itemBundle else { return } - self?.viewModel.handleAction(type.leftAction, remoteId: bundle.remoteId, type: bundle.subjectType) - } - viewModel.bind(leftControlButton.rx.touchUp) { [weak self] _ in - guard let bundle = self?.itemBundle else { return } - self?.viewModel.handleAction(.stop, remoteId: bundle.remoteId, type: bundle.subjectType) - } - viewModel.bind(rightControlButton.rx.tap) { [weak self] type in + viewModel.bind(windowControls.action) { [weak self] action in guard let bundle = self?.itemBundle else { return } - self?.viewModel.handleAction(type.rightAction, remoteId: bundle.remoteId, type: bundle.subjectType) - } - viewModel.bind(stopControlButton.tapObservable) { [weak self] in - guard let bundle = self?.itemBundle else { return } - self?.viewModel.handleAction(.stop, remoteId: bundle.remoteId, type: bundle.subjectType) + self?.viewModel.handleAction(action, remoteId: bundle.remoteId, type: bundle.subjectType) } viewModel.bind(topView.rx.calibrate) { [weak self] in guard let bundle = self?.itemBundle else { return } @@ -230,20 +184,9 @@ class BaseWindowVC, S: BaseWindowViewSta windowView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: Dimens.distanceDefault), windowView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -42), - buttonsPositionGuide.topAnchor.constraint(equalTo: windowView.bottomAnchor, constant: Dimens.distanceSmall), - buttonsPositionGuide.heightAnchor.constraint(equalToConstant: UP_DOWN_CONTROLL_BUTTON_HEIGHT), - - leftControlButton.centerYAnchor.constraint(equalTo: buttonsPositionGuide.centerYAnchor), - leftControlButton.rightAnchor.constraint(equalTo: view.centerXAnchor, constant: -56), - - rightControlButton.centerYAnchor.constraint(equalTo: buttonsPositionGuide.centerYAnchor), - rightControlButton.leftAnchor.constraint(equalTo: view.centerXAnchor, constant: 56), - - stopControlButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - stopControlButton.centerYAnchor.constraint(equalTo: buttonsPositionGuide.centerYAnchor), - - moveTimeView.topAnchor.constraint(equalTo: windowView.bottomAnchor, constant: Dimens.distanceDefault), - moveTimeView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + windowControls.topAnchor.constraint(equalTo: windowView.bottomAnchor, constant: Dimens.distanceSmall), + windowControls.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Dimens.distanceDefault), + windowControls.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Dimens.distanceDefault), issuesView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Dimens.distanceDefault), issuesView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Dimens.distanceDefault), @@ -265,11 +208,12 @@ class BaseWindowVC, S: BaseWindowViewSta if (slatTiltSlider.isHidden) { dynamicConstraints.append( - issuesView.topAnchor.constraint(equalTo: buttonsPositionGuide.bottomAnchor, constant: Dimens.distanceSmall) + issuesView.topAnchor.constraint(equalTo: windowControls.bottomAnchor, constant: Dimens.distanceSmall) + ) } else { dynamicConstraints.append(contentsOf: [ - slatTiltSlider.topAnchor.constraint(equalTo: buttonsPositionGuide.bottomAnchor, constant: Dimens.distanceSmall), + slatTiltSlider.topAnchor.constraint(equalTo: windowControls.bottomAnchor, constant: Dimens.distanceSmall), slatTiltSlider.widthAnchor.constraint(equalToConstant: 240), slatTiltSlider.heightAnchor.constraint(equalToConstant: 40), slatTiltSlider.centerXAnchor.constraint(equalTo: view.centerXAnchor), @@ -553,64 +497,6 @@ class TopView: UIView { } } -private class MoveTimeView: UIView { - var value: String? { - get { label.text } - set { - label.text = newValue - setNeedsLayout() // because label width will change - } - } - - private lazy var iconView: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - view.image = .iconTouchHand?.withRenderingMode(.alwaysTemplate) - view.tintColor = .black - return view - }() - - private lazy var label: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = .body2 - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - translatesAutoresizingMaskIntoConstraints = false - - addSubview(iconView) - addSubview(label) - - setupLayout() - } - - private func setupLayout() { - NSLayoutConstraint.activate([ - iconView.centerXAnchor.constraint(equalTo: centerXAnchor), - iconView.topAnchor.constraint(equalTo: topAnchor), - - label.topAnchor.constraint(equalTo: iconView.bottomAnchor), - label.centerXAnchor.constraint(equalTo: centerXAnchor) - ]) - } - - override class var requiresConstraintBasedLayout: Bool { - return true - } -} - private class IssuesView: UIStackView { override var intrinsicContentSize: CGSize { CGSize(width: UIView.noIntrinsicMetric, height: Dimens.iconSizeList * CGFloat(issues.count)) @@ -661,17 +547,19 @@ private class IssuesView: UIStackView { } extension ControlButtonType { - var leftAction: RollerShutterAction { + var holdAction: RollerShutterAction { switch (self) { - case .up: .moveUp - case .down: .moveDown + case .up, .left: .moveUp + case .down, .right: .moveDown + case .middle: .stop } } - var rightAction: RollerShutterAction { + var pressAction: RollerShutterAction { switch (self) { - case .up: .open - case .down: .close + case .up, .left: .open + case .down, .right: .close + case .middle: .stop } } } diff --git a/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVM.swift b/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVM.swift index b7f25ddc..664866e5 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVM.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/BaseWindowVM.swift @@ -232,6 +232,10 @@ private extension Completable { } } +enum ShadingSystemOrientation { + case vertical, horizontal +} + enum BaseWindowViewEvent: ViewEvent { case showCalibrationDialog case showAuthorizationDialog diff --git a/SUPLA/Features/Details/WindowDetail/Base/Model/CurtainWindowState.swift b/SUPLA/Features/Details/WindowDetail/Base/Model/CurtainWindowState.swift new file mode 100644 index 00000000..cad19762 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/Model/CurtainWindowState.swift @@ -0,0 +1,33 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +struct CurtainWindowState: WindowState, Equatable, Changeable { + /** + * The blind roller position in percentage + * 0 - open + * 100 - closed + */ + var position: WindowGroupedValue + + var positionTextFormat: WindowGroupedValueFormat = .percentage + + /** + * Used for groups - shows positions of single roller shutter + */ + var markers: [CGFloat] = [] +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/ControlButtonType.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/ControlButtonType.swift new file mode 100644 index 00000000..443174fe --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/ControlButtonType.swift @@ -0,0 +1,72 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import Foundation + +enum ControlButtonType { + case up, down, left, right, middle + + var maskedCorners: CACornerMask { + switch (self) { + case .up: + [.layerMinXMinYCorner, .layerMaxXMinYCorner] + case .down: + [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + case .left: + [.layerMinXMinYCorner, .layerMinXMaxYCorner] + case .right: + [.layerMaxXMinYCorner, .layerMaxXMaxYCorner] + case .middle: [] + } + } + + var roundingCorners: UIRectCorner { + switch (self) { + case .up: + [.topLeft, .topRight] + case .down: + [.bottomLeft, .bottomRight] + case .left: + [.topLeft, .bottomLeft] + case .right: + [.topRight, .bottomRight] + case .middle: [] + } + } + + var width: CGFloat { + switch (self) { + case .up, .down: 64 + case .left, .right, .middle: 94 + } + } + + var height: CGFloat { + switch (self) { + case .up, .down: 94 + case .left, .right, .middle: 64 + } + } + + var cornerRadius: CGFloat { + switch (self) { + case .up, .down, .left, .right: 32 + case .middle: 0 + } + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftMiddleRightControlButton.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftMiddleRightControlButton.swift new file mode 100644 index 00000000..3b9b19ef --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftMiddleRightControlButton.swift @@ -0,0 +1,124 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxCocoa +import RxRelay +import RxSwift + + +// MARK: - Button self + +class LeftMiddleRightControlButton: UIView { + override var intrinsicContentSize: CGSize { + CGSize(width: ControlButtonType.left.width * 2, height: ControlButtonType.left.height) + } + + var isEnabled: Bool = true { + didSet { + leftButton.isEnabled = isEnabled + rightButton.isEnabled = isEnabled + } + } + + var leftIcon: UIImage? { + get { leftButton.icon } + set { leftButton.icon = newValue } + } + + var middleIcon: UIImage? { + get { middleButton.icon } + set { middleButton.icon = newValue } + } + + var rightIcon: UIImage? { + get { rightButton.icon } + set { rightButton.icon = newValue } + } + + fileprivate lazy var leftButton: WindowControlButton = .init(buttonType: .left) + fileprivate lazy var middleButton: WindowControlButton = .init(buttonType: .middle) + fileprivate lazy var rightButton: WindowControlButton = .init(buttonType: .right) + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + + addSubview(leftButton) + addSubview(middleButton) + addSubview(rightButton) + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + middleButton.centerXAnchor.constraint(equalTo: centerXAnchor), + middleButton.topAnchor.constraint(equalTo: topAnchor), + middleButton.widthAnchor.constraint(equalToConstant: ControlButtonType.middle.width), + + leftButton.leftAnchor.constraint(equalTo: leftAnchor), + leftButton.topAnchor.constraint(equalTo: topAnchor), + leftButton.rightAnchor.constraint(equalTo: middleButton.leftAnchor), + + rightButton.leftAnchor.constraint(equalTo: middleButton.rightAnchor), + rightButton.rightAnchor.constraint(equalTo: rightAnchor), + rightButton.topAnchor.constraint(equalTo: topAnchor), + ]) + } + + override class var requiresConstraintBasedLayout: Bool { + return true + } +} + +extension Reactive where Base: LeftMiddleRightControlButton { + var tap: Observable { + Observable.merge( + base.leftButton.rx.tap.map { .up }, + base.rightButton.rx.tap.map { .down }, + base.middleButton.rx.tap.map { .middle } + ) + } + + var touchDown: Observable { + Observable.merge( + base.leftButton.rx.touchDown.map { .up }, + base.rightButton.rx.touchDown.map { .down }, + base.middleButton.rx.tap.map { .middle } + ) + } + + var touchUp: Observable { + Observable.merge( + base.leftButton.rx.touchUp.map { .up }, + base.rightButton.rx.touchUp.map { .down }, + base.middleButton.rx.tap.map { .middle } + ) + } +} + + diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftRightControlButton.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftRightControlButton.swift new file mode 100644 index 00000000..daa9d509 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/LeftRightControlButton.swift @@ -0,0 +1,110 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxCocoa +import RxRelay +import RxSwift + + +// MARK: - Button self + +class LeftRightControlButton: UIView { + override var intrinsicContentSize: CGSize { + CGSize(width: ControlButtonType.left.width * 2, height: ControlButtonType.left.height) + } + + var isEnabled: Bool = true { + didSet { + leftButton.isEnabled = isEnabled + rightButton.isEnabled = isEnabled + } + } + + var leftIcon: UIImage? { + get { leftButton.icon } + set { leftButton.icon = newValue } + } + + var rightIcon: UIImage? { + get { rightButton.icon } + set { rightButton.icon = newValue } + } + + fileprivate lazy var leftButton: WindowControlButton = .init(buttonType: .left) + fileprivate lazy var rightButton: WindowControlButton = .init(buttonType: .right) + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + + addSubview(leftButton) + addSubview(rightButton) + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + leftButton.leftAnchor.constraint(equalTo: leftAnchor), + leftButton.topAnchor.constraint(equalTo: topAnchor), + leftButton.rightAnchor.constraint(equalTo: centerXAnchor), + + rightButton.leftAnchor.constraint(equalTo: centerXAnchor), + rightButton.rightAnchor.constraint(equalTo: rightAnchor), + rightButton.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + override class var requiresConstraintBasedLayout: Bool { + return true + } +} + +extension Reactive where Base: LeftRightControlButton { + var tap: Observable { + Observable.merge( + base.leftButton.rx.tap.map { .up }, + base.rightButton.rx.tap.map { .down } + ) + } + + var touchDown: Observable { + Observable.merge( + base.leftButton.rx.touchDown.map { .up }, + base.rightButton.rx.touchDown.map { .down } + ) + } + + var touchUp: Observable { + Observable.merge( + base.leftButton.rx.touchUp.map { .up }, + base.rightButton.rx.touchUp.map { .down } + ) + } +} + + diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/UpDownControllButton.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/UpDownControllButton.swift new file mode 100644 index 00000000..84466f01 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/UpDownControllButton.swift @@ -0,0 +1,107 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxCocoa +import RxRelay +import RxSwift + + +// MARK: - Button self + +class UpDownControlButton: UIView { + override var intrinsicContentSize: CGSize { + CGSize(width: ControlButtonType.up.width, height: ControlButtonType.up.height * 2) + } + + var isEnabled: Bool = true { + didSet { + upButton.isEnabled = isEnabled + downButton.isEnabled = isEnabled + } + } + + var upIcon: UIImage? { + get { upButton.icon } + set { upButton.icon = newValue } + } + + var downIcon: UIImage? { + get { downButton.icon } + set { downButton.icon = newValue } + } + + fileprivate lazy var upButton: WindowControlButton = .init(buttonType: .up) + fileprivate lazy var downButton: WindowControlButton = .init(buttonType: .down) + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + + addSubview(upButton) + addSubview(downButton) + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + upButton.leftAnchor.constraint(equalTo: leftAnchor), + upButton.topAnchor.constraint(equalTo: topAnchor), + + downButton.leftAnchor.constraint(equalTo: leftAnchor), + downButton.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + override class var requiresConstraintBasedLayout: Bool { + return true + } +} + +extension Reactive where Base: UpDownControlButton { + var tap: Observable { + Observable.merge( + base.upButton.rx.tap.map { .up }, + base.downButton.rx.tap.map { .down } + ) + } + + var touchDown: Observable { + Observable.merge( + base.upButton.rx.touchDown.map { .up }, + base.downButton.rx.touchDown.map { .down } + ) + } + + var touchUp: Observable { + Observable.merge( + base.upButton.rx.touchUp.map { .up }, + base.downButton.rx.touchUp.map { .down } + ) + } +} + diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/UpDownControllButton.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControlButton.swift similarity index 66% rename from SUPLA/Features/Details/WindowDetail/Base/UI/UpDownControllButton.swift rename to SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControlButton.swift index 4d747c36..cf9b0d44 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/UpDownControllButton.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControlButton.swift @@ -20,122 +20,10 @@ import RxCocoa import RxRelay import RxSwift -private let BUTTON_WIDTH: CGFloat = 64 -private let BUTTON_HEIGHT: CGFloat = 94 - -let UP_DOWN_CONTROLL_BUTTON_HEIGHT = BUTTON_HEIGHT * 2 - -// MARK: - Button type - -enum ControlButtonType { - case up, down - - fileprivate var maskedCorners: CACornerMask { - switch (self) { - case .up: - [.layerMinXMinYCorner, .layerMaxXMinYCorner] - case .down: - [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - } - } - - fileprivate var roundingCorners: UIRectCorner { - switch (self) { - case .up: - [.topLeft, .topRight] - case .down: - [.bottomLeft, .bottomRight] - } - } -} - -// MARK: - Button self - -class UpDownControlButton: UIView { - override var intrinsicContentSize: CGSize { - CGSize(width: BUTTON_WIDTH, height: UP_DOWN_CONTROLL_BUTTON_HEIGHT) - } - - var isEnabled: Bool = true { - didSet { - upButton.isEnabled = isEnabled - downButton.isEnabled = isEnabled - } - } - - var upIcon: UIImage? { - get { upButton.icon } - set { upButton.icon = newValue } - } - - var downIcon: UIImage? { - get { downButton.icon } - set { downButton.icon = newValue } - } - - fileprivate lazy var upButton: ControlButton = .init(buttonType: .up) - fileprivate lazy var downButton: ControlButton = .init(buttonType: .down) - - override init(frame: CGRect) { - super.init(frame: frame) - setupView() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - translatesAutoresizingMaskIntoConstraints = false - - addSubview(upButton) - addSubview(downButton) - - setupLayout() - } - - private func setupLayout() { - NSLayoutConstraint.activate([ - upButton.leftAnchor.constraint(equalTo: leftAnchor), - upButton.topAnchor.constraint(equalTo: topAnchor), - - downButton.leftAnchor.constraint(equalTo: leftAnchor), - downButton.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - } - - override class var requiresConstraintBasedLayout: Bool { - return true - } -} - -extension Reactive where Base: UpDownControlButton { - var tap: Observable { - Observable.merge( - base.upButton.rx.tap.map { .up }, - base.downButton.rx.tap.map { .down } - ) - } - - var touchDown: Observable { - Observable.merge( - base.upButton.rx.touchDown.map { .up }, - base.downButton.rx.touchDown.map { .down } - ) - } - - var touchUp: Observable { - Observable.merge( - base.upButton.rx.touchUp.map { .up }, - base.downButton.rx.touchUp.map { .down } - ) - } -} // MARK: - Control button -private class ControlButton: UIView { +class WindowControlButton: UIView { var isEnabled: Bool = true { didSet { disabledOverlay.isHidden = isEnabled @@ -148,7 +36,7 @@ private class ControlButton: UIView { } override var intrinsicContentSize: CGSize { - CGSize(width: BUTTON_WIDTH, height: BUTTON_HEIGHT) + CGSize(width: controlType.width, height: controlType.height) } private lazy var iconView: UIImageView = { @@ -169,7 +57,7 @@ private class ControlButton: UIView { private lazy var disabledOverlay: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false - view.layer.cornerRadius = BUTTON_WIDTH / 2 + view.layer.cornerRadius = controlType.cornerRadius view.backgroundColor = .disabledOverlay view.isHidden = true return view @@ -228,7 +116,7 @@ private class ControlButton: UIView { BaseControlButtonView.setupLayer(layer) - layer.cornerRadius = BUTTON_WIDTH / 2 + layer.cornerRadius = controlType.cornerRadius layer.maskedCorners = controlType.maskedCorners disabledOverlay.layer.maskedCorners = controlType.maskedCorners @@ -261,7 +149,7 @@ private class ControlButton: UIView { } } -extension Reactive where Base: ControlButton { +extension Reactive where Base: WindowControlButton { var tap: Observable { base.tapRelay.asObservable() } var touchDown: Observable { base.touchDownRelay.asObservable() } @@ -300,19 +188,19 @@ private class InnerShadowView: UIView { innerShadow.shadowOffset = CGSize(width: 0, height: 3) innerShadow.shadowOpacity = 0.4 innerShadow.shadowRadius = 3 - innerShadow.cornerRadius = frame.size.width / 2 + innerShadow.cornerRadius = type.cornerRadius innerShadow.maskedCorners = type.maskedCorners } private func setupView() { layer.addSublayer(innerShadow) - layer.cornerRadius = BUTTON_WIDTH / 2 + layer.cornerRadius = type.cornerRadius layer.maskedCorners = type.maskedCorners } private func createPath() -> CGPath { // Shadow path (1pt ring around bounds) - let radius = frame.size.width / 2 + let radius = type.cornerRadius let radiusSize = CGSize(width: radius, height: radius) let path = UIBezierPath(roundedRect: innerShadow.frame.insetBy(dx: -3, dy: -3), byRoundingCorners: type.roundingCorners, cornerRadii: radiusSize) diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControls.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControls.swift new file mode 100644 index 00000000..6e9c358b --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowControls.swift @@ -0,0 +1,33 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +class WindowControls: UIView { + var isEnabled: Bool { + get { false } + set {} + } + var moveTime: CGFloat? { + get { nil } + set {} + } + var action: Observable { + get { .empty() } + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowHorizontalControls.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowHorizontalControls.swift new file mode 100644 index 00000000..bfc65a2b --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowHorizontalControls.swift @@ -0,0 +1,105 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxSwift + +class WindowHorizontalControls: WindowControls { + override var intrinsicContentSize: CGSize { + return CGSize(width: ControlButtonType.left.width * 2, height: ControlButtonType.up.height * 2) + } + + override var isEnabled: Bool { + get { holdToMoveButton.isEnabled } + set { + holdToMoveButton.isEnabled = newValue + } + } + + override var moveTime: CGFloat? { + get { nil } + set { + moveTimeView.isHidden = newValue == nil + if let touchTime = newValue { + moveTimeView.value = String(format: "%.1fs", touchTime) + } + } + } + + override var action: Observable { + Observable.merge( + holdToMoveButton.rx.touchDown.map { $0.holdAction }, + holdToMoveButton.rx.touchUp.map { _ in .stop }, + pressToMoveButton.rx.tap.map { $0.pressAction } + ) + } + + fileprivate lazy var holdToMoveButton: LeftRightControlButton = { + let button = LeftRightControlButton() + button.leftIcon = .iconArrowRevealHold + button.rightIcon = .iconArrowCoverHold + return button + }() + + fileprivate lazy var pressToMoveButton: LeftMiddleRightControlButton = { + let button = LeftMiddleRightControlButton() + button.leftIcon = .iconArrowRevealTap + button.middleIcon = .iconStop + button.rightIcon = .iconArrowCoverTap + return button + }() + + private lazy var moveTimeView: MoveTimeView = { + let view = MoveTimeView(textLocation: .right) + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + + addSubview(holdToMoveButton) + addSubview(pressToMoveButton) + addSubview(moveTimeView) + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + holdToMoveButton.centerYAnchor.constraint(equalTo: centerYAnchor), + holdToMoveButton.leftAnchor.constraint(equalTo: leftAnchor), + holdToMoveButton.rightAnchor.constraint(equalTo: rightAnchor), + + pressToMoveButton.topAnchor.constraint(equalTo: holdToMoveButton.bottomAnchor, constant: Dimens.distanceSmall), + pressToMoveButton.leftAnchor.constraint(equalTo: leftAnchor), + pressToMoveButton.rightAnchor.constraint(equalTo: rightAnchor), + + moveTimeView.centerXAnchor.constraint(equalTo: centerXAnchor), + moveTimeView.bottomAnchor.constraint(equalTo: holdToMoveButton.topAnchor, constant: -Dimens.distanceSmall) + ]) + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowVerticalControls.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowVerticalControls.swift new file mode 100644 index 00000000..d708ca72 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Controls/WindowVerticalControls.swift @@ -0,0 +1,117 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +import RxSwift + +class WindowVerticalControls: WindowControls { + override var intrinsicContentSize: CGSize { + let width = ControlButtonType.up.width * 3 + Dimens.distanceDefault * 2 + return CGSize(width: width, height: ControlButtonType.up.height * 2) + } + + override var isEnabled: Bool { + get { leftControlButton.isEnabled && rightControlButton.isEnabled && stopControlButton.isEnabled } + set { + leftControlButton.isEnabled = newValue + rightControlButton.isEnabled = newValue + stopControlButton.isEnabled = newValue + } + } + + override var moveTime: CGFloat? { + get { nil } + set { + moveTimeView.isHidden = newValue == nil + if let touchTime = newValue { + moveTimeView.value = String(format: "%.1fs", touchTime) + } + } + } + + override var action: Observable { + Observable.merge( + leftControlButton.rx.touchDown.map { $0.holdAction }, + leftControlButton.rx.touchUp.map { _ in .stop }, + rightControlButton.rx.tap.map { $0.pressAction }, + stopControlButton.tapObservable.map { _ in .stop } + ) + } + + fileprivate lazy var leftControlButton: UpDownControlButton = { + let button = UpDownControlButton() + button.upIcon = .iconArrowUp + button.downIcon = .iconArrowDown + return button + }() + + fileprivate lazy var rightControlButton: UpDownControlButton = { + let button = UpDownControlButton() + button.upIcon = .iconArrowOpen + button.downIcon = .iconArrowClose + return button + }() + + fileprivate lazy var stopControlButton: CircleControlButtonView = { + let button = CircleControlButtonView(size: ControlButtonType.up.width) + button.translatesAutoresizingMaskIntoConstraints = false + button.icon = .suplaIcon(icon: .iconStop) + return button + }() + + private lazy var moveTimeView: MoveTimeView = { + let view = MoveTimeView() + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + + addSubview(leftControlButton) + addSubview(rightControlButton) + addSubview(stopControlButton) + addSubview(moveTimeView) + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + leftControlButton.centerYAnchor.constraint(equalTo: centerYAnchor), + leftControlButton.rightAnchor.constraint(equalTo: centerXAnchor, constant: -56), + + rightControlButton.centerYAnchor.constraint(equalTo: centerYAnchor), + rightControlButton.leftAnchor.constraint(equalTo: centerXAnchor, constant: 56), + + stopControlButton.centerXAnchor.constraint(equalTo: centerXAnchor), + stopControlButton.centerYAnchor.constraint(equalTo: centerYAnchor), + + moveTimeView.topAnchor.constraint(equalTo: topAnchor), + moveTimeView.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + } +} + diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/Curtain/CurtainView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/Curtain/CurtainView.swift new file mode 100644 index 00000000..fe57ebf4 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/Curtain/CurtainView.swift @@ -0,0 +1,157 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import RxRelay +import RxSwift + +// MARK: - RollerShutterView + +class CurtainView: BaseWallWindowView { + override var windowState: CurtainWindowState? { + didSet { + setNeedsLayout() + } + } + + init() { + super.init(CurtainRuntimeDimens()) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func drawShadowingElements(_ context: CGContext, _ dimens: CurtainRuntimeDimens) { + guard let positionValue = windowState?.position.value, + let markers = windowState?.markers + else { return } + + let position: CGFloat = markers.isEmpty ? positionValue : (markers.max() ?? 0) + let widthDiff = (dimens.leftCurtainRect.width - dimens.curtainMinWidth) * (100 - position) / 100 + + drawCurtain(context, dimens.leftCurtainRect.narrowToLeft(by: widthDiff)) + drawCurtain(context, dimens.rightCurtainRect.narrowToRight(by: widthDiff)) + } + + override func drawMarkers(_ context: CGContext, _ dimens: CurtainRuntimeDimens) { + guard let markers = windowState?.markers else { return } + + markers.forEach { + let widthDiff = (dimens.leftCurtainRect.width - dimens.curtainMinWidth) * (100 - $0) / 100 + + drawMarker(context, dimens.leftCurtainRect.maxX - widthDiff) + drawMarker(context, dimens.rightCurtainRect.minX + widthDiff) + } + } + + override func handleMovement( + _ positionRelay: PublishRelay, + _ startPosition: CGPoint, + _ startPercentage: CGFloat, + _ currentPosition: CGPoint + ) { + let positionDiffAsPercentage = (currentPosition.x - startPosition.x) + .divideToPercentage(value: touchRect.width / 2) + .run { startPosition.x < touchRect.midX ? $0 : -$0 } + + + let position = (startPercentage + positionDiffAsPercentage).limit(max: 100) + windowState?.position = .similar(position) + positionRelay.accept(position) + } + + private func drawCurtain(_ context: CGContext, _ rect: CGRect) { + drawPath(context, fillColor: colors.slatBackground) { UIBezierPath(rect: rect).cgPath } + drawPath(context, strokeColor: colors.slatBorder) { UIBezierPath(rect: rect).cgPath } + } + + private func drawMarker(_ context: CGContext, _ offsetX: CGFloat) { + let transformation = CGAffineTransform(translationX: offsetX-dimens.halfMarkerWidth, y: 0) + dimens.markerPath.apply(transformation) + drawPath(context, fillColor: colors.markerBackground) { dimens.markerPath.cgPath } + drawPath(context, strokeColor: colors.markerBorder) { dimens.markerPath.cgPath } + dimens.markerPath.apply(transformation.inverted()) + } +} + +// MARK: - Runtime dimensions + +class CurtainRuntimeDimens: BaseWallWindowDimens { + static let curtainWidth: CGFloat = 142 + static let curtainHeight: CGFloat = 328 + static let curtainMinWidth: CGFloat = 24 + static let markerWidth: CGFloat = 8 + static let markerHeight: CGFloat = 28 + + var curtainMinWidth: CGFloat = 0 + var leftCurtainRect: CGRect = .zero + var rightCurtainRect: CGRect = .zero + var markerPath: UIBezierPath = .init() + var halfMarkerWidth: CGFloat = 0 + + override func update(_ frame: CGRect) { + super.update(frame) + + curtainMinWidth = CurtainRuntimeDimens.curtainMinWidth * scale + + createLeftCurtainRect() + createRightCurtainRect() + createMarkerPath() + } + + private func createLeftCurtainRect() { + let curtainWidth = CurtainRuntimeDimens.curtainWidth * scale + let curtainHeight = CurtainRuntimeDimens.curtainHeight * scale - topLineRect.height + + leftCurtainRect = CGRect( + x: topLineRect.minX, + y: topLineRect.maxY, + width: curtainWidth, + height: curtainHeight + ) + } + + private func createRightCurtainRect() { + let curtainWidth = CurtainRuntimeDimens.curtainWidth * scale + let curtainHeight = CurtainRuntimeDimens.curtainHeight * scale - topLineRect.height + + rightCurtainRect = CGRect( + x: topLineRect.maxX - curtainWidth, + y: topLineRect.maxY, + width: curtainWidth, + height: curtainHeight + ) + } + + private func createMarkerPath() { + let height = CurtainRuntimeDimens.markerHeight * scale + let width = CurtainRuntimeDimens.markerWidth * scale + halfMarkerWidth = width / 2 + let left = topLineRect.minX + let top = topLineRect.maxY + + markerPath.removeAllPoints() + markerPath.move(to: CGPoint(x: left, y: top)) + markerPath.addLine(to: CGPoint(x: left + halfMarkerWidth, y: top + halfMarkerWidth)) + markerPath.addLine(to: CGPoint(x: left + halfMarkerWidth, y: top + height)) + markerPath.addLine(to: CGPoint(x: left - halfMarkerWidth, y: top + height)) + markerPath.addLine(to: CGPoint(x: left - halfMarkerWidth, y: top + halfMarkerWidth)) + markerPath.close() + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/FacadeBlinds/FacadeBlindsView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/FacadeBlinds/FacadeBlindsView.swift index f48ebd74..04eae372 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/FacadeBlinds/FacadeBlindsView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/FacadeBlinds/FacadeBlindsView.swift @@ -21,7 +21,7 @@ import RxSwift // MARK: - RollerShutterView -class FacadeBlindsView: BaseWallWindowView { +class FacadeBlindsView: BaseWallWindowView { override var windowState: FacadeBlindWindowState? { didSet { @@ -50,7 +50,7 @@ class FacadeBlindsView: BaseWallWindowView { private lazy var markerPath: UIBezierPath = .init() init() { - super.init(RuntimeDimens()) + super.init(FacadeBlindRuntimeDimens()) } @available(*, unavailable) @@ -58,7 +58,7 @@ class FacadeBlindsView: BaseWallWindowView { fatalError("init(coder:) has not been implemented") } - override func updateSlatsPositions(_ slatsLayers: [CAShapeLayer], _ dimens: RuntimeWindowDimens) { + override func drawShadowingElements(_ context: CGContext, _ dimens: FacadeBlindRuntimeDimens) { guard let position = windowState?.position.value else { return @@ -70,62 +70,30 @@ class FacadeBlindsView: BaseWallWindowView { .also { tiltDegrees <= tiltHalfRangeDegrees ? $0 : -$0 } var currentCorrection = dimens.windowRect.height * (1 - position / 100) - for i in 0 ..< DefaultWindowDimens.slatsCount { + + for i in 0 ..< SlatDimens.count { let frame = dimens.slats[i].offsetBy(dx: 0, dy: -currentCorrection) let maxSlatCorrection = frame.height - 4 let verticalSlatCorrection = maxSlatCorrection * correctedTilt / tiltHalfRangeDegrees / 2 - if (frame.maxY < 0) { - slatsLayers[i].frame = CGRect(x: frame.minX, y: 0, width: frame.width, height: 0) + let rect = if (frame.maxY < 0) { + CGRect(x: frame.minX, y: 0, width: frame.width, height: 0) } else if (frame.minY < 0) { - slatsLayers[i].frame = CGRect(x: frame.minX, y: 0, width: frame.width, height: frame.maxY) + CGRect(x: frame.minX, y: 0, width: frame.width, height: frame.maxY) } else { - slatsLayers[i].frame = frame + frame } - if (frame.maxY < dimens.topLineRect.maxY) { - slatsLayers[i].path = nil - } else { - let path = UIBezierPath() - path.move( - to: CGPoint( - x: horizontalSlatCorrection, - y: 1 + verticalSlatCorrection - ) - ) - path.addLine( - to: CGPoint( - x: slatsLayers[i].frame.width - horizontalSlatCorrection, - y: 1 + verticalSlatCorrection - ) - ) - path.addLine( - to: CGPoint( - x: slatsLayers[i].frame.width + horizontalSlatCorrection, - y: frame.height - verticalSlatCorrection - ) - ) - path.addLine( - to: CGPoint( - x: -horizontalSlatCorrection, - y: frame.height - verticalSlatCorrection - ) - ) - path.close() - slatsLayers[i].path = path.cgPath + if (rect.maxY > dimens.topLineRect.maxY) { + let path = getSlatPath(rect, horizontalSlatCorrection, verticalSlatCorrection) + + drawPath(context, fillColor: colors.slatBackground) { path } + drawPath(context, strokeColor: colors.slatBorder) { path } } - - currentCorrection -= dimens.slatDistance } } - override func setupSlat(_ layer: CAShapeLayer, _ colors: WindowColors) { - layer.fillColor = colors.slatBackground.cgColor - layer.strokeColor = colors.slatBorder.cgColor - layer.lineWidth = 1 - } - override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) @@ -137,8 +105,13 @@ class FacadeBlindsView: BaseWallWindowView { } } - override func handleMovement(_ startPosition: CGPoint, _ startPercentage: CGFloat, _ currentPosition: CGPoint) { - super.handleMovement(startPosition, startPercentage, currentPosition) + override func handleMovement( + _ positionRelay: PublishRelay, + _ startPosition: CGPoint, + _ startPercentage: CGFloat, + _ currentPosition: CGPoint + ) { + super.handleMovement(positionRelay, startPosition, startPercentage, currentPosition) guard let startTilt = startTilt else { return } @@ -147,7 +120,7 @@ class FacadeBlindsView: BaseWallWindowView { let tiltDiffAsPercentage = (currentPosition.x - startPosition.x) .divideToPercentage(value: touchRect.width / 2) - let tilt = (startTilt + tiltDiffAsPercentage).toPercentage(max: 100) + let tilt = (startTilt + tiltDiffAsPercentage).limit(max: 100) let position = windowState?.position.value ?? 0 if (tiltChangeAllowed) { @@ -229,6 +202,40 @@ class FacadeBlindsView: BaseWallWindowView { markersLayers.append(markerLayer) } } + + private func getSlatPath( + _ rect: CGRect, + _ horizontalSlatCorrection: CGFloat, + _ verticalSlatCorrection: CGFloat + ) -> CGPath { + dimens.slatPath.removeAllPoints() + dimens.slatPath.move( + to: CGPoint( + x: rect.minX + horizontalSlatCorrection, + y: rect.minY + 1 + verticalSlatCorrection + ) + ) + dimens.slatPath.addLine( + to: CGPoint( + x: rect.maxX - horizontalSlatCorrection, + y: rect.minY + 1 + verticalSlatCorrection + ) + ) + dimens.slatPath.addLine( + to: CGPoint( + x: rect.maxX + horizontalSlatCorrection, + y: rect.maxY - verticalSlatCorrection + ) + ) + dimens.slatPath.addLine( + to: CGPoint( + x: rect.minX - horizontalSlatCorrection, + y: rect.maxY - verticalSlatCorrection + ) + ) + dimens.slatPath.close() + return dimens.slatPath.cgPath + } } extension Reactive where Base: FacadeBlindsView { @@ -243,19 +250,38 @@ extension Reactive where Base: FacadeBlindsView { // MARK: - Runtime dimensions -private class RuntimeDimens: RuntimeWindowDimens { - override func getSlatDistance() -> CGFloat { 0 } +class FacadeBlindRuntimeDimens: BaseWallWindowDimens { + + static let markerInfoRadius: CGFloat = 14 + + var slats: [CGRect] = .init(repeating: .zero, count: SlatDimens.count) + var markerInfoRadius: CGFloat = 0 + var markers: [CGRect] = [] + var slatPath: UIBezierPath = .init() + + override func update(_ frame: CGRect) { + super.update(frame) + + createSlatRects() + + markerInfoRadius = FacadeBlindRuntimeDimens.markerInfoRadius * scale + createMarkersRects() + } + + func setMarkers(_ markersCount: Int) { + markers = .init(repeating: .zero, count: markersCount) + } - override func createSlatRects() { - let slatHorizontalMargin = DefaultWindowDimens.slatHorizontalMargin * scale + private func createSlatRects() { + let slatHorizontalMargin = SlatDimens.horizontalMargin * scale let slatSize = CGSize( width: canvasRect.width - slatHorizontalMargin * 2, - height: DefaultWindowDimens.slatHeight * scale + height: SlatDimens.height * scale ) - let top = windowRect.maxY - CGFloat(DefaultWindowDimens.slatsCount) * slatSize.height + let top = windowRect.maxY - CGFloat(SlatDimens.count) * slatSize.height - for i in 0 ..< DefaultWindowDimens.slatsCount { + for i in 0 ..< SlatDimens.count { slats[i] = CGRect( origin: CGPoint(x: canvasRect.minX + slatHorizontalMargin, y: top + CGFloat(i) * slatSize.height), size: slatSize @@ -263,7 +289,7 @@ private class RuntimeDimens: RuntimeWindowDimens { } } - override func createMarkersRects() { + private func createMarkersRects() { let markerDiameter = markerInfoRadius * 2 for i in 0 ..< markers.count { markers[i] = CGRect( diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/RollerShutter/RollerShutterView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/RollerShutter/RollerShutterView.swift index 71c212b3..1b03c90d 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/RollerShutter/RollerShutterView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/RollerShutter/RollerShutterView.swift @@ -21,7 +21,7 @@ import RxSwift // MARK: - RollerShutterView -class RollerShutterView: BaseWallWindowView { +class RollerShutterView: BaseWallWindowView { override var windowState: RollerShutterWindowState? { didSet { if let markers = windowState?.markers, @@ -37,7 +37,7 @@ class RollerShutterView: BaseWallWindowView { private lazy var markersLayers: [CAShapeLayer] = [] init() { - super.init(RuntimeDimens()) + super.init(RollerShutterRuntimeDimens()) } @available(*, unavailable) @@ -45,7 +45,7 @@ class RollerShutterView: BaseWallWindowView { fatalError("init(coder:) has not been implemented") } - override func updateSlatsPositions(_ slatsLayers: [CAShapeLayer], _ dimens: RuntimeWindowDimens) { + override func drawShadowingElements(_ context: CGContext, _ dimens: RollerShutterRuntimeDimens) { guard let position = windowState?.position.value, let bottomPosition = windowState?.bottomPosition else { @@ -62,16 +62,20 @@ class RollerShutterView: BaseWallWindowView { var availableSpaceForDistances = dimens.slatsDistances * slatDistancesPercentage var currentCorrection = topCorrection + dimens.slatsDistances * slatDistancesPercentage - for i in 0 ..< DefaultWindowDimens.slatsCount { + + for i in 0 ..< SlatDimens.count { let frame = dimens.slats[i].offsetBy(dx: 0, dy: -currentCorrection) - if (frame.maxY < 0) { - slatsLayers[i].frame = CGRect(x: frame.minX, y: 0, width: frame.width, height: 0) + let rect = if (frame.maxY < 0) { + CGRect(x: frame.minX, y: 0, width: frame.width, height: 0) } else if (frame.minY < 0) { - slatsLayers[i].frame = CGRect(x: frame.minX, y: 0, width: frame.width, height: frame.maxY) + CGRect(x: frame.minX, y: 0, width: frame.width, height: frame.maxY) } else { - slatsLayers[i].frame = frame + frame } + drawPath(context, fillColor: colors.slatBackground) { UIBezierPath(rect: rect).cgPath } + drawPath(context, strokeColor: colors.slatBorder) { UIBezierPath(rect: rect).cgPath } + if (availableSpaceForDistances > dimens.slatDistance) { currentCorrection -= dimens.slatDistance } else if (availableSpaceForDistances > 0) { @@ -118,19 +122,44 @@ class RollerShutterView: BaseWallWindowView { // MARK: - Runtime dimensions -private class RuntimeDimens: RuntimeWindowDimens { - override func getSlatDistance() -> CGFloat { DefaultWindowDimens.slatDistance * scale } +class RollerShutterRuntimeDimens: BaseWallWindowDimens { + + static let markerHeight: CGFloat = 8 + static let markerWidth: CGFloat = 28 + + var slats: [CGRect] = .init(repeating: .zero, count: SlatDimens.count) + var markerPath: UIBezierPath = .init() + var markers: [CGRect] = [] + var slatDistance: CGFloat = 0 + var slatsDistances: CGFloat = 0 + + override func update(_ frame: CGRect) { + super.update(frame) + + createSlatRects() + + slatDistance = getSlatDistance() + slatsDistances = slatDistance * CGFloat(SlatDimens.count - 1) + + createMarkersRects() + } + + func setMarkers(_ markersCount: Int) { + markers = .init(repeating: .zero, count: markersCount) + } + + private func getSlatDistance() -> CGFloat { SlatDimens.distance * scale } - override func createSlatRects() { - let slatHorizontalMargin = DefaultWindowDimens.slatHorizontalMargin * scale + private func createSlatRects() { + let slatHorizontalMargin = SlatDimens.horizontalMargin * scale let slatSize = CGSize( width: canvasRect.width - slatHorizontalMargin * 2, - height: DefaultWindowDimens.slatHeight * scale + height: SlatDimens.height * scale ) - let top = windowRect.maxY - CGFloat(DefaultWindowDimens.slatsCount) * slatSize.height + let top = windowRect.maxY - CGFloat(SlatDimens.count) * slatSize.height - for i in 0 ..< DefaultWindowDimens.slatsCount { + for i in 0 ..< SlatDimens.count { slats[i] = CGRect( origin: CGPoint(x: canvasRect.minX + slatHorizontalMargin, y: top + CGFloat(i) * slatSize.height), size: slatSize @@ -138,9 +167,9 @@ private class RuntimeDimens: RuntimeWindowDimens { } } - override func createMarkersRects() { - let markerWidth = DefaultWindowDimens.markerWidth * scale - let markerHeight = DefaultWindowDimens.markerHeight * scale + private func createMarkersRects() { + let markerWidth = RollerShutterRuntimeDimens.markerWidth * scale + let markerHeight = RollerShutterRuntimeDimens.markerHeight * scale let halfHeight = markerHeight / 2 markerPath.removeAllPoints() diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift index 1111feec..5ca20bce 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift @@ -83,7 +83,7 @@ final class TerraceAwningView: BaseWindowView { } private func drawWindow(_ context: CGContext) { - let path = UIBezierPath(roundedRect: dimens.windowRect, cornerRadius: DefaultWindowDimens.windowCornerRadius) + let path = UIBezierPath(roundedRect: dimens.windowRect, cornerRadius: WindowDimens.cornerRadius) drawPath(context, fillColor: colors.window, withShadow: true) { path.cgPath } @@ -96,31 +96,9 @@ final class TerraceAwningView: BaseWindowView { size: CGSize(width: glassWidth, height: glassHeight) ) - drawGlass(context, glassRect) - drawGlass(context, glassRect.offsetBy(dx: glassWidth + glassMargin, dy: 0)) - } - - private func drawGlass(_ context: CGContext, _ glassRect: CGRect) { - context.saveGState() - - context.beginPath() - context.addRect(glassRect) - context.closePath() - context.clip() - - let colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] as CFArray - let colorSpace = CGColorSpaceCreateDeviceRGB() - let colorLocations: [CGFloat] = [0.0, 1.0] - - let startPoint = CGPoint(x: 0, y: glassRect.minY) - let endPoint = CGPoint(x: 0, y: glassRect.maxY) - - let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: colorLocations)! - - context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) - context.fillPath() - - context.restoreGState() + let colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] + drawGlass(context, glassRect, colors) + drawGlass(context, glassRect.offsetBy(dx: glassWidth + glassMargin, dy: 0), colors) } private func drawAwning(_ context: CGContext) { diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/RuntimeWindowDimens.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowDimens.swift similarity index 54% rename from SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/RuntimeWindowDimens.swift rename to SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowDimens.swift index a76966db..5d163c05 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/RuntimeWindowDimens.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowDimens.swift @@ -16,67 +16,34 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -class RuntimeWindowDimens { - var scale: CGFloat = 1 +class BaseWallWindowDimens { + var scale: CGFloat = 0 var canvasRect: CGRect = .zero var topLineRect: CGRect = .zero var windowRect: CGRect = .zero var leftGlassRect: CGRect = .zero var rightGlassRect: CGRect = .zero - var slats: [CGRect] = .init(repeating: .zero, count: DefaultWindowDimens.slatsCount) - var markerInfoRadius: CGFloat = 0 - var markerPath: UIBezierPath = .init() - var markers: [CGRect] = [] - var slatDistance: CGFloat = 0 - var slatsDistances: CGFloat = 0 func update(_ frame: CGRect) { createCanvasRect(frame) - scale = canvasRect.width / DefaultWindowDimens.width + + scale = canvasRect.width / WindowDimens.width createTopLineRect() createWindowRect() createGlassRects() - createSlatRects() - - slatDistance = getSlatDistance() - slatsDistances = slatDistance * CGFloat(DefaultWindowDimens.slatsCount - 1) - - markerInfoRadius = DefaultWindowDimens.markerInfoRadius * scale - createMarkersRects() - } - - func setMarkers(_ markersCount: Int) { - markers = .init(repeating: .zero, count: markersCount) - } - - func createSlatRects() { - fatalError("createSlatRects() needs to be implemented") - } - - func getSlatDistance() -> CGFloat { - fatalError("createSlatDistance() needs to be implemented") - } - - func createMarkersRects() { - fatalError("createMarkersRects() needs to be implemented") - } - - private func createCanvasRect(_ frame: CGRect) { - let size = getSize(frame) - canvasRect = CGRect(origin: CGPoint(x: (frame.width - size.width) / 2.0, y: 0.0), size: size) } private func createTopLineRect() { topLineRect = CGRect( origin: canvasRect.origin, - size: CGSize(width: canvasRect.width, height: DefaultWindowDimens.topLineHeight * scale) + size: CGSize(width: canvasRect.width, height: WindowDimens.topLineHeight * scale) ) } private func createWindowRect() { - let windowHorizontalMargin = DefaultWindowDimens.windowHorizontalMargin * scale + let windowHorizontalMargin = WindowDimens.windowHorizontalMargin * scale let windowTop = topLineRect.height / 2 let windowSize = CGSize( width: canvasRect.width - windowHorizontalMargin * 2, @@ -88,32 +55,42 @@ class RuntimeWindowDimens { } private func createGlassRects() { - let glassHorizontalMargin = DefaultWindowDimens.glassHorizontalMargin * scale - let glassVerticalMargin = DefaultWindowDimens.glassVerticalMargin * scale - let glassMiddleMargin = DefaultWindowDimens.glassMiddelMargin * scale + let glassHorizontalMargin = WindowDimens.glassHorizontalMargin * scale + let glassVerticalMargin = WindowDimens.glassVerticalMargin * scale + let glassMiddleMargin = WindowDimens.glassMiddelMargin * scale let glassWidth = (windowRect.width - (glassHorizontalMargin * 2) - glassMiddleMargin) / 2 - let glassHeight = canvasRect.height - (glassVerticalMargin * 2) + let glassHeight = windowRect.height - (glassVerticalMargin * 2) let left = windowRect.minX + glassHorizontalMargin + let top = windowRect.minY + glassVerticalMargin let size = CGSize(width: glassWidth, height: glassHeight) leftGlassRect = CGRect( - origin: CGPoint(x: left, y: glassVerticalMargin), + origin: CGPoint(x: left, y: top), size: size ) rightGlassRect = CGRect( - origin: CGPoint(x: left + glassWidth + glassMiddleMargin, y: glassVerticalMargin), + origin: CGPoint(x: left + glassWidth + glassMiddleMargin, y: top), + size: size + ) + } + + private func createCanvasRect(_ frame: CGRect) { + let size = getSize(frame) + canvasRect = CGRect( + origin: CGPoint(x: (frame.width - size.width) / 2.0, y: WindowDimens.padding), size: size ) } private func getSize(_ frame: CGRect) -> CGSize { let ratio = frame.width / frame.height - if (ratio > DefaultWindowDimens.ratio) { - return CGSize(width: frame.height * DefaultWindowDimens.ratio, height: frame.height) + if (ratio > WindowDimens.ratio) { + let height = frame.height - WindowDimens.padding * 2 + return CGSize(width: height * WindowDimens.ratio, height: height) } else { - return CGSize(width: frame.width, height: frame.width / DefaultWindowDimens.ratio) + let width = frame.width - WindowDimens.padding * 2 + return CGSize(width: width, height: width / WindowDimens.ratio) } } } - diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowView.swift index 4408cd72..a4349f4e 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWallWindowView.swift @@ -16,7 +16,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -class BaseWallWindowView: BaseWindowView { +class BaseWallWindowView: BaseWindowView { override var isEnabled: Bool { didSet { if (isEnabled) { @@ -24,60 +24,20 @@ class BaseWallWindowView: BaseWindowView { } else { colors = WindowColors.offline(traitCollection) } - updateColors() + setNeedsDisplay() } } override var intrinsicContentSize: CGSize { - CGSize(width: DefaultWindowDimens.width, height: DefaultWindowDimens.height) + CGSize(width: WindowDimens.width, height: WindowDimens.height) } override var touchRect: CGRect { dimens.windowRect } - private lazy var windowLayer: CAShapeLayer = { - let layer = CAShapeLayer() - layer.backgroundColor = colors.window.cgColor - layer.cornerRadius = DefaultWindowDimens.windowCornerRadius - layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - layer.setupShadow(colors.shadow) - return layer - }() - - private lazy var topLineLayer: CAShapeLayer = { - let layer = CAShapeLayer() - layer.backgroundColor = colors.window.cgColor - layer.setupShadow(colors.shadow) - return layer - }() - - private lazy var leftGlassLayer: CAGradientLayer = { - let layer = CAGradientLayer() - layer.colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] - return layer - }() - - private lazy var rightGlassLayer: CAGradientLayer = { - let layer = CAGradientLayer() - layer.colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] - return layer - }() - - private lazy var slatsLayers: [CAShapeLayer] = { - var layers: [CAShapeLayer] = [] - - for i in 0 ..< DefaultWindowDimens.slatsCount { - let layer = CAShapeLayer() - setupSlat(layer, colors) - layers.append(layer) - } - - return layers - }() - - let dimens: RuntimeWindowDimens + let dimens: D lazy var colors = WindowColors.standard(traitCollection) - init(_ dimens: RuntimeWindowDimens) { + init(_ dimens: D) { self.dimens = dimens super.init(frame: .zero) setupView() @@ -91,72 +51,70 @@ class BaseWallWindowView: BaseWindowView { override func layoutSubviews() { dimens.update(frame) - windowLayer.frame = dimens.windowRect - leftGlassLayer.frame = dimens.leftGlassRect - rightGlassLayer.frame = dimens.rightGlassRect - CALayer.performWithoutAnimation { - updateSlatsPositions(slatsLayers, dimens) - } - topLineLayer.frame = dimens.topLineRect updateMarkersPositions() } override func draw(_ rect: CGRect) { + super.draw(rect) + CALayer.performWithoutAnimation { - updateSlatsPositions(slatsLayers, dimens) updateMarkersPositions() } + + guard let context = UIGraphicsGetCurrentContext() else { return } + + context.setShouldAntialias(true) + context.setLineWidth(1) + + // window frame + drawPath(context, fillColor: colors.window, withShadow: true) { + UIBezierPath(roundedRect: dimens.windowRect, cornerRadius: WindowDimens.cornerRadius).cgPath + } + + // glasses + let slatColors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] + drawGlass(context, dimens.leftGlassRect, slatColors) + drawGlass(context, dimens.rightGlassRect, slatColors) + + // shadowing elemnts (slats, curtain, etc) + context.saveGState() + let clipingRect = CGRect( + origin: CGPoint(x: dimens.topLineRect.minX, y: dimens.topLineRect.maxY), + size: CGSize(width: dimens.topLineRect.width, height: dimens.windowRect.height) + ) + context.clip(to: [clipingRect]) + drawShadowingElements(context, dimens) + context.restoreGState() + + // markers - for groups + drawMarkers(context, dimens) + + // top line rect + drawPath(context, fillColor: colors.window, withShadow: true) { + UIBezierPath(rect: dimens.topLineRect).cgPath + } } - func updateSlatsPositions(_ slatsLayers: [CAShapeLayer], _ dimens: RuntimeWindowDimens) { + func drawShadowingElements(_ context: CGContext, _ dimens: D) { fatalError("updateSlatsPositions(:,:) needs to be implemented") } - func setupSlat(_ layer: CAShapeLayer, _ colors: WindowColors) { - layer.backgroundColor = colors.slatBackground.cgColor - layer.borderWidth = 1 - layer.borderColor = colors.slatBorder.cgColor + func drawMarkers(_ context: CGContext, _ dimens: D) { + // intentionally left empty - markers may stay unsupported } private func setupView() { translatesAutoresizingMaskIntoConstraints = false + backgroundColor = .transparent + clipsToBounds = false - layer.addSublayer(windowLayer) - layer.addSublayer(leftGlassLayer) - layer.addSublayer(rightGlassLayer) - slatsLayers.forEach { layer.addSublayer($0) } - layer.addSublayer(topLineLayer) } func updateMarkersPositions() { // intentionally left empty } - private func updateColors() { - windowLayer.backgroundColor = colors.window.cgColor - windowLayer.setupShadow(colors.shadow) - - topLineLayer.backgroundColor = colors.window.cgColor - topLineLayer.setupShadow(colors.shadow) - - leftGlassLayer.colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] - rightGlassLayer.colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] - - for slatLayer in slatsLayers { - setupSlat(slatLayer, colors) - } - } - override class var requiresConstraintBasedLayout: Bool { return true } } - -private extension CAShapeLayer { - func setupShadow(_ color: UIColor) { - shadowColor = color.cgColor - shadowRadius = DefaultWindowDimens.shadowRadius - shadowOpacity = DefaultWindowDimens.shadowOpacity - shadowOffset = DefaultWindowDimens.shadowOffset - } -} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWindowView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWindowView.swift index d6aad6b3..ec848cea 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWindowView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/BaseWindowView.swift @@ -64,7 +64,7 @@ class BaseWindowView: UIView { let startPercentage = startPercentage, let currentPosition = event?.allTouches?.first?.location(in: self) else { return } - handleMovement(startPosition, startPercentage, currentPosition) + handleMovement(positionRelay, startPosition, startPercentage, currentPosition) } override func touchesEnded(_ touches: Set, with event: UIEvent?) { @@ -77,11 +77,16 @@ class BaseWindowView: UIView { } } - func handleMovement(_ startPosition: CGPoint, _ startPercentage: CGFloat, _ currentPosition: CGPoint) { + func handleMovement( + _ positionRelay: PublishRelay, + _ startPosition: CGPoint, + _ startPercentage: CGFloat, + _ currentPosition: CGPoint + ) { let positionDiffAsPercentage = (currentPosition.y - startPosition.y) .divideToPercentage(value: touchRect.height) - let position = (startPercentage + positionDiffAsPercentage).toPercentage(max: 100) + let position = (startPercentage + positionDiffAsPercentage).limit(max: 100) windowState?.position = .similar(position) positionRelay.accept(position) } diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/DefaultWindowDimens.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/DefaultWindowDimens.swift index 85def76f..36497d2f 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/DefaultWindowDimens.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/DefaultWindowDimens.swift @@ -16,29 +16,30 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -enum DefaultWindowDimens { + +enum WindowDimens { + static let padding: CGFloat = 5 // Needed for shadow + static let width: CGFloat = 288 static let height: CGFloat = 336 static var ratio: CGFloat { width / height } static let topLineHeight: CGFloat = 16 - static let slatsCount: Int = .init(ceil((height - topLineHeight) / slatHeight)) - static let slatHeight: CGFloat = 24 - static let slatDistance: CGFloat = 5 - static let windowHorizontalMargin: CGFloat = 16 static let glassMiddelMargin: CGFloat = 20 static let glassHorizontalMargin: CGFloat = 18 static let glassVerticalMargin: CGFloat = 24 - static let slatHorizontalMargin: CGFloat = 8 - static let markerHeight: CGFloat = 8 - static let markerWidth: CGFloat = 28 - static let markerInfoRadius: CGFloat = 14 - - static let windowCornerRadius: CGFloat = 8 + static let cornerRadius: CGFloat = 8 static let shadowRadius: CGFloat = 3 static let shadowOffset: CGSize = .init(width: 0, height: 1.5) - static let shadowOpacity: Float = 0.15 + static let shadowOpacity: Float = 0.10 +} + +enum SlatDimens { + static let count: Int = .init(ceil((WindowDimens.height - WindowDimens.topLineHeight) / height)) + static let height: CGFloat = 24 + static let distance: CGFloat = 5 + static let horizontalMargin: CGFloat = 8 } diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/MoveTimeView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/MoveTimeView.swift new file mode 100644 index 00000000..9e2adea6 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/WindowView/MoveTimeView.swift @@ -0,0 +1,95 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +class MoveTimeView: UIView { + var value: String? { + get { label.text } + set { + label.text = newValue + setNeedsLayout() // because label width will change + } + } + + private lazy var iconView: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.image = .iconTouchHand?.withRenderingMode(.alwaysTemplate) + view.tintColor = .black + return view + }() + + private lazy var label: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .body2 + return label + }() + + private let textLocation: TextLocation + + init(textLocation: TextLocation = .bottom) { + self.textLocation = textLocation + super.init(frame: .zero) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + + addSubview(iconView) + addSubview(label) + + setupLayout() + } + + private func setupLayout() { + switch (textLocation) { + case .bottom: + NSLayoutConstraint.activate([ + iconView.centerXAnchor.constraint(equalTo: centerXAnchor), + iconView.topAnchor.constraint(equalTo: topAnchor), + + label.topAnchor.constraint(equalTo: iconView.bottomAnchor), + label.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + case .right: + NSLayoutConstraint.activate([ + iconView.topAnchor.constraint(equalTo: topAnchor), + iconView.leftAnchor.constraint(equalTo: leftAnchor), + iconView.bottomAnchor.constraint(equalTo: bottomAnchor), + + label.centerYAnchor.constraint(equalTo: centerYAnchor), + label.leftAnchor.constraint(equalTo: iconView.rightAnchor), + label.rightAnchor.constraint(equalTo: rightAnchor) + ]) + } + } + + override class var requiresConstraintBasedLayout: Bool { + return true + } + + enum TextLocation { + case bottom, right + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVC.swift b/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVC.swift new file mode 100644 index 00000000..147aff81 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVC.swift @@ -0,0 +1,34 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +final class CurtainVC: BaseWindowVC { + init(itemBundle: ItemBundle) { + super.init(itemBundle: itemBundle, viewModel: CurtainVM(), windowControls: WindowHorizontalControls()) + } + + override func getWindowView() -> CurtainView { CurtainView() } + + override func handle(state: CurtainViewState) { + windowView.windowState = state.curtainWindowState + + slatTiltSlider.isHidden = true + topView.valueBottom = nil + + super.handle(state: state) + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift b/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift new file mode 100644 index 00000000..5122ddaf --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift @@ -0,0 +1,96 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +final class CurtainVM: BaseWindowVM { + override func defaultViewState() -> CurtainViewState { CurtainViewState() } + + override func handleChannel(_ channel: SAChannel) { + guard let value = channel.value?.asRollerShutterValue() else { return } + + updateView { + if ($0.manualMoving) { + return $0 + } + + let position = value.hasValidPosition ? value.position : 0 + let positionValue: WindowGroupedValue = .similar(value.online ? CGFloat(position) : 25) + let windowState = $0.curtainWindowState + .changing(path: \.position, to: positionValue) + .changing(path: \.positionTextFormat, to: positionTextFormat) + + return updateChannel($0, channel, value) { + $0.changing(path: \.curtainWindowState, to: windowState) + } + } + } + + override func handleGroup(_ group: SAChannelGroup, _ onlineSummary: GroupOnlineSummary) { + updateView { + if ($0.manualMoving) { + return $0 + } + + let positions = group.getCurtainPositions() + let overallPosition = getGroupPercentage(positions, !$0.curtainWindowState.markers.isEmpty) + let windowState = $0.curtainWindowState + .changing(path: \.position, to: group.isOnline() ? overallPosition : .similar(25)) + .changing(path: \.positionTextFormat, to: positionTextFormat) + .changing(path: \.markers, to: overallPosition.isDifferent() ? positions : []) + + return updateGroup($0, group, onlineSummary) { + $0.changing(path: \.curtainWindowState, to: windowState) + .changing(path: \.positionUnknown, to: overallPosition == .invalid) + } + } + } +} + +struct CurtainViewState: BaseWindowViewState { + var remoteId: Int32? = nil + var curtainWindowState: CurtainWindowState = .init(position: .similar(0)) + var issues: [ChannelIssueItem] = [] + var offline: Bool = true + var showClosingPercentage: Bool = false + var calibrating: Bool = false + var calibrationPossible: Bool = false + var positionUnknown: Bool = false + var touchTime: CGFloat? = nil + var isGroup: Bool = false + var onlineStatusString: String? = nil + var moveStartTime: TimeInterval? = nil + var manualMoving: Bool = false + + var windowState: any WindowState { curtainWindowState } +} + +private extension SAChannelGroup { + func getCurtainPositions() -> [CGFloat] { + guard let totalValue = total_value as? GroupTotalValue else { return [] } + return totalValue.values.compactMap { valueToPosition($0) } + } + + private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { + guard let value = baseGroupValue as? RollerShutterGroupValue else { return nil } + + return if (value.position < 100 && value.closedSensorActive) { + CGFloat(100) + } else { + CGFloat(value.position) + } + } +} diff --git a/SUPLA/Model/CoreData/Extensions/SAChannelBase+Ext.swift b/SUPLA/Model/CoreData/Extensions/SAChannelBase+Ext.swift index bd3dafd0..b04898fa 100644 --- a/SUPLA/Model/CoreData/Extensions/SAChannelBase+Ext.swift +++ b/SUPLA/Model/CoreData/Extensions/SAChannelBase+Ext.swift @@ -53,7 +53,9 @@ extension SAChannelBase { func isShadingSystem() -> Bool { return self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER || self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW || - self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND + self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND || + self.func == SUPLA_CHANNELFNC_TERRACE_AWNING || + self.func == SUPLA_CHANNELFNC_CURTAIN } func hasMeasurements() -> Bool { diff --git a/SUPLA/Resources/Default.strings b/SUPLA/Resources/Default.strings index 577125ff..49303c3b 100644 --- a/SUPLA/Resources/Default.strings +++ b/SUPLA/Resources/Default.strings @@ -68,6 +68,7 @@ "channel_caption_facade_blinds" = "Facade blinds"; "channel_caption_terrace_awning" = "Terrace awning"; "channel_caption_projector_screen" = "Projector screen"; +"channel_caption_curtain" = "Curtain"; /* Main */ "dialog_new_gesture_info_text" = "Swipe gesture to open details was removed, tap on particular channel to open it."; diff --git a/SUPLA/Resources/Extensions/String+Icons.swift b/SUPLA/Resources/Extensions/String+Icons.swift index 69d6ee34..27034b0e 100644 --- a/SUPLA/Resources/Extensions/String+Icons.swift +++ b/SUPLA/Resources/Extensions/String+Icons.swift @@ -62,6 +62,10 @@ extension String { static let arrowClose = "icon_arrow_close" static let arrowUp = "icon_arrow_up" static let arrowDown = "icon_arrow_down" + static let arrowCoverTap = "icon_arrow_cover_tap" + static let arrowCoverHold = "icon_arrow_cover_hold" + static let arrowRevealTap = "icon_arrow_reveal_tap" + static let arrowRevealHold = "icon_arrow_reveal_hold" // MARK: Functions static let fncUnknown = "unknown_channel" diff --git a/SUPLA/Resources/Extensions/UIImage+Supla.swift b/SUPLA/Resources/Extensions/UIImage+Supla.swift index e67d3d50..51146c80 100644 --- a/SUPLA/Resources/Extensions/UIImage+Supla.swift +++ b/SUPLA/Resources/Extensions/UIImage+Supla.swift @@ -59,6 +59,10 @@ extension UIImage { static let iconArrowClose = UIImage(named: .Icons.arrowClose) static let iconArrowUp = UIImage(named: .Icons.arrowUp) static let iconArrowDown = UIImage(named: .Icons.arrowDown) + static let iconArrowCoverTap = UIImage(named: .Icons.arrowCoverTap) + static let iconArrowCoverHold = UIImage(named: .Icons.arrowCoverHold) + static let iconArrowRevealTap = UIImage(named: .Icons.arrowRevealTap) + static let iconArrowRevealHold = UIImage(named: .Icons.arrowRevealHold) // MARK: Functions static let fncUnknown = UIImage(named: .Icons.fncUnknown) diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/Contents.json new file mode 100644 index 00000000..b7f5c224 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "curtain-1.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "curtain - dark mode-1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain - dark mode-1.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain - dark mode-1.svg new file mode 100644 index 00000000..4aa146b9 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain - dark mode-1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain-1.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain-1.svg new file mode 100644 index 00000000..1156f094 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-closed.imageset/curtain-1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/Contents.json new file mode 100644 index 00000000..146e698a --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "curtain.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "curtain - dark mode.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain - dark mode.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain - dark mode.svg new file mode 100644 index 00000000..02d8c93c --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain - dark mode.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain.svg new file mode 100644 index 00000000..ec337cb0 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_curtain-open.imageset/curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/Contents.json index 9c723c68..80afc17e 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "facade_blind-closed.svg", + "filename" : "facade blinders.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "facade_blind-closed-nm.svg", + "filename" : "facade blinders_dark mode.svg", "idiom" : "universal" } ], diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade_blind-closed.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade blinders.svg similarity index 97% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade_blind-closed.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade blinders.svg index bdbba902..d32dc661 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade_blind-closed.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade blinders.svg @@ -1,5 +1,4 @@ - @@ -21,4 +20,5 @@ + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade_blind-closed-nm.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade blinders_dark mode.svg similarity index 97% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade_blind-closed-nm.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade blinders_dark mode.svg index 5b240021..9ec09ce1 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade_blind-closed-nm.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-closed.imageset/facade blinders_dark mode.svg @@ -1,5 +1,4 @@ - @@ -21,4 +20,5 @@ + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/Contents.json index caba444d..52b631ee 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "facade_blind-open.svg", + "filename" : "facade blinders-1.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "facade_blind-open-nm.svg", + "filename" : "facade blinders-2.svg", "idiom" : "universal" } ], diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-1.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-1.svg new file mode 100644 index 00000000..b806957f --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-1.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-2.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-2.svg new file mode 100644 index 00000000..a1cc50ae --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade blinders-2.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open-nm.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open-nm.svg deleted file mode 100644 index ea8d9d1f..00000000 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open-nm.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open.svg deleted file mode 100644 index db51e815..00000000 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_facade_blind-open.imageset/facade_blind-open.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/Contents.json index 59c612dd..461379eb 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "projector screenclosed.svg", + "filename" : "projector screen-1.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "projector screen_dark modeclosed.svg", + "filename" : "projector screen_dark mode-1.svg", "idiom" : "universal" } ], diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screenclosed.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen-1.svg similarity index 77% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screenclosed.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen-1.svg index 48b596be..1dfd6d62 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screenclosed.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen-1.svg @@ -1,7 +1,7 @@ + + - - diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen_dark modeclosed.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen_dark mode-1.svg similarity index 77% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen_dark modeclosed.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen_dark mode-1.svg index 530bb54b..2e8efc74 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen_dark modeclosed.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-closed.imageset/projector screen_dark mode-1.svg @@ -1,7 +1,7 @@ + + - - diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/Contents.json index 72960c50..a0a91d37 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "projector screenopened.svg", + "filename" : "projector screen.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "projector screen_dark modeopened.svg", + "filename" : "projector screen_dark mode.svg", "idiom" : "universal" } ], diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screenopened.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen.svg similarity index 97% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screenopened.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen.svg index 71097523..4cf97cb8 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screenopened.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen.svg @@ -1,12 +1,12 @@ + + - + - - diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen_dark modeopened.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen_dark mode.svg similarity index 97% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen_dark modeopened.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen_dark mode.svg index af49ad79..561269f7 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen_dark modeopened.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_projector_screen-open.imageset/projector screen_dark mode.svg @@ -1,12 +1,12 @@ + + - + - - diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json index bac2850e..86de26c8 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "fnc_terrase_awning-open.svg", + "filename" : "awnings.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "fnc_terrase_awning-open-nm.svg", + "filename" : "awnings_dark mode.svg", "idiom" : "universal" } ], diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/awnings.svg similarity index 80% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/awnings.svg index 9230eed5..a53c3332 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/awnings.svg @@ -2,6 +2,6 @@ - - + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open-nm.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/awnings_dark mode.svg similarity index 80% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open-nm.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/awnings_dark mode.svg index 4954baf8..b3ebdbe9 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open-nm.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/awnings_dark mode.svg @@ -2,6 +2,6 @@ - - + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json index 326b3cca..5d7a7ac9 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "fnc_terrase_awning-closed.svg", + "filename" : "awnings-1.svg", "idiom" : "universal" }, { @@ -11,7 +11,7 @@ "value" : "dark" } ], - "filename" : "fnc_terrase_awning-closed-nm.svg", + "filename" : "awnings_dark mode-1.svg", "idiom" : "universal" } ], diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/awnings-1.svg similarity index 73% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/awnings-1.svg index 3a0e0683..569bbded 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/awnings-1.svg @@ -2,6 +2,6 @@ - - + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed-nm.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/awnings_dark mode-1.svg similarity index 73% rename from SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed-nm.svg rename to SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/awnings_dark mode-1.svg index 4d0aa391..511812dd 100644 --- a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed-nm.svg +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/awnings_dark mode-1.svg @@ -2,6 +2,6 @@ - - + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Contents.json new file mode 100644 index 00000000..6b504cfc --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 391.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Frame 391.svg b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Frame 391.svg new file mode 100644 index 00000000..d3396f7a --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_hold.imageset/Frame 391.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Contents.json new file mode 100644 index 00000000..eff7ae14 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 394.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Frame 394.svg b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Frame 394.svg new file mode 100644 index 00000000..7f929313 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_cover_tap.imageset/Frame 394.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Contents.json new file mode 100644 index 00000000..760060f8 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 392.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Frame 392.svg b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Frame 392.svg new file mode 100644 index 00000000..2f61ca23 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_hold.imageset/Frame 392.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Contents.json new file mode 100644 index 00000000..97e8bb27 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 393.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Frame 393.svg b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Frame 393.svg new file mode 100644 index 00000000..a4c9fc26 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/Icons/icon_arrow_reveal_tap.imageset/Frame 393.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Strings.swift b/SUPLA/Resources/Strings.swift index 411c390d..c150e06e 100644 --- a/SUPLA/Resources/Strings.swift +++ b/SUPLA/Resources/Strings.swift @@ -305,6 +305,7 @@ struct Strings { static let captionFacadeBlinds = "channel_caption_facade_blinds".toLocalized() static let captionTerraceAwning = "channel_caption_terrace_awning".toLocalized() static let captionProjectorScreen = "channel_caption_projector_screen".toLocalized() + static let captionCurtain = "channel_caption_curtain".toLocalized() } } diff --git a/SUPLA/Resources/de.lproj/Localizable.strings b/SUPLA/Resources/de.lproj/Localizable.strings index 7962dc27..ae4fc8b7 100644 --- a/SUPLA/Resources/de.lproj/Localizable.strings +++ b/SUPLA/Resources/de.lproj/Localizable.strings @@ -325,6 +325,7 @@ "channel_caption_facade_blinds" = "Fassadenjalousien"; "channel_caption_terrace_awning" = "Terrassenmarkise"; "channel_caption_projector_screen" = "Leinwand"; +"channel_caption_curtain" = "Vorhang"; /* Main */ "dialog_new_gesture_info_text" = "Wischgeste zum Öffnen der Kanaldetails wurde gelöscht, tippe auf dem Kanal, um sie zu sehen."; diff --git a/SUPLA/Resources/pl.lproj/Localizable.strings b/SUPLA/Resources/pl.lproj/Localizable.strings index f78f367a..82eb2f58 100644 --- a/SUPLA/Resources/pl.lproj/Localizable.strings +++ b/SUPLA/Resources/pl.lproj/Localizable.strings @@ -350,6 +350,7 @@ "channel_caption_facade_blinds" = "Żaluzja fasadowa"; "channel_caption_terrace_awning" = "Markiza tarasowa"; "channel_caption_projector_screen" = "Ekran projekcyjny"; +"channel_caption_curtain" = "Zasłona"; /* Main */ "dialog_new_gesture_info_text" = "Usunęliśmy gest przesunięcia otwierający szczegóły, aby je zobaczyć dotknij wybrany kanał."; diff --git a/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift b/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift index 9fceeeca..7f7dfd4b 100644 --- a/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift +++ b/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift @@ -117,6 +117,8 @@ final class GetChannelBaseDefaultCaptionUseCaseImpl: GetChannelBaseDefaultCaptio return Strings.General.Channel.captionTerraceAwning case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: return Strings.General.Channel.captionProjectorScreen + case SUPLA_CHANNELFNC_CURTAIN: + return Strings.General.Channel.captionCurtain default: return NSLocalizedString("Not supported function", comment: "") } diff --git a/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift b/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift index b43f7720..c5ce9495 100644 --- a/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift +++ b/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift @@ -47,7 +47,8 @@ final class GetChannelBaseStateUseCaseImpl: GetChannelBaseStateUseCase { case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, - SUPLA_CHANNELFNC_TERRACE_AWNING: + SUPLA_CHANNELFNC_TERRACE_AWNING, + SUPLA_CHANNELFNC_CURTAIN: return valueWrapper.rollerShutterClosed ? .closed : .opened case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: return valueWrapper.projectorScreenClosed ? .closed : .opened @@ -102,6 +103,7 @@ final class GetChannelBaseStateUseCaseImpl: GetChannelBaseStateUseCase { SUPLA_CHANNELFNC_OPENINGSENSOR_WINDOW, SUPLA_CHANNELFNC_OPENINGSENSOR_ROOFWINDOW, SUPLA_CHANNELFNC_TERRACE_AWNING, + SUPLA_CHANNELFNC_CURTAIN, SUPLA_CHANNELFNC_VALVE_OPENCLOSE, SUPLA_CHANNELFNC_VALVE_PERCENTAGE: .opened case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: .closed diff --git a/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift b/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift index 4cd06b45..172f9f8b 100644 --- a/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift +++ b/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift @@ -40,6 +40,8 @@ final class ProvideDetailTypeUseCaseImpl: ProvideDetailTypeUseCase { return .windowDetail(pages: [.terraceAwning]) case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: return .windowDetail(pages: [.projectorScreen]) + case SUPLA_CHANNELFNC_CURTAIN: + return .windowDetail(pages: [.curtain]) case SUPLA_CHANNELFNC_LIGHTSWITCH, SUPLA_CHANNELFNC_POWERSWITCH, @@ -139,4 +141,5 @@ enum DetailPage { case facadeBlind case terraceAwning case projectorScreen + case curtain } diff --git a/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift b/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift index b6ac108c..d0bb1c21 100644 --- a/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift +++ b/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift @@ -21,11 +21,12 @@ final class ShadingSystemGroupActivePercentageProvider: GroupActivePercentagePro switch (function) { case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, - SUPLA_CHANNELFNC_TERRACE_AWNING: true + SUPLA_CHANNELFNC_TERRACE_AWNING, + SUPLA_CHANNELFNC_CURTAIN: true default: false } } - + func getActivePercentage(_ valueIndex: Int, _ totalValue: GroupTotalValue) -> Int { totalValue.values .map { $0 as! RollerShutterGroupValue } diff --git a/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift b/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift index daf98e4b..acca6caf 100644 --- a/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift +++ b/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift @@ -123,7 +123,8 @@ private extension SAChannelGroup { case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: return IntegerGroupValue(value: value.asRollerShutterValue().alwaysValidPosition) case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, - SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW: + SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, + SUPLA_CHANNELFNC_CURTAIN: return RollerShutterGroupValue( position: value.asRollerShutterValue().alwaysValidPosition, closedSensorActive: value.hiSubValue() == 1 diff --git a/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift b/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift index dba84dc9..6f87c6ed 100644 --- a/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift +++ b/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift @@ -78,6 +78,7 @@ final class GetDefaultIconNameUseCaseImpl: GetDefaultIconNameUseCase { StaticIconNameProducer(function: SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, name: "fnc_facade_blind"), StaticIconNameProducer(function: SUPLA_CHANNELFNC_TERRACE_AWNING, name: "fnc_terrace_awning"), StaticIconNameProducer(function: SUPLA_CHANNELFNC_PROJECTOR_SCREEN, name: "fnc_projector_screen"), + StaticIconNameProducer(function: SUPLA_CHANNELFNC_CURTAIN, name: "fnc_curtain"), PowerSwitchIconNameProducer(), LightSwitchIconNameProducer(), StaircaseTimerIconNameProducer(), diff --git a/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift b/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift new file mode 100644 index 00000000..a0797c6b --- /dev/null +++ b/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift @@ -0,0 +1,123 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +@testable import SUPLA +import XCTest + +final class CurtainVMTests: ViewModelTest { + + private lazy var readChannelByRemoteIdUseCase: ReadChannelByRemoteIdUseCaseMock! = ReadChannelByRemoteIdUseCaseMock() + + private lazy var readGroupByRemoteIdUseCase: ReadGroupByRemoteIdUseCaseMock! = ReadGroupByRemoteIdUseCaseMock() + + private lazy var getGroupOnlineSummaryUseCase: GetGroupOnlineSummaryUseCaseMock! = GetGroupOnlineSummaryUseCaseMock() + + private lazy var settings: GlobalSettingsMock! = GlobalSettingsMock() + + private lazy var viewModel: CurtainVM! = CurtainVM() + + override func setUp() { + + DiContainer.register(ReadChannelByRemoteIdUseCase.self, readChannelByRemoteIdUseCase!) + DiContainer.register(ReadGroupByRemoteIdUseCase.self, readGroupByRemoteIdUseCase!) + DiContainer.register(GetGroupOnlineSummaryUseCase.self, getGroupOnlineSummaryUseCase!) + DiContainer.register(GlobalSettings.self, settings!) + } + + override func tearDown() { + super.tearDown() + + readChannelByRemoteIdUseCase = nil + settings = nil + viewModel = nil + } + + func test_shouldLoadChannel() { + // given + let channel = SAChannel(testContext: nil) + channel.remote_id = 123 + channel.flags = Int64(SUPLA_CHANNEL_FLAG_CALCFG_RECALIBRATE) + channel.value = SAChannelValue(testContext: nil) + channel.value?.value = NSData(data: RollerShutterValue.mockData(position: 50, flags: SuplaRollerShutterFlag.motorProblem.rawValue)) + channel.value?.online = true + + settings.showOpeningPercentReturns = false + readChannelByRemoteIdUseCase.returns = .just(channel) + + // when + observe(viewModel) + viewModel.loadData(remoteId: 123, type: .channel) + + // then + assertStates(expected: [ + CurtainViewState(), + CurtainViewState( + remoteId: 123, + curtainWindowState: CurtainWindowState(position: .similar(50)), + issues: [ + ChannelIssueItem( + issueIconType: .warning, + description: Strings.RollerShutterDetail.calibrationFailed + ) + ], + offline: false, + showClosingPercentage: true, + calibrating: false, + calibrationPossible: true + ) + ]) + assertEvents(expected: []) + } + + func test_shouldLoadGroup() { + // given + let groupOnlineSummary = GroupOnlineSummary(onlineCount: 2, count: 3) + let group = SAChannelGroup(testContext: nil) + group.remote_id = 234 + group.online = 1 + group.total_value = GroupTotalValue(values: [ + RollerShutterGroupValue(position: 50, closedSensorActive: false), + RollerShutterGroupValue(position: 80, closedSensorActive: false) + ]) + + settings.showOpeningPercentReturns = true + readGroupByRemoteIdUseCase.returns = .just(group) + getGroupOnlineSummaryUseCase.returns = .just(groupOnlineSummary) + + // when + observe(viewModel) + viewModel.loadData(remoteId: 234, type: .group) + + // then + assertStates(expected: [ + CurtainViewState(), + CurtainViewState( + remoteId: 234, + curtainWindowState: CurtainWindowState( + position: .different(min: 50, max: 80), + positionTextFormat: .openingPercentage, + markers: [50, 80] + ), + offline: false, + isGroup: true, + onlineStatusString: "2/3" + ) + ]) + } +} + diff --git a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift index eb25f76d..9080aece 100644 --- a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift @@ -84,6 +84,8 @@ final class GetChannelBaseDefaultCaptionUseCaseTests: XCTestCase { doTest(function: SUPLA_CHANNELFNC_GENERAL_PURPOSE_METER, Strings.General.Channel.captionGeneralPurposeMeter) doTest(function: SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, Strings.General.Channel.captionFacadeBlinds) doTest(function: SUPLA_CHANNELFNC_TERRACE_AWNING, Strings.General.Channel.captionTerraceAwning) + doTest(function: SUPLA_CHANNELFNC_PROJECTOR_SCREEN, Strings.General.Channel.captionProjectorScreen) + doTest(function: SUPLA_CHANNELFNC_CURTAIN, Strings.General.Channel.captionCurtain) doTest(function: -1, "Not supported function") } diff --git a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift index f54527b6..e04a4298 100644 --- a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift @@ -446,4 +446,46 @@ final class GetChannelBaseStateUseCaseTests: XCTestCase { XCTAssertEqual(state, .closed) XCTAssertTrue(state.isActive()) } + + func test_curtainScreenClosedState() { + // given + let channel = SAChannel(testContext: nil) + channel.func = SUPLA_CHANNELFNC_CURTAIN + channel.value = mockChannelValue(byte0: 100) + + // when + let state = useCase.invoke(channelBase: channel) + + // then + XCTAssertEqual(state, .closed) + XCTAssertTrue(state.isActive()) + } + + func test_curtainOpenedState() { + // given + let channel = SAChannel(testContext: nil) + channel.func = SUPLA_CHANNELFNC_CURTAIN + channel.value = mockChannelValue() + + // when + let state = useCase.invoke(channelBase: channel) + + // then + XCTAssertEqual(state, .opened) + XCTAssertFalse(state.isActive()) + } + + func test_curtainOfflineState() { + // given + let channel = SAChannel(testContext: nil) + channel.func = SUPLA_CHANNELFNC_CURTAIN + channel.value = mockChannelValue(online: false) + + // when + let state = useCase.invoke(channelBase: channel) + + // then + XCTAssertEqual(state, .opened) + XCTAssertFalse(state.isActive()) + } } diff --git a/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift b/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift index 481052ac..842e83fc 100644 --- a/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift @@ -91,6 +91,24 @@ final class ProvideDetailTypeUseCaseTests: XCTestCase { } } + func test_shouldProvideRs_forProjectorScreenFunction() { + doTest(expectedResult: .windowDetail(pages: [.projectorScreen])) { + let channel = SAChannel(testContext: nil) + channel.func = SUPLA_CHANNELFNC_PROJECTOR_SCREEN + + return channel + } + } + + func test_shouldProvideRs_forCurtainFunction() { + doTest(expectedResult: .windowDetail(pages: [.curtain])) { + let channel = SAChannel(testContext: nil) + channel.func = SUPLA_CHANNELFNC_CURTAIN + + return channel + } + } + func test_shouldProvideEm_forEmFunction() { doTest(expectedResult: .legacy(type: .em)) { let channel = SAChannel(testContext: nil) diff --git a/SUPLATests/Tests/UseCase/Group/UpdateChannelGroupTotalValueUseCaseTests.swift b/SUPLATests/Tests/UseCase/Group/UpdateChannelGroupTotalValueUseCaseTests.swift index 9ee0ec7c..07b57138 100644 --- a/SUPLATests/Tests/UseCase/Group/UpdateChannelGroupTotalValueUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Group/UpdateChannelGroupTotalValueUseCaseTests.swift @@ -41,7 +41,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { useCase = nil } - func testIfTotalStringIsCreated() { + func testIfTotalValueIsCreated() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -120,7 +120,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForGate() { + func testIfTotalValueIsCreatedForGate() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -156,7 +156,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForPowerSwitch() { + func testIfTotalValueIsCreatedForPowerSwitch() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -192,7 +192,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForValvePercentage() { + func testIfTotalValueIsCreatedForValvePercentage() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -228,7 +228,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForDimmer() { + func testIfTotalValueIsCreatedForDimmer() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -264,7 +264,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForRgbLighting() { + func testIfTotalValueIsCreatedForRgbLighting() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -301,7 +301,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForDimmerAndRgbLighting() { + func testIfTotalValueIsCreatedForDimmerAndRgbLighting() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -339,7 +339,7 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { ]) } - func testIfTotalStringIsCreatedForHeatpolThermostat() { + func testIfTotalValueIsCreatedForHeatpolThermostat() { // given let firstGroup = SAChannelGroup(testContext: nil) firstGroup.remote_id = 11 @@ -376,4 +376,40 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { .completed ]) } + + func testIfTotalValueIsCreatedForCurtain() { + // given + let group = SAChannelGroup(testContext: nil) + group.remote_id = 11 + group.func = SUPLA_CHANNELFNC_CURTAIN + + let groupRelation = SAChannelGroupRelation(testContext: nil) + groupRelation.group = group + groupRelation.value = SAChannelValue.mockRollerShutter(position: 18) + + channelGroupRelationRepository.getAllVisibleRelationsForActiveProfileReturns = .just([ + groupRelation, + ]) + channelGroupRelationRepository.saveObservable = .just(()) + + // when + useCase.invoke().subscribe(observer).disposed(by: disposeBag) + + // then + XCTAssertEqual(group.online, 100) + XCTAssertTrue(group.total_value is GroupTotalValue) + if let groupTotalValue = group.total_value as? GroupTotalValue, + let firstRelationValue = groupTotalValue.values[0] as? RollerShutterGroupValue + { + XCTAssertEqual(groupTotalValue.values.count, 1) + XCTAssertEqual(firstRelationValue.position, 18) + } else { + XCTFail("First group total value not created!") + } + + assertEvents([ + .next([11]), // in third group there are no changes so it should not be present here. + .completed + ]) + } } diff --git a/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift b/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift index ac4e6ac4..17ec6fee 100644 --- a/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift @@ -1617,4 +1617,80 @@ final class GetDefaultIconNameUseCaseTests: XCTestCase { // then XCTAssertEqual(iconName, "fnc_terrace_awning-closed") } + + func test_projectorScreenOpened() { + // given + let function = SUPLA_CHANNELFNC_PROJECTOR_SCREEN + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .opened, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_projector_screen-open") + } + + func test_projectorScreenClose() { + // given + let function = SUPLA_CHANNELFNC_PROJECTOR_SCREEN + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .closed, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_projector_screen-closed") + } + + func test_curtainOpened() { + // given + let function = SUPLA_CHANNELFNC_CURTAIN + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .opened, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_curtain-open") + } + + func test_curtainClose() { + // given + let function = SUPLA_CHANNELFNC_CURTAIN + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .closed, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_curtain-closed") + } }