diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift new file mode 100644 index 00000000..0235fa0b --- /dev/null +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/SidebarVisibilityKey.swift @@ -0,0 +1,12 @@ +import SwiftUI + +public struct SidebarVisibilityKey: EnvironmentKey { + public static let defaultValue: Bool = false +} + +public extension EnvironmentValues { + var isSidebarVisible: Bool { + get { self[SidebarVisibilityKey.self] } + set { self[SidebarVisibilityKey.self] = newValue } + } +} diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift b/MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift new file mode 100644 index 00000000..76ade5d9 --- /dev/null +++ b/MacMagazine/MacMagazine/Features/Social/InstagramNavigationDelegate.swift @@ -0,0 +1,60 @@ +import SwiftUI +import WebKit + +final class InstagramNavigationDelegate: NSObject, WKNavigationDelegate { + var onStart: (() -> Void)? + var onFinish: (() -> Void)? + var onFail: ((Error) -> Void)? + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation?) { + onStart?() + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { + onFinish?() + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation?, withError error: Error) { + onFail?(error) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation?, withError error: Error) { + onFail?(error) + } + + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard let url = navigationAction.request.url else { + decisionHandler(.cancel) + return + } + + guard navigationAction.navigationType == .linkActivated else { + decisionHandler(.allow) + return + } + + if url.host?.lowercased().contains("instagram.com") == true { + if let appURL = makeInstagramAppURL(from: url), + UIApplication.shared.canOpenURL(appURL) { + UIApplication.shared.open(appURL) + } else { + UIApplication.shared.open(url) + } + decisionHandler(.cancel) + return + } + + decisionHandler(.allow) + } + + private func makeInstagramAppURL(from webURL: URL) -> URL? { + var components = URLComponents(url: webURL, resolvingAgainstBaseURL: false) + components?.scheme = "instagram" + components?.host = nil + return components?.url + } +} diff --git a/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift new file mode 100644 index 00000000..d9c2c510 --- /dev/null +++ b/MacMagazine/MacMagazine/Features/Social/InstagramPostsWebView.swift @@ -0,0 +1,133 @@ +import MacMagazineLibrary +import SwiftUI +import UIComponentsLibrary +import WebKit +#if canImport(UIKit) +import UIKit +#endif + +struct InstagramPostsWebView: View { + let url: URL + let userAgent: String + let shouldUseSidebar: Bool + + private let darkMode: Bool + + @Environment(\.theme) private var theme: ThemeColor + + @State private var isPresenting = true + @State private var isLoading = true + @State private var loadError: String? + + private let navigationDelegate = InstagramNavigationDelegate() + + init( + colorSchema: ColorScheme?, + url: URL, + userAgent: String, + shouldUseSidebar: Bool + ) { + self.url = url + self.userAgent = userAgent + self.shouldUseSidebar = shouldUseSidebar + + self.darkMode = if colorSchema == nil { + Self.isDarkMode() + } else { + colorSchema == .dark + } + } + + private var userScripts: [WKUserScript] { + [ + WKUserScript( + source: """ + (function() { + var style = document.createElement('style'); + style.innerHTML = ` + html, body { + padding-top: 50px !important; + } + `; + document.head.appendChild(style); + })(); + """, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true + ) + ] + } + + var body: some View { + ZStack { + (theme.main.background.color ?? Color.secondary) + .ignoresSafeArea() + + Webview( + title: nil, + url: url.absoluteString, + isPresenting: $isPresenting, + standAlone: true, + navigationDelegate: navigationDelegate, + userScripts: userScripts, + cookies: makeCookies(), + userAgent: userAgent + ) + .ignoresSafeArea(.container, edges: [.top, .bottom]) + .opacity(isLoading ? 0 : 1) + .animation(.easeInOut(duration: 0.25), value: isLoading) + + if isLoading { + ProgressView() + .transition(.opacity) + } + + if loadError != nil { + ContentUnavailableView( + "Estamos com um problema", + systemImage: "square.and.arrow.down.badge.xmark", + description: Text("No momento estamos com um problema técnico. Tente novamente mais tarde.") + ) + } + } + .onAppear { + navigationDelegate.onStart = { isLoading = true; loadError = nil } + navigationDelegate.onFinish = { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + isLoading = false + } + } + navigationDelegate.onFail = { error in + isLoading = false + loadError = error.localizedDescription + } + } + } +} + +private extension InstagramPostsWebView { + func makeCookies() -> [HTTPCookie]? { + var cookies = [HTTPCookie]() + if let darkMode = Cookies.createDarkMode(darkMode ? "true" : "false") { + cookies.append(darkMode) + } + return cookies + } +} + +#if canImport(UIKit) +private extension InstagramPostsWebView { + static func isDarkMode() -> Bool { + (UIApplication.shared.connectedScenes.first as? UIWindowScene)? + .windows.first? + .rootViewController? + .traitCollection.userInterfaceStyle == .dark + } +} +#else +private extension InstagramPostsWebView { + static func isDarkMode() -> Bool { + false + } +} +#endif diff --git a/MacMagazine/MacMagazine/Features/Social/SocialView.swift b/MacMagazine/MacMagazine/Features/Social/SocialView.swift index f72780ea..2352b0d6 100644 --- a/MacMagazine/MacMagazine/Features/Social/SocialView.swift +++ b/MacMagazine/MacMagazine/Features/Social/SocialView.swift @@ -32,7 +32,8 @@ struct SocialView: View { content } .contentMargins(.top, 20, for: .scrollContent) - .navigation(shouldUseSidebar: shouldUseSidebar, title: viewModel.social.rawValue) + .navigation(shouldUseSidebar: shouldUseSidebar, + title: viewModel.social.rawValue) .toolbar { ToolbarItem(placement: .primaryAction) { menuView @@ -75,11 +76,24 @@ private extension SocialView { scrollPosition: $scrollPosition ).transition(.opacity) case .instagram: - ContentUnavailableView( - "Página em construção", - systemImage: "square.and.arrow.down.badge.xmark", - description: Text("Conteúdo ainda em desenvolvimento e estará disponível em breve.") - ) + if let url = URL(string: "https://macmagazine.com.br/posts-instagram-app/") { + InstagramPostsWebView( + colorSchema: viewModel.settingsViewModel.colorSchema, + url: url, + userAgent: "MacMagazine", + shouldUseSidebar: shouldUseSidebar + ) + .transition(.opacity) + } else { + ContentUnavailableView( + "Estamos com um problema", + systemImage: "square.and.arrow.down.badge.xmark", + description: Text( + "No momento estamos com um problema técnico. Tente novamente mais tarde." + ) + ) + .transition(.opacity) + } } } diff --git a/MacMagazine/MacMagazine/MainApp/MainView.swift b/MacMagazine/MacMagazine/MainApp/MainView.swift index ea932475..74a7b7d6 100644 --- a/MacMagazine/MacMagazine/MainApp/MainView.swift +++ b/MacMagazine/MacMagazine/MainApp/MainView.swift @@ -17,6 +17,7 @@ struct MainView: View { @Environment(MainViewModel.self) var viewModel @State var searchText: String = "" + @State var splitViewVisibility: NavigationSplitViewVisibility = .all @State private var currentlayout: LayoutType = .tabbar diff --git a/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift b/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift index 4bd876c4..839f5c08 100644 --- a/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift +++ b/MacMagazine/MacMagazine/MainApp/Sizecalss/MainView+sidebar.swift @@ -5,11 +5,15 @@ import SwiftUI extension MainView { var sideBarContentView: some View { - NavigationSplitView { + let isSidebarVisible = + splitViewVisibility == .all || splitViewVisibility == .doubleColumn + + return NavigationSplitView(columnVisibility: $splitViewVisibility) { sidebar .searchable(text: $searchText, prompt: "Search items") } detail: { animateContentStackView(for: navigationState.selectedItem) + .environment(\.isSidebarVisible, isSidebarVisible) .podcastMiniPlayer() } .navigationSplitViewStyle(.balanced) diff --git a/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift b/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift index 5f65aa77..d64d9f44 100644 --- a/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift +++ b/MacMagazine/MacMagazine/Modifiers/NavigationModifier.swift @@ -1,7 +1,10 @@ import SwiftUI extension View { - func navigation(shouldUseSidebar: Bool, title: String? = nil) -> some View { + func navigation( + shouldUseSidebar: Bool, + title: String? = nil + ) -> some View { modifier(NavigationModifier( shouldUseSidebar: shouldUseSidebar, title: title @@ -13,11 +16,14 @@ private struct NavigationModifier: ViewModifier { let shouldUseSidebar: Bool let title: String? + @Environment(\.isSidebarVisible) private var isSidebarVisible + func body(content: Content) -> some View { if shouldUseSidebar { if let title { content .navigationTitle(title) + .navigationBarTitleDisplayMode(isSidebarVisible ? .inline : .large ) } else { content } diff --git a/MacMagazine/MacMagazine/Resources/Info.plist b/MacMagazine/MacMagazine/Resources/Info.plist index 656a9c28..dfc55d0e 100644 --- a/MacMagazine/MacMagazine/Resources/Info.plist +++ b/MacMagazine/MacMagazine/Resources/Info.plist @@ -8,5 +8,9 @@ audio fetch + LSApplicationQueriesSchemes + + instagram +