diff --git a/Applite.xcodeproj/project.pbxproj b/Applite.xcodeproj/project.pbxproj index ce7eee1..1aa7090 100644 --- a/Applite.xcodeproj/project.pbxproj +++ b/Applite.xcodeproj/project.pbxproj @@ -55,6 +55,45 @@ 4192560A2D1C9FF800D9EF10 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256092D1C9FF800D9EF10 /* StringExtension.swift */; }; 4192560D2D1CA02C00D9EF10 /* ShellError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192560C2D1CA02C00D9EF10 /* ShellError.swift */; }; 4192560F2D1CC09500D9EF10 /* DependencyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192560E2D1CC09500D9EF10 /* DependencyError.swift */; }; + 419256122D1DDBD400D9EF10 /* AlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256112D1DDBD400D9EF10 /* AlertManager.swift */; }; + 419256162D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256152D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift */; }; + 419256182D1DEA4400D9EF10 /* AppView+ActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256172D1DEA4400D9EF10 /* AppView+ActionsView.swift */; }; + 4192561A2D1DEB2100D9EF10 /* AppView+OpenAndManageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256192D1DEB2100D9EF10 /* AppView+OpenAndManageView.swift */; }; + 4192561C2D1DEB7E00D9EF10 /* AppView+DownloadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192561B2D1DEB7E00D9EF10 /* AppView+DownloadButton.swift */; }; + 4192561E2D1DEBE700D9EF10 /* AppView+UninstallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192561D2D1DEBE700D9EF10 /* AppView+UninstallButton.swift */; }; + 419256202D1DEC0D00D9EF10 /* AppView+UpdateButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192561F2D1DEC0D00D9EF10 /* AppView+UpdateButton.swift */; }; + 419256232D1DF14C00D9EF10 /* SetupView+PageControllerButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256222D1DF14C00D9EF10 /* SetupView+PageControllerButtons.swift */; }; + 419256252D1DF17F00D9EF10 /* SetupView+Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256242D1DF17F00D9EF10 /* SetupView+Welcome.swift */; }; + 419256272D1DF1AC00D9EF10 /* SetupView+BrewTypeSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256262D1DF1AC00D9EF10 /* SetupView+BrewTypeSelection.swift */; }; + 419256292D1DF1CF00D9EF10 /* SetupView+BrewPathSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256282D1DF1CF00D9EF10 /* SetupView+BrewPathSelection.swift */; }; + 4192562C2D1DF20600D9EF10 /* SetupView+BrewInstall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192562B2D1DF20600D9EF10 /* SetupView+BrewInstall.swift */; }; + 4192562E2D1DF22500D9EF10 /* SetupView+AllSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192562D2D1DF22500D9EF10 /* SetupView+AllSet.swift */; }; + 419256312D1DF2B600D9EF10 /* SettingsView+GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256302D1DF2B600D9EF10 /* SettingsView+GeneralSettings.swift */; }; + 419256332D1DF2E000D9EF10 /* SettingsView+BrewPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256322D1DF2E000D9EF10 /* SettingsView+BrewPath.swift */; }; + 419256352D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256342D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift */; }; + 419256372D1DF34200D9EF10 /* SettingsView+ProxySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256362D1DF34200D9EF10 /* SettingsView+ProxySettings.swift */; }; + 419256392D1DF35C00D9EF10 /* SettingsView+Uninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256382D1DF35C00D9EF10 /* SettingsView+Uninstaller.swift */; }; + 4192563C2D1DF3C900D9EF10 /* ContentView+SidebarItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192563B2D1DF3C900D9EF10 /* ContentView+SidebarItems.swift */; }; + 4192563E2D1DF3E900D9EF10 /* ContentView+DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192563D2D1DF3E900D9EF10 /* ContentView+DetailView.swift */; }; + 419256402D1DF41300D9EF10 /* ContentView+LoadCasks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192563F2D1DF41300D9EF10 /* ContentView+LoadCasks.swift */; }; + 419256432D1E0A0200D9EF10 /* BrewPathSelectorView+PathOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256422D1E0A0200D9EF10 /* BrewPathSelectorView+PathOption.swift */; }; + 419256452D1E0A7000D9EF10 /* BrewPathSelectorView+CustomPathOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256442D1E0A7000D9EF10 /* BrewPathSelectorView+CustomPathOption.swift */; }; + 419256472D1E0B0900D9EF10 /* BrewPathSelectorView+GetPathDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256462D1E0B0900D9EF10 /* BrewPathSelectorView+GetPathDescription.swift */; }; + 4192564B2D1E0B9B00D9EF10 /* DownloadView+NoSearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192564A2D1E0B9B00D9EF10 /* DownloadView+NoSearchResults.swift */; }; + 4192564D2D1E0BDD00D9EF10 /* DownloadView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192564C2D1E0BDD00D9EF10 /* DownloadView+Search.swift */; }; + 4192564F2D1E0C1E00D9EF10 /* DownloadView+SortingOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192564E2D1E0C1E00D9EF10 /* DownloadView+SortingOptions.swift */; }; + 419256522D1E0D0500D9EF10 /* DiscoverSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256512D1E0D0500D9EF10 /* DiscoverSectionView.swift */; }; + 419256552D1E0E2500D9EF10 /* DiscoverSectionView+CategoryHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256542D1E0E2500D9EF10 /* DiscoverSectionView+CategoryHeader.swift */; }; + 419256572D1E0E5F00D9EF10 /* DiscoverSectionView+AppRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256562D1E0E5F00D9EF10 /* DiscoverSectionView+AppRow.swift */; }; + 419256592D1E0EA000D9EF10 /* DiscoverSectionView+OffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256582D1E0EA000D9EF10 /* DiscoverSectionView+OffsetKey.swift */; }; + 4192565B2D1E0ECF00D9EF10 /* DiscoverSectionView+Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192565A2D1E0ECF00D9EF10 /* DiscoverSectionView+Placeholder.swift */; }; + 4192565E2D1E155400D9EF10 /* UpdateView+UpdateAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192565D2D1E155400D9EF10 /* UpdateView+UpdateAllButton.swift */; }; + 419256622D1E15EA00D9EF10 /* UpdateView+UpdateUnavailable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256612D1E15EA00D9EF10 /* UpdateView+UpdateUnavailable.swift */; }; + 419256642D1E164600D9EF10 /* UpdateView+ToolbarItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256632D1E164600D9EF10 /* UpdateView+ToolbarItems.swift */; }; + 419256682D1E18D100D9EF10 /* AlertManagerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256672D1E18D100D9EF10 /* AlertManagerViewModifier.swift */; }; + 4192566B2D1F286B00D9EF10 /* Cask+BrewFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192566A2D1F286B00D9EF10 /* Cask+BrewFunctions.swift */; }; + 4192566E2D1F293700D9EF10 /* Cask+LaunchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192566D2D1F293700D9EF10 /* Cask+LaunchApp.swift */; }; + 419256702D1F299E00D9EF10 /* Cask+ProtocolConformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192566F2D1F299E00D9EF10 /* Cask+ProtocolConformances.swift */; }; 419506A42964A27F00FE5802 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A32964A27F00FE5802 /* SetupView.swift */; }; 419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */; }; 4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */; }; @@ -114,6 +153,45 @@ 419256092D1C9FF800D9EF10 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; 4192560C2D1CA02C00D9EF10 /* ShellError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellError.swift; sourceTree = ""; }; 4192560E2D1CC09500D9EF10 /* DependencyError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyError.swift; sourceTree = ""; }; + 419256112D1DDBD400D9EF10 /* AlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertManager.swift; sourceTree = ""; }; + 419256152D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+IconAndDescriptionView.swift"; sourceTree = ""; }; + 419256172D1DEA4400D9EF10 /* AppView+ActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+ActionsView.swift"; sourceTree = ""; }; + 419256192D1DEB2100D9EF10 /* AppView+OpenAndManageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+OpenAndManageView.swift"; sourceTree = ""; }; + 4192561B2D1DEB7E00D9EF10 /* AppView+DownloadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+DownloadButton.swift"; sourceTree = ""; }; + 4192561D2D1DEBE700D9EF10 /* AppView+UninstallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+UninstallButton.swift"; sourceTree = ""; }; + 4192561F2D1DEC0D00D9EF10 /* AppView+UpdateButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppView+UpdateButton.swift"; sourceTree = ""; }; + 419256222D1DF14C00D9EF10 /* SetupView+PageControllerButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupView+PageControllerButtons.swift"; sourceTree = ""; }; + 419256242D1DF17F00D9EF10 /* SetupView+Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupView+Welcome.swift"; sourceTree = ""; }; + 419256262D1DF1AC00D9EF10 /* SetupView+BrewTypeSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupView+BrewTypeSelection.swift"; sourceTree = ""; }; + 419256282D1DF1CF00D9EF10 /* SetupView+BrewPathSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupView+BrewPathSelection.swift"; sourceTree = ""; }; + 4192562B2D1DF20600D9EF10 /* SetupView+BrewInstall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupView+BrewInstall.swift"; sourceTree = ""; }; + 4192562D2D1DF22500D9EF10 /* SetupView+AllSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SetupView+AllSet.swift"; sourceTree = ""; }; + 419256302D1DF2B600D9EF10 /* SettingsView+GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+GeneralSettings.swift"; sourceTree = ""; }; + 419256322D1DF2E000D9EF10 /* SettingsView+BrewPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+BrewPath.swift"; sourceTree = ""; }; + 419256342D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+UpdateSettings.swift"; sourceTree = ""; }; + 419256362D1DF34200D9EF10 /* SettingsView+ProxySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+ProxySettings.swift"; sourceTree = ""; }; + 419256382D1DF35C00D9EF10 /* SettingsView+Uninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsView+Uninstaller.swift"; sourceTree = ""; }; + 4192563B2D1DF3C900D9EF10 /* ContentView+SidebarItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+SidebarItems.swift"; sourceTree = ""; }; + 4192563D2D1DF3E900D9EF10 /* ContentView+DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+DetailView.swift"; sourceTree = ""; }; + 4192563F2D1DF41300D9EF10 /* ContentView+LoadCasks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContentView+LoadCasks.swift"; sourceTree = ""; }; + 419256422D1E0A0200D9EF10 /* BrewPathSelectorView+PathOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewPathSelectorView+PathOption.swift"; sourceTree = ""; }; + 419256442D1E0A7000D9EF10 /* BrewPathSelectorView+CustomPathOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewPathSelectorView+CustomPathOption.swift"; sourceTree = ""; }; + 419256462D1E0B0900D9EF10 /* BrewPathSelectorView+GetPathDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewPathSelectorView+GetPathDescription.swift"; sourceTree = ""; }; + 4192564A2D1E0B9B00D9EF10 /* DownloadView+NoSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DownloadView+NoSearchResults.swift"; sourceTree = ""; }; + 4192564C2D1E0BDD00D9EF10 /* DownloadView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DownloadView+Search.swift"; sourceTree = ""; }; + 4192564E2D1E0C1E00D9EF10 /* DownloadView+SortingOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DownloadView+SortingOptions.swift"; sourceTree = ""; }; + 419256512D1E0D0500D9EF10 /* DiscoverSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverSectionView.swift; sourceTree = ""; }; + 419256542D1E0E2500D9EF10 /* DiscoverSectionView+CategoryHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoverSectionView+CategoryHeader.swift"; sourceTree = ""; }; + 419256562D1E0E5F00D9EF10 /* DiscoverSectionView+AppRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoverSectionView+AppRow.swift"; sourceTree = ""; }; + 419256582D1E0EA000D9EF10 /* DiscoverSectionView+OffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoverSectionView+OffsetKey.swift"; sourceTree = ""; }; + 4192565A2D1E0ECF00D9EF10 /* DiscoverSectionView+Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiscoverSectionView+Placeholder.swift"; sourceTree = ""; }; + 4192565D2D1E155400D9EF10 /* UpdateView+UpdateAllButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UpdateView+UpdateAllButton.swift"; sourceTree = ""; }; + 419256612D1E15EA00D9EF10 /* UpdateView+UpdateUnavailable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UpdateView+UpdateUnavailable.swift"; sourceTree = ""; }; + 419256632D1E164600D9EF10 /* UpdateView+ToolbarItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UpdateView+ToolbarItems.swift"; sourceTree = ""; }; + 419256672D1E18D100D9EF10 /* AlertManagerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertManagerViewModifier.swift; sourceTree = ""; }; + 4192566A2D1F286B00D9EF10 /* Cask+BrewFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cask+BrewFunctions.swift"; sourceTree = ""; }; + 4192566D2D1F293700D9EF10 /* Cask+LaunchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cask+LaunchApp.swift"; sourceTree = ""; }; + 4192566F2D1F299E00D9EF10 /* Cask+ProtocolConformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cask+ProtocolConformances.swift"; sourceTree = ""; }; 419506A32964A27F00FE5802 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; }; 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPathSelectorView.swift; sourceTree = ""; }; 419506A729696A5300FE5802 /* Applite-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Applite-Info.plist"; sourceTree = SOURCE_ROOT; }; @@ -182,7 +260,7 @@ 412635412A77FA1A00155034 /* Components */ = { isa = PBXGroup; children = ( - 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */, + 419256412D1E072700D9EF10 /* Brew Path Selector */, 415563A32A98C54300AE2F2E /* AppdirSelectorView.swift */, 418989AE2A33B65A004AC23B /* SmallProgressView.swift */, 4120AB662A755B0700F68EFE /* Sparkle Updater */, @@ -241,7 +319,6 @@ 414074F428DF53E80073EB22 /* AppliteApp.swift */, 414074F828DF53EB0073EB22 /* Assets.xcassets */, 4140750328DF5ED00073EB22 /* Views */, - 412635412A77FA1A00155034 /* Components */, 4126354C2A79195200155034 /* Styles */, 418F332228EC8B120023D76F /* Model */, 4191393129269FD600F1D75D /* Utilities */, @@ -265,14 +342,15 @@ 4140750328DF5ED00073EB22 /* Views */ = { isa = PBXGroup; children = ( - 414074F628DF53E80073EB22 /* ContentView.swift */, - 419506A32964A27F00FE5802 /* SetupView.swift */, + 4192563A2D1DF3A800D9EF10 /* Content View */, + 4196C8F728F9CB5200EADDDA /* Detail Views */, + 419256212D1DF12900D9EF10 /* Setup */, + 4192562F2D1DF27E00D9EF10 /* Settings */, + 4196C8F628F9CB4100EADDDA /* App Views */, 41062C982A3A263F00FD48EA /* UninstallSelfView.swift */, - 41524B98295E352200D0046A /* SettingsView.swift */, 4166EE6F28F5D4C900CE305A /* Commands.swift */, - 4196C8F628F9CB4100EADDDA /* App Views */, - 4196C8F728F9CB5200EADDDA /* Detail Views */, 415563A12A98BB2500AE2F2E /* ErrorWindowView.swift */, + 412635412A77FA1A00155034 /* Components */, ); path = Views; sourceTree = ""; @@ -286,15 +364,15 @@ path = Resources; sourceTree = ""; }; - 41483CCA29101C7200BB10C2 /* Cask Data */ = { + 41483CCA29101C7200BB10C2 /* Cask Models */ = { isa = PBXGroup; children = ( + 419256692D1F284100D9EF10 /* Cask */, 418F332528EC921D0023D76F /* CaskData.swift */, - 418F332328EC8BA10023D76F /* Cask.swift */, 4166EE7C28F73B2300CE305A /* BrewAnalytics.swift */, 4191392B29159B5C00F1D75D /* CaskDTO.swift */, ); - path = "Cask Data"; + path = "Cask Models"; sourceTree = ""; }; 41483CCB29101C7D00BB10C2 /* Categories */ = { @@ -317,7 +395,7 @@ 418F332228EC8B120023D76F /* Model */ = { isa = PBXGroup; children = ( - 41483CCA29101C7200BB10C2 /* Cask Data */, + 41483CCA29101C7200BB10C2 /* Cask Models */, 41483CCB29101C7D00BB10C2 /* Categories */, 4104D7442A8FC53200F84F9B /* Preferences */, ); @@ -337,20 +415,155 @@ 412635422A77FB0000155034 /* Brew Installation */, 4126353F2A77C71F00155034 /* Shell */, 41B731352A878993008BF6B9 /* Import Export */, + 413E60B52BBAE58B00978F6A /* Network Proxy */, + 418989B02A35D628004AC23B /* Verify Brew Installation */, + 419256652D1E18A500D9EF10 /* Alert Manager */, + 419256102D1DDB9700D9EF10 /* Other */, + ); + path = Utilities; + sourceTree = ""; + }; + 419256102D1DDB9700D9EF10 /* Other */ = { + isa = PBXGroup; + children = ( 41062C942A3794EA00FD48EA /* BrewPaths.swift */, 41524B9D295FA36E00D0046A /* DebounceObject.swift */, 41DF006329EAA094004EB7AE /* SendNotification.swift */, 41062C962A3A20F900FD48EA /* UninstallSelf.swift */, - 413E60B52BBAE58B00978F6A /* Network Proxy */, - 418989B02A35D628004AC23B /* Verify Brew Installation */, ); - path = Utilities; + path = Other; sourceTree = ""; }; - 4196C8F628F9CB4100EADDDA /* App Views */ = { + 419256132D1DE9C500D9EF10 /* App View */ = { isa = PBXGroup; children = ( 4140750428DF5FA60073EB22 /* AppView.swift */, + 419256152D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift */, + 419256172D1DEA4400D9EF10 /* AppView+ActionsView.swift */, + 4192561B2D1DEB7E00D9EF10 /* AppView+DownloadButton.swift */, + 419256192D1DEB2100D9EF10 /* AppView+OpenAndManageView.swift */, + 4192561D2D1DEBE700D9EF10 /* AppView+UninstallButton.swift */, + 4192561F2D1DEC0D00D9EF10 /* AppView+UpdateButton.swift */, + ); + path = "App View"; + sourceTree = ""; + }; + 419256212D1DF12900D9EF10 /* Setup */ = { + isa = PBXGroup; + children = ( + 419506A32964A27F00FE5802 /* SetupView.swift */, + 419256242D1DF17F00D9EF10 /* SetupView+Welcome.swift */, + 419256262D1DF1AC00D9EF10 /* SetupView+BrewTypeSelection.swift */, + 419256282D1DF1CF00D9EF10 /* SetupView+BrewPathSelection.swift */, + 4192562B2D1DF20600D9EF10 /* SetupView+BrewInstall.swift */, + 4192562D2D1DF22500D9EF10 /* SetupView+AllSet.swift */, + 419256222D1DF14C00D9EF10 /* SetupView+PageControllerButtons.swift */, + ); + path = Setup; + sourceTree = ""; + }; + 4192562F2D1DF27E00D9EF10 /* Settings */ = { + isa = PBXGroup; + children = ( + 41524B98295E352200D0046A /* SettingsView.swift */, + 419256302D1DF2B600D9EF10 /* SettingsView+GeneralSettings.swift */, + 419256322D1DF2E000D9EF10 /* SettingsView+BrewPath.swift */, + 419256342D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift */, + 419256362D1DF34200D9EF10 /* SettingsView+ProxySettings.swift */, + 419256382D1DF35C00D9EF10 /* SettingsView+Uninstaller.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 4192563A2D1DF3A800D9EF10 /* Content View */ = { + isa = PBXGroup; + children = ( + 414074F628DF53E80073EB22 /* ContentView.swift */, + 4192563B2D1DF3C900D9EF10 /* ContentView+SidebarItems.swift */, + 4192563D2D1DF3E900D9EF10 /* ContentView+DetailView.swift */, + 4192563F2D1DF41300D9EF10 /* ContentView+LoadCasks.swift */, + ); + path = "Content View"; + sourceTree = ""; + }; + 419256412D1E072700D9EF10 /* Brew Path Selector */ = { + isa = PBXGroup; + children = ( + 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */, + 419256422D1E0A0200D9EF10 /* BrewPathSelectorView+PathOption.swift */, + 419256442D1E0A7000D9EF10 /* BrewPathSelectorView+CustomPathOption.swift */, + 419256462D1E0B0900D9EF10 /* BrewPathSelectorView+GetPathDescription.swift */, + ); + path = "Brew Path Selector"; + sourceTree = ""; + }; + 419256482D1E0B6A00D9EF10 /* Download */ = { + isa = PBXGroup; + children = ( + 4196C8F828F9CDF700EADDDA /* DownloadView.swift */, + 4192564A2D1E0B9B00D9EF10 /* DownloadView+NoSearchResults.swift */, + 4192564C2D1E0BDD00D9EF10 /* DownloadView+Search.swift */, + 4192564E2D1E0C1E00D9EF10 /* DownloadView+SortingOptions.swift */, + ); + path = Download; + sourceTree = ""; + }; + 419256502D1E0CE000D9EF10 /* Discover */ = { + isa = PBXGroup; + children = ( + 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */, + 419256532D1E0DFB00D9EF10 /* Discover Section */, + ); + path = Discover; + sourceTree = ""; + }; + 419256532D1E0DFB00D9EF10 /* Discover Section */ = { + isa = PBXGroup; + children = ( + 419256512D1E0D0500D9EF10 /* DiscoverSectionView.swift */, + 419256542D1E0E2500D9EF10 /* DiscoverSectionView+CategoryHeader.swift */, + 419256562D1E0E5F00D9EF10 /* DiscoverSectionView+AppRow.swift */, + 419256582D1E0EA000D9EF10 /* DiscoverSectionView+OffsetKey.swift */, + 4192565A2D1E0ECF00D9EF10 /* DiscoverSectionView+Placeholder.swift */, + ); + path = "Discover Section"; + sourceTree = ""; + }; + 4192565C2D1E153D00D9EF10 /* Update */ = { + isa = PBXGroup; + children = ( + 4196C8FD28F9E13600EADDDA /* UpdateView.swift */, + 4192565D2D1E155400D9EF10 /* UpdateView+UpdateAllButton.swift */, + 419256612D1E15EA00D9EF10 /* UpdateView+UpdateUnavailable.swift */, + 419256632D1E164600D9EF10 /* UpdateView+ToolbarItems.swift */, + ); + path = Update; + sourceTree = ""; + }; + 419256652D1E18A500D9EF10 /* Alert Manager */ = { + isa = PBXGroup; + children = ( + 419256112D1DDBD400D9EF10 /* AlertManager.swift */, + 419256672D1E18D100D9EF10 /* AlertManagerViewModifier.swift */, + ); + path = "Alert Manager"; + sourceTree = ""; + }; + 419256692D1F284100D9EF10 /* Cask */ = { + isa = PBXGroup; + children = ( + 418F332328EC8BA10023D76F /* Cask.swift */, + 4192566A2D1F286B00D9EF10 /* Cask+BrewFunctions.swift */, + 4192566D2D1F293700D9EF10 /* Cask+LaunchApp.swift */, + 4192566F2D1F299E00D9EF10 /* Cask+ProtocolConformances.swift */, + ); + path = Cask; + sourceTree = ""; + }; + 4196C8F628F9CB4100EADDDA /* App Views */ = { + isa = PBXGroup; + children = ( + 419256132D1DE9C500D9EF10 /* App View */, 418F331B28EB3D540023D76F /* AppGridView.swift */, 413E60C12BBFF98A00978F6A /* AppIconView.swift */, 4125BB8929539907000FBD25 /* PlaceholderAppView.swift */, @@ -362,9 +575,9 @@ 4196C8F728F9CB5200EADDDA /* Detail Views */ = { isa = PBXGroup; children = ( - 4196C8F828F9CDF700EADDDA /* DownloadView.swift */, - 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */, - 4196C8FD28F9E13600EADDDA /* UpdateView.swift */, + 419256482D1E0B6A00D9EF10 /* Download */, + 419256502D1E0CE000D9EF10 /* Discover */, + 4192565C2D1E153D00D9EF10 /* Update */, 4196C8FF28F9E1F400EADDDA /* InstalledView.swift */, 41857B742912D94A004A1894 /* CategoryView.swift */, 418989AC2A33A5C4004AC23B /* BrewManagementView.swift */, @@ -475,52 +688,91 @@ files = ( 4196C90028F9E1F400EADDDA /* InstalledView.swift in Sources */, 415563A42A98C54300AE2F2E /* AppdirSelectorView.swift in Sources */, + 4192564D2D1E0BDD00D9EF10 /* DownloadView+Search.swift in Sources */, 4140750528DF5FA60073EB22 /* AppView.swift in Sources */, 418F332428EC8BA10023D76F /* Cask.swift in Sources */, 4126353E2A77C6EF00155034 /* ArrayExtension.swift in Sources */, + 4192564B2D1E0B9B00D9EF10 /* DownloadView+NoSearchResults.swift in Sources */, + 419256432D1E0A0200D9EF10 /* BrewPathSelectorView+PathOption.swift in Sources */, + 4192564F2D1E0C1E00D9EF10 /* DownloadView+SortingOptions.swift in Sources */, + 419256122D1DDBD400D9EF10 /* AlertManager.swift in Sources */, 4166EE7028F5D4C900CE305A /* Commands.swift in Sources */, + 419256312D1DF2B600D9EF10 /* SettingsView+GeneralSettings.swift in Sources */, 4166EE7D28F73B2300CE305A /* BrewAnalytics.swift in Sources */, 4104D7432A8FC52C00F84F9B /* Preferences.swift in Sources */, + 419256682D1E18D100D9EF10 /* AlertManagerViewModifier.swift in Sources */, 414074F728DF53E80073EB22 /* ContentView.swift in Sources */, + 419256392D1DF35C00D9EF10 /* SettingsView+Uninstaller.swift in Sources */, + 419256472D1E0B0900D9EF10 /* BrewPathSelectorView+GetPathDescription.swift in Sources */, 4189CE41293C980E009C836D /* BigButtonStyle.swift in Sources */, 41DF006429EAA094004EB7AE /* SendNotification.swift in Sources */, + 419256622D1E15EA00D9EF10 /* UpdateView+UpdateUnavailable.swift in Sources */, 41857B752912D94A004A1894 /* CategoryView.swift in Sources */, 4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */, + 4192561C2D1DEB7E00D9EF10 /* AppView+DownloadButton.swift in Sources */, + 419256232D1DF14C00D9EF10 /* SetupView+PageControllerButtons.swift in Sources */, 4125BB8A29539907000FBD25 /* PlaceholderAppView.swift in Sources */, + 419256182D1DEA4400D9EF10 /* AppView+ActionsView.swift in Sources */, 413F77A52972B2E70053349A /* DependencyManager.swift in Sources */, 418989B42A35D67C004AC23B /* isCommandLineToolsInstalled.swift in Sources */, + 419256352D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift in Sources */, + 4192563C2D1DF3C900D9EF10 /* ContentView+SidebarItems.swift in Sources */, 4192560F2D1CC09500D9EF10 /* DependencyError.swift in Sources */, + 4192566E2D1F293700D9EF10 /* Cask+LaunchApp.swift in Sources */, + 419256592D1E0EA000D9EF10 /* DiscoverSectionView+OffsetKey.swift in Sources */, + 4192565E2D1E155400D9EF10 /* UpdateView+UpdateAllButton.swift in Sources */, + 419256572D1E0E5F00D9EF10 /* DiscoverSectionView+AppRow.swift in Sources */, 419506A42964A27F00FE5802 /* SetupView.swift in Sources */, + 4192561E2D1DEBE700D9EF10 /* AppView+UninstallButton.swift in Sources */, 41524B99295E352200D0046A /* SettingsView.swift in Sources */, 415563A02A9265CE00AE2F2E /* CaskExportType.swift in Sources */, 41524B9E295FA36E00D0046A /* DebounceObject.swift in Sources */, + 419256292D1DF1CF00D9EF10 /* SetupView+BrewPathSelection.swift in Sources */, 415563A22A98BB2500AE2F2E /* ErrorWindowView.swift in Sources */, 4196C8FE28F9E13600EADDDA /* UpdateView.swift in Sources */, 41062C992A3A263F00FD48EA /* UninstallSelfView.swift in Sources */, + 419256372D1DF34200D9EF10 /* SettingsView+ProxySettings.swift in Sources */, 414074F528DF53E80073EB22 /* AppliteApp.swift in Sources */, 41062C9B2A3E4AFA00FD48EA /* BundleExtension.swift in Sources */, + 419256522D1E0D0500D9EF10 /* DiscoverSectionView.swift in Sources */, 418989AF2A33B65A004AC23B /* SmallProgressView.swift in Sources */, 41B731392A879353008BF6B9 /* ActiveTasksView.swift in Sources */, 411EDDD72A9F58180051E07B /* URLExtension.swift in Sources */, 419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */, + 419256402D1DF41300D9EF10 /* ContentView+LoadCasks.swift in Sources */, + 419256452D1E0A7000D9EF10 /* BrewPathSelectorView+CustomPathOption.swift in Sources */, + 419256642D1E164600D9EF10 /* UpdateView+ToolbarItems.swift in Sources */, + 419256272D1DF1AC00D9EF10 /* SetupView+BrewTypeSelection.swift in Sources */, 4192560A2D1C9FF800D9EF10 /* StringExtension.swift in Sources */, 4191392C29159B5C00F1D75D /* CaskDTO.swift in Sources */, + 419256702D1F299E00D9EF10 /* Cask+ProtocolConformances.swift in Sources */, 413E60C22BBFF98A00978F6A /* AppIconView.swift in Sources */, 418989AD2A33A5C4004AC23B /* BrewManagementView.swift in Sources */, 418989B22A35D651004AC23B /* isBrewPathValid.swift in Sources */, 4192560D2D1CA02C00D9EF10 /* ShellError.swift in Sources */, + 4192566B2D1F286B00D9EF10 /* Cask+BrewFunctions.swift in Sources */, + 4192561A2D1DEB2100D9EF10 /* AppView+OpenAndManageView.swift in Sources */, + 4192562C2D1DF20600D9EF10 /* SetupView+BrewInstall.swift in Sources */, 41062C952A3794EA00FD48EA /* BrewPaths.swift in Sources */, + 4192565B2D1E0ECF00D9EF10 /* DiscoverSectionView+Placeholder.swift in Sources */, 41483CCD29101C9900BB10C2 /* Category.swift in Sources */, 418F331C28EB3D540023D76F /* AppGridView.swift in Sources */, + 419256332D1DF2E000D9EF10 /* SettingsView+BrewPath.swift in Sources */, + 419256552D1E0E2500D9EF10 /* DiscoverSectionView+CategoryHeader.swift in Sources */, 41062C972A3A20F900FD48EA /* UninstallSelf.swift in Sources */, 418F332628EC921D0023D76F /* CaskData.swift in Sources */, 4178CF922A8689AF0037F270 /* ExportCasks.swift in Sources */, + 4192562E2D1DF22500D9EF10 /* SetupView+AllSet.swift in Sources */, 413E60B72BBAE5E000978F6A /* NetworkProxyManager.swift in Sources */, 4196C8F928F9CDF700EADDDA /* DownloadView.swift in Sources */, 4120AB682A755B5A00F68EFE /* CheckForUpdatesView.swift in Sources */, 4120AB652A754B1700F68EFE /* AppliteAppView.swift in Sources */, 412635442A77FB1600155034 /* BrewInstallationProgress.swift in Sources */, + 419256202D1DEC0D00D9EF10 /* AppView+UpdateButton.swift in Sources */, + 419256252D1DF17F00D9EF10 /* SetupView+Welcome.swift in Sources */, 419256082D1C734600D9EF10 /* Shell.swift in Sources */, + 4192563E2D1DF3E900D9EF10 /* ContentView+DetailView.swift in Sources */, + 419256162D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Applite/Components/BrewPathSelectorView.swift b/Applite/Components/BrewPathSelectorView.swift deleted file mode 100755 index 8eeeedc..0000000 --- a/Applite/Components/BrewPathSelectorView.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// BrewPathSelectorView.swift -// Applite -// -// Created by Milán Várady on 2023. 01. 03.. -// - -import SwiftUI - -/// Provides a picker so the user can select the brew executable path they want to use -struct BrewPathSelectorView: View { - @Binding var isSelectedPathValid: Bool - - @StateObject var customBrewPathDebounced = DebounceObject() - - @AppStorage(Preferences.customUserBrewPath.rawValue) var customUserBrewPath: String = BrewPaths.getBrewExectuablePath(for: .defaultAppleSilicon, shellFriendly: false) - @AppStorage(Preferences.brewPathOption.rawValue) var brewPathOption = BrewPaths.PathOption.defaultAppleSilicon.rawValue - - @State var choosingCustomFolder = false - - private func getPathDescription(for option: BrewPaths.PathOption) -> String { - switch option { - case .appPath: - return "\(Bundle.main.appName)'s installation" - - case .defaultAppleSilicon: - return "Apple Silicon Mac" - - case .defaultIntel: - return "Intel Mac" - - case .custom: - return "" - } - } - - var body: some View { - VStack(alignment: .leading) { - Picker("", selection: $brewPathOption) { - ForEach(BrewPaths.PathOption.allCases) { option in - if option != .custom { - HStack { - Text("\(getPathDescription(for: option))") - Text("(\(BrewPaths.getBrewExectuablePath(for: option, shellFriendly: false)))").truncationMode(.middle).lineLimit(1).foregroundColor(.gray) - if option.rawValue == brewPathOption { - if isSelectedPathValid { - Image(systemName: "checkmark.circle") - .font(.system(size: 16)) - .foregroundColor(.green) - } else { - Image(systemName: "xmark.circle") - .font(.system(size: 16)) - .foregroundColor(.red) - } - } - } - .tag(option.rawValue) - } else { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text("Custom") - - if option.rawValue == brewPathOption { - if isSelectedPathValid { - Image(systemName: "checkmark.circle") - .font(.system(size: 16)) - .foregroundColor(.green) - } else { - Image(systemName: "xmark.circle") - .font(.system(size: 16)) - .foregroundColor(.red) - } - } - } - - HStack { - TextField("Custom brew path", text: $customBrewPathDebounced.text, prompt: Text("/path/to/brew")) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 300) - .autocorrectionDisabled() - - Button("Browse") { - choosingCustomFolder = true - } - .fileImporter( - isPresented: $choosingCustomFolder, - allowedContentTypes: [.unixExecutable] - ) { result in - switch result { - case .success(let file): - customBrewPathDebounced.text = file.path - case .failure(let error): - print(error.localizedDescription) - } - } - } - .disabled(brewPathOption != BrewPaths.PathOption.custom.rawValue) - } - .tag(option.rawValue) - } - } - } - .pickerStyle(.radioGroup) - } - .onAppear { - customBrewPathDebounced.text = customUserBrewPath - isSelectedPathValid = BrewPaths.isSelectedBrewPathValid() - } - .onChange(of: brewPathOption) { _ in - isSelectedPathValid = BrewPaths.isSelectedBrewPathValid() - } - .onChange(of: customBrewPathDebounced.debouncedText) { newPath in - customUserBrewPath = newPath - - if brewPathOption == BrewPaths.PathOption.custom.rawValue { - isSelectedPathValid = isBrewPathValid(path: newPath) - } - } - } -} - -struct BrewPathSelectorView_Previews: PreviewProvider { - static var previews: some View { - BrewPathSelectorView(isSelectedPathValid: .constant(false)) - } -} diff --git a/Applite/Model/Cask Data/Cask.swift b/Applite/Model/Cask Data/Cask.swift deleted file mode 100755 index 3fcbdbb..0000000 --- a/Applite/Model/Cask Data/Cask.swift +++ /dev/null @@ -1,334 +0,0 @@ -// -// Cask.swift -// Applite -// -// Created by Milán Várady on 2022. 10. 04.. -// - -import SwiftUI -import os - -/// Holds all essential data of a Homebrew cask and provides methods to run brew commands on it (e.g. install, uninstall, update) -final class Cask: Identifiable, Decodable, Hashable, ObservableObject { - /// Unique id of the class, this is the same name you would use to download the cask with brew - let id: String - /// Longer format cask name - let name: String - /// Short description - let description: String - let homepageURL: URL? - /// Number of downloads in the last 365 days - var downloadsIn365days: Int = 0 - /// Description of any caveats with the app - let caveats: String? - /// If true app has a .pkg installer - let pkgInstaller: Bool - - /// Cask progress state when installing, updating or uninstalling - public enum ProgressState: Equatable, Hashable { - case idle - case busy(withTask: String) - case downloading(percent: Double) - case success - case failed(output: String) - } - - @MainActor - @Published var isInstalled: Bool = false - - @MainActor - @Published var isOutdated: Bool = false - - /// Progress state of the cask when installing, updating or uninstalling - @MainActor - @Published public var progressState: ProgressState = .idle - - private static let logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: String(describing: Cask.self) - ) - - init(from decoder: Decoder) throws { - let rawData = try? CaskDTO(from: decoder) - - let homepage: String = rawData?.homepage ?? "https://brew.sh/" - - self.id = rawData?.token ?? "N/A" - self.name = rawData?.nameArray[0] ?? "N/A" - self.description = rawData?.desc ?? "N/A" - self.homepageURL = URL(string: homepage) - self.caveats = rawData?.caveats - self.pkgInstaller = rawData?.url.hasSuffix("pkg") ?? false - } - - init() { - self.id = "test" - self.name = "Test app" - self.description = "An application to test this application" - self.homepageURL = URL(string: "https://aerolite.dev/") - self.caveats = nil - self.pkgInstaller = false - } - - /// Installs the cask - /// - /// - Parameters: - /// - force: If `true` install will be run with the `--force` flag - /// - Returns: `Void` - func install(caskData: CaskData, force: Bool = false) async { - defer { - resetProgressState(caskData: caskData) - } - - Self.logger.info("Cask \"\(self.id)\" installation started") - - // Appdir argument - let appdirOn = UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue) - let appdirPath = UserDefaults.standard.string(forKey: Preferences.appdirPath.rawValue) - let appdirArgument = "--appdir=\"\(appdirPath ?? "/Applications")\"" - - // Install command - let command = "\(BrewPaths.currentBrewExecutable) install --cask \(force ? "--force" : "") \(self.id) \(appdirOn ? appdirArgument : "")" - - // Setup progress - await MainActor.run { - self.progressState = .busy(withTask: "") - caskData.busyCasks.insert(self) - } - - var completeOutput = "" - - // Run install command and stream output - do { - for try await line in Shell.stream(command) { - completeOutput += line - - await MainActor.run { - self.progressState = self.parseBrewInstall(output: line) - } - } - } catch { - Self.logger.error("Failed to install cask \(self.id).") - - // Capture output - let output = completeOutput - - await MainActor.run { - progressState = .failed(output: output) - caskData.busyCasks.remove(self) - } - - sendNotification(title: String(localized: "Failed to download \(self.name)"), reason: .failure) - } - - Self.logger.info("Successfully installed cask \(self.id)") - - sendNotification(title: String(localized: "\(self.name) successfully installed!"), reason: .success) - - await MainActor.run { - progressState = .success - self.isInstalled = true - } - - // Show success for 2 seconds - try? await Task.sleep(for: .seconds(2)) - } - - /// Parses the shell output when installing a cask - private func parseBrewInstall(output: String) -> ProgressState { - if output.contains("Downloading") { - return .busy(withTask: "") - } else if output.contains("#") { - let regex = /#+\s+(\d+\.\d+)%/ - - if let result = output.firstMatch(of: regex) { - return .downloading(percent: (Double(result.1) ?? 0) / 100) - } - } - else if output.contains("Installing") || output.contains("Moving") || output.contains("Linking") { - return .busy(withTask: String(localized: "Installing")) - } - else if output.contains("successfully installed") { - return .success - } - - return .busy(withTask: "") - } - - /// Uninstalls the cask - /// - Parameters: - /// - caskData: ``CaskData`` object - /// - zap: If true the app will be uninstalled completely using the brew --zap flag - func uninstall(caskData: CaskData, zap: Bool = false) async { - defer { - resetProgressState(caskData: caskData) - } - - _ = await MainActor.run { - caskData.busyCasks.insert(self) - } - - let arguments: [String] = if zap { ["--zap", self.id] } else { [self.id] } - - await runBrewCommand( - command: "uninstall", - arguments: arguments, - taskDescription: "Uninstalling", - notificationSuccess: String(localized:"\(self.name) successfully uninstalled"), - notificationFailure: "Failed to uninstall \(self.name)", - onSuccess: { self.isInstalled = false } - ) - } - - /// Updates the cask - /// - Returns: Bool - Whether the task has failed or not - func update(caskData: CaskData) async { - defer { - resetProgressState(caskData: caskData) - } - - _ = await MainActor.run { - caskData.busyCasks.insert(self) - } - - await runBrewCommand( - command: "upgrade", - arguments: [self.id], - taskDescription: "Updating", - notificationSuccess: String(localized: "\(self.name) successfully updated"), - notificationFailure: String(localized: "Failed to update \(self.name)"), - onSuccess: { - self.isOutdated = false - caskData.outdatedCasks.remove(self) - }) - } - - /// Updates the cask - /// - Returns: Bool - Whether the task has failed or not - func reinstall(caskData: CaskData) async { - defer { - resetProgressState(caskData: caskData) - } - - _ = await MainActor.run { - caskData.busyCasks.insert(self) - } - - await runBrewCommand( - command: "reinstall", - arguments: [self.id], - taskDescription: "Reinstalling", - notificationSuccess: String(localized: "\(self.name) successfully reinstalled"), - notificationFailure: String(localized:"Failed to reinstall \(self.name)"), - onSuccess: { - caskData.busyCasks.remove(self) - } - ) - } - - /// Runs a shell command with the currently selected brew path - /// - /// - Parameters: - /// - command: Brew command to be run - /// - arguments: Command arguments - /// - taskDesctiption: Description showed under the progress indicator in the UI - /// - notificationSuccess: Notification message if succeeds - /// - notificationFailure: Notification message if fails - /// - onSuccess: Closure run if task succeeds - /// - Returns: Bool - Whether the the task has failed or not - private func runBrewCommand( - command: String, - arguments: [String], - taskDescription: String, - notificationSuccess: String, - notificationFailure: String, - onSuccess: (@MainActor () -> Void)? = nil - ) async { - await MainActor.run { - let localizedTaskDescription = String.LocalizationValue(stringLiteral: taskDescription) - self.progressState = .busy(withTask: String(localized: localizedTaskDescription)) - } - - let command = "HOMEBREW_NO_AUTO_UPDATE=1 \(BrewPaths.currentBrewExecutable) \(command) --cask \(arguments.joined(separator: " "))" - - var output: String = "" - - do { - output = try await Shell.runAsync(command) - } catch { - Self.logger.error("Failed to run brew command: \(error.localizedDescription)") - - sendNotification(title: notificationFailure, reason: .failure) - - await MainActor.run { self.progressState = .failed(output: error.localizedDescription) } - } - - if let onSuccess { - await MainActor.run { - onSuccess() - } - } - - // Log and Notify - Self.logger.notice("Successfully run brew command \"\(command)\" with arguments \"\(arguments)\", output: \(output)") - - sendNotification(title: notificationSuccess, reason: .success) - - // Show success for 2 seconds - await MainActor.run { self.progressState = .success } - try? await Task.sleep(for: .seconds(2)) - } - - public func launchApp() throws { - let appPath: String - - if self.pkgInstaller { - // Open PKG type app - var applicationsDirectory = "/Applications" - - // Appdir - if UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue) { - applicationsDirectory = UserDefaults.standard.string(forKey: Preferences.appdirPath.rawValue) ?? "/Applications" - - // Remove trailing "/" - if applicationsDirectory.hasSuffix("/") { - applicationsDirectory.removeLast() - } - } - - appPath = "\"\(applicationsDirectory)/\(self.name).app\"" - } else { - // Open normal app - let brewDirectory = BrewPaths.currentBrewDirectory - - appPath = "\(brewDirectory.replacingOccurrences(of: " ", with: "\\ ") )/Caskroom/\(self.id)/*/*.app" - } - - try Shell.run("open \(appPath)") - } - - /// Resets progress state and removes self from ``CaskData.busyCasks`` - private func resetProgressState(caskData: CaskData) { - Task { - await MainActor.run { - // Only reset state if it's not failed - if case .failed(_) = self.progressState { - } else { - self.progressState = .idle - caskData.busyCasks.remove(self) - - // Filter busy casks to make sure - caskData.filterBusyCasks() - } - } - } - } - - static func == (lhs: Cask, rhs: Cask) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.id) - } -} diff --git a/Applite/Model/Cask Data/BrewAnalytics.swift b/Applite/Model/Cask Models/BrewAnalytics.swift similarity index 100% rename from Applite/Model/Cask Data/BrewAnalytics.swift rename to Applite/Model/Cask Models/BrewAnalytics.swift diff --git a/Applite/Model/Cask Models/Cask/Cask+BrewFunctions.swift b/Applite/Model/Cask Models/Cask/Cask+BrewFunctions.swift new file mode 100644 index 0000000..de13fba --- /dev/null +++ b/Applite/Model/Cask Models/Cask/Cask+BrewFunctions.swift @@ -0,0 +1,264 @@ +// +// Cask+BrewFunctions.swift +// Applite +// +// Created by Milán Várady on 2024.12.27. +// + +import Foundation + +extension Cask { + /// Installs the cask + /// + /// - Parameters: + /// - caskData: ``CaskData`` object passed in by the view + /// - force: If `true` install will be run with the `--force` flag + func install(caskData: CaskData, force: Bool = false) async { + defer { + resetProgressState(caskData: caskData) + } + + Self.logger.info("Cask \"\(self.id)\" installation started") + + // Appdir argument + let appdirOn = UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue) + let appdirPath = UserDefaults.standard.string(forKey: Preferences.appdirPath.rawValue) + let appdirArgument = "--appdir=\"\(appdirPath ?? "/Applications")\"" + + // Install command + let command = "\(BrewPaths.currentBrewExecutable) install --cask \(force ? "--force" : "") \(self.id) \(appdirOn ? appdirArgument : "")" + + // Setup progress + self.progressState = .busy(withTask: "") + caskData.busyCasks.insert(self) + + /// Holds the complete output of the install process + var completeOutput = "" + + // Run install command and stream output + do { + for try await line in Shell.stream(command) { + completeOutput += line + self.progressState = self.parseBrewInstall(output: line) + } + } catch { + let alertMessage = switch completeOutput { + // Already installed + case _ where completeOutput.contains("It seems there is already an App"): + String(localized: "\(self.name) is already installed. If you want to add it to \(Bundle.main.appName) click more options (chevron icon) and press Force Install.") + // Network error + case _ where completeOutput.contains("Could not resolve host"): + String(localized: "Couldn't download app. No internet connection, or host is unreachable.") + default: + error.localizedDescription + } + + showFailure( + error: error, + output: completeOutput, + alertTitle: "Failed to install \(self.name)", + alertMessage: alertMessage + ) + return + } + + showSuccess( + logMessage: "Successfully installed cask \(self.id)", + alertTitle: "\(self.name) successfully installed!" + ) + + // Update state + self.isInstalled = true + } + + /// Uninstalls the cask + /// - Parameters: + /// - caskData: ``CaskData`` object + /// - zap: If true the app will be uninstalled completely using the brew --zap flag + func uninstall(caskData: CaskData, zap: Bool = false) async { + defer { + resetProgressState(caskData: caskData) + } + + progressState = .busy(withTask: "Uninstalling") + caskData.busyCasks.insert(self) + + var arguments: [String] = [self.id] + + // Add -- zap argument + if zap { + arguments.append("--zap") + } + + var output: String = "" + + do { + output = try await Shell.runBrewCommand("uninstall", arguments: arguments) + } catch { + showFailure( + error: error, + output: output, + alertTitle: "Failed to uninstall \(self.name)", + alertMessage: error.localizedDescription + ) + return + } + + showSuccess( + logMessage: "Successfully uninstalled \(self.id)", + alertTitle: "\(self.name) successfully uninstalled" + ) + + // Update state + self.isInstalled = false + } + + /// Updates the cask + func update(caskData: CaskData) async { + defer { + resetProgressState(caskData: caskData) + } + + progressState = .busy(withTask: "Updating") + caskData.busyCasks.insert(self) + + var output: String = "" + + do { + output = try await Shell.runBrewCommand("uninstall", arguments: [self.id]) + } catch { + showFailure( + error: error, + output: output, + alertTitle: "Failed to update \(self.name)", + alertMessage: error.localizedDescription + ) + return + } + + showSuccess( + logMessage: "Successfully updated \(self.id)", + alertTitle: "\(self.name) successfully updated" + ) + + // Update state + caskData.outdatedCasks.remove(self) + } + + /// Reinstalls the cask + func reinstall(caskData: CaskData) async { + defer { + resetProgressState(caskData: caskData) + } + + progressState = .busy(withTask: "Reinstalling") + caskData.busyCasks.insert(self) + + var output: String = "" + + do { + output = try await Shell.runBrewCommand("uninstall", arguments: [self.id]) + } catch { + showFailure( + error: error, + output: output, + alertTitle: "Failed to reinstall \(self.name)", + alertMessage: error.localizedDescription + ) + return + } + + showSuccess( + logMessage: "Successfully reinstalled \(self.id)", + alertTitle: "\(self.name) successfully reinstalled" + ) + } + + // MARK: - Helper functions + + /// Parses the shell output when installing a cask + private func parseBrewInstall(output: String) -> ProgressState { + if output.contains("Downloading") { + return .busy(withTask: "") + } else if output.contains("#") { + let regex = /#+\s+(\d+\.\d+)%/ + + if let result = output.firstMatch(of: regex) { + return .downloading(percent: (Double(result.1) ?? 0) / 100) + } + } + else if output.contains("Installing") || output.contains("Moving") || output.contains("Linking") { + return .busy(withTask: String(localized: "Installing")) + } + else if output.contains("successfully installed") { + return .success + } + + return .busy(withTask: "") + } + + /// Register successful task + /// + /// - Logs success + /// - Sends notification + /// - Sets progress state to success for 2 seconds + private func showSuccess( + logMessage: String, + alertTitle: String, + alertMessage: String = "" + ) { + Self.logger.info("\(logMessage)") + sendNotification(title: alertTitle, body: alertMessage, reason: .success) + + // Show success for 2 seconds + progressState = .success + Task { + try? await Task.sleep(for: .seconds(2)) + progressState = .idle + } + } + + /// Register failed task + /// + /// - Logs error + /// - Shows alert and notification + /// - Sets progress state to failed + private func showFailure( + error: Error, + output: String, + alertTitle: String, + alertMessage: String, + notificationTitle: String? = nil, + notificationMessage: String = "" + ) { + // Log error + Self.logger.error("\(alertTitle)\nError: \(error.localizedDescription)\nOutput: \(output)") + + // Alert + alert.show(title: alertTitle, message: alertMessage) + + // Send notification + let notificationTitle = notificationTitle ?? alertTitle + sendNotification(title: notificationTitle, body: notificationMessage, reason: .failure) + + // Set progress state to failed + progressState = .failed(output: output) + } + + /// Resets progress state and removes self from ``CaskData.busyCasks`` + private func resetProgressState(caskData: CaskData) { + Task { + caskData.busyCasks.remove(self) + + // Reset state unless it's not succes or failed + switch self.progressState { + case .success: + break + case .failed(_): + break + default: + self.progressState = .idle + } + } + } +} diff --git a/Applite/Model/Cask Models/Cask/Cask+LaunchApp.swift b/Applite/Model/Cask Models/Cask/Cask+LaunchApp.swift new file mode 100644 index 0000000..8c49f1b --- /dev/null +++ b/Applite/Model/Cask Models/Cask/Cask+LaunchApp.swift @@ -0,0 +1,38 @@ +// +// Cask+LaunchApp.swift +// Applite +// +// Created by Milán Várady on 2024.12.27. +// + +import Foundation + +extension Cask { + func launchApp() throws { + let appPath: String + + if self.pkgInstaller { + // Open PKG type app + var applicationsDirectory = "/Applications" + + // Appdir + if UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue) { + applicationsDirectory = UserDefaults.standard.string(forKey: Preferences.appdirPath.rawValue) ?? "/Applications" + + // Remove trailing "/" + if applicationsDirectory.hasSuffix("/") { + applicationsDirectory.removeLast() + } + } + + appPath = "\"\(applicationsDirectory)/\(self.name).app\"" + } else { + // Open normal app + let brewDirectory = BrewPaths.currentBrewDirectory + + appPath = "\(brewDirectory.replacingOccurrences(of: " ", with: "\\ ") )/Caskroom/\(self.id)/*/*.app" + } + + try Shell.run("open \(appPath)") + } +} diff --git a/Applite/Model/Cask Models/Cask/Cask+ProtocolConformances.swift b/Applite/Model/Cask Models/Cask/Cask+ProtocolConformances.swift new file mode 100644 index 0000000..944abf5 --- /dev/null +++ b/Applite/Model/Cask Models/Cask/Cask+ProtocolConformances.swift @@ -0,0 +1,20 @@ +// +// Cask+ProtocolConformances.swift +// Applite +// +// Created by Milán Várady on 2024.12.27. +// + +import Foundation + +extension Cask { + // Equatable + nonisolated static func == (lhs: Cask, rhs: Cask) -> Bool { + lhs.id == rhs.id + } + + // Hashable + nonisolated func hash(into hasher: inout Hasher) { + hasher.combine(self.id) + } +} diff --git a/Applite/Model/Cask Models/Cask/Cask.swift b/Applite/Model/Cask Models/Cask/Cask.swift new file mode 100755 index 0000000..3960d1c --- /dev/null +++ b/Applite/Model/Cask Models/Cask/Cask.swift @@ -0,0 +1,77 @@ +// +// Cask.swift +// Applite +// +// Created by Milán Várady on 2022. 10. 04.. +// + +import SwiftUI +import os + +/// Holds all essential data of a Homebrew cask and provides methods to run brew commands on it (e.g. install, uninstall, update) +@MainActor +final class Cask: Identifiable, Decodable, Hashable, ObservableObject { + // MARK: - Static properties + + /// Unique id of the class, this is the same name you would use to download the cask with brew + let id: String + /// Longer format cask name + let name: String + /// Short description + let description: String + let homepageURL: URL? + /// Number of downloads in the last 365 days + var downloadsIn365days: Int = 0 + /// Description of any caveats with the app + let caveats: String? + /// If true app has a .pkg installer + let pkgInstaller: Bool + + // MARK: - Published properties + + @Published var isInstalled: Bool = false + @Published var isOutdated: Bool = false + + /// Progress state of the cask when installing, updating or uninstalling + @Published var progressState: ProgressState = .idle + + @Published var alert = AlertManager() + + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: Cask.self) + ) + + /// Cask progress state when installing, updating or uninstalling + enum ProgressState: Equatable, Hashable { + case idle + case busy(withTask: String) + case downloading(percent: Double) + case success + case failed(output: String) + } + + // MARK: - Initializers + + nonisolated init(from decoder: Decoder) throws { + let rawData = try? CaskDTO(from: decoder) + + let homepage: String = rawData?.homepage ?? "https://brew.sh/" + + self.id = rawData?.token ?? "N/A" + self.name = rawData?.nameArray[0] ?? "N/A" + self.description = rawData?.desc ?? "N/A" + self.homepageURL = URL(string: homepage) + self.caveats = rawData?.caveats + self.pkgInstaller = rawData?.url.hasSuffix("pkg") ?? false + } + + init() { + self.id = "test" + self.name = "Test app" + self.description = "An application to test this application" + self.homepageURL = URL(string: "https://aerolite.dev/") + self.caveats = nil + self.pkgInstaller = false + } +} diff --git a/Applite/Model/Cask Data/CaskDTO.swift b/Applite/Model/Cask Models/CaskDTO.swift similarity index 100% rename from Applite/Model/Cask Data/CaskDTO.swift rename to Applite/Model/Cask Models/CaskDTO.swift diff --git a/Applite/Model/Cask Data/CaskData.swift b/Applite/Model/Cask Models/CaskData.swift similarity index 100% rename from Applite/Model/Cask Data/CaskData.swift rename to Applite/Model/Cask Models/CaskData.swift diff --git a/Applite/Utilities/Alert Manager/AlertManager.swift b/Applite/Utilities/Alert Manager/AlertManager.swift new file mode 100644 index 0000000..4f4b538 --- /dev/null +++ b/Applite/Utilities/Alert Manager/AlertManager.swift @@ -0,0 +1,58 @@ +// +// AlertManager.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import Foundation +import SwiftUI + +/// A helper class for easier alert management +@MainActor +final class AlertManager: ObservableObject { + @Published var isPresented: Bool = false + @Published var title: String = "" + @Published var message: String = "" + @Published var primaryButtonTitle: String = "OK" + @Published var primaryAction: (() -> Void)? + + /// Presents alert + func show( + title: String, + message: String = "", + primaryButtonTitle: String = "OK", + primaryAction: (() -> Void)? = nil + ) { + self.title = title + self.message = message + self.primaryButtonTitle = primaryButtonTitle + self.primaryAction = primaryAction + self.isPresented = true + } + + /// Shows an alert based on an error + func show( + error: LocalizedError, + overrideTitle: String? = nil, + primaryButtonTitle: String = "OK", + primaryAction: (() -> Void)? = nil + ) { + let title = overrideTitle ?? error.errorDescription ?? error.localizedDescription + + show( + title: title, + message: error.failureReason ?? "", + primaryButtonTitle: primaryButtonTitle, + primaryAction: primaryAction + ) + } + + /// Resets values + func dismiss() { + title = "" + message = "" + primaryButtonTitle = "OK" + primaryAction = nil + } +} diff --git a/Applite/Utilities/Alert Manager/AlertManagerViewModifier.swift b/Applite/Utilities/Alert Manager/AlertManagerViewModifier.swift new file mode 100644 index 0000000..2e5a0d2 --- /dev/null +++ b/Applite/Utilities/Alert Manager/AlertManagerViewModifier.swift @@ -0,0 +1,37 @@ +// +// AlertManagerViewModifier.swift +// Applite +// +// Created by Milán Várady on 2024.12.27. +// + +import SwiftUI + +struct AlertModifier: ViewModifier { + @ObservedObject var manager: AlertManager + + func body(content: Content) -> some View { + content + .alert(manager.title, isPresented: $manager.isPresented) { + Button(manager.primaryButtonTitle) { + manager.primaryAction?() + manager.dismiss() + } + + // Add cancel button if we have a primary action + if manager.primaryAction != nil { + Button("Cancel", role: .cancel) { + manager.dismiss() + } + } + } message: { + Text(manager.message) + } + } +} + +extension View { + func alertManager(_ manager: AlertManager) -> some View { + modifier(AlertModifier(manager: manager)) + } +} diff --git a/Applite/Utilities/Brew Installation/DependencyError.swift b/Applite/Utilities/Brew Installation/DependencyError.swift index 58a0398..89c1126 100644 --- a/Applite/Utilities/Brew Installation/DependencyError.swift +++ b/Applite/Utilities/Brew Installation/DependencyError.swift @@ -7,15 +7,20 @@ import Foundation -enum DependencyError: Error { +enum DependencyError: LocalizedError { case xcodeCommandLineToolsTimeout -} -extension DependencyError: LocalizedError { - public var errorDescription: String? { + var errorDescription: String? { + switch self { + case .xcodeCommandLineToolsTimeout: + return "Couldn't install Xcode Command Line Tools" + } + } + + var failureReason: String? { switch self { case .xcodeCommandLineToolsTimeout: - return "Xcode Command Line Tools install timeout" + return "Couldn't install Xcode Command Line Tools in a reasonable amount of time" } } } diff --git a/Applite/Utilities/Network Proxy/NetworkProxyManager.swift b/Applite/Utilities/Network Proxy/NetworkProxyManager.swift index 7b2fcba..c7e7534 100644 --- a/Applite/Utilities/Network Proxy/NetworkProxyManager.swift +++ b/Applite/Utilities/Network Proxy/NetworkProxyManager.swift @@ -177,9 +177,22 @@ enum NetworkProxyType: String, CaseIterable, Identifiable { } } -enum NetworkProxyError: Error { +enum NetworkProxyError: LocalizedError { case failedToGetSystemSettings case proxyNotEnabled case noProxyHost case noProxyPort + + var errorDescription: String? { + switch self { + case .failedToGetSystemSettings: + return "Failed to get system proxy settings" + case .proxyNotEnabled: + return "Proxy is not enabled" + case .noProxyHost: + return "No proxy host specified" + case .noProxyPort: + return "No proxy port specified" + } + } } diff --git a/Applite/Utilities/BrewPaths.swift b/Applite/Utilities/Other/BrewPaths.swift similarity index 100% rename from Applite/Utilities/BrewPaths.swift rename to Applite/Utilities/Other/BrewPaths.swift diff --git a/Applite/Utilities/DebounceObject.swift b/Applite/Utilities/Other/DebounceObject.swift similarity index 100% rename from Applite/Utilities/DebounceObject.swift rename to Applite/Utilities/Other/DebounceObject.swift diff --git a/Applite/Utilities/SendNotification.swift b/Applite/Utilities/Other/SendNotification.swift similarity index 100% rename from Applite/Utilities/SendNotification.swift rename to Applite/Utilities/Other/SendNotification.swift diff --git a/Applite/Utilities/UninstallSelf.swift b/Applite/Utilities/Other/UninstallSelf.swift similarity index 97% rename from Applite/Utilities/UninstallSelf.swift rename to Applite/Utilities/Other/UninstallSelf.swift index 44f0045..87bb114 100755 --- a/Applite/Utilities/UninstallSelf.swift +++ b/Applite/Utilities/Other/UninstallSelf.swift @@ -9,10 +9,6 @@ import Foundation import OSLog import Kingfisher -enum UninstallError: Error { - case fileError -} - /// This function will uninstall Applite and all it's related files func uninstallSelf(deleteBrewCache: Bool) throws { let logger = Logger() diff --git a/Applite/Utilities/Shell/Shell.swift b/Applite/Utilities/Shell/Shell.swift index 2d4b05f..d188913 100644 --- a/Applite/Utilities/Shell/Shell.swift +++ b/Applite/Utilities/Shell/Shell.swift @@ -47,6 +47,13 @@ public enum Shell { try run(command) } + /// Executes a brew command asynchronously + @discardableResult + static func runBrewCommand(_ brewCommand: String, arguments: [String]) async throws -> String { + let command = "brew \(brewCommand) --cask \(arguments.joined(separator: " "))" + return try await runAsync(command) + } + /// Executes a shell command and streams the output static func stream(_ command: String) -> AsyncThrowingStream { AsyncThrowingStream { continuation in @@ -110,8 +117,8 @@ public enum Shell { task.standardOutput = pipe task.standardError = pipe task.environment = environment - task.arguments = ["-l", "-c", command] task.executableURL = URL(fileURLWithPath: "/bin/zsh") + task.arguments = ["-l", "-c", command] task.standardInput = nil return (task, pipe) diff --git a/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift b/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift index 39e4e58..e5c009c 100755 --- a/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift +++ b/Applite/Utilities/Verify Brew Installation/isBrewPathValid.swift @@ -25,7 +25,7 @@ public func isBrewPathValid(path: String) -> Bool { if !path.hasSuffix("brew") && !path.hasSuffix("brew\"") { return false } - + // Check if Homebrew is returned when checking version guard let output = try? Shell.run("\(path) --version") else { return false diff --git a/Applite/Views/App Views/App View/AppView+ActionsView.swift b/Applite/Views/App Views/App View/AppView+ActionsView.swift new file mode 100644 index 0000000..e987d4e --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+ActionsView.swift @@ -0,0 +1,117 @@ +// +// AppView+ActionsView.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI +import CircularProgress + +extension AppView { + @ViewBuilder + var actionsView: some View { + if self.cask.progressState == .idle { + if !keepSuccessIndicator { + mainButtons + } else { + successCheckmark + } + } else { + progressView + } + } + + @ViewBuilder + private var mainButtons: some View { + switch role { + case .installAndManage: + if cask.isInstalled { + OpenAndManageView(cask: cask, deleteButton: false) + } else { + DownloadButton(cask: cask) + .padding(.trailing, 5) + } + + case .update: + UpdateButton(cask: cask) + + case .installed: + OpenAndManageView(cask: cask, deleteButton: true) + .padding(.trailing, 5) + } + } + + private var successCheckmark: some View { + Image(systemName: "checkmark") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.green) + } + + @ViewBuilder + private var progressView: some View { + switch cask.progressState { + case .busy(let task): + ProgressView() { + if !task.isEmpty { + Text(task) + .font(.system(size: 12)) + } + } + .scaleEffect(0.8) + + case .downloading(let percent): + CircularProgressView(count: Int(percent * 100), + total: 100, + progress: CGFloat(percent), + fontOne: Font.system(size: 16).bold(), + lineWidth: 6, + showBottomText: false) + .frame(width: 40, height: 40) + + case .success: + Image(systemName: "checkmark") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.green) + .scaleEffect(successCheckmarkScale) + .onAppear { + withAnimation(.spring(blendDuration: 0.5)) { + successCheckmarkScale = 1 + } + + if self.role == .installAndManage { + Task { @MainActor in + try await Task.sleep(for: .seconds(1.5)) + withAnimation(.spring(blendDuration: 1)) { + successCheckmarkScale = 0.0001 + } + } + } else { + keepSuccessIndicator = true + } + } + + case .failed(let output): + HStack { + Text("Error") + .foregroundStyle(.red) + + Button { + // Open new window with shell output + openWindow(value: output) + } label: { + Image(systemName: "info.circle") + } + .buttonStyle(.bordered) + + Button("OK") { + cask.progressState = .idle + } + .buttonStyle(.bordered) + } + + case .idle: + EmptyView() + } + } +} diff --git a/Applite/Views/App Views/App View/AppView+DownloadButton.swift b/Applite/Views/App Views/App View/AppView+DownloadButton.swift new file mode 100644 index 0000000..3d27b74 --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+DownloadButton.swift @@ -0,0 +1,112 @@ +// +// AppView+DownloadButton.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension AppView { + /// Button used in the Download section, downloads the app + struct DownloadButton: View { + @ObservedObject var cask: Cask + + @EnvironmentObject var caskData: CaskData + + // Alerts + @State var showingPopover = false + @State var showingCaveats = false + @State var showingBrewError = false + @State var showingForceInstallConfirmation = false + + @State var buttonFill = false + + var body: some View { + /// Download button + Button { + if cask.caveats != nil { + // Show caveats dialog + showingCaveats = true + return + } + + download() + } label: { + Image(systemName: "arrow.down.to.line.circle\(buttonFill ? ".fill" : "")") + .font(.system(size: 22)) + .foregroundColor(.accentColor) + } + .padding(.trailing, -8) + .onHover { isHovering in + // Hover effect + withAnimation(.snappy) { + buttonFill = isHovering + } + } + .alert("App caveats", isPresented: $showingCaveats) { + Button("Download Anyway") { + download() + } + + Button("Cancel", role: .cancel) { } + } message: { + Text(cask.caveats ?? "") + } + .alert("Broken Brew Path", isPresented: $showingBrewError) {} message: { + Text(DependencyManager.brokenPathOrIstallMessage) + } + + // More actions popover + Button() { + showingPopover = true + } label: { + Image(systemName: "chevron.down") + .padding(.vertical) + .contentShape(Rectangle()) + } + .popover(isPresented: $showingPopover) { + VStack(alignment: .leading, spacing: 6) { + // Open homepage + if let homepageLink = cask.homepageURL { + Link(destination: homepageLink, label: { + Label("Homepage", systemImage: "house") + }) + .foregroundColor(.primary) + } else { + Text("No homepage found") + .fontWeight(.thin) + } + + // Force install button + Button { + showingForceInstallConfirmation = true + } label: { + Label("Force Install", systemImage: "bolt.trianglebadge.exclamationmark.fill") + } + } + .padding(8) + .buttonStyle(.plain) + } + .confirmationDialog("Are you sure you want to force install \(cask.name)? This will override any current installation!", isPresented: $showingForceInstallConfirmation) { + Button("Yes") { + download(force: true) + } + + Button("Cancel", role: .cancel) { } + } + } + + private func download(force: Bool = false) { + // Check if brew path is valid + guard BrewPaths.isSelectedBrewPathValid() else { + showingBrewError = true + return + } + + Task { + await cask.install(caskData: caskData, force: force) + } + } + } +} diff --git a/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift b/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift new file mode 100644 index 0000000..d14b377 --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+IconAndDescriptionView.swift @@ -0,0 +1,36 @@ +// +// AppView+IconAndDescriptionView.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension AppView { + var iconAndDescriptionView: some View { + return HStack { + if let iconURL = URL(string: "https://github.com/App-Fair/appcasks/releases/download/cask-\(cask.id)/AppIcon.png"), + let faviconURL = URL(string: "https://icon.horse/icon/\(cask.homepageURL?.host ?? "")") { + AppIconView( + iconURL: iconURL, + faviconURL: faviconURL, + cacheKey: cask.id + ) + .padding(.leading, 5) + } + + // Name and description + VStack(alignment: .leading) { + Text(cask.name) + .font(.system(size: 16, weight: .bold)) + + Text(cask.description) + .foregroundColor(.secondary) + } + + Spacer() + } + .contentShape(Rectangle()) + } +} diff --git a/Applite/Views/App Views/App View/AppView+OpenAndManageView.swift b/Applite/Views/App Views/App View/AppView+OpenAndManageView.swift new file mode 100644 index 0000000..196ac50 --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+OpenAndManageView.swift @@ -0,0 +1,85 @@ +// +// AppView+OpenAndManageView.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension AppView { + /// Button used in the Download section, launches, uninstalls or reinstalls the app + struct OpenAndManageView: View { + @StateObject var cask: Cask + let deleteButton: Bool + + @EnvironmentObject var caskData: CaskData + + @State var showAppNotFoundAlert = false + @State var showPopover = false + + @State private var isOptionKeyDown = false + + var body: some View { + // Lauch app + Button("Open") { + do { + try cask.launchApp() + } catch { + showAppNotFoundAlert = true + } + } + .font(.system(size: 14)) + .buttonStyle(.bordered) + .clipShape(Capsule()) + .alert("App couldn't be located", isPresented: $showAppNotFoundAlert) {} + + if deleteButton { + UninstallButton(cask: cask) + } + + // More options popover + Button() { + showPopover = true + } label: { + Image(systemName: "chevron.down") + .padding(.vertical) + .contentShape(Rectangle()) + } + .popover(isPresented: $showPopover) { + VStack(alignment: .leading, spacing: 6) { + // Reinstall button + Button { + Task { + await cask.reinstall(caskData: caskData) + } + } label: { + Label("Reinstall", systemImage: "arrow.2.squarepath") + } + + // Uninstall button + Button(role: .destructive) { + Task { + await cask.uninstall(caskData: caskData) + } + } label: { + Label("Uninstall", systemImage: "trash") + .foregroundStyle(.red) + } + + // Uninstall completely button + Button(role: .destructive) { + Task { + await cask.uninstall(caskData: caskData, zap: true) + } + } label: { + Label("Uninstall Completely", systemImage: "trash.fill") + .foregroundStyle(.red) + } + } + .padding(8) + .buttonStyle(.plain) + } + } + } +} diff --git a/Applite/Views/App Views/App View/AppView+UninstallButton.swift b/Applite/Views/App Views/App View/AppView+UninstallButton.swift new file mode 100644 index 0000000..9a4bfcc --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+UninstallButton.swift @@ -0,0 +1,29 @@ +// +// AppView+UninstallButton.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension AppView { + struct UninstallButton: View { + @StateObject var cask: Cask + @EnvironmentObject var caskData: CaskData + + @State var showingError = false + + var body: some View { + Button { + Task { + await cask.uninstall(caskData: caskData) + } + } label: { + Image(systemName: "trash.fill") + .font(.system(size: 20)) + } + .foregroundColor(.primary) + } + } +} diff --git a/Applite/Views/App Views/App View/AppView+UpdateButton.swift b/Applite/Views/App Views/App View/AppView+UpdateButton.swift new file mode 100644 index 0000000..50997ea --- /dev/null +++ b/Applite/Views/App Views/App View/AppView+UpdateButton.swift @@ -0,0 +1,27 @@ +// +// AppView+UpdateButton.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension AppView { + struct UpdateButton: View { + @EnvironmentObject var caskData: CaskData + @StateObject var cask: Cask + + var body: some View { + Button { + Task { + await cask.update(caskData: caskData) + } + } label: { + Image(systemName: "arrow.uturn.down.circle.fill") + .font(.system(size: 20)) + } + .foregroundColor(.blue) + } + } +} diff --git a/Applite/Views/App Views/App View/AppView.swift b/Applite/Views/App Views/App View/AppView.swift new file mode 100755 index 0000000..49c336c --- /dev/null +++ b/Applite/Views/App Views/App View/AppView.swift @@ -0,0 +1,58 @@ +// +// AppView.swift +// Applite +// +// Created by Milán Várady on 2022. 09. 24.. +// + +import SwiftUI +import CircularProgress + +/// App view role +enum AppRole { + case installAndManage // Used in the download section, or when searching + case update // Used in the update section + case installed // Used in the installed section +} + +/// Shows an application's icon and provides controls for installing, updating, uninstalling and opening the app. Used all across the app. +struct AppView: View { + /// A ``Cask`` object to display + @ObservedObject var cask: Cask + /// Role of the app, e.g. install, updated or uninstall + var role: AppRole + + @Environment(\.openWindow) var openWindow + + @EnvironmentObject var caskData: CaskData + + // Alerts + @State var failureAlertMessage = "" + @State var showingFailureAlert = false + + // Success animation + @State var successCheckmarkScale = 0.0001 + @State var keepSuccessIndicator = false + + /// App view dimensions, and spacing + public static let dimensions: (width: CGFloat, height: CGFloat, spacing: CGFloat) = (width: 320, height: 80, spacing: 20) + + var body: some View { + HStack { + // Icon name and description + iconAndDescriptionView + + // Buttons + actionsView + } + .buttonStyle(.plain) + .frame(width: Self.dimensions.width, height: Self.dimensions.height) + .alertManager(cask.alert) + } +} + +struct AppView_Previews: PreviewProvider { + static var previews: some View { + AppView(cask: Cask(), role: .installAndManage) + } +} diff --git a/Applite/Views/App Views/AppView.swift b/Applite/Views/App Views/AppView.swift deleted file mode 100755 index 5c9e354..0000000 --- a/Applite/Views/App Views/AppView.swift +++ /dev/null @@ -1,429 +0,0 @@ -// -// AppView.swift -// Applite -// -// Created by Milán Várady on 2022. 09. 24.. -// - -import SwiftUI -import CircularProgress - -/// App view role -enum AppRole { - case installAndManage // Used in the download section, or when searching - case update // Used in the update section - case installed // Used in the installed section -} - -/// Shows an application's icon and provides controls for installing, updating, uninstalling and opening the app. Used all across the app. -struct AppView: View { - /// A ``Cask`` object to display - @ObservedObject var cask: Cask - /// Role of the app, e.g. install, updated or uninstall - var role: AppRole - - @Environment(\.openWindow) var openWindow - - @EnvironmentObject var caskData: CaskData - - // Alerts - @State var showingBrewPathError = false - @State var failureAlertMessage = "" - @State var showingFailureAlert = false - - // Success animation - @State var successCheckmarkScale = 0.0001 - @State var keepSuccessIndicator = false - - /// App view dimensions, and spacing - public static let dimensions: (width: CGFloat, height: CGFloat, spacing: CGFloat) = (width: 320, height: 80, spacing: 20) - - var body: some View { - HStack { - // Icon name and description - iconAndDescriptionView - - // Buttons - actionsView - } - .buttonStyle(.plain) - .frame(width: Self.dimensions.width, height: Self.dimensions.height) - } - - private var iconAndDescriptionView: some View { - return HStack { - if let iconURL = URL(string: "https://github.com/App-Fair/appcasks/releases/download/cask-\(cask.id)/AppIcon.png"), - let faviconURL = URL(string: "https://icon.horse/icon/\(cask.homepageURL?.host ?? "")") { - AppIconView( - iconURL: iconURL, - faviconURL: faviconURL, - cacheKey: cask.id - ) - .padding(.leading, 5) - } - - // Name and description - VStack(alignment: .leading) { - Text(cask.name) - .font(.system(size: 16, weight: .bold)) - - Text(cask.description) - .foregroundColor(.secondary) - } - - Spacer() - } - .contentShape(Rectangle()) - .alert("Broken Brew Path", isPresented: $showingBrewPathError) { - Button("OK", role: .cancel) { - showingBrewPathError = false - } - } message: { - Text(LocalizedStringKey(DependencyManager.brokenPathOrIstallMessage)) - } - } - - @ViewBuilder - private var actionsView: some View { - if self.cask.progressState == .idle { - if !keepSuccessIndicator { - // Buttons - switch role { - case .installAndManage: - if cask.isInstalled { - OpenAndManageAppView(cask: cask, deleteButton: false) - } else { - DownloadButton(cask: cask) - .padding(.trailing, 5) - } - - case .update: - UpdateButton(cask: cask) - - case .installed: - OpenAndManageAppView(cask: cask, deleteButton: true) - .padding(.trailing, 5) - } - } else { - // Success checkmark - Image(systemName: "checkmark") - .font(.system(size: 18, weight: .bold)) - .foregroundColor(.green) - } - } else { - // Progress indicator - switch cask.progressState { - case .busy(let task): - ProgressView() { - if !task.isEmpty { - Text(task) - .font(.system(size: 12)) - } - } - .scaleEffect(0.8) - - case .downloading(let percent): - CircularProgressView(count: Int(percent * 100), - total: 100, - progress: CGFloat(percent), - fontOne: Font.system(size: 16).bold(), - lineWidth: 6, - showBottomText: false) - .frame(width: 40, height: 40) - - case .success: - Image(systemName: "checkmark") - .font(.system(size: 18, weight: .bold)) - .foregroundColor(.green) - .scaleEffect(successCheckmarkScale) - .onAppear { - withAnimation(.spring(blendDuration: 0.5)) { - successCheckmarkScale = 1 - } - - if self.role == .installAndManage { - Task { @MainActor in - try await Task.sleep(for: .seconds(1.5)) - withAnimation(.spring(blendDuration: 1)) { - successCheckmarkScale = 0.0001 - } - } - } else { - keepSuccessIndicator = true - } - } - - case .failed(let output): - HStack { - Text("Error") - .foregroundStyle(.red) - - Button { - // Open new window with shell output - openWindow(value: output) - } label: { - Image(systemName: "info.circle") - } - .buttonStyle(.bordered) - - Button("OK") { - cask.progressState = .idle - } - .buttonStyle(.bordered) - } - .onAppear { - // Alert for install errors - if output.contains("It seems there is already an App") { - failureAlertMessage = String(localized: "\(cask.name) is already installed. If you want to add it to \(Bundle.main.appName) click more options (chevron icon) and press Force Install.") - showingFailureAlert = true - } else if output.contains("Could not resolve host") { - failureAlertMessage = String(localized: "Couldn't download app. No internet connection, or host is unreachable.") - showingFailureAlert = true - } - } - .alert("Error", isPresented: $showingFailureAlert) { - Button("OK") { } - - Button("View Error") { - // Open new window with shell output - openWindow(value: output) - cask.progressState = .idle - } - } message: { - Text(failureAlertMessage) - } - - case .idle: - EmptyView() - } - } - } - - /// Button used in the Download section, downloads the app - private struct DownloadButton: View { - @ObservedObject var cask: Cask - - @EnvironmentObject var caskData: CaskData - - // Alerts - @State var showingPopover = false - @State var showingCaveats = false - @State var showingBrewError = false - @State var showingForceInstallConfirmation = false - - @State var buttonFill = false - - var body: some View { - /// Download button - Button { - if cask.caveats != nil { - // Show caveats dialog - showingCaveats = true - return - } - - download() - } label: { - Image(systemName: "arrow.down.to.line.circle\(buttonFill ? ".fill" : "")") - .font(.system(size: 22)) - .foregroundColor(.accentColor) - } - .padding(.trailing, -8) - .onHover { isHovering in - // Hover effect - withAnimation(.snappy) { - buttonFill = isHovering - } - } - .alert("App caveats", isPresented: $showingCaveats) { - Button("Download Anyway") { - download() - } - - Button("Cancel", role: .cancel) { } - } message: { - Text(cask.caveats ?? "") - } - .alert("Broken Brew Path", isPresented: $showingBrewError) { - Button("OK", role: .cancel) { } - } message: { - Text(DependencyManager.brokenPathOrIstallMessage) - } - - // More actions popover - Button() { - showingPopover = true - } label: { - Image(systemName: "chevron.down") - .padding(.vertical) - .contentShape(Rectangle()) - } - .popover(isPresented: $showingPopover) { - VStack(alignment: .leading, spacing: 6) { - // Open homepage - if let homepageLink = cask.homepageURL { - Link(destination: homepageLink, label: { - Label("Homepage", systemImage: "house") - }) - .foregroundColor(.primary) - } else { - Text("No homepage found") - .fontWeight(.thin) - } - - // Force install button - Button { - showingForceInstallConfirmation = true - } label: { - Label("Force Install", systemImage: "bolt.trianglebadge.exclamationmark.fill") - } - } - .padding(8) - .buttonStyle(.plain) - } - .confirmationDialog("Are you sure you want to force install \(cask.name)? This will override any current installation!", isPresented: $showingForceInstallConfirmation) { - Button("Yes") { - download(force: true) - } - - Button("Cancel", role: .cancel) { } - } - } - - private func download(force: Bool = false) { - // Check if brew path is valid - if !BrewPaths.isSelectedBrewPathValid() { - showingBrewError = true - return - } - - Task { - await cask.install(caskData: caskData, force: force) - } - } - } - - /// Button used in the Download section, launches, uninstalls or reinstalls the app - private struct OpenAndManageAppView: View { - @StateObject var cask: Cask - let deleteButton: Bool - - @EnvironmentObject var caskData: CaskData - - @State var appNotFoundShowing = false - @State var showingPopover = false - - @State private var isOptionKeyDown = false - - var body: some View { - // Lauch app - Button("Open") { - do { - try cask.launchApp() - } catch { - appNotFoundShowing = true - } - } - .font(.system(size: 14)) - .buttonStyle(.bordered) - .clipShape(Capsule()) - .alert("App couldn't be located", isPresented: $appNotFoundShowing) { - Button("OK", role: .cancel) { } - } - - if deleteButton { - UninstallButton(cask: cask) - } - - // More options popover - Button() { - showingPopover = true - } label: { - Image(systemName: "chevron.down") - .padding(.vertical) - .contentShape(Rectangle()) - } - .popover(isPresented: $showingPopover) { - VStack(alignment: .leading, spacing: 6) { - // Reinstall button - Button { - Task { - await cask.reinstall(caskData: caskData) - } - } label: { - Label("Reinstall", systemImage: "arrow.2.squarepath") - } - - // Uninstall button - Button(role: .destructive) { - Task { - await cask.uninstall(caskData: caskData) - } - } label: { - Label("Uninstall", systemImage: "trash") - .foregroundStyle(.red) - } - - // Uninstall completely button - Button(role: .destructive) { - Task { - await cask.uninstall(caskData: caskData, zap: true) - } - } label: { - Label("Uninstall Completely", systemImage: "trash.fill") - .foregroundStyle(.red) - } - } - .padding(8) - .buttonStyle(.plain) - } - } - } - - private struct UpdateButton: View { - @EnvironmentObject var caskData: CaskData - @StateObject var cask: Cask - - var body: some View { - Button { - Task { - await MainActor.run { cask.progressState = .busy(withTask: "Updating") } - - _ = await cask.update(caskData: caskData) - } - } label: { - Image(systemName: "arrow.uturn.down.circle.fill") - .font(.system(size: 20)) - } - .foregroundColor(.blue) - } - } - - private struct UninstallButton: View { - @StateObject var cask: Cask - - @EnvironmentObject var caskData: CaskData - - @State var showingError = false - - var body: some View { - Button { - Task { - await MainActor.run { cask.progressState = .busy(withTask: "Uninstalling") } - - _ = await cask.uninstall(caskData: caskData) - } - } label: { - Image(systemName: "trash.fill") - .font(.system(size: 20)) - } - .foregroundColor(.primary) - } - } -} - -struct AppView_Previews: PreviewProvider { - static var previews: some View { - AppView(cask: Cask(), role: .installAndManage) - } -} diff --git a/Applite/Components/AppdirSelectorView.swift b/Applite/Views/Components/AppdirSelectorView.swift similarity index 100% rename from Applite/Components/AppdirSelectorView.swift rename to Applite/Views/Components/AppdirSelectorView.swift diff --git a/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+CustomPathOption.swift b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+CustomPathOption.swift new file mode 100644 index 0000000..53a360a --- /dev/null +++ b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+CustomPathOption.swift @@ -0,0 +1,39 @@ +// +// BrewPathSelectorView+CustomPathOption.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension BrewPathSelectorView { + func customPathOption(option: BrewPaths.PathOption) -> some View { + VStack(alignment: .leading, spacing: 5) { + pathOption(option, showPath: false) + + HStack { + TextField("Custom brew path", text: $customBrewPathDebounced.text, prompt: Text("/path/to/brew")) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 300) + .autocorrectionDisabled() + + Button("Browse") { + choosingCustomFolder = true + } + .fileImporter( + isPresented: $choosingCustomFolder, + allowedContentTypes: [.unixExecutable] + ) { result in + switch result { + case .success(let file): + customBrewPathDebounced.text = file.path + case .failure(let error): + print(error.localizedDescription) + } + } + } + .disabled(brewPathOption != BrewPaths.PathOption.custom.rawValue) + } + } +} diff --git a/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+GetPathDescription.swift b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+GetPathDescription.swift new file mode 100644 index 0000000..d7afb00 --- /dev/null +++ b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+GetPathDescription.swift @@ -0,0 +1,26 @@ +// +// BrewPathSelectorView+GetPathDescription.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension BrewPathSelectorView { + func getPathDescription(for option: BrewPaths.PathOption) -> String { + switch option { + case .appPath: + return "\(Bundle.main.appName)'s installation" + + case .defaultAppleSilicon: + return "Apple Silicon Mac" + + case .defaultIntel: + return "Intel Mac" + + case .custom: + return "Custom" + } + } +} diff --git a/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+PathOption.swift b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+PathOption.swift new file mode 100644 index 0000000..f76b8e5 --- /dev/null +++ b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView+PathOption.swift @@ -0,0 +1,35 @@ +// +// BrewPathSelectorView+PathOption.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension BrewPathSelectorView { + func pathOption(_ option: BrewPaths.PathOption, showPath: Bool = true) -> some View { + HStack { + Text("\(getPathDescription(for: option))") + + if showPath { + Text("(\(BrewPaths.getBrewExectuablePath(for: option, shellFriendly: false)))") + .truncationMode(.middle) + .lineLimit(1) + .foregroundColor(.gray) + } + + if option.rawValue == brewPathOption { + if isSelectedPathValid { + Image(systemName: "checkmark.circle") + .font(.system(size: 16)) + .foregroundColor(.green) + } else { + Image(systemName: "xmark.circle") + .font(.system(size: 16)) + .foregroundColor(.red) + } + } + } + } +} diff --git a/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView.swift b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView.swift new file mode 100755 index 0000000..4fa9bf0 --- /dev/null +++ b/Applite/Views/Components/Brew Path Selector/BrewPathSelectorView.swift @@ -0,0 +1,57 @@ +// +// BrewPathSelectorView.swift +// Applite +// +// Created by Milán Várady on 2023. 01. 03.. +// + +import SwiftUI + +/// Provides a picker so the user can select the brew executable path they want to use +struct BrewPathSelectorView: View { + @Binding var isSelectedPathValid: Bool + + @StateObject var customBrewPathDebounced = DebounceObject() + + @AppStorage(Preferences.customUserBrewPath.rawValue) var customUserBrewPath: String = BrewPaths.getBrewExectuablePath(for: .defaultAppleSilicon, shellFriendly: false) + @AppStorage(Preferences.brewPathOption.rawValue) var brewPathOption = BrewPaths.PathOption.defaultAppleSilicon.rawValue + + @State var choosingCustomFolder = false + + var body: some View { + VStack(alignment: .leading) { + Picker("", selection: $brewPathOption) { + ForEach(BrewPaths.PathOption.allCases) { option in + if option != .custom { + pathOption(option) + .tag(option.rawValue) + } else { + customPathOption(option: option) + .tag(option.rawValue) + } + } + } + .pickerStyle(.radioGroup) + } + .onAppear { + customBrewPathDebounced.text = customUserBrewPath + isSelectedPathValid = BrewPaths.isSelectedBrewPathValid() + } + .onChange(of: brewPathOption) { _ in + isSelectedPathValid = BrewPaths.isSelectedBrewPathValid() + } + .onChange(of: customBrewPathDebounced.debouncedText) { newPath in + customUserBrewPath = newPath + + if brewPathOption == BrewPaths.PathOption.custom.rawValue { + isSelectedPathValid = isBrewPathValid(path: newPath) + } + } + } +} + +struct BrewPathSelectorView_Previews: PreviewProvider { + static var previews: some View { + BrewPathSelectorView(isSelectedPathValid: .constant(false)) + } +} diff --git a/Applite/Components/SmallProgressView.swift b/Applite/Views/Components/SmallProgressView.swift similarity index 100% rename from Applite/Components/SmallProgressView.swift rename to Applite/Views/Components/SmallProgressView.swift diff --git a/Applite/Components/Sparkle Updater/CheckForUpdatesView.swift b/Applite/Views/Components/Sparkle Updater/CheckForUpdatesView.swift similarity index 100% rename from Applite/Components/Sparkle Updater/CheckForUpdatesView.swift rename to Applite/Views/Components/Sparkle Updater/CheckForUpdatesView.swift diff --git a/Applite/Views/Content View/ContentView+DetailView.swift b/Applite/Views/Content View/ContentView+DetailView.swift new file mode 100644 index 0000000..20d69b4 --- /dev/null +++ b/Applite/Views/Content View/ContentView+DetailView.swift @@ -0,0 +1,55 @@ +// +// ContentView+DetailView.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension ContentView { + @ViewBuilder + var detailView: some View { + switch selection { + case "home": + if !brokenInstall { + DownloadView(navigationSelection: $selection, searchText: $searchTextSubmitted) + } else { + // Broken install + VStack(alignment: .center) { + Text(DependencyManager.brokenPathOrIstallMessage) + + Button { + Task { + await loadCasks() + } + } label: { + Label("Retry load", systemImage: "arrow.clockwise.circle") + } + .bigButton() + .disabled(false) + } + .frame(maxWidth: 600) + } + + case "updates": + UpdateView() + + case "installed": + InstalledView() + + case "activeTasks": + ActiveTasksView() + + case "brew": + BrewManagementView(modifyingBrew: $modifyingBrew) + + default: + if let category = categories.first(where: { $0.id == selection }) { + CategoryView(category: category) + } else { + Text("No Selection") + } + } + } +} diff --git a/Applite/Views/Content View/ContentView+LoadCasks.swift b/Applite/Views/Content View/ContentView+LoadCasks.swift new file mode 100644 index 0000000..9025dde --- /dev/null +++ b/Applite/Views/Content View/ContentView+LoadCasks.swift @@ -0,0 +1,37 @@ +// +// ContentView+LoadCasks.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension ContentView { + func loadCasks() async { + guard BrewPaths.isSelectedBrewPathValid() else { + loadAlert.show(title: "Couldn't load app catalog", message: DependencyManager.brokenPathOrIstallMessage) + brokenInstall = true + + let output = (try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version")) ?? "n/a" + + logger.error( + """ + Initial cask load failure. Reason: selected brew path seems invalid. + Brew executable path path: \(BrewPaths.currentBrewExecutable) + brew --version output: \(output) + """ + ) + + return + } + + do { + try await caskData.loadData() + brokenInstall = false + } catch { + loadAlert.show(title: "Couldn't load app catalog", message: error.localizedDescription) + logger.error("Initial cask load failure. Reason: \(error.localizedDescription)") + } + } +} diff --git a/Applite/Views/Content View/ContentView+SidebarItems.swift b/Applite/Views/Content View/ContentView+SidebarItems.swift new file mode 100644 index 0000000..7404571 --- /dev/null +++ b/Applite/Views/Content View/ContentView+SidebarItems.swift @@ -0,0 +1,44 @@ +// +// ContentView+SidebarItems.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension ContentView { + var sidebarItems: some View { + List(selection: $selection) { + Divider() + + Label("Discover", systemImage: "house.fill") + .tag("home") + + Label("Updates", systemImage: "arrow.clockwise.circle.fill") + .badge(caskData.outdatedCasks.count) + .tag("updates") + + Label("Installed", systemImage: "externaldrive.fill.badge.checkmark") + .tag("installed") + + Label("Active Tasks", systemImage: "gearshape.arrow.triangle.2.circlepath") + .badge(caskData.busyCasks.count) + .tag("activeTasks") + + Section("Categories") { + ForEach(categories) { category in + Label(LocalizedStringKey(category.id), systemImage: category.sfSymbol) + .tag(category.id) + } + } + + Section("Homebrew") { + NavigationLink(value: "brew", label: { + Label("Manage Homebrew", systemImage: "mug") + }) + } + } + } + +} diff --git a/Applite/Views/Content View/ContentView.swift b/Applite/Views/Content View/ContentView.swift new file mode 100755 index 0000000..de2b780 --- /dev/null +++ b/Applite/Views/Content View/ContentView.swift @@ -0,0 +1,81 @@ +// +// ContentView.swift +// Applite +// +// Created by Milán Várady on 2022. 09. 24.. +// + +import SwiftUI +import os + +struct ContentView: View { + @EnvironmentObject var caskData: CaskData + + /// Currently selected tab in the sidebar + @State var selection: String = "home" + + /// App search query + @State var searchText = "" + /// This variable is set to the value of searchText whenever the user submits the search quiery + @State var searchTextSubmitted = "" + + @StateObject var loadAlert = AlertManager() + + @State var brokenInstall = false + + /// If true the sidebar is disabled + @State var modifyingBrew = false + + let logger = Logger() + + var body: some View { + NavigationSplitView { + sidebarItems + .disabled(modifyingBrew) + } detail: { + detailView + } + // Load casks + .task { + await loadCasks() + } + // App search + .searchable(text: $searchText, placement: .sidebar) + .onSubmit(of: .search) { + searchTextSubmitted = searchText + + if !searchText.isEmpty && selection != "home" { + selection = "home" + } + } + .onChange(of: searchText) { newSearchText in + if newSearchText.isEmpty { + searchTextSubmitted = "" + } + } + // Load failure alert + .alert(loadAlert.title, isPresented: $loadAlert.isPresented) { + Button { + Task { @MainActor in + await loadCasks() + } + } label: { + Label("Retry", systemImage: "arrow.clockwise") + } + + Button("Quit", role: .destructive) { + NSApplication.shared.terminate(self) + } + + Button("OK", role: .cancel) { } + } message: { + Text(loadAlert.message) + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Applite/Views/ContentView.swift b/Applite/Views/ContentView.swift deleted file mode 100755 index 65be6d8..0000000 --- a/Applite/Views/ContentView.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// ContentView.swift -// Applite -// -// Created by Milán Várady on 2022. 09. 24.. -// - -import SwiftUI -import os - -struct ContentView: View { - @EnvironmentObject var caskData: CaskData - - /// Currently selected tab in the sidebar - @State var selection: String = "home" - - /// App search query - @State var searchText = "" - /// This variable is set to the value of searchText whenever the user submits the search quiery - @State var searchTextSubmitted = "" - - @State var loadAlertShowing = false - @State var errorMessage = "" - - @State var brokenInstall = false - - /// If true the sidebar is disabled - @State var modifyingBrew = false - - let logger = Logger() - - var body: some View { - NavigationSplitView { - List(selection: $selection) { - Divider() - - Label("Discover", systemImage: "house.fill") - .tag("home") - - Label("Updates", systemImage: "arrow.clockwise.circle.fill") - .badge(caskData.outdatedCasks.count) - .tag("updates") - - Label("Installed", systemImage: "externaldrive.fill.badge.checkmark") - .tag("installed") - - Label("Active Tasks", systemImage: "gearshape.arrow.triangle.2.circlepath") - .badge(caskData.busyCasks.count) - .tag("activeTasks") - - Section("Categories") { - ForEach(categories) { category in - Label(LocalizedStringKey(category.id), systemImage: category.sfSymbol) - .tag(category.id) - } - } - - Section("Homebrew") { - NavigationLink(value: "brew", label: { - Label("Manage Homebrew", systemImage: "mug") - }) - } - } - .disabled(modifyingBrew) - } detail: { - switch selection { - case "home": - if !brokenInstall { - DownloadView(navigationSelection: $selection, searchText: $searchTextSubmitted) - } else { - // Broken install - VStack(alignment: .center) { - Text(DependencyManager.brokenPathOrIstallMessage) - - Button { - Task { - await loadCasks() - } - } label: { - Label("Retry load", systemImage: "arrow.clockwise.circle") - } - .bigButton() - .disabled(false) - } - .frame(maxWidth: 600) - } - - case "updates": - UpdateView() - - case "installed": - InstalledView() - - case "activeTasks": - ActiveTasksView() - - case "brew": - BrewManagementView(modifyingBrew: $modifyingBrew) - - default: - if let category = categories.first(where: { $0.id == selection }) { - CategoryView(category: category) - } else { - Text("No Selection") - } - } - } - .task { - await loadCasks() - } - .searchable(text: $searchText, placement: .sidebar) - .onSubmit(of: .search) { - searchTextSubmitted = searchText - - if !searchText.isEmpty && selection != "home" { - selection = "home" - } - } - .onChange(of: searchText) { newSearchText in - if newSearchText.isEmpty { - searchTextSubmitted = "" - } - } - .alert("App load error", isPresented: $loadAlertShowing) { - Button { - Task { @MainActor in - await loadCasks() - } - } label: { - Label("Retry", systemImage: "arrow.clockwise") - } - - Button("Quit", role: .destructive) { - NSApplication.shared.terminate(self) - } - - Button("OK", role: .cancel) { } - } message: { - Text(errorMessage) - } - } - - private func loadCasks() async { - if !BrewPaths.isSelectedBrewPathValid() { - errorMessage = DependencyManager.brokenPathOrIstallMessage - loadAlertShowing = true - brokenInstall = true - - let output = (try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version")) ?? "n/a" - - logger.error(""" -Initial cask load failure. Reason: selected brew path seems invalid. -Brew executable path path: \(BrewPaths.currentBrewExecutable) -brew --version output: \(output) -""") - - return - } - - do { - try await caskData.loadData() - brokenInstall = false - } catch { - errorMessage = "Couldn't load app catalog. Check internet your connection, or try restarting the app." - - loadAlertShowing = true - - logger.error("Initial cask load failure. Reason: \(error.localizedDescription)") - } - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+AppRow.swift b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+AppRow.swift new file mode 100644 index 0000000..f0de471 --- /dev/null +++ b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+AppRow.swift @@ -0,0 +1,85 @@ +// +// DiscoverSectionView+AppRow.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DiscoverSectionView { + var appRowAndControls: some View { + ScrollViewReader { proxy in + HStack { + // Backward button + scrollButton( + icon: "chevron.compact.left", + proxy: proxy, + direction: - + ) + .opacity(scrollOffset <= 0 ? 0.2 : 1) + + // App row + appRow + + .coordinateSpace(name: "\(category.id)Scroll") + + // Forward button + scrollButton( + icon: "chevron.compact.right", + proxy: proxy, + direction: + + ) + .padding(.leading, 15) + } + } + } + + private var appRow: some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + if caskData.casksByCategoryCoupled[category.id]?.count ?? 0 > 0 { + ForEach(Array((caskData.casksByCategoryCoupled[category.id]?.enumerated())!), id: \.offset) { index, casks in + VStack { + ForEach(casks) { cask in + AppView(cask: cask, role: .installAndManage) + .frame(width: AppView.dimensions.width, height: AppView.dimensions.height) + } + + Spacer() + } + .id(index) + } + } else { + // Placeholders + ForEach(0..<6) { _ in + PlaceholderAppGroup() + } + } + }.background(GeometryReader { geometry in + Color.clear.preference(key: ViewOffsetKey.self, + value: -geometry.frame(in: .named("\(category.id)Scroll")).origin.x) + }) + .onPreferenceChange(ViewOffsetKey.self) { scrollOffset = $0 } + } + } + + func scrollButton(icon: String, proxy: ScrollViewProxy, direction: (CGFloat, CGFloat) -> CGFloat) -> some View { + // Calculate new scroll position + let appViewWidthWithPadding = AppView.dimensions.width + AppView.dimensions.spacing + let newScrollOffset = direction(scrollOffset, appViewWidthWithPadding) + let scrollTo: Int = Int((newScrollOffset / appViewWidthWithPadding).rounded()) + let scrollUpperBound = category.casks.count - 1 + let scrollToClamped = min(max(scrollTo, 0), scrollUpperBound) + + return Button { + withAnimation(.spring()) { + proxy.scrollTo(scrollToClamped, anchor: .leading) + } + } label: { + Image(systemName: icon) + .font(.system(size: 38)) + } + .buttonStyle(.plain) + } +} diff --git a/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+CategoryHeader.swift b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+CategoryHeader.swift new file mode 100644 index 0000000..b9a05f7 --- /dev/null +++ b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+CategoryHeader.swift @@ -0,0 +1,27 @@ +// +// DiscoverSectionView+CategoryHeader.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DiscoverSectionView { + var categoryHeader: some View { + HStack(alignment: .bottom) { + Image(systemName: category.sfSymbol) + .font(.system(size: 24)) + + Text(LocalizedStringKey(category.id)) + .font(.system(size: 24, weight: .bold)) + + Button("See All") { + navigationSelection = category.id + } + .buttonStyle(.plain) + .foregroundColor(.blue) + .padding(.bottom, 3) + } + } +} diff --git a/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+OffsetKey.swift b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+OffsetKey.swift new file mode 100644 index 0000000..91c28bf --- /dev/null +++ b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+OffsetKey.swift @@ -0,0 +1,20 @@ +// +// DiscoverSectionView+OffsetKey.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DiscoverSectionView { + /// Preference key used to get the scroll offset of the app row + struct ViewOffsetKey: PreferenceKey { + typealias Value = CGFloat + static var defaultValue = CGFloat.zero + + static func reduce(value: inout Value, nextValue: () -> Value) { + value += nextValue() + } + } +} diff --git a/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+Placeholder.swift b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+Placeholder.swift new file mode 100644 index 0000000..e7d073f --- /dev/null +++ b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView+Placeholder.swift @@ -0,0 +1,25 @@ +// +// DiscoverSectionView+Placeholder.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DiscoverSectionView { + /// Two placeholder app views on top of each other for the discover app row + struct PlaceholderAppGroup: View { + var body: some View { + VStack { + PlaceholderAppView() + .shimmering() + .frame(width: AppView.dimensions.width, height: AppView.dimensions.height) + + PlaceholderAppView() + .shimmering() + .frame(width: AppView.dimensions.width, height: AppView.dimensions.height) + } + } + } +} diff --git a/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView.swift b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView.swift new file mode 100644 index 0000000..9cf9619 --- /dev/null +++ b/Applite/Views/Detail Views/Discover/Discover Section/DiscoverSectionView.swift @@ -0,0 +1,26 @@ +// +// DiscoverSectionView.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +struct DiscoverSectionView: View { + let category: Category + @Binding var navigationSelection: String + + @EnvironmentObject var caskData: CaskData + + @State var scrollOffset: CGFloat = 0 + + var body: some View { + VStack(alignment: .leading) { + categoryHeader + + appRowAndControls + .frame(height: AppView.dimensions.height * 2 + 20) + } + } +} diff --git a/Applite/Views/Detail Views/Discover/DiscoverView.swift b/Applite/Views/Detail Views/Discover/DiscoverView.swift new file mode 100755 index 0000000..b3d6e60 --- /dev/null +++ b/Applite/Views/Detail Views/Discover/DiscoverView.swift @@ -0,0 +1,38 @@ +// +// DiscoverView.swift +// Applite +// +// Created by Milán Várady on 2022. 10. 14.. +// + +import SwiftUI +import Shimmer + +/// Shows apps in categories +struct DiscoverView: View { + @Binding var navigationSelection: String + @State var currentPage: Float = 0 + + var body: some View { + LazyVStack(alignment: .leading) { + Text("Discover") + .font(.system(size: 52, weight: .bold)) + .padding(.bottom) + + ForEach(categories) { category in + DiscoverSectionView(category: category, navigationSelection: $navigationSelection) + + Divider() + .padding(.vertical, 20) + } + } + .padding() + } +} + +struct DiscoverView_Previews: PreviewProvider { + static var previews: some View { + DiscoverView(navigationSelection: .constant("")) + .environmentObject(CaskData()) + } +} diff --git a/Applite/Views/Detail Views/DiscoverView.swift b/Applite/Views/Detail Views/DiscoverView.swift deleted file mode 100755 index 0959195..0000000 --- a/Applite/Views/Detail Views/DiscoverView.swift +++ /dev/null @@ -1,158 +0,0 @@ -// -// DiscoverView.swift -// Applite -// -// Created by Milán Várady on 2022. 10. 14.. -// - -import SwiftUI -import Shimmer - -/// Shows apps in categories -struct DiscoverView: View { - @Binding var navigationSelection: String - @State var currentPage: Float = 0 - - var body: some View { - LazyVStack(alignment: .leading) { - Text("Discover") - .font(.system(size: 52, weight: .bold)) - .padding(.bottom) - - ForEach(categories) { category in - DiscoverSection(category: category, navigationSelection: $navigationSelection) - - Divider() - .padding(.vertical, 20) - } - } - .padding() - } -} - -/// Category section -private struct DiscoverSection: View { - let category: Category - @Binding var navigationSelection: String - - @EnvironmentObject var caskData: CaskData - - @State private var scrollOffset: CGFloat = 0 - - var body: some View { - VStack(alignment: .leading) { - // Category header - HStack(alignment: .bottom) { - Image(systemName: category.sfSymbol) - .font(.system(size: 24)) - - Text(LocalizedStringKey(category.id)) - .font(.system(size: 24, weight: .bold)) - - Button("See All") { - navigationSelection = category.id - } - .buttonStyle(.plain) - .foregroundColor(.blue) - .padding(.bottom, 3) - } - - // App row - ScrollViewReader { proxy in - // Backward button - HStack { - Button(action: { - let appViewWidthWithPadding = AppView.dimensions.width + AppView.dimensions.spacing - let scrollTo: Int = Int(((scrollOffset - appViewWidthWithPadding) / appViewWidthWithPadding).rounded()) - - withAnimation(.spring()) { - proxy.scrollTo(max(scrollTo, 0), anchor: .leading) - } - }) { - Image(systemName: "chevron.compact.left") - .font(.system(size: 38)) - } - .buttonStyle(.plain) - .opacity(scrollOffset <= 0 ? 0.2 : 1) - - // App row - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - if caskData.casksByCategoryCoupled[category.id]?.count ?? 0 > 0 { - ForEach(Array((caskData.casksByCategoryCoupled[category.id]?.enumerated())!), id: \.offset) { index, casks in - VStack { - ForEach(casks) { cask in - AppView(cask: cask, role: .installAndManage) - .frame(width: AppView.dimensions.width, height: AppView.dimensions.height) - } - - Spacer() - } - .id(index) - } - } else { - // Placeholders - ForEach(0..<6) { _ in - PlaceholderAppGroup() - } - } - }.background(GeometryReader { geometry in - Color.clear.preference(key: ViewOffsetKey.self, - value: -geometry.frame(in: .named("\(category.id)Scroll")).origin.x) - }) - .onPreferenceChange(ViewOffsetKey.self) { scrollOffset = $0 } - } - .coordinateSpace(name: "\(category.id)Scroll") - - // Forward button - Button { - let appViewWidthWithPadding = AppView.dimensions.width + AppView.dimensions.spacing - let scrollTo: Int = Int(((scrollOffset + appViewWidthWithPadding) / appViewWidthWithPadding).rounded()) - - withAnimation(.spring()) { - proxy.scrollTo(min(scrollTo, category.casks.count - 1), anchor: .leading) - } - } label: { - Image(systemName: "chevron.compact.right") - .font(.system(size: 38)) - } - .buttonStyle(.plain) - .padding(.leading, 15) - } - } - .frame(height: AppView.dimensions.height * 2 + 20) - } - } - - /// Preference key used to get the scroll offset of the app row - private struct ViewOffsetKey: PreferenceKey { - typealias Value = CGFloat - static var defaultValue = CGFloat.zero - - static func reduce(value: inout Value, nextValue: () -> Value) { - value += nextValue() - } - } - - /// Two placeholder app views on top of each other for the discover app row - private struct PlaceholderAppGroup: View { - var body: some View { - VStack { - PlaceholderAppView() - .shimmering() - .frame(width: AppView.dimensions.width, height: AppView.dimensions.height) - - PlaceholderAppView() - .shimmering() - .frame(width: AppView.dimensions.width, height: AppView.dimensions.height) - } - } - } -} - -struct DiscoverView_Previews: PreviewProvider { - static var previews: some View { - DiscoverView(navigationSelection: .constant("")) - .environmentObject(CaskData()) - } -} diff --git a/Applite/Views/Detail Views/Download/DownloadView+NoSearchResults.swift b/Applite/Views/Detail Views/Download/DownloadView+NoSearchResults.swift new file mode 100644 index 0000000..53e79fa --- /dev/null +++ b/Applite/Views/Detail Views/Download/DownloadView+NoSearchResults.swift @@ -0,0 +1,36 @@ +// +// DownloadView+NoSearchResults.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DownloadView { + var noSearchResults: some View { + VStack { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.red) + .font(.system(size: 32)) + + Text("\"\(searchText)\" didn't match any app. Either it's not available in the Homebrew catalog or you misspelled it.") + .font(.system(size: 20)) + } + + .padding(.bottom) + + // Turn of filtering + if hideUnpopularApps { + Button { + hideUnpopularApps = false + } label: { + Label("Turn off few downloads filter", systemImage: "slider.horizontal.2.square.on.square") + } + .bigButton() + .help("Apps with few downloads are hidden, consider turning off this filter") + } + } + } +} diff --git a/Applite/Views/Detail Views/Download/DownloadView+Search.swift b/Applite/Views/Detail Views/Download/DownloadView+Search.swift new file mode 100644 index 0000000..a433c6d --- /dev/null +++ b/Applite/Views/Detail Views/Download/DownloadView+Search.swift @@ -0,0 +1,48 @@ +// +// DownloadView+Search.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DownloadView { + /// Filters a list of casks + /// + /// - Parameters: + /// - casks: List of ``Cask`` objects to filter + /// - searchText: Search query + /// - Returns: List of filtered casks + func fuzzyFilter(casks: [Cask], searchText: String) -> [Cask] { + var casks = casks + + if searchText.isEmpty { + casks = caskData.casks + } else { + // A score of 0 means a perfect match, a score of one matches everything + casks = caskData.casks.filter { + ($0.name.lowercased().contains(searchText.lowercased()) || $0.description.lowercased().contains(searchText.lowercased())) || + (fuseSearch.search(searchText.lowercased(), in: $0.name.lowercased())?.score ?? 1) < 0.25 || + (fuseSearch.search(searchText.lowercased(), in: $0.description.lowercased())?.score ?? 1) < 0.25 + } + } + + // Filters + if sortBy == .mostDownloaded { + casks = casks.sorted(by: { $0.downloadsIn365days > $1.downloadsIn365days }) + } + + if hideUnpopularApps { + casks = casks.filter { + $0.downloadsIn365days > 500 + } + } + + return casks + } + + func search() { + self.searchResults = fuzzyFilter(casks: caskData.casks, searchText: searchText) + } +} diff --git a/Applite/Views/Detail Views/Download/DownloadView+SortingOptions.swift b/Applite/Views/Detail Views/Download/DownloadView+SortingOptions.swift new file mode 100644 index 0000000..85b792c --- /dev/null +++ b/Applite/Views/Detail Views/Download/DownloadView+SortingOptions.swift @@ -0,0 +1,30 @@ +// +// DownloadView+SortingOptions.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension DownloadView { + var sortingOptions: some ToolbarContent { + ToolbarItem { + Menu { + Picker("Sort by", selection: $sortBy) { + ForEach(SortingOptions.allCases) { option in + Text(LocalizedStringKey(option.rawValue)).tag(option) + } + } + .pickerStyle(.inline) + + Toggle(isOn: $hideUnpopularApps) { + Text("Hide apps with few downloads") + } + } label: { + Label("Sorting Options", systemImage: "slider.horizontal.3") + .labelStyle(.titleAndIcon) + } + } + } +} diff --git a/Applite/Views/Detail Views/Download/DownloadView.swift b/Applite/Views/Detail Views/Download/DownloadView.swift new file mode 100755 index 0000000..b8e7ea1 --- /dev/null +++ b/Applite/Views/Detail Views/Download/DownloadView.swift @@ -0,0 +1,73 @@ +// +// DownloadView.swift +// Applite +// +// Created by Milán Várady on 2022. 10. 14.. +// + +import SwiftUI +import Fuse + +/// Download section. Either dispays the `DiscoverView` or search results +struct DownloadView: View { + @Binding var navigationSelection: String + @Binding var searchText: String + + @EnvironmentObject var caskData: CaskData + + @State var searchResults: [Cask] = [] + + // Sorting options + @State var hideUnpopularApps = false + @State var sortBy = SortingOptions.mostDownloaded + + enum SortingOptions: String, CaseIterable, Identifiable { + case mostDownloaded = "Most downloaded (default)" + case aToZ = "A-Z" + + var id: SortingOptions { self } + } + + let fuseSearch = Fuse() + + var body: some View { + ScrollView { + if searchText.isEmpty { + DiscoverView(navigationSelection: $navigationSelection) + } else { + AppGridView(casks: searchResults, appRole: .installAndManage) + .padding() + + // If search result is empty + if searchResults.isEmpty { + noSearchResults + .frame(maxWidth: 800) + .padding() + } + } + } + .onChange(of: searchText) { newSearchText in + // Filter apps + searchResults = fuzzyFilter(casks: caskData.casks, searchText: newSearchText) + } + .onChange(of: sortBy) { _newValue in + // Refilter if sorting options change + search() + } + .onChange(of: hideUnpopularApps) { _newValue in + // Refilter if sorting options change + search() + } + .onAppear { search() } + .toolbar { + sortingOptions + } + } +} + +struct DownloadView_Previews: PreviewProvider { + static var previews: some View { + DownloadView(navigationSelection: .constant(""), searchText: .constant("")) + .environmentObject(CaskData()) + } +} diff --git a/Applite/Views/Detail Views/DownloadView.swift b/Applite/Views/Detail Views/DownloadView.swift deleted file mode 100755 index 0d43937..0000000 --- a/Applite/Views/Detail Views/DownloadView.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// DownloadView.swift -// Applite -// -// Created by Milán Várady on 2022. 10. 14.. -// - -import SwiftUI -import Fuse - -/// Download section. Either dispays the `DiscoverView` or search results -struct DownloadView: View { - @Binding var navigationSelection: String - @Binding var searchText: String - - @EnvironmentObject var caskData: CaskData - - @State var searchResults: [Cask] = [] - - // Sorting options - @State var hideUnpopularApps = false - @State var sortBy = SortingOptions.mostDownloaded - - enum SortingOptions: String, CaseIterable, Identifiable { - case mostDownloaded = "Most downloaded (default)" - case aToZ = "A-Z" - - var id: SortingOptions { self } - } - - let fuseSearch = Fuse() - - var body: some View { - ScrollView { - if searchText.isEmpty { - DiscoverView(navigationSelection: $navigationSelection) - } else { - AppGridView(casks: searchResults, appRole: .installAndManage) - .padding() - - // If search result is empty - if searchResults.isEmpty { - VStack { - HStack { - Image(systemName: "magnifyingglass") - .foregroundColor(.red) - .font(.system(size: 32)) - - Text("\"\(searchText)\" didn't match any app. Either it's not available in the Homebrew catalog or you misspelled it.") - .font(.system(size: 20)) - } - - .padding(.bottom) - - // Turn of filtering - if hideUnpopularApps { - Button { - hideUnpopularApps = false - } label: { - Label("Turn off few downloads filter", systemImage: "slider.horizontal.2.square.on.square") - } - .bigButton() - .help("Apps with few downloads are hidden, consider turning off this filter") - } - } - .frame(maxWidth: 800) - .padding() - } - } - } - .onChange(of: searchText) { newSearchText in - // Filter apps - searchResults = fuzzyFilter(casks: caskData.casks, searchText: newSearchText) - } - .onChange(of: sortBy) { _newValue in - // Refilter if sorting options change - search() - } - .onChange(of: hideUnpopularApps) { _newValue in - // Refilter if sorting options change - search() - } - .onAppear { search() } - .toolbar { - // Sorting options - ToolbarItem { - Menu { - Picker("Sort by", selection: $sortBy) { - ForEach(SortingOptions.allCases) { option in - Text(LocalizedStringKey(option.rawValue)).tag(option) - } - } - .pickerStyle(.inline) - - Toggle(isOn: $hideUnpopularApps) { - Text("Hide apps with few downloads") - } - } label: { - Label("Sorting Options", systemImage: "slider.horizontal.3") - .labelStyle(.titleAndIcon) - } - } - } - } - - /// Filters a list of casks - /// - /// - Parameters: - /// - casks: List of ``Cask`` objects to filter - /// - searchText: Search query - /// - Returns: List of filtered casks - func fuzzyFilter(casks: [Cask], searchText: String) -> [Cask] { - var casks = casks - - if searchText.isEmpty { - casks = caskData.casks - } else { - // A score of 0 means a perfect match, a score of one matches everything - casks = caskData.casks.filter { - ($0.name.lowercased().contains(searchText.lowercased()) || $0.description.lowercased().contains(searchText.lowercased())) || - (fuseSearch.search(searchText.lowercased(), in: $0.name.lowercased())?.score ?? 1) < 0.25 || - (fuseSearch.search(searchText.lowercased(), in: $0.description.lowercased())?.score ?? 1) < 0.25 - } - } - - // Filters - if sortBy == .mostDownloaded { - casks = casks.sorted(by: { $0.downloadsIn365days > $1.downloadsIn365days }) - } - - if hideUnpopularApps { - casks = casks.filter { - $0.downloadsIn365days > 500 - } - } - - return casks - } - - public func search() { - self.searchResults = fuzzyFilter(casks: caskData.casks, searchText: searchText) - } -} - -struct DownloadView_Previews: PreviewProvider { - static var previews: some View { - DownloadView(navigationSelection: .constant(""), searchText: .constant("")) - .environmentObject(CaskData()) - } -} diff --git a/Applite/Views/Detail Views/Update/UpdateView+ToolbarItems.swift b/Applite/Views/Detail Views/Update/UpdateView+ToolbarItems.swift new file mode 100644 index 0000000..77e5f25 --- /dev/null +++ b/Applite/Views/Detail Views/Update/UpdateView+ToolbarItems.swift @@ -0,0 +1,65 @@ +// +// UpdateView+ToolbarItems.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension UpdateView { + var toolbarItems: some ToolbarContent { + ToolbarItemGroup { + greedyUpdateButton + + // Refresh outdated casks + if refreshing { + SmallProgressView() + } else { + refreshButton + } + } + } + + private var greedyUpdateButton: some View { + Button { + showingGreedyUpdateConfirm = true + } label: { + Label("Show All Updates", systemImage: "eye") + } + .labelStyle(.titleAndIcon) + .alert("Notice", isPresented: $showingGreedyUpdateConfirm) { + Button("Show All") { + Task { + do { + try await caskData.refreshOutdatedApps(greedy: true) + } catch { + loadAlert.show(title: "Failed to load updates", message: error.localizedDescription) + } + } + } + + Button("Cancel", role: .cancel) {} + } message: { + Text("This will show updates from applications that have auto-update turned off, i.e., applications that are taking care of their own updates.") + } + } + + private var refreshButton: some View { + Button { + Task { + refreshing = true + + do { + try await caskData.refreshOutdatedApps(greedy: true) + } catch { + loadAlert.show(title: "Failed to refresh updates", message: error.localizedDescription) + } + + refreshing = false + } + } label: { + Image(systemName: "arrow.clockwise") + } + } +} diff --git a/Applite/Views/Detail Views/Update/UpdateView+UpdateAllButton.swift b/Applite/Views/Detail Views/Update/UpdateView+UpdateAllButton.swift new file mode 100644 index 0000000..69ed9e2 --- /dev/null +++ b/Applite/Views/Detail Views/Update/UpdateView+UpdateAllButton.swift @@ -0,0 +1,48 @@ +// +// UpdateView+UpdateAllButton.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension UpdateView { + var updateAllButton: some View { + Button { + isUpdatingAll = true + + withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { + updateAllButtonRotation = 360.0 + } + + Task { + await withTaskGroup(of: Void.self) { group in + for cask in casks { + group.addTask { + await cask.update(caskData: caskData) + } + } + } + + await MainActor.run { + withAnimation(.linear(duration: 0.2)) { + updateAllButtonRotation = 0.0 + } + } + + updateAllFinished = true + } + } label: { + HStack { + Image(systemName: updateAllFinished ? "checkmark" : "arrow.2.circlepath") + .rotationEffect(.degrees(updateAllButtonRotation)) + + Text("Update All") + } + } + .bigButton(backgroundColor: .accentColor) + .padding(.vertical) + .disabled(isUpdatingAll) + } +} diff --git a/Applite/Views/Detail Views/Update/UpdateView+UpdateUnavailable.swift b/Applite/Views/Detail Views/Update/UpdateView+UpdateUnavailable.swift new file mode 100644 index 0000000..2c23d16 --- /dev/null +++ b/Applite/Views/Detail Views/Update/UpdateView+UpdateUnavailable.swift @@ -0,0 +1,21 @@ +// +// UpdateView+UpdateUnavailable.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension UpdateView { + var updateUnavailable: some View { + VStack { + Spacer() + + Text("No Updates Available") + .font(.title) + + Spacer() + } + } +} diff --git a/Applite/Views/Detail Views/Update/UpdateView.swift b/Applite/Views/Detail Views/Update/UpdateView.swift new file mode 100755 index 0000000..45c9249 --- /dev/null +++ b/Applite/Views/Detail Views/Update/UpdateView.swift @@ -0,0 +1,65 @@ +// +// UpdateView.swift +// Applite +// +// Created by Milán Várady on 2022. 10. 14.. +// + +import SwiftUI +import Fuse + +/// Update section +struct UpdateView: View { + @EnvironmentObject var caskData: CaskData + + @State var searchText = "" + @State var refreshing = false + @State var isUpdatingAll = false + @State var updateAllFinished = false + @State var updateAllButtonRotation = 0.0 + + @State var showingGreedyUpdateConfirm = false + @StateObject var loadAlert = AlertManager() + + // Filter outdated casks + var casks: [Cask] { + var filteredCasks = caskData.casks.filter { $0.isOutdated } + + if !$searchText.wrappedValue.isEmpty { + filteredCasks = filteredCasks.filter { + (fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.4 + } + } + + return filteredCasks + } + + let fuseSearch = Fuse() + + var body: some View { + ScrollView { + // App grid + AppGridView(casks: Array(caskData.outdatedCasks), appRole: .update) + .padding() + + if casks.count > 1 { + updateAllButton + } else { + updateUnavailable + } + } + .searchable(text: $searchText) + .toolbar { + toolbarItems + } + .alertManager(loadAlert) + } +} + +struct UpdateView_Previews: PreviewProvider { + static var previews: some View { + UpdateView() + .environmentObject(CaskData()) + .frame(width: 500, height: 400) + } +} diff --git a/Applite/Views/Detail Views/UpdateView.swift b/Applite/Views/Detail Views/UpdateView.swift deleted file mode 100755 index 374612c..0000000 --- a/Applite/Views/Detail Views/UpdateView.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// UpdateView.swift -// Applite -// -// Created by Milán Várady on 2022. 10. 14.. -// - -import SwiftUI -import Fuse - -/// Update section -struct UpdateView: View { - @EnvironmentObject var caskData: CaskData - - @State var searchText = "" - @State var refreshing = false - @State var isUpdatingAll = false - @State var updateAllFinished = false - @State var updateAllButtonRotation = 0.0 - - @State var showingGreedyUpdateConfirm = false - @State var showOutdatedFailAlert = false - - // Filter outdated casks - var casks: [Cask] { - var filteredCasks = caskData.casks.filter { $0.isOutdated } - - if !$searchText.wrappedValue.isEmpty { - filteredCasks = filteredCasks.filter { - (fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.4 - } - } - - return filteredCasks - } - - let fuseSearch = Fuse() - - var body: some View { - ScrollView { - // App grid - AppGridView(casks: Array(caskData.outdatedCasks), appRole: .update) - .padding() - - if casks.count > 1 { - // Update all button - Button { - isUpdatingAll = true - - withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) { - updateAllButtonRotation = 360.0 - } - - Task { - await withTaskGroup(of: Void.self) { group in - for cask in casks { - group.addTask { - await cask.update(caskData: caskData) - } - } - } - - await MainActor.run { - withAnimation(.linear(duration: 0.2)) { - updateAllButtonRotation = 0.0 - } - } - - updateAllFinished = true - } - } label: { - HStack { - Image(systemName: updateAllFinished ? "checkmark" : "arrow.2.circlepath") - .rotationEffect(.degrees(updateAllButtonRotation)) - - Text("Update All") - } - } - .bigButton(backgroundColor: .accentColor) - .padding(.vertical) - .disabled(isUpdatingAll) - } - - // No updates availabe - if casks.count == 0 { - VStack { - Spacer() - - Text("No Updates Available") - .font(.title) - - Spacer() - } - } - } - .searchable(text: $searchText) - .toolbar { - Button { - showingGreedyUpdateConfirm = true - } label: { - Label("Show All Updates", systemImage: "eye") - } - .labelStyle(.titleAndIcon) - .alert("Notice", isPresented: $showingGreedyUpdateConfirm) { - Button("Show All") { - Task { - do { - try await caskData.refreshOutdatedApps(greedy: true) - } catch { - showOutdatedFailAlert = true - } - } - } - - Button("Cancel", role: .cancel) { } - } message: { - VStack { - Text("This will show updates from applications that have auto-update turned off, i.e., applications that are taking care of their own updates.") - } - } - - // Refresh outdated casks - if refreshing { - SmallProgressView() - } - else { - Button { - Task { - refreshing = true - - do { - try await caskData.refreshOutdatedApps(greedy: true) - } catch { - showOutdatedFailAlert = true - } - - refreshing = false - } - } label: { - Image(systemName: "arrow.clockwise") - } - } - } - .alert("Failed to load outdated apps", isPresented: $showOutdatedFailAlert) {} - } -} - -struct UpdateView_Previews: PreviewProvider { - static var previews: some View { - UpdateView() - .environmentObject(CaskData()) - .frame(width: 500, height: 400) - } -} diff --git a/Applite/Views/Settings/SettingsView+BrewPath.swift b/Applite/Views/Settings/SettingsView+BrewPath.swift new file mode 100644 index 0000000..65d5e11 --- /dev/null +++ b/Applite/Views/Settings/SettingsView+BrewPath.swift @@ -0,0 +1,58 @@ +// +// SettingsView+BrewPath.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SettingsView { + struct BrewPathView: View { + @AppStorage(Preferences.customUserBrewPath.rawValue) var customUserBrewPath: String = "/opt/homebrew/bin/brew" + @AppStorage(Preferences.brewPathOption.rawValue) var brewPathOption = BrewPaths.PathOption.appPath.rawValue + + @State var isSelectedBrewPathValid = false + + /// Brew installation option before making changes + @State var previousBrewOption: Int = 0 + + var body: some View { + VStack(alignment: .leading) { + Text("Brew Executable Path") + .bold() + + BrewPathSelectorView(isSelectedPathValid: $isSelectedBrewPathValid) + + Text("Currently selected brew path is invalid") + .foregroundColor(.red) + .opacity(isSelectedBrewPathValid ? 0 : 1) + + // Brew path changed + if previousBrewOption != brewPathOption && isSelectedBrewPathValid { + Text("Brew path has been modified. Restart app for changes to take effect.") + .foregroundColor(.red) + .fixedSize(horizontal: false, vertical: true) + + Button("Relaunch", role: .destructive) { + Task.detached { + try? await Shell.runAsync("/usr/bin/osascript -e 'tell application \"\(Bundle.main.appName)\" to quit' && sleep 2 && open \"\(Bundle.main.bundlePath)\"") + } + } + } + + Divider() + .padding(.vertical, 8) + + Text("Appdir") + .bold() + + AppdirSelectorView() + } + .onAppear { + previousBrewOption = BrewPaths.selectedBrewOption.rawValue + } + .padding() + } + } +} diff --git a/Applite/Views/Settings/SettingsView+GeneralSettings.swift b/Applite/Views/Settings/SettingsView+GeneralSettings.swift new file mode 100644 index 0000000..250b47c --- /dev/null +++ b/Applite/Views/Settings/SettingsView+GeneralSettings.swift @@ -0,0 +1,68 @@ +// +// SettingsView+GeneralSettings.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SettingsView { + struct GeneralSettingsView: View { + @AppStorage(Preferences.colorSchemePreference.rawValue) var colorSchemePreference: ColorSchemePreference = .system + @AppStorage(Preferences.notificationSuccess.rawValue) var notificationOnSuccess: Bool = false + @AppStorage(Preferences.notificationFailure.rawValue) var notificationOnFailure: Bool = true + + /// Needed for a workaround for changing the color scheme + @State var fixingColor = false + + var body: some View { + VStack(alignment: .leading) { + Text("Appearance") + .bold() + + Picker("Color Scheme:", selection: $colorSchemePreference) { + ForEach(ColorSchemePreference.allCases) { color in + Text(LocalizedStringKey(color.rawValue.capitalized)) + } + } + .pickerStyle(.segmented) + + Divider() + .padding(.vertical) + + Text("Notifications") + .bold() + + Toggle("Task completions", isOn: $notificationOnSuccess) + Toggle("Task errors", isOn: $notificationOnFailure) + } + .padding() + .onChange(of: colorSchemePreference) { + // Don't remove this! + // This is here because changing the .preferredColorScheme view modifier is bugged + // When it's set back to nil, parts of the UI don't default back to the system color scheme + if $0 == .system && !fixingColor { + // Set fixingColor to true, so we don't recursively call this function + self.fixingColor = true + + // Get system color scheme + let darkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" + + Task { + // Set color scheme to system + colorSchemePreference = darkMode ? .dark : .light + // Wait + try? await Task.sleep(for: .seconds(0.1)) + // Set it back to nil (.system) + colorSchemePreference = .system + // Wait + try? await Task.sleep(for: .seconds(0.1)) + // Set fixing color back to false + await MainActor.run { self.fixingColor = false } + } + } + } + } + } +} diff --git a/Applite/Views/Settings/SettingsView+ProxySettings.swift b/Applite/Views/Settings/SettingsView+ProxySettings.swift new file mode 100644 index 0000000..c80f1f0 --- /dev/null +++ b/Applite/Views/Settings/SettingsView+ProxySettings.swift @@ -0,0 +1,34 @@ +// +// SettingsView+ProxySettings.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SettingsView { + struct ProxySettingsView: View { + @AppStorage(Preferences.networkProxyEnabled.rawValue) var proxyEnabled: Bool = true + @AppStorage(Preferences.preferredProxyType.rawValue) var preferredProxyType: NetworkProxyType = .http + + var body: some View { + VStack(alignment: .center) { + Toggle("Use system proxy", isOn: $proxyEnabled) + + Picker("Preferred proxy protocol", selection: $preferredProxyType) { + ForEach(NetworkProxyType.allCases, id: \.self) { proxyType in + Text(proxyType.displayName) + .tag(proxyType.rawValue) + } + } + .padding(.bottom) + + Text("\(Bundle.main.appName) uses the system network proxy, but it can only use one protocol (HTTP, HTTPS, or SOCKS5). Select your preferred method.") + .font(.system(.body, weight: .light)) + .frame(minHeight: 60) + } + .padding() + } + } +} diff --git a/Applite/Views/Settings/SettingsView+Uninstaller.swift b/Applite/Views/Settings/SettingsView+Uninstaller.swift new file mode 100644 index 0000000..9292768 --- /dev/null +++ b/Applite/Views/Settings/SettingsView+Uninstaller.swift @@ -0,0 +1,28 @@ +// +// SettingsView+Uninstaller.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SettingsView { + struct UninstallView: View { + @Environment(\.openWindow) var openWindow + + var body: some View { + VStack(alignment: .center) { + Button(role: .destructive) { + openWindow(id: "uninstall-self") + } label: { + Label("Uninstall", systemImage: "trash.fill") + } + .bigButton(foregroundColor: .white, backgroundColor: .red) + + Text("Uninstall \(Bundle.main.appName), related files and cache.") + } + .padding() + } + } +} diff --git a/Applite/Views/Settings/SettingsView+UpdateSettings.swift b/Applite/Views/Settings/SettingsView+UpdateSettings.swift new file mode 100644 index 0000000..853aa65 --- /dev/null +++ b/Applite/Views/Settings/SettingsView+UpdateSettings.swift @@ -0,0 +1,51 @@ +// +// SettingsView+UpdateSettings.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI +import Sparkle + +extension SettingsView { + struct UpdateSettingsView: View { + private let updater: SPUUpdater + + @State private var automaticallyChecksForUpdates: Bool + @State private var automaticallyDownloadsUpdates: Bool + + init(updater: SPUUpdater) { + self.updater = updater + self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates + self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates + } + + var body: some View { + VStack { + CheckForUpdatesView(updater: updater) { + Label("Check for Updates...", systemImage: "arrow.uturn.down") + } + + Text("Current app version: \(Bundle.main.version) (\(Bundle.main.buildNumber))") + .font(.system(.body, weight: .light)) + .foregroundColor(.secondary) + + Spacer() + .frame(height: 20) + + Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates) + .onChange(of: automaticallyChecksForUpdates) { newValue in + updater.automaticallyChecksForUpdates = newValue + } + + Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates) + .disabled(!automaticallyChecksForUpdates) + .onChange(of: automaticallyDownloadsUpdates) { newValue in + updater.automaticallyDownloadsUpdates = newValue + } + } + .padding() + } + } +} diff --git a/Applite/Views/Settings/SettingsView.swift b/Applite/Views/Settings/SettingsView.swift new file mode 100755 index 0000000..e6b8d0b --- /dev/null +++ b/Applite/Views/Settings/SettingsView.swift @@ -0,0 +1,68 @@ +// +// SettingsView.swift +// Applite +// +// Created by Milán Várady on 2022. 12. 29.. +// + +import SwiftUI +import AppKit +import Sparkle + +public enum ColorSchemePreference: String, CaseIterable, Identifiable { + case system + case light + case dark + + public var id: Self { self } +} + +/// Settings pane +struct SettingsView: View { + let updater: SPUUpdater + + var body: some View { + TabView { + GeneralSettingsView() + .tabItem { + Label("General", systemImage: "gearshape") + } + + BrewPathView() + .tabItem { + Label("Brew Path", systemImage: "mug") + } + + UpdateSettingsView(updater: updater) + .tabItem { + Label("Updates", systemImage: "arrow.clockwise") + } + + ProxySettingsView() + .tabItem { + Label("Proxy", systemImage: "network.badge.shield.half.filled") + } + + UninstallView() + .tabItem { + Label("Uninstall", systemImage: "trash") + } + } + .labelStyle(.titleAndIcon) + .presentedWindowToolbarStyle(.expanded) + .contentShape(Rectangle()) + .onTapGesture { + // Deselect textfield when clicking away + Task { @MainActor in + NSApp.keyWindow?.makeFirstResponder(nil) + } + } + .frame(width: 400) + } +} + +struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView(updater: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil).updater) + } +} diff --git a/Applite/Views/SettingsView.swift b/Applite/Views/SettingsView.swift deleted file mode 100755 index 206a5d7..0000000 --- a/Applite/Views/SettingsView.swift +++ /dev/null @@ -1,257 +0,0 @@ -// -// SettingsView.swift -// Applite -// -// Created by Milán Várady on 2022. 12. 29.. -// - -import SwiftUI -import AppKit -import Sparkle - -public enum ColorSchemePreference: String, CaseIterable, Identifiable { - case system - case light - case dark - - public var id: Self { self } -} - -/// Settings pane -struct SettingsView: View { - let updater: SPUUpdater - - var body: some View { - TabView { - GeneralSettingsView() - .tabItem { - Label("General", systemImage: "gearshape") - } - - BrewPathView() - .tabItem { - Label("Brew Path", systemImage: "mug") - } - - UpdateSettingsView(updater: updater) - .tabItem { - Label("Updates", systemImage: "arrow.clockwise") - } - - ProxySettingsView() - .tabItem { - Label("Proxy", systemImage: "network.badge.shield.half.filled") - } - - UninstallView() - .tabItem { - Label("Uninstall", systemImage: "trash") - } - } - .labelStyle(.titleAndIcon) - .presentedWindowToolbarStyle(.expanded) - - .contentShape(Rectangle()) - .onTapGesture { - // Deselect textfield when clicking away - Task { @MainActor in - NSApp.keyWindow?.makeFirstResponder(nil) - } - } - .frame(width: 400) - } -} - -fileprivate struct GeneralSettingsView: View { - @AppStorage(Preferences.colorSchemePreference.rawValue) var colorSchemePreference: ColorSchemePreference = .system - @AppStorage(Preferences.notificationSuccess.rawValue) var notificationOnSuccess: Bool = false - @AppStorage(Preferences.notificationFailure.rawValue) var notificationOnFailure: Bool = true - - /// Needed for a workaround for changing the color scheme - @State var fixingColor = false - - var body: some View { - VStack(alignment: .leading) { - Text("Appearance") - .bold() - - Picker("Color Scheme:", selection: $colorSchemePreference) { - ForEach(ColorSchemePreference.allCases) { color in - Text(LocalizedStringKey(color.rawValue.capitalized)) - } - } - .pickerStyle(.segmented) - - Divider() - .padding(.vertical) - - Text("Notifications") - .bold() - - Toggle("Task completions", isOn: $notificationOnSuccess) - Toggle("Task errors", isOn: $notificationOnFailure) - } - .padding() - .onChange(of: colorSchemePreference) { - // Don't remove this! - // This is here because changing the .preferredColorScheme view modifier is bugged - // When it's set back to nil, parts of the UI don't default back to the system color scheme - if $0 == .system && !fixingColor { - // Set fixingColor to true, so we don't recursively call this function - self.fixingColor = true - - // Get system color scheme - let darkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" - - Task { - // Set color scheme to system - colorSchemePreference = darkMode ? .dark : .light - // Wait - try? await Task.sleep(for: .seconds(0.1)) - // Set it back to nil (.system) - colorSchemePreference = .system - // Wait - try? await Task.sleep(for: .seconds(0.1)) - // Set fixing color back to false - await MainActor.run { self.fixingColor = false } - } - } - } - } -} - -fileprivate struct BrewPathView: View { - @AppStorage(Preferences.customUserBrewPath.rawValue) var customUserBrewPath: String = "/opt/homebrew/bin/brew" - @AppStorage(Preferences.brewPathOption.rawValue) var brewPathOption = BrewPaths.PathOption.appPath.rawValue - - @State var isSelectedBrewPathValid = false - - /// Brew installation option before making changes - @State var previousBrewOption: Int = 0 - - var body: some View { - VStack(alignment: .leading) { - Text("Brew Executable Path") - .bold() - - BrewPathSelectorView(isSelectedPathValid: $isSelectedBrewPathValid) - - Text("Currently selected brew path is invalid") - .foregroundColor(.red) - .opacity(isSelectedBrewPathValid ? 0 : 1) - - // Brew path changed - if previousBrewOption != brewPathOption && isSelectedBrewPathValid { - Text("Brew path has been modified. Restart app for changes to take effect.") - .foregroundColor(.red) - .fixedSize(horizontal: false, vertical: true) - - Button("Relaunch", role: .destructive) { - Task.detached { - try? await Shell.runAsync("/usr/bin/osascript -e 'tell application \"\(Bundle.main.appName)\" to quit' && sleep 2 && open \"\(Bundle.main.bundlePath)\"") - } - } - } - - Divider() - .padding(.vertical, 8) - - Text("Appdir") - .bold() - - AppdirSelectorView() - } - .onAppear { - previousBrewOption = BrewPaths.selectedBrewOption.rawValue - } - .padding() - } -} - -fileprivate struct UpdateSettingsView: View { - private let updater: SPUUpdater - - @State private var automaticallyChecksForUpdates: Bool - @State private var automaticallyDownloadsUpdates: Bool - - init(updater: SPUUpdater) { - self.updater = updater - self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates - self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates - } - - var body: some View { - VStack { - CheckForUpdatesView(updater: updater) { - Label("Check for Updates...", systemImage: "arrow.uturn.down") - } - - Text("Current app version: \(Bundle.main.version) (\(Bundle.main.buildNumber))") - .font(.system(.body, weight: .light)) - .foregroundColor(.secondary) - - Spacer() - .frame(height: 20) - - Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates) - .onChange(of: automaticallyChecksForUpdates) { newValue in - updater.automaticallyChecksForUpdates = newValue - } - - Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates) - .disabled(!automaticallyChecksForUpdates) - .onChange(of: automaticallyDownloadsUpdates) { newValue in - updater.automaticallyDownloadsUpdates = newValue - } - } - .padding() - } -} - -fileprivate struct ProxySettingsView: View { - @AppStorage(Preferences.networkProxyEnabled.rawValue) var proxyEnabled: Bool = true - @AppStorage(Preferences.preferredProxyType.rawValue) var preferredProxyType: NetworkProxyType = .http - - var body: some View { - VStack(alignment: .center) { - Toggle("Use system proxy", isOn: $proxyEnabled) - - Picker("Preferred proxy protocol", selection: $preferredProxyType) { - ForEach(NetworkProxyType.allCases, id: \.self) { proxyType in - Text(proxyType.displayName) - .tag(proxyType.rawValue) - } - } - .padding(.bottom) - - Text("\(Bundle.main.appName) uses the system network proxy, but it can only use one protocol (HTTP, HTTPS, or SOCKS5). Select your preferred method.") - .font(.system(.body, weight: .light)) - .frame(minHeight: 60) - } - .padding() - } -} - -fileprivate struct UninstallView: View { - @Environment(\.openWindow) var openWindow - - var body: some View { - VStack(alignment: .center) { - Button(role: .destructive) { - openWindow(id: "uninstall-self") - } label: { - Label("Uninstall", systemImage: "trash.fill") - } - .bigButton(foregroundColor: .white, backgroundColor: .red) - - Text("Uninstall \(Bundle.main.appName), related files and cache.") - } - .padding() - } -} - -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView(updater: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil).updater) - } -} diff --git a/Applite/Views/Setup/SetupView+AllSet.swift b/Applite/Views/Setup/SetupView+AllSet.swift new file mode 100644 index 0000000..ab5a226 --- /dev/null +++ b/Applite/Views/Setup/SetupView+AllSet.swift @@ -0,0 +1,26 @@ +// +// SetupView+AllSet.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SetupView { + /// Page shown when setup is complete + struct AllSet: View { + @AppStorage(Preferences.setupComplete.rawValue) var setupComplete = false + + var body: some View { + Text("All set!") + .font(.system(size: 52, weight: .bold)) + .padding(.top, 40) + + Button("Start Using \(Bundle.main.appName)") { + setupComplete = true + } + .bigButton(backgroundColor: .accentColor) + } + } +} diff --git a/Applite/Views/Setup/SetupView+BrewInstall.swift b/Applite/Views/Setup/SetupView+BrewInstall.swift new file mode 100644 index 0000000..298c9ce --- /dev/null +++ b/Applite/Views/Setup/SetupView+BrewInstall.swift @@ -0,0 +1,136 @@ +// +// SetupView+BrewInstall.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SetupView { + /// Brew installation page + struct BrewInstall: View { + /// This is needed so the parent view knows it can continue to the next page + @Binding var isDone: Bool + + @State var failed = false + + // Alerts + @State var showCommandLineToolsInstallAlert = false + @State var showInstallFailAlert = false + + @StateObject var installationProgress = BrewInstallationProgress() + + var body: some View { + VStack { + Text("Installing dependencies") + .font(.system(size: 32, weight: .bold)) + .padding(.vertical) + + // Xcode Command Line Tools + VStack(alignment: .leading, spacing: 20) { + // Xcode Command Line Tools + dependencyView(title: "Xcode Command Line Tools", + description: "You will be prompted to install the Xcode Command Line Tools, please click \"Install\" as it is required for this application to work. It will take a few minutes, you can see the progress on the installation window.", + progressOrder: .waitingForXcodeCommandLineTools) + + + // Homebrew + dependencyView(title: "Homebrew", + description: "[Homebrew](https://brew.sh) is a free and open source package manager tool that makes installing third party applications really easy. \(Bundle.main.appName) uses Homebrew under the hood to download and manage applications.", + progressOrder: .fetchingHomebrew) + + // Retry button + if failed { + Button { + Task { + await installDependencies() + } + } label: { + Label("Retry Install", systemImage: "arrow.clockwise.circle") + } + .bigButton(backgroundColor: .secondary) + } + } + .frame(width: 440) + .task { + // Start installation when view loads + await installDependencies() + } + .onAppear() { + if !isCommandLineToolsInstalled() { + showCommandLineToolsInstallAlert = true + } + } + .alert("Xcode Command Line Tools", isPresented: $showCommandLineToolsInstallAlert) {} message: { + Text("You will be prompted to install Xcode Command Line Tools. Please select \"Install\" as it is required for this application to work.") + } + .alert("Installation failed", isPresented: $showInstallFailAlert) { + Button("Troubleshooting") { + if let url = URL(string: "https://aerolite.dev/applite/troubleshooting.html") { + NSWorkspace.shared.open(url) + } + } + + Button("Quit", role: .destructive) { NSApplication.shared.terminate(self) } + } message: { + Text("Retry the installation or visit the troubleshooting page.") + } + } + } + + private func dependencyView(title: LocalizedStringKey, description: LocalizedStringKey, progressOrder: InstallPhase) -> some View { + VStack(alignment: .leading) { + HStack { + Text(title) + .font(.system(size: 16, weight: .bold)) + .padding(.trailing, 4) + + if !failed { + if installationProgress.phase.rawValue > progressOrder.rawValue { + installedBadge + } else { + ProgressView() + .controlSize(.small) + } + } else { + Image(systemName: "xmark.circle") + .font(.system(size: 18)) + .foregroundColor(.red) + } + } + .frame(height: 30) + + Text(description) + } + } + + private func installDependencies() async { + // Reset progress + failed = false + installationProgress.phase = .waitingForXcodeCommandLineTools + + do { + try await DependencyManager.install(progressObject: installationProgress) + } catch { + failed = true + } + + if !failed { + self.isDone = true + } + } + + /// A little bagde that says "Installed" + private var installedBadge: some View { + HStack { + Image(systemName: "checkmark") + Text("Installed") + } + .padding(3) + .foregroundColor(.white) + .background(.green) + .cornerRadius(4) + } + } +} diff --git a/Applite/Views/Setup/SetupView+BrewPathSelection.swift b/Applite/Views/Setup/SetupView+BrewPathSelection.swift new file mode 100644 index 0000000..37c255b --- /dev/null +++ b/Applite/Views/Setup/SetupView+BrewPathSelection.swift @@ -0,0 +1,45 @@ +// +// SetupView+BrewPathSelection.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SetupView { + /// User can provide installed brew path here + struct BrewPathSelection: View { + @Binding var isBrewPathValid: Bool + + var body: some View { + VStack(alignment: .center) { + Spacer() + + Text("Provide Brew Executable Path") + .font(.system(size: 26, weight: .bold)) + .padding(.bottom, 30) + + + VStack(alignment: .leading) { + BrewPathSelectorView(isSelectedPathValid: $isBrewPathValid) + + Text("Selected brew path is invalid!") + .foregroundColor(.red) + .opacity(isBrewPathValid ? 0 : 1) + .padding(.bottom) + + Text("Appdir (optional)") + .font(.system(size: 16, weight: .bold)) + + AppdirSelectorView() + } + .frame(width: 500) + + Spacer() + } + .frame(maxWidth: 540) + .padding() + } + } +} diff --git a/Applite/Views/Setup/SetupView+BrewTypeSelection.swift b/Applite/Views/Setup/SetupView+BrewTypeSelection.swift new file mode 100644 index 0000000..d9865e1 --- /dev/null +++ b/Applite/Views/Setup/SetupView+BrewTypeSelection.swift @@ -0,0 +1,47 @@ +// +// SetupView+BrewTypeSelection.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SetupView { + /// Brew installation method selection page. User can choose to use their brew if they have or create a new installation. + struct BrewTypeSelection: View { + @Binding var page: Pages + + var body: some View { + VStack { + Spacer() + + Text("Do you already have Homebrew installed?") + .font(.system(size: 26, weight: .bold)) + .padding(.top, 10) + .padding(.bottom) + + HStack { + Button("Yes") { + page = .brewPathSelection + BrewPaths.selectedBrewOption = .defaultAppleSilicon + } + .bigButton() + + Button("No (I don't know what it is)") { + page = .brewInstall + BrewPaths.selectedBrewOption = .appPath + } + .bigButton(backgroundColor: .accentColor) + } + + Spacer() + + Text("This application uses the free and open source [Homebrew](https://brew.sh/) package manager to download and manage applications. If you already have it installed on your system, you can use it right away. If you don't have brew installed or don't know what it is, select **No**. This will create a new brew installation just for \(Bundle.main.appName).") + .padding(.bottom, 22) + } + .frame(maxWidth: 540) + .padding() + } + } +} diff --git a/Applite/Views/Setup/SetupView+PageControllerButtons.swift b/Applite/Views/Setup/SetupView+PageControllerButtons.swift new file mode 100644 index 0000000..bc527e6 --- /dev/null +++ b/Applite/Views/Setup/SetupView+PageControllerButtons.swift @@ -0,0 +1,58 @@ +// +// SetupView+PageControllerButtons.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SetupView { + /// Adds a Back and Continue button to the bottom of the page + /// + /// - Parameters: + /// - page: Page binding so it can change the current page + /// - canContinue: Controls whether it can go to the next page yet or not + /// - pageAfter: Page when clicking on Continue + /// - pageBefore: Page when clicking on Back + /// + /// - Returns: ``View`` + struct PageControlButtons: View { + @Binding var page: Pages + @Binding var pushDirection: PushDirection + let canContinue: Bool + let pageAfter: Pages + let pageBefore: Pages? + + var body: some View { + Spacer() + + Divider() + + HStack { + Spacer() + + if let pageBefore { + Button("Back") { + pushDirection = .backward + withAnimation { + page = pageBefore + } + } + .bigButton(backgroundColor: Color(red: 0.7, green: 0.7, blue: 0.7)) + } + + Button("Continue") { + pushDirection = .forward + withAnimation { + page = pageAfter + } + } + .disabled(!canContinue) + .bigButton(backgroundColor: canContinue ? .accentColor : .gray) + } + .padding(.trailing) + .padding(.bottom, 8) + } + } +} diff --git a/Applite/Views/Setup/SetupView+Welcome.swift b/Applite/Views/Setup/SetupView+Welcome.swift new file mode 100644 index 0000000..0ef5134 --- /dev/null +++ b/Applite/Views/Setup/SetupView+Welcome.swift @@ -0,0 +1,62 @@ +// +// SetupView+Welcome.swift +// Applite +// +// Created by Milán Várady on 2024.12.26. +// + +import SwiftUI + +extension SetupView { + /// Welcome page + struct Welcome: View { + var body: some View { + VStack { + Text("Welcome to \(Bundle.main.appName)") + .font(.system(size: 36, weight: .bold)) + .padding(.top, 50) + .padding(.bottom, 25) + + VStack(alignment: .leading, spacing: 16) { + Feature(sfSymbol: "square.and.arrow.down.on.square", + title: "Download apps with ease", + description: "Download third party applications with a single click. No more \"Drag to Applications folder\".") + + Feature(sfSymbol: "cursorarrow.and.square.on.square.dashed", + title: "Manage applications", + description: "Update and uninstall your applications. No more leftover files from deleted applications.") + + Feature(sfSymbol: "sparkle.magnifyingglass", + title: "Discover", + description: "Browse through a handpicked list of awesome apps.") + } + } + .frame(maxWidth: 500) + } + + /// A feature of the app to be displayed in the Welcome view + private struct Feature: View { + let sfSymbol: String + let title: LocalizedStringKey + let description: LocalizedStringKey + + var body: some View { + HStack { + Image(systemName: sfSymbol) + .font(.system(size: 22)) + .padding(.trailing, 5) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(.blue) + + VStack(alignment: .leading) { + Text(title, comment: "Title") + .font(.system(size: 14, weight: .bold)) + + Text(description, comment: "Description") + .font(.system(size: 12, weight: .light)) + } + } + } + } + } +} diff --git a/Applite/Views/Setup/SetupView.swift b/Applite/Views/Setup/SetupView.swift new file mode 100755 index 0000000..a085c20 --- /dev/null +++ b/Applite/Views/Setup/SetupView.swift @@ -0,0 +1,88 @@ +// +// SetupView.swift +// Applite +// +// Created by Milán Várady on 2023. 01. 03.. +// + +import SwiftUI +import AppKit + +/// This view is shown when first launching the app. It welcomes the user and installs dependencies (Homebrew, Xcode Command Line Tools). +struct SetupView: View { + enum Pages { + case welcome, + brewTypeSelection, + brewPathSelection, + brewInstall, + allSet + } + + @State var page: Pages = .welcome + + @State var isBrewPathValid = false + @State var isBrewInstallDone = false + + enum PushDirection { + case forward, backward + } + + @State var pushDirection: PushDirection = .forward + + var body: some View { + VStack { + switch page { + case .welcome: + Welcome() + .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) + + PageControlButtons( + page: $page, + pushDirection: $pushDirection, + canContinue: true, + pageAfter: .brewTypeSelection, + pageBefore: nil + ) + + case .brewTypeSelection: + BrewTypeSelection(page: $page) + .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) + + case .brewPathSelection: + BrewPathSelection(isBrewPathValid: $isBrewPathValid) + .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) + + PageControlButtons( + page: $page, + pushDirection: $pushDirection, + canContinue: isBrewPathValid, + pageAfter: .allSet, + pageBefore: .brewTypeSelection + ) + + case .brewInstall: + BrewInstall(isDone: $isBrewInstallDone) + .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) + + PageControlButtons( + page: $page, + pushDirection: $pushDirection, + canContinue: isBrewInstallDone, + pageAfter: .allSet, + pageBefore: nil + ) + + case .allSet: + AllSet() + .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) + } + } + } +} + +struct SetupView_Previews: PreviewProvider { + static var previews: some View { + SetupView() + .frame(width: 600, height: 400) + } +} diff --git a/Applite/Views/SetupView.swift b/Applite/Views/SetupView.swift deleted file mode 100755 index 1c42748..0000000 --- a/Applite/Views/SetupView.swift +++ /dev/null @@ -1,407 +0,0 @@ -// -// SetupView.swift -// Applite -// -// Created by Milán Várady on 2023. 01. 03.. -// - -import SwiftUI -import AppKit - -/// This view is shown when first launching the app. It welcomes the user and installs dependencies (Homebrew, Xcode Command Line Tools). -struct SetupView: View { - enum Pages { - case welcome, - brewTypeSelection, - brewPathSelection, - brewInstall, - allSet - } - - @State var page: Pages = .welcome - - @State var isBrewPathValid = false - @State var isBrewInstallDone = false - - enum PushDirection { - case forward, backward - } - - @State var pushDirection: PushDirection = .forward - - var body: some View { - VStack { - switch page { - case .welcome: - Welcome() - .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) - - PageControlButtons( - page: $page, - pushDirection: $pushDirection, - canContinue: true, - pageAfter: .brewTypeSelection, - pageBefore: nil - ) - - case .brewTypeSelection: - BrewTypeSelection(page: $page) - .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) - - case .brewPathSelection: - BrewPathSelection(isBrewPathValid: $isBrewPathValid) - .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) - - PageControlButtons( - page: $page, - pushDirection: $pushDirection, - canContinue: isBrewPathValid, - pageAfter: .allSet, - pageBefore: .brewTypeSelection - ) - - case .brewInstall: - BrewInstall(isDone: $isBrewInstallDone) - .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) - - PageControlButtons( - page: $page, - pushDirection: $pushDirection, - canContinue: isBrewInstallDone, - pageAfter: .allSet, - pageBefore: nil - ) - - case .allSet: - AllSet() - .transition(.push(from: pushDirection == .forward ? .trailing : .leading)) - } - } - } - - /// Adds a Back and Continue button to the bottom of the page - /// - /// - Parameters: - /// - page: Page binding so it can change the current page - /// - canContinue: Controls whether it can go to the next page yet or not - /// - pageAfter: Page when clicking on Continue - /// - pageBefore: Page when clicking on Back - /// - /// - Returns: ``View`` - private struct PageControlButtons: View { - @Binding var page: Pages - @Binding var pushDirection: PushDirection - let canContinue: Bool - let pageAfter: Pages - let pageBefore: Pages? - - var body: some View { - Spacer() - - Divider() - - HStack { - Spacer() - - if let pageBefore { - Button("Back") { - pushDirection = .backward - withAnimation { - page = pageBefore - } - } - .bigButton(backgroundColor: Color(red: 0.7, green: 0.7, blue: 0.7)) - } - - Button("Continue") { - pushDirection = .forward - withAnimation { - page = pageAfter - } - } - .disabled(!canContinue) - .bigButton(backgroundColor: canContinue ? .accentColor : .gray) - } - .padding(.trailing) - .padding(.bottom, 8) - } - } - - /// Welcome page - private struct Welcome: View { - var body: some View { - VStack { - Text("Welcome to \(Bundle.main.appName)") - .font(.system(size: 36, weight: .bold)) - .padding(.top, 50) - .padding(.bottom, 25) - - VStack(alignment: .leading, spacing: 16) { - Feature(sfSymbol: "square.and.arrow.down.on.square", - title: "Download apps with ease", - description: "Download third party applications with a single click. No more \"Drag to Applications folder\".") - - Feature(sfSymbol: "cursorarrow.and.square.on.square.dashed", - title: "Manage applications", - description: "Update and uninstall your applications. No more leftover files from deleted applications.") - - Feature(sfSymbol: "sparkle.magnifyingglass", - title: "Discover", - description: "Browse through a handpicked list of awesome apps.") - } - } - .frame(maxWidth: 500) - } - - /// A feature of the app to be displayed in the Welcome view - private struct Feature: View { - let sfSymbol: String - let title: LocalizedStringKey - let description: LocalizedStringKey - - var body: some View { - HStack { - Image(systemName: sfSymbol) - .font(.system(size: 22)) - .padding(.trailing, 5) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(.blue) - - VStack(alignment: .leading) { - Text(title, comment: "Title") - .font(.system(size: 14, weight: .bold)) - - Text(description, comment: "Description") - .font(.system(size: 12, weight: .light)) - } - } - } - } - } - - /// Brew installation method selection page. User can choose to use their brew if they have or create a new installation. - private struct BrewTypeSelection: View { - @Binding var page: Pages - - var body: some View { - VStack { - Spacer() - - Text("Do you already have Homebrew installed?") - .font(.system(size: 26, weight: .bold)) - .padding(.top, 10) - .padding(.bottom) - - HStack { - Button("Yes") { - page = .brewPathSelection - BrewPaths.selectedBrewOption = .defaultAppleSilicon - } - .bigButton() - - Button("No (I don't know what it is)") { - page = .brewInstall - BrewPaths.selectedBrewOption = .appPath - } - .bigButton(backgroundColor: .accentColor) - } - - Spacer() - - Text("This application uses the free and open source [Homebrew](https://brew.sh/) package manager to download and manage applications. If you already have it installed on your system, you can use it right away. If you don't have brew installed or don't know what it is, select **No**. This will create a new brew installation just for \(Bundle.main.appName).") - .padding(.bottom, 22) - } - .frame(maxWidth: 540) - .padding() - } - } - - /// User can provide installed brew path here - private struct BrewPathSelection: View { - @Binding var isBrewPathValid: Bool - - var body: some View { - VStack(alignment: .center) { - Spacer() - - Text("Provide Brew Executable Path") - .font(.system(size: 26, weight: .bold)) - .padding(.bottom, 30) - - - VStack(alignment: .leading) { - BrewPathSelectorView(isSelectedPathValid: $isBrewPathValid) - - Text("Selected brew path is invalid!") - .foregroundColor(.red) - .opacity(isBrewPathValid ? 0 : 1) - .padding(.bottom) - - Text("Appdir (optional)") - .font(.system(size: 16, weight: .bold)) - - AppdirSelectorView() - } - .frame(width: 500) - - Spacer() - } - .frame(maxWidth: 540) - .padding() - } - } - - /// Brew installation page - private struct BrewInstall: View { - /// This is needed so the parent view knows it can continue to the next page - @Binding var isDone: Bool - - @State var failed = false - - // Alerts - @State var showingAlert = false - @State var showingCommandLineToolsAlert = false - - @StateObject var installationProgress = BrewInstallationProgress() - - var body: some View { - VStack { - Text("Installing dependencies") - .font(.system(size: 32, weight: .bold)) - .padding(.vertical) - - // Xcode Command Line Tools - VStack(alignment: .leading, spacing: 20) { - // Xcode Command Line Tools - dependencyView(title: "Xcode Command Line Tools", - description: "You will be prompted to install the Xcode Command Line Tools, please click \"Install\" as it is required for this application to work. It will take a few minutes, you can see the progress on the installation window.", - progressOrder: .waitingForXcodeCommandLineTools) - - - // Homebrew - dependencyView(title: "Homebrew", - description: "[Homebrew](https://brew.sh) is a free and open source package manager tool that makes installing third party applications really easy. \(Bundle.main.appName) uses Homebrew under the hood to download and manage applications.", - progressOrder: .fetchingHomebrew) - - // Retry button - if failed { - Button { - Task { - await installDependencies() - } - } label: { - Label("Retry Install", systemImage: "arrow.clockwise.circle") - } - .bigButton(backgroundColor: .secondary) - } - } - .frame(width: 440) - .task { - // Start installation when view loads - await installDependencies() - } - .onAppear() { - if !isCommandLineToolsInstalled() { - showingCommandLineToolsAlert = true - } - } - .alert(isPresented: $showingCommandLineToolsAlert) { - Alert(title: Text("Xcode Command Line Tools"), - message: Text("You will be prompted to install Xcode Command Line Tools. Please select \"Install\" as it is required for this application to work.")) - } - .alert("Installation failed", isPresented: $showingAlert, actions: { - Button("Troubleshooting") { - if let url = URL(string: "https://aerolite.dev/applite/troubleshooting.html") { - NSWorkspace.shared.open(url) - } - } - - Button("Retry") { - - } - - Button("Quit", role: .destructive) { NSApplication.shared.terminate(self) } - }, message: { - Text("Retry the installation or visit the troubleshooting page.") - }) - } - } - - private func dependencyView(title: LocalizedStringKey, description: LocalizedStringKey, progressOrder: InstallPhase) -> some View { - VStack(alignment: .leading) { - HStack { - Text(title) - .font(.system(size: 16, weight: .bold)) - .padding(.trailing, 4) - - if !failed { - if installationProgress.phase.rawValue > progressOrder.rawValue { - installedBadge - } else { - ProgressView() - .controlSize(.small) - } - } else { - Image(systemName: "xmark.circle") - .font(.system(size: 18)) - .foregroundColor(.red) - } - } - .frame(height: 30) - - Text(description) - } - } - - private func installDependencies() async { - // Reset progress - failed = false - installationProgress.phase = .waitingForXcodeCommandLineTools - - do { - try await DependencyManager.install(progressObject: installationProgress) - } catch { - failed = true - } - - if !failed { - self.isDone = true - } - } - - /// A little bagde that says "Installed" - private var installedBadge: some View { - HStack { - Image(systemName: "checkmark") - Text("Installed") - } - .padding(3) - .foregroundColor(.white) - .background(.green) - .cornerRadius(4) - } - } - - /// Page shown when setup is complete - private struct AllSet: View { - @AppStorage(Preferences.setupComplete.rawValue) var setupComplete = false - - var body: some View { - Text("All set!") - .font(.system(size: 52, weight: .bold)) - .padding(.top, 40) - - Button("Start Using \(Bundle.main.appName)") { - setupComplete = true - } - .bigButton(backgroundColor: .accentColor) - } - } -} - -struct SetupView_Previews: PreviewProvider { - static var previews: some View { - SetupView() - .frame(width: 600, height: 400) - } -} diff --git a/Applite/Views/UninstallSelfView.swift b/Applite/Views/UninstallSelfView.swift index f4fed1f..cc60b49 100755 --- a/Applite/Views/UninstallSelfView.swift +++ b/Applite/Views/UninstallSelfView.swift @@ -12,7 +12,7 @@ struct UninstallSelfView: View { @State var deleteBrewCache = false @State var showConfirmation = false - @State var showUninstallFailedAlert = false + @StateObject var uninstallAlert = AlertManager() var body: some View { VStack(alignment: .leading) { @@ -44,13 +44,13 @@ struct UninstallSelfView: View { do { try uninstallSelf(deleteBrewCache: deleteBrewCache) } catch { - showUninstallFailedAlert = true + uninstallAlert.show(title: "Failed to uninstall", message: error.localizedDescription) } } Button("Cancel", role: .cancel) { } } - .alert("Failed to uninstall", isPresented: $showUninstallFailedAlert) {} + .alertManager(uninstallAlert) } } diff --git a/Localizable.xcstrings b/Localizable.xcstrings index acb8409..dbc83bd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -9,13 +9,13 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "" @@ -37,13 +37,13 @@ "value" : ": " } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : ": " } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : ": " @@ -71,13 +71,13 @@ "value" : "?" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "?" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "?" @@ -105,16 +105,16 @@ "value" : "\"%@\" ne correspond à aucune application. Soit il n'est pas disponible dans le catalogue Homebrew, soit vous l'avez mal orthographié." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@は、どのアプリケーションにも一致しません\nHomebrewで入手出来ないかスペルが間違っています" + "value" : "Nincs találat a(z) \"%@\" kifejezésre. Nem elérhető az alkalmazás a Homebrew-n, vagy rosszul írta." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs találat a(z) \"%@\" kifejezésre. Nem elérhető az alkalmazás a Homebrew-n, vagy rosszul írta." + "value" : "%@は、どのアプリケーションにも一致しません\nHomebrewで入手出来ないかスペルが間違っています" } }, "zh-Hans" : { @@ -139,13 +139,13 @@ "value" : "(%@)" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "(%@)" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "(%@)" @@ -173,16 +173,16 @@ "value" : "[Homebrew](https://brew.sh) est un outil de gestion de packages gratuit et open source qui facilite l'installation d'applications tierces. %@ utilise Homebrew sous le capot pour télécharger et gérer des applications." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "[Homebrew](https://brew.sh)は、サードパーティアプリケーションのインストールを簡単にする無料のオープンソースパッケージ管理ツールです。%@は内部でHomebrewを使用してアプリケーションをダウンロードして管理します" + "value" : "Ez az alkalmazás a [Homebrew](https://brew.sh) nevű csomagkezelőt használja az appok letöltéséhez. A Homebrew egy ingyenes és nyílt forráskódú szoftver, ami megkönnyíti az appok letöltését." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez az alkalmazás a [Homebrew](https://brew.sh) nevű csomagkezelőt használja az appok letöltéséhez. A Homebrew egy ingyenes és nyílt forráskódú szoftver, ami megkönnyíti az appok letöltését." + "value" : "[Homebrew](https://brew.sh)は、サードパーティアプリケーションのインストールを簡単にする無料のオープンソースパッケージ管理ツールです。%@は内部でHomebrewを使用してアプリケーションをダウンロードして管理します" } }, "zh-Hans" : { @@ -207,16 +207,16 @@ "value" : "**Attention** : Le cache Homebrew est partagé entre les installations Homebrew. La suppression du cache supprimera le cache pour toutes les installations!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "**警告**: HomebrewキャッシュはそれぞれのHomebrewで共有されます\nキャッシュを削除すると全てのインストールキャッシュが削除されます" + "value" : "**Figyelem:** A Homebrew cache tárhelye közös a telepítések között. Ha törli a cachet, az minden Homebrew telepítésre hatással lesz." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "**Figyelem:** A Homebrew cache tárhelye közös a telepítések között. Ha törli a cachet, az minden Homebrew telepítésre hatással lesz." + "value" : "**警告**: HomebrewキャッシュはそれぞれのHomebrewで共有されます\nキャッシュを削除すると全てのインストールキャッシュが削除されます" } }, "zh-Hans" : { @@ -241,16 +241,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "/path/to/brew" + "value" : "/brew/elérési/útvonal" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "/brew/elérési/útvonal" + "value" : "/path/to/brew" } }, "zh-Hans" : { @@ -275,16 +275,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "/path/to/dir" + "value" : "/elérési/útvonal" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "/elérési/útvonal" + "value" : "/path/to/dir" } }, "zh-Hans" : { @@ -309,13 +309,13 @@ "value" : "%@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "%@" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@" @@ -343,12 +343,6 @@ "value" : "%1$@ is already installed. If you want to add it to %2$@ click more options (chevron icon) and press Force Install." } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@はすでにインストールされています\n%2$@に追加する場合は\nVアイコンをクリックし強制インストールを押します" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -361,6 +355,12 @@ "value" : "A(z) %1$@ már telepítve van. Ha hozzá akarja adni az %2$@-hoz, kattintson a további opciók gombra, és válassza a Kényszerített telepítés opciót." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@はすでにインストールされています\n%2$@に追加する場合は\nVアイコンをクリックし強制インストールを押します" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -376,6 +376,7 @@ } }, "%@ successfully installed!" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -383,16 +384,16 @@ "value" : "%@ installé avec succès!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@は正常にインストールされました" + "value" : "%@ sikeresen telepítve!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ sikeresen telepítve!" + "value" : "%@は正常にインストールされました" } }, "zh-Hans" : { @@ -410,6 +411,7 @@ } }, "%@ successfully reinstalled" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -417,16 +419,16 @@ "value" : "%@ réinstaller avec succès!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@の再インストールに成功しました" + "value" : "%@ sikeresen újratelepítve!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ sikeresen újratelepítve!" + "value" : "%@の再インストールに成功しました" } }, "zh-Hans" : { @@ -444,6 +446,7 @@ } }, "%@ successfully uninstalled" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -451,16 +454,16 @@ "value" : "%@ déinstaller avec succès!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@は正常にアンインストールされました" + "value" : "%@ sikeresen törölve!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ sikeresen törölve!" + "value" : "%@は正常にアンインストールされました" } }, "zh-Hans" : { @@ -478,6 +481,7 @@ } }, "%@ successfully updated" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -485,16 +489,16 @@ "value" : "%@ mis à jour avec succès!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@は正常に更新されました" + "value" : "%@ sikeresen frissítve!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ sikeresen frissítve!" + "value" : "%@は正常に更新されました" } }, "zh-Hans" : { @@ -519,16 +523,16 @@ "value" : "%@ utilise le proxy réseau du système, mais il ne peut utiliser qu'un seul protocole (HTTP, HTTPS, ou SOCKS5). Sélectionnez la méthode que vous préférez." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@はシステムネットワークプロキシを使用しますが、使用できるプロトコルは1つだけです\n(HTTP、HTTPS、SOCKS5)お好みの方法を選択して下さい" + "value" : "Ez az alkalmazás a rendszer hálózati proxy beállításait használja, de csak egy protokollt tud használni (HTTP, HTTPS vagy SOCKS5). Válassza ki a kívánt módszert." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez az alkalmazás a rendszer hálózati proxy beállításait használja, de csak egy protokollt tud használni (HTTP, HTTPS vagy SOCKS5). Válassza ki a kívánt módszert." + "value" : "%@はシステムネットワークプロキシを使用しますが、使用できるプロトコルは1つだけです\n(HTTP、HTTPS、SOCKS5)お好みの方法を選択して下さい" } }, "zh-Hans" : { @@ -553,16 +557,16 @@ "value" : "Une nouvelle installation Homebrew sera installée dans ~/Library/Application Support/%@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "新しいHomebrewのインストール先は ~/Library/Application Support/%@にインストールされます" + "value" : "Egy új Homebrew telepítés fog kerülni a ~/Library/Application Support/%@ elérési helyre" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Egy új Homebrew telepítés fog kerülni a ~/Library/Application Support/%@ elérési helyre" + "value" : "新しいHomebrewのインストール先は ~/Library/Application Support/%@にインストールされます" } }, "zh-Hans" : { @@ -587,16 +591,16 @@ "value" : "À propos de %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@について" + "value" : "Az Applite névjegye" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Az Applite névjegye" + "value" : "%@について" } }, "zh-Hans" : { @@ -621,16 +625,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アクション" + "value" : "Módosítás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Módosítás" + "value" : "アクション" } }, "zh-Hans" : { @@ -655,16 +659,16 @@ "value" : "Tâches Actives" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アクティブなタスク" + "value" : "Aktív feladatok" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Aktív feladatok" + "value" : "アクティブなタスク" } }, "zh-Hans" : { @@ -689,12 +693,6 @@ "value" : "After reinstalling, all currently installed apps will be unlinked from %1$@. They won't be deleted, but you won't be able to update or uninstall them via %2$@." } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "再インストール後、現在インストールされている全てのアプリは %1$@からリンク解除されます\nこれらは削除されませんが、%2$@経由で更新又はアンインストールする事は出来ません" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -707,6 +705,12 @@ "value" : "Az újratelepítés után az összes jelenleg letöltött alkalmazás nem lesz elérhető az %1$@ felületén. Nem törlődnek, de nem lehet majd őket frissíteni vagy letörölni az %2$@-on keresztül." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "再インストール後、現在インストールされている全てのアプリは %1$@からリンク解除されます\nこれらは削除されませんが、%2$@経由で更新又はアンインストールする事は出来ません" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -729,16 +733,16 @@ "value" : "Toutes les applications actuellement installées seront dissociées de %@." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "現在インストールされている全てのアプリケーションは %@からリンク解除されます" + "value" : "Minden jelenleg letöltött alkalmazás nem lesz elérhető az %@-on keresztül." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Minden jelenleg letöltött alkalmazás nem lesz elérhető az %@-on keresztül." + "value" : "現在インストールされている全てのアプリケーションは %@からリンク解除されます" } }, "zh-Hans" : { @@ -763,16 +767,16 @@ "value" : "Toutes les autres fonctions de l'application seront désactivées pendant la mise à jour!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデート中はアプリの他の機能が全て無効になります" + "value" : "A frissítés közben az alkalmazás minden más funkciója nem lesz elérhető!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A frissítés közben az alkalmazás minden más funkciója nem lesz elérhető!" + "value" : "アップデート中はアプリの他の機能が全て無効になります" } }, "zh-Hans" : { @@ -797,16 +801,16 @@ "value" : "Tout est prêt!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "全ての準備完了" + "value" : "Minden készen áll!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Minden készen áll!" + "value" : "全ての準備完了" } }, "zh-Hans" : { @@ -831,16 +835,16 @@ "value" : "Mises en garde concernant l'application" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "使用上の注意" + "value" : "Az alkalmazással kapcsolatos fenntartások" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Az alkalmazással kapcsolatos fenntartások" + "value" : "使用上の注意" } }, "zh-Hans" : { @@ -865,62 +869,28 @@ "value" : "L'application est introuvable" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "アプリケーションが見つかりません" - } - }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "Nem található elérési útvonal az alkalmazáshoz" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法找到应用程序" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無法找到應用程式" - } - } - } - }, - "App load error" : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erreur de chargement de l'application" - } - }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "アプリケーション読み込みエラー" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alkalmazás betöltési hiba" + "value" : "アプリケーションが見つかりません" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "应用程序加载错误" + "value" : "无法找到应用程序" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "應用程式載入錯誤" + "value" : "無法找到應用程式" } } } @@ -933,16 +903,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリケーションディレクトリ" + "value" : "Appdir" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Appdir" + "value" : "アプリケーションディレクトリ" } }, "zh-Hans" : { @@ -967,16 +937,16 @@ "value" : "Appdir (Optionnel)" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリケーションディレクトリ(オプション)" + "value" : "Appdir (nem kötelező)" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Appdir (nem kötelező)" + "value" : "アプリケーションディレクトリ(オプション)" } }, "zh-Hans" : { @@ -1001,16 +971,16 @@ "value" : "Apparence" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "外観" + "value" : "Megjelenés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Megjelenés" + "value" : "外観" } }, "zh-Hans" : { @@ -1035,13 +1005,13 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "Applite" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "Applite" @@ -1069,18 +1039,18 @@ "value" : "Les applications avec peu de téléchargements sont masquées, pensez à désactiver ce filtre" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ダウンロードの少ないアプリケーションは非表示になります。このフィルターを無効にする事を検討して下さい" - } - }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "A kevés letöltéssel rendelkező alkalmazások el vannak rejtve. Fontolja meg ennek a szűrőnek a kikapcsolását" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ダウンロードの少ないアプリケーションは非表示になります。このフィルターを無効にする事を検討して下さい" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1103,16 +1073,16 @@ "value" : "Voulez-vous vraiment %@installer Homebrew?" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewを%@installしてもよろしいですか?" + "value" : "Biztos telepíteni szeretné a Homebrew-t?" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Biztos telepíteni szeretné a Homebrew-t?" + "value" : "Homebrewを%@installしてもよろしいですか?" } }, "zh-Hans" : { @@ -1137,16 +1107,16 @@ "value" : "Voulez-vous vraiment forcer l'installation de %@ ? Cela remplacera toute installation actuelle!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@の強制インストールをしてよろしいですか? これにより現在のインストール先が置き換えられます" + "value" : "Biztos telepíteni szeretné a(z) %@ alkalmazást? A jelenlegi telepítés felül lesz írva." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Biztos telepíteni szeretné a(z) %@ alkalmazást? A jelenlegi telepítés felül lesz írva." + "value" : "%@の強制インストールをしてよろしいですか? これにより現在のインストール先が置き換えられます" } }, "zh-Hans" : { @@ -1171,16 +1141,16 @@ "value" : "Êtes-vous sûr de vouloir désinstaller définitivement %@?" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@を完全にアンインストールしてもよろしいですか?" + "value" : "Biztos véglegesen törli az %@-tot?" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Biztos véglegesen törli az %@-tot?" + "value" : "%@を完全にアンインストールしてもよろしいですか?" } }, "zh-Hans" : { @@ -1205,16 +1175,16 @@ "value" : "Rechercher automatiquement des mises à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデートを自動的にチェックする" + "value" : "Frissítések automatikus ellenőrzése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Frissítések automatikus ellenőrzése" + "value" : "アップデートを自動的にチェックする" } }, "zh-Hans" : { @@ -1239,16 +1209,16 @@ "value" : "Télécharger automatiquement les mises à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデートを自動的にダウンロードする" + "value" : "Frissítések automatikus letöltése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Frissítések automatikus letöltése" + "value" : "アップデートを自動的にダウンロードする" } }, "zh-Hans" : { @@ -1273,16 +1243,16 @@ "value" : "Retour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "戻る" + "value" : "Vissza" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Vissza" + "value" : "戻る" } }, "zh-Hans" : { @@ -1307,16 +1277,16 @@ "value" : "Chemin d'accès de l'exécutable Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Brewの実行可能パス" + "value" : "Brew futtatható állomány (executable) elérési útja" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Brew futtatható állomány (executable) elérési útja" + "value" : "Brewの実行可能パス" } }, "zh-Hans" : { @@ -1341,16 +1311,16 @@ "value" : "Chemin d'accès" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Brewのパス" + "value" : "Brew elérési útja" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Brew elérési útja" + "value" : "Brewのパス" } }, "zh-Hans" : { @@ -1375,16 +1345,16 @@ "value" : "Le chemin d'accès à Homebrew a été modifié. Redémarrez l'application pour que les modifications prennent effet." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewのパスが変更されました\n変更を有効にするにはAppliteを再起動します" + "value" : "A Brew elérési útja megváltozott. Indítsa újra az alkalmazást, hogy a változtatás érvénybe lépjen." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A Brew elérési útja megváltozott. Indítsa újra az alkalmazást, hogy a változtatás érvénybe lépjen." + "value" : "Homebrewのパスが変更されました\n変更を有効にするにはAppliteを再起動します" } }, "zh-Hans" : { @@ -1409,16 +1379,16 @@ "value" : "Le chemin d'accès à Homebrew est cassé" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Brewのパスが見つかりません" + "value" : "Brew elérési útvonal hiba" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Brew elérési útvonal hiba" + "value" : "Brewのパスが見つかりません" } }, "zh-Hans" : { @@ -1443,16 +1413,16 @@ "value" : "Parcourir" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ブラウズ" + "value" : "Tallózás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Tallózás" + "value" : "ブラウズ" } }, "zh-Hans" : { @@ -1478,16 +1448,16 @@ "value" : "Parcourez une liste triée sur le volet d'applications géniales." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "厳選された素晴らしいアプリのリストをご覧下さい" + "value" : "Válogasson egy gondosan kiválogatott alkalmazás listából" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Válogasson egy gondosan kiválogatott alkalmazás listából" + "value" : "厳選された素晴らしいアプリのリストをご覧下さい" } }, "zh-Hans" : { @@ -1513,16 +1483,16 @@ "value" : "Navigateurs" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ブラウザ" + "value" : "Böngészők" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Böngészők" + "value" : "ブラウザ" } }, "zh-Hans" : { @@ -1547,16 +1517,16 @@ "value" : "Annuler" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "キャンセル" + "value" : "Mégse" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Mégse" + "value" : "キャンセル" } }, "zh-Hans" : { @@ -1582,16 +1552,16 @@ "value" : "Liste Cask (fichier .txt)" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Caskリスト(.txtファイル)" + "value" : "Cask lista (.txt fájl)" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Cask lista (.txt fájl)" + "value" : "Caskリスト(.txtファイル)" } }, "zh-Hans" : { @@ -1616,16 +1586,16 @@ "value" : "Catégories" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "カテゴリー" + "value" : "Kategóriák" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kategóriák" + "value" : "カテゴリー" } }, "zh-Hans" : { @@ -1650,16 +1620,16 @@ "value" : "Rechercher des mises à jour..." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデートをチェック..." + "value" : "Frissítések keresése..." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Frissítések keresése..." + "value" : "アップデートをチェック..." } }, "zh-Hans" : { @@ -1684,16 +1654,16 @@ "value" : "Thème:" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "テーマ:" + "value" : "Színséma:" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Színséma:" + "value" : "テーマ:" } }, "zh-Hans" : { @@ -1719,16 +1689,16 @@ "value" : "Communications" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "コミュニケーション" + "value" : "Kommunikáció" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kommunikáció" + "value" : "コミュニケーション" } }, "zh-Hans" : { @@ -1753,16 +1723,16 @@ "value" : "Continuer" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "続く" + "value" : "Tovább" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Tovább" + "value" : "続く" } }, "zh-Hans" : { @@ -1787,16 +1757,16 @@ "value" : "Impossible de télécharger l'application. Pas de connexion Internet ou l'hôte est inaccessible." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリケーションをダウンロード出来ません。インターネット接続が無いかホストにアクセス出来ません" + "value" : "Alkalmazás letöltése sikertelen. Nincs internetkapcsolat, vagy a forrás nem elérhető." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Alkalmazás letöltése sikertelen. Nincs internetkapcsolat, vagy a forrás nem elérhető." + "value" : "アプリケーションをダウンロード出来ません。インターネット接続が無いかホストにアクセス出来ません" } }, "zh-Hans" : { @@ -1822,16 +1792,16 @@ "value" : "Outils de Création" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "作成ツール" + "value" : "Digitális alkotás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Digitális alkotás" + "value" : "作成ツール" } }, "zh-Hans" : { @@ -1856,12 +1826,6 @@ "value" : "Current app version: %1$@ (%2$@)" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "現在のアプリのバージョン : %1$@ (%2$@)" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -1874,6 +1838,12 @@ "value" : "Jelenlegi verzió: %1$@ (%2$@)" } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "現在のアプリのバージョン : %1$@ (%2$@)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1896,62 +1866,28 @@ "value" : "Chemin d'accès actuel à Homebrew n'est pas valide" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Homebrewの現在のパスが無効です" - } - }, "hu" : { "stringUnit" : { "state" : "translated", "value" : "A kiválasztott elérési útvonal helytelen" } }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "当前选择的 Brew 路径无效" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "目前選擇的沖泡路徑無效" - } - } - } - }, - "Custom" : { - "localizations" : { - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Personnalisé" - } - }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "カスタム" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saját" + "value" : "Homebrewの現在のパスが無効です" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "自定义" + "value" : "当前选择的 Brew 路径无效" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "自訂" + "value" : "目前選擇的沖泡路徑無效" } } } @@ -1964,16 +1900,16 @@ "value" : "Chemin d'accès à Homebrew personnalisé" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Brewのカスタムパス" + "value" : "Saját brew elérési útvonal" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Saját brew elérési útvonal" + "value" : "Brewのカスタムパス" } }, "zh-Hans" : { @@ -1998,16 +1934,16 @@ "value" : "Répertoire d'installation personnalisé" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "カスタムインストールディレクトリ" + "value" : "Saját telepítési mappa" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Saját telepítési mappa" + "value" : "カスタムインストールディレクトリ" } }, "zh-Hans" : { @@ -2033,16 +1969,16 @@ "value" : "Sombre" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ダーク" + "value" : "Sötét" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Sötét" + "value" : "ダーク" } }, "zh-Hans" : { @@ -2067,16 +2003,16 @@ "value" : "Supprimer le cache de Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewのキャッシュを削除する" + "value" : "Homebrew cache törlése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrew cache törlése" + "value" : "Homebrewのキャッシュを削除する" } }, "zh-Hans" : { @@ -2102,16 +2038,16 @@ "value" : "Outils de Développement" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "開発ツール" + "value" : "Fejlesztői eszközök" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Fejlesztői eszközök" + "value" : "開発ツール" } }, "zh-Hans" : { @@ -2136,16 +2072,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Discord" + "value" : "" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "" + "value" : "Discord" } }, "zh-Hans" : { @@ -2170,16 +2106,16 @@ "value" : "Découvrir" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリを探す" + "value" : "Letöltés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Letöltés" + "value" : "アプリを探す" } }, "zh-Hans" : { @@ -2204,16 +2140,16 @@ "value" : "Avez-vous déjà installé Homebrew ?" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewはインストール済みですか?" + "value" : "Ön már rendelkezik a Homebrew csomagkezelővel?" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ön már rendelkezik a Homebrew csomagkezelővel?" + "value" : "Homebrewはインストール済みですか?" } }, "zh-Hans" : { @@ -2238,16 +2174,16 @@ "value" : "Télécharger quand même" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "強制ダウンロード" + "value" : "Telepítés mindenképp" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Telepítés mindenképp" + "value" : "強制ダウンロード" } }, "zh-Hans" : { @@ -2273,16 +2209,16 @@ "value" : "Téléchargez des applications en toute simplicité" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリを簡単にダウンロード" + "value" : "Alkalmazások letöltése könnyedén" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Alkalmazások letöltése könnyedén" + "value" : "アプリを簡単にダウンロード" } }, "zh-Hans" : { @@ -2308,16 +2244,16 @@ "value" : "Téléchargez des applications tierces en un seul clic. Plus besoin de \"Glisser dans le dossier Applications\"." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ワンクリックでサードパーティのアプリをダウンロード出来ます\nアプリケーションフォルダーにドラッグする必要はありません" + "value" : "Harmadik féltől származó alkalmazások letöltése egyetlen kattintással. Nincs többé \"Húzd az applikációk mappába\"." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Harmadik féltől származó alkalmazások letöltése egyetlen kattintással. Nincs többé \"Húzd az applikációk mappába\"." + "value" : "ワンクリックでサードパーティのアプリをダウンロード出来ます\nアプリケーションフォルダーにドラッグする必要はありません" } }, "zh-Hans" : { @@ -2342,16 +2278,16 @@ "value" : "Erreur" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "エラー" + "value" : "Hiba" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hiba" + "value" : "エラー" } }, "zh-Hans" : { @@ -2376,16 +2312,16 @@ "value" : "Exportez un fichier contenant toutes les applications actuellement installées. Le fichier peut être importé sur un autre appareil." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "現在インストールされている全てのアプリケーションを含むファイルをエクスポートします。ファイルは別のデバイスにインポート出来ます" + "value" : "Az összes jelenleg telepített alkalmazást tartalmazó fájl exportálása. Ez importálható egy másik eszközre." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Az összes jelenleg telepített alkalmazást tartalmazó fájl exportálása. Ez importálható egy másik eszközre." + "value" : "現在インストールされている全てのアプリケーションを含むファイルをエクスポートします。ファイルは別のデバイスにインポート出来ます" } }, "zh-Hans" : { @@ -2410,16 +2346,16 @@ "value" : "Exporter les applications vers un fichier" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリをファイルにエクスポートする" + "value" : "Alakalmazások exportálása fájlba" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Alakalmazások exportálása fájlba" + "value" : "アプリをファイルにエクスポートする" } }, "zh-Hans" : { @@ -2444,16 +2380,16 @@ "value" : "Echec de l'export" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "エクスポートに失敗しました" + "value" : "Exportálás sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Exportálás sikertelen" + "value" : "エクスポートに失敗しました" } }, "zh-Hans" : { @@ -2478,16 +2414,16 @@ "value" : "Type de fichier d'exportation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "エクスポートファイルの種類" + "value" : "Exportált fálj formátum" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Exportált fálj formátum" + "value" : "エクスポートファイルの種類" } }, "zh-Hans" : { @@ -2505,6 +2441,7 @@ } }, "Failed to download %@" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -2512,16 +2449,16 @@ "value" : "Echec du téléchargement de %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@のダウンロードに失敗しました" + "value" : "%@ letöltése sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ letöltése sikertelen" + "value" : "%@のダウンロードに失敗しました" } }, "zh-Hans" : { @@ -2537,11 +2474,9 @@ } } } - }, - "Failed to load outdated apps" : { - }, "Failed to reinstall %@" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -2549,16 +2484,16 @@ "value" : "Echec de la réinstallation de %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@の再インストールに失敗しました" + "value" : "Újratelepítés sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Újratelepítés sikertelen" + "value" : "%@の再インストールに失敗しました" } }, "zh-Hans" : { @@ -2574,11 +2509,9 @@ } } } - }, - "Failed to uninstall" : { - }, "Failed to update %@" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -2586,16 +2519,16 @@ "value" : "Echec de la mise à jour de %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@を更新出来ませんでした" + "value" : "%@ letöltése sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ letöltése sikertelen" + "value" : "%@を更新出来ませんでした" } }, "zh-Hans" : { @@ -2620,16 +2553,16 @@ "value" : "Forcer l'installation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "強制インストール" + "value" : "Kényszerített telepítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kényszerített telepítés" + "value" : "強制インストール" } }, "zh-Hans" : { @@ -2654,16 +2587,16 @@ "value" : "Général" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "一般" + "value" : "Általános" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Általános" + "value" : "一般" } }, "zh-Hans" : { @@ -2688,13 +2621,13 @@ "value" : "GitHub" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "GitHub" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "GitHub" @@ -2722,16 +2655,16 @@ "value" : "Masquer les applications avec peu de téléchargements" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ダウンロード数が少ないアプリを非表示にする" + "value" : "Kevés letöltéssel rendelkező alkalmazások elrejtése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kevés letöltéssel rendelkező alkalmazások elrejtése" + "value" : "ダウンロード数が少ないアプリを非表示にする" } }, "zh-Hans" : { @@ -2756,13 +2689,13 @@ "value" : "Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" @@ -2790,16 +2723,16 @@ "value" : "Version Homebrew: " } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewのバージョン : " + "value" : "Homebrew verzió: " } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrew verzió: " + "value" : "Homebrewのバージョン : " } }, "zh-Hans" : { @@ -2824,16 +2757,16 @@ "value" : "Page d'accueil" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ホームページ" + "value" : "Honlap" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Honlap" + "value" : "ホームページ" } }, "zh-Hans" : { @@ -2859,16 +2792,16 @@ "value" : "IDEs" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "IDEs" + "value" : "IDE-k" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "IDE-k" + "value" : "IDEs" } }, "zh-Hans" : { @@ -2893,16 +2826,16 @@ "value" : "Importer des applications" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリをインポートする" + "value" : "Alkalmazások importálása" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Alkalmazások importálása" + "value" : "アプリをインポートする" } }, "zh-Hans" : { @@ -2927,16 +2860,16 @@ "value" : "Echec de l'importation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インポートに失敗しました" + "value" : "Importálás sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Importálás sikertelen" + "value" : "インポートに失敗しました" } }, "zh-Hans" : { @@ -2961,16 +2894,16 @@ "value" : "Importer/Exporter des applications" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリのインポート/エクスポート" + "value" : "Alkalmazások importálása/exportálása" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Alkalmazások importálása/exportálása" + "value" : "アプリのインポート/エクスポート" } }, "zh-Hans" : { @@ -2995,16 +2928,16 @@ "value" : "En cours..." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "進行中..." + "value" : "Folyamatban..." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Folyamatban..." + "value" : "進行中..." } }, "zh-Hans" : { @@ -3029,16 +2962,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "情報" + "value" : "Infó" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Infó" + "value" : "情報" } }, "zh-Hans" : { @@ -3063,16 +2996,16 @@ "value" : "Installer un Homebrew séparé" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "別のHomebrewをインストールする" + "value" : "Külön brew telepítése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Külön brew telepítése" + "value" : "別のHomebrewをインストールする" } }, "zh-Hans" : { @@ -3097,16 +3030,16 @@ "value" : "Echec de l'installation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インストールに失敗しました" + "value" : "Telepítés sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Telepítés sikertelen" + "value" : "インストールに失敗しました" } }, "zh-Hans" : { @@ -3131,16 +3064,16 @@ "value" : "Installé" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インストール済み" + "value" : "Telepítve" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Telepítve" + "value" : "インストール済み" } }, "zh-Hans" : { @@ -3166,16 +3099,16 @@ "value" : "Installation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インストール" + "value" : "Telepítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Telepítés" + "value" : "インストール" } }, "zh-Hans" : { @@ -3200,16 +3133,16 @@ "value" : "Installation des dépendances" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "依存関係のインストール" + "value" : "Tartozékok telepítése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Tartozékok telepítése" + "value" : "依存関係のインストール" } }, "zh-Hans" : { @@ -3235,16 +3168,16 @@ "value" : "Clair" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ライト" + "value" : "Világos" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Világos" + "value" : "ライト" } }, "zh-Hans" : { @@ -3270,16 +3203,16 @@ "value" : "Gérer vos applications" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリを管理" + "value" : "Alkalmazások kezelése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Alkalmazások kezelése" + "value" : "アプリを管理" } }, "zh-Hans" : { @@ -3304,16 +3237,16 @@ "value" : "Gérer Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewの管理" + "value" : "A Homebrew kezelése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A Homebrew kezelése" + "value" : "Homebrewの管理" } }, "zh-Hans" : { @@ -3339,16 +3272,16 @@ "value" : "Média" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "メディア" + "value" : "Média" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Média" + "value" : "メディア" } }, "zh-Hans" : { @@ -3374,16 +3307,16 @@ "value" : "Barre des Menus" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "メニューバー" + "value" : "Menüsáv" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Menüsáv" + "value" : "メニューバー" } }, "zh-Hans" : { @@ -3409,16 +3342,16 @@ "value" : "Plus téléchergé (par défaut)" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "最もダウンロードされたCask(デフォルト)" + "value" : "Legtöbb letöltés (alapértelmezett)" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Legtöbb letöltés (alapértelmezett)" + "value" : "最もダウンロードされたCask(デフォルト)" } }, "zh-Hans" : { @@ -3443,16 +3376,16 @@ "value" : "Non (je ne sais pas ce que c'est)" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "いいえ(それが何なのか分かりません)" + "value" : "Nem (nem tudom mi az)" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nem (nem tudom mi az)" + "value" : "いいえ(それが何なのか分かりません)" } }, "zh-Hans" : { @@ -3477,16 +3410,16 @@ "value" : "Aucune tâches actives" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アクティブなタスクはありません" + "value" : "Nincs aktív feladat" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs aktív feladat" + "value" : "アクティブなタスクはありません" } }, "zh-Hans" : { @@ -3511,16 +3444,16 @@ "value" : "Aucune page d'accueil trouvée" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ホームページが見つかりませんでした" + "value" : "Nem található honlap" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nem található honlap" + "value" : "ホームページが見つかりませんでした" } }, "zh-Hans" : { @@ -3545,16 +3478,16 @@ "value" : "Aucune sélection" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "選択無し" + "value" : "Nincs kiválasztás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs kiválasztás" + "value" : "選択無し" } }, "zh-Hans" : { @@ -3579,16 +3512,16 @@ "value" : "Aucune mise à jour disponible" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "利用可能なアップデートはありません" + "value" : "Nincs elérhető frissítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Nincs elérhető frissítés" + "value" : "利用可能なアップデートはありません" } }, "zh-Hans" : { @@ -3614,16 +3547,16 @@ "value" : "Remarque" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "備考" + "value" : "Megjegyzés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Megjegyzés" + "value" : "備考" } }, "zh-Hans" : { @@ -3648,16 +3581,16 @@ "value" : "Attention" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "注意" + "value" : "Figyelem!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Figyelem!" + "value" : "注意" } }, "zh-Hans" : { @@ -3682,16 +3615,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "通知" + "value" : "Értesítések" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Értesítések" + "value" : "通知" } }, "zh-Hans" : { @@ -3716,16 +3649,16 @@ "value" : "Nombre d'applications installées:" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インストールされているアプリケーションの数 : " + "value" : "Letöltött alkalmazások száma: " } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Letöltött alkalmazások száma: " + "value" : "インストールされているアプリケーションの数 : " } }, "zh-Hans" : { @@ -3751,16 +3684,16 @@ "value" : "Outils Bureautiques" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "オフィスツール" + "value" : "Irodai eszközök" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Irodai eszközök" + "value" : "オフィスツール" } }, "zh-Hans" : { @@ -3785,16 +3718,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Ok" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "OK" } }, "zh-Hans" : { @@ -3819,16 +3752,16 @@ "value" : "Ouvrir" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "開く" + "value" : "Megnyitás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Megnyitás" + "value" : "開く" } }, "zh-Hans" : { @@ -3854,16 +3787,16 @@ "value" : "Gestionnaires de Mots de Passe" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "パスワードマネージャー" + "value" : "Jelszókezelők" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Jelszókezelők" + "value" : "パスワードマネージャー" } }, "zh-Hans" : { @@ -3888,16 +3821,16 @@ "value" : "Protocole proxy préféré" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "優先プロキシプロトコル" + "value" : "Preferált proxy protokoll" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Preferált proxy protokoll" + "value" : "優先プロキシプロトコル" } }, "zh-Hans" : { @@ -3923,16 +3856,16 @@ "value" : "Productivité" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "生産性" + "value" : "Hatékonyság" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hatékonyság" + "value" : "生産性" } }, "zh-Hans" : { @@ -3957,16 +3890,16 @@ "value" : "Donner le chemin d'accès de Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewの実行パスを指定して下さい" + "value" : "Adja meg a brew elérési útvonalát" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Adja meg a brew elérési útvonalát" + "value" : "Homebrewの実行パスを指定して下さい" } }, "zh-Hans" : { @@ -3991,16 +3924,16 @@ "value" : "Proxy" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "プロキシ" + "value" : "Proxy" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Proxy" + "value" : "プロキシ" } }, "zh-Hans" : { @@ -4025,16 +3958,16 @@ "value" : "Quitter" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "終了" + "value" : "Kilépés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kilépés" + "value" : "終了" } }, "zh-Hans" : { @@ -4059,16 +3992,16 @@ "value" : "Réinstaller" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "再インストール" + "value" : "Újratelepítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Újratelepítés" + "value" : "再インストール" } }, "zh-Hans" : { @@ -4093,16 +4026,16 @@ "value" : "Echec de la réinstallation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "再インストールに失敗しました" + "value" : "Újratelepítés sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Újratelepítés sikertelen" + "value" : "再インストールに失敗しました" } }, "zh-Hans" : { @@ -4127,16 +4060,16 @@ "value" : "Réinstaller Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewを再インストールする" + "value" : "Homebrew újratelepítése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrew újratelepítése" + "value" : "Homebrewを再インストールする" } }, "zh-Hans" : { @@ -4162,16 +4095,16 @@ "value" : "Réinstallation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "再インストール" + "value" : "Újratelepítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Újratelepítés" + "value" : "再インストール" } }, "zh-Hans" : { @@ -4196,16 +4129,16 @@ "value" : "Relancer" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "再起動" + "value" : "Újraindítás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Újraindítás" + "value" : "再起動" } }, "zh-Hans" : { @@ -4230,16 +4163,16 @@ "value" : "Réessayer" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "もう一度やり直して下さい" + "value" : "Próbáld újra" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Próbáld újra" + "value" : "もう一度やり直して下さい" } }, "zh-Hans" : { @@ -4264,16 +4197,16 @@ "value" : "Retry Install" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インストールを再試行する" + "value" : "Próbálja meg újra a telepítést" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Próbálja meg újra a telepítést" + "value" : "インストールを再試行する" } }, "zh-Hans" : { @@ -4298,16 +4231,16 @@ "value" : "Réessayez de charger" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "もう一度読み込んで下さい" + "value" : "Próbálja meg újra a betöltést" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Próbálja meg újra a betöltést" + "value" : "もう一度読み込んで下さい" } }, "zh-Hans" : { @@ -4332,16 +4265,16 @@ "value" : "Réessayez l'installation ou visitez la page de dépannage." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "インストールを再試行するかトラブルシューティングページにアクセスして下さい" + "value" : "Próbálja meg újra a telepítést, vagy látogasson el a hibaelhárítási oldalra." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Próbálja meg újra a telepítést, vagy látogasson el a hibaelhárítási oldalra." + "value" : "インストールを再試行するかトラブルシューティングページにアクセスして下さい" } }, "zh-Hans" : { @@ -4366,16 +4299,16 @@ "value" : "Voir tout" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "全て表示" + "value" : "Összes" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Összes" + "value" : "全て表示" } }, "zh-Hans" : { @@ -4400,16 +4333,16 @@ "value" : "Sélectionner le dossier" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "フォルダーを選択" + "value" : "Mappa tallózása" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Mappa tallózása" + "value" : "フォルダーを選択" } }, "zh-Hans" : { @@ -4434,16 +4367,16 @@ "value" : "Le chemin d'accès de Homebrew n'est pas valide!" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Brewのパスが無効です" + "value" : "A kiválasztott brew elérési útvonal helytelen!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A kiválasztott brew elérési útvonal helytelen!" + "value" : "Brewのパスが無効です" } }, "zh-Hans" : { @@ -4468,16 +4401,16 @@ "value" : "" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "シェルの出力" + "value" : "Shell kimenete" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Shell kimenete" + "value" : "シェルの出力" } }, "zh-Hans" : { @@ -4502,16 +4435,16 @@ "value" : "Afficher tout" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "全て表示" + "value" : "Mutasd mindet" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Mutasd mindet" + "value" : "全て表示" } }, "zh-Hans" : { @@ -4536,16 +4469,16 @@ "value" : "Afficher toutes les mises à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "全てのアップデートを表示" + "value" : "Összes frissítés megjelenítése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Összes frissítés megjelenítése" + "value" : "全てのアップデートを表示" } }, "zh-Hans" : { @@ -4570,16 +4503,16 @@ "value" : "Trier par" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "並べ替え" + "value" : "Rendezés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Rendezés" + "value" : "並べ替え" } }, "zh-Hans" : { @@ -4604,16 +4537,16 @@ "value" : "Trier" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "並べ替えオプション" + "value" : "Rendezési opciók" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Rendezési opciók" + "value" : "並べ替えオプション" } }, "zh-Hans" : { @@ -4638,16 +4571,16 @@ "value" : "Commence à utiliser %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@の使用を開始する" + "value" : "Kezdjünk bele!" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kezdjünk bele!" + "value" : "%@の使用を開始する" } }, "zh-Hans" : { @@ -4673,16 +4606,16 @@ "value" : "Système" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "システム" + "value" : "Rendszer" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Rendszer" + "value" : "システム" } }, "zh-Hans" : { @@ -4707,16 +4640,16 @@ "value" : "Tâches terminées" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "完了したタスク" + "value" : "Sikeres akció" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Sikeres akció" + "value" : "完了したタスク" } }, "zh-Hans" : { @@ -4741,16 +4674,16 @@ "value" : "Erreurs de tâche" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "タスクエラー" + "value" : "Sikertelen akció" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Sikertelen akció" + "value" : "タスクエラー" } }, "zh-Hans" : { @@ -4776,16 +4709,16 @@ "value" : "Terminals" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ターミナル" + "value" : "Terminálok" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Terminálok" + "value" : "ターミナル" } }, "zh-Hans" : { @@ -4810,16 +4743,16 @@ "value" : "Cette app" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "このアプリ" + "value" : "Ez az alakalmazás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez az alakalmazás" + "value" : "このアプリ" } }, "zh-Hans" : { @@ -4844,16 +4777,16 @@ "value" : "Cette application utilise le gestionnaire de packages [Homebrew](https://brew.sh/) (brew en abrégé) pour télécharger des applications. Homebrew est un utilitaire de ligne de commande gratuit et open source qui peut télécharger des outils de développement utiles ainsi que des applications de bureau." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "このアプリケーションは[Homebrew](https://brew.sh/)パッケージ マネージャー(略してBrew)を使用してアプリケーションをダウンロードします\nHomebrewは便利な開発ツールやデスクトップアプリケーションをダウンロード出来る無料のオープンソースコマンドラインユーティリティです" + "value" : "Ez az alkalmazás a [Homebrew](https://brew.sh/) (röviden brew) csomagkezelőt használja az alkalmazások letöltéséhez. A Homebrew egy ingyenes és nyílt forráskódú parancssoros segédprogram, amellyel hasznos fejlesztői eszközöket, valamint asztali alkalmazásokat tölthetünk le." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez az alkalmazás a [Homebrew](https://brew.sh/) (röviden brew) csomagkezelőt használja az alkalmazások letöltéséhez. A Homebrew egy ingyenes és nyílt forráskódú parancssoros segédprogram, amellyel hasznos fejlesztői eszközöket, valamint asztali alkalmazásokat tölthetünk le." + "value" : "このアプリケーションは[Homebrew](https://brew.sh/)パッケージ マネージャー(略してBrew)を使用してアプリケーションをダウンロードします\nHomebrewは便利な開発ツールやデスクトップアプリケーションをダウンロード出来る無料のオープンソースコマンドラインユーティリティです" } }, "zh-Hans" : { @@ -4878,16 +4811,16 @@ "value" : "Cette application utilise le gestionnaire de packages gratuit et open source [Homebrew](https://brew.sh/) pour télécharger et gérer les applications. Si vous l'avez déjà installé sur votre système, vous pouvez l'utiliser immédiatement. Si vous n'avez pas installé Homebrew ou si vous ne savez pas ce que c'est, sélectionnez **Non**. Cela créera une nouvelle installation de Homebrew juste pour %@." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "このアプリケーションは無料のオープンソースパッケージマネージャー[Homebrew](https://brew.sh/)を使用してアプリケーションのダウンロードと管理を行います。すでにシステムにインストールされている場合はすぐに使用できます。Homebrewがインストールされていない場合、又はHomebrewが何であるかわからない場合は、**いいえ** を選択して下さい。これにより %@専用のHomebrewの新しいインストール先が作成されます" + "value" : "Ez az alkalmazás az ingyenes és nyílt forráskódú [Homebrew](https://brew.sh/) csomagkezelőt használja az alkalmazások letöltésére és kezelésére. Ha már telepítve van a számítógépére, használhatja azt. Ha nincs telepítve a brew, vagy nem tudja, mi az, válassza a **Nem** lehetőséget. Ez egy új brew telepítést hoz létre csak az %@ számára." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez az alkalmazás az ingyenes és nyílt forráskódú [Homebrew](https://brew.sh/) csomagkezelőt használja az alkalmazások letöltésére és kezelésére. Ha már telepítve van a számítógépére, használhatja azt. Ha nincs telepítve a brew, vagy nem tudja, mi az, válassza a **Nem** lehetőséget. Ez egy új brew telepítést hoz létre csak az %@ számára." + "value" : "このアプリケーションは無料のオープンソースパッケージマネージャー[Homebrew](https://brew.sh/)を使用してアプリケーションのダウンロードと管理を行います。すでにシステムにインストールされている場合はすぐに使用できます。Homebrewがインストールされていない場合、又はHomebrewが何であるかわからない場合は、**いいえ** を選択して下さい。これにより %@専用のHomebrewの新しいインストール先が作成されます" } }, "zh-Hans" : { @@ -4912,12 +4845,6 @@ "value" : "This will (re)install %1$@'s Homebrew installation at: ~/Library/Application Support/%2$@/homebrew" } }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "これにより、%1$@のHomebrewのインストール先が\n~/Library/Application Support/%2$@/homebrew に(再)インストールされます" - } - }, "fr" : { "stringUnit" : { "state" : "translated", @@ -4930,6 +4857,12 @@ "value" : "Ez a ~/Library/Application Support/%2$@/homebrew elérési útvonalon található Homebrew telepítést fogja (újra)telepíteni." } }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "これにより、%1$@のHomebrewのインストール先が\n~/Library/Application Support/%2$@/homebrew に(再)インストールされます" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4952,16 +4885,16 @@ "value" : "Cette option affiche les mises à jour des applications dont la mise à jour automatique est désactivée, c'est-à-dire les applications qui s'occupent elles-mêmes de leurs mises à jour." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "このオプションは自動更新が無効になっているアプリケーション、つまり独自の更新を処理するアプリケーションの更新を表示します" + "value" : "Ez olyan alkalmazások frissítéseit jeleníti meg, amelyeknél az automatikus frissítés ki van kapcsolva, azaz amelyek maguk gondoskodnak a saját frissítéseikről." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez olyan alkalmazások frissítéseit jeleníti meg, amelyeknél az automatikus frissítés ki van kapcsolva, azaz amelyek maguk gondoskodnak a saját frissítéseikről." + "value" : "このオプションは自動更新が無効になっているアプリケーション、つまり独自の更新を処理するアプリケーションの更新を表示します" } }, "zh-Hans" : { @@ -4986,16 +4919,16 @@ "value" : "Cela désinstallera tous les fichiers ainsi que le cache associés à %@." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "これにより%@に関連付けられた全てのファイルとキャッシュがアンインストールされます" + "value" : "Ez az %@-al kapcsolatos összes fájlt és cachet törölni fogja." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ez az %@-al kapcsolatos összes fájlt és cachet törölni fogja." + "value" : "これにより%@に関連付けられた全てのファイルとキャッシュがアンインストールされます" } }, "zh-Hans" : { @@ -5020,16 +4953,16 @@ "value" : "Dépannage" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "トラブルシューティング" + "value" : "Hibaelhárítás" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hibaelhárítás" + "value" : "トラブルシューティング" } }, "zh-Hans" : { @@ -5054,16 +4987,16 @@ "value" : "Désactiver le filtre téléchargements minoritaire" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "少数のダウンロードフィルターを無効にする" + "value" : "Kevés letöltés szűrő kikapcsolása" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Kevés letöltés szűrő kikapcsolása" + "value" : "少数のダウンロードフィルターを無効にする" } }, "zh-Hans" : { @@ -5088,16 +5021,16 @@ "value" : "Désinstaller" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アンインストール" + "value" : "Törlés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Törlés" + "value" : "アンインストール" } }, "zh-Hans" : { @@ -5122,16 +5055,16 @@ "value" : "Désinstaller %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@をアンインストール" + "value" : "%@ törlése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "%@ törlése" + "value" : "%@をアンインストール" } }, "zh-Hans" : { @@ -5156,16 +5089,16 @@ "value" : "Désinstallez %@, les fichiers associés ainsi que le cache." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@関連ファイル及びキャッシュをアンインストール" + "value" : "Az %@ és minden kapcsolatos fájl törlése." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Az %@ és minden kapcsolatos fájl törlése." + "value" : "%@関連ファイル及びキャッシュをアンインストール" } }, "zh-Hans" : { @@ -5190,16 +5123,16 @@ "value" : "Désinstaller Applite..." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Appliteをアンインストール..." + "value" : "Az Applite törlése..." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Az Applite törlése..." + "value" : "Appliteをアンインストール..." } }, "zh-Hans" : { @@ -5224,16 +5157,16 @@ "value" : "Désinstaller totalement" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "完全にアンインストールする" + "value" : "Teljes törlés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Teljes törlés" + "value" : "完全にアンインストールする" } }, "zh-Hans" : { @@ -5259,16 +5192,16 @@ "value" : "Désinstallation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アンインストール" + "value" : "Törlés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Törlés" + "value" : "アンインストール" } }, "zh-Hans" : { @@ -5293,16 +5226,16 @@ "value" : "Mettre à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "更新" + "value" : "Frissítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Frissítés" + "value" : "更新" } }, "zh-Hans" : { @@ -5327,16 +5260,16 @@ "value" : "Tout mettre à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "全て更新" + "value" : "Összes frissítése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Összes frissítése" + "value" : "全て更新" } }, "zh-Hans" : { @@ -5362,16 +5295,16 @@ "value" : "Mettez à jour et désinstallez vos applications. Fini les résidus des applications supprimées." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アプリの更新やアンインストールをします。削除したアプリのファイルは残りません" + "value" : "Frissítse és törölje az alkalmazásait. Nincsenek többé hátramaradt fájlok a törölt alkalmazások után." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Frissítse és törölje az alkalmazásait. Nincsenek többé hátramaradt fájlok a törölt alkalmazások után." + "value" : "アプリの更新やアンインストールをします。削除したアプリのファイルは残りません" } }, "zh-Hans" : { @@ -5396,16 +5329,16 @@ "value" : "Echec de la mise à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデートに失敗しました" + "value" : "Firssítés sikertelen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Firssítés sikertelen" + "value" : "アップデートに失敗しました" } }, "zh-Hans" : { @@ -5430,16 +5363,16 @@ "value" : "Mettre à jour Homebrew" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Homebrewを更新する" + "value" : "A Homebrew frissítése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A Homebrew frissítése" + "value" : "Homebrewを更新する" } }, "zh-Hans" : { @@ -5464,16 +5397,16 @@ "value" : "Mises à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデート" + "value" : "Frissítések" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Frissítések" + "value" : "アップデート" } }, "zh-Hans" : { @@ -5499,16 +5432,16 @@ "value" : "Mise à jour" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "アップデート" + "value" : "Firssítés" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Firssítés" + "value" : "アップデート" } }, "zh-Hans" : { @@ -5533,16 +5466,16 @@ "value" : "Utiliser le répertoire d'installation personnalisé" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "カスタムインストールディレクトリを使用する" + "value" : "Saját telepítési mappa használata" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Saját telepítési mappa használata" + "value" : "カスタムインストールディレクトリを使用する" } }, "zh-Hans" : { @@ -5567,16 +5500,16 @@ "value" : "Utiliser le proxy système" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "システムプロキシを使用する" + "value" : "Rendszer proxy használata" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Rendszer proxy használata" + "value" : "システムプロキシを使用する" } }, "zh-Hans" : { @@ -5602,16 +5535,16 @@ "value" : "Utilitaires" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "ユーティリティ" + "value" : "Eszközök" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Eszközök" + "value" : "ユーティリティ" } }, "zh-Hans" : { @@ -5629,6 +5562,7 @@ } }, "View Error" : { + "extractionState" : "stale", "localizations" : { "fr" : { "stringUnit" : { @@ -5636,16 +5570,16 @@ "value" : "Voir l'erreur" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "エラーを参照して下さい" + "value" : "Hiba Megtekintése" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hiba Megtekintése" + "value" : "エラーを参照して下さい" } }, "zh-Hans" : { @@ -5671,16 +5605,16 @@ "value" : "Virtualisation" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "仮想化" + "value" : "Virtualizáció" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Virtualizáció" + "value" : "仮想化" } }, "zh-Hans" : { @@ -5706,16 +5640,16 @@ "value" : "Attention" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "注意" + "value" : "Figyelem" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Figyelem" + "value" : "注意" } }, "zh-Hans" : { @@ -5740,16 +5674,16 @@ "value" : "Site web" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Webサイト" + "value" : "Weboldal" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Weboldal" + "value" : "Webサイト" } }, "zh-Hans" : { @@ -5774,16 +5708,16 @@ "value" : "Bienvenue sur %@" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "%@へようこそ" + "value" : "Üdvözöljük a %@-on" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Üdvözöljük a %@-on" + "value" : "%@へようこそ" } }, "zh-Hans" : { @@ -5808,16 +5742,16 @@ "value" : "Lors de l'importation d'un Brewfile, seuls les casks seront installés. D'autres éléments tels que les formules et les taps seront ignorés." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "BrewfileをインポートするとCaskのみがインストールされます。FormulaやTapなどの他の要素は無視されます" + "value" : "A Brewfile importálásakor csak a cask-ok kerülnek telepítésre. A többi elem, mint például a formulák és a tapok mellőzve lesznek." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A Brewfile importálásakor csak a cask-ok kerülnek telepítésre. A többi elem, mint például a formulák és a tapok mellőzve lesznek." + "value" : "BrewfileをインポートするとCaskのみがインストールされます。FormulaやTapなどの他の要素は無視されます" } }, "zh-Hans" : { @@ -5842,16 +5776,16 @@ "value" : "Outils de ligne de commande Xcode" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Xcodeコマンドラインツール" + "value" : "Xcode Command Line Tools" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Xcode Command Line Tools" + "value" : "Xcodeコマンドラインツール" } }, "zh-Hans" : { @@ -5876,16 +5810,16 @@ "value" : "Oui" } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "はい" + "value" : "Igen" } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Igen" + "value" : "はい" } }, "zh-Hans" : { @@ -5910,16 +5844,16 @@ "value" : "Vous serez invité à installer les outils de ligne de commande Xcode. Veuillez sélectionner \"Installer\" car ils sont nécessaires pour que cette application fonctionne." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Xcodeコマンドラインツールをインストールするように求められます。このアプリの動作に必要な為、インストールを選択して下さい" + "value" : "A program kérni fogja az Xcode Command Line Tools telepítését, kérjük, kattintson a \"Telepítés\" gombra, mivel ez szükséges az alkalmazás működéséhez. Ez néhány percet vesz igénybe, a telepítés előrehaladását a telepítési ablakban láthatja." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A program kérni fogja az Xcode Command Line Tools telepítését, kérjük, kattintson a \"Telepítés\" gombra, mivel ez szükséges az alkalmazás működéséhez. Ez néhány percet vesz igénybe, a telepítés előrehaladását a telepítési ablakban láthatja." + "value" : "Xcodeコマンドラインツールをインストールするように求められます。このアプリの動作に必要な為、インストールを選択して下さい" } }, "zh-Hans" : { @@ -5944,16 +5878,16 @@ "value" : "Vous serez invité à installer les outils de ligne de commande Xcode. Veuillez sélectionner \"Installer\" car ils sont nécessaires pour que cette application fonctionne." } }, - "ja" : { + "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Xcodeコマンドラインツールをインストールするように求められます。このアプリの動作に必要な為、インストールを選択して下さい" + "value" : "A program kérni fogja az Xcode Command Line Tools telepítését. Kérjük, válassza a \"Telepítés\" lehetőséget, mivel ez szükséges az alkalmazás működéséhez." } }, - "hu" : { + "ja" : { "stringUnit" : { "state" : "translated", - "value" : "A program kérni fogja az Xcode Command Line Tools telepítését. Kérjük, válassza a \"Telepítés\" lehetőséget, mivel ez szükséges az alkalmazás működéséhez." + "value" : "Xcodeコマンドラインツールをインストールするように求められます。このアプリの動作に必要な為、インストールを選択して下さい" } }, "zh-Hans" : { @@ -5972,4 +5906,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file