diff --git a/Rewind/App/AppEntryPoint.swift b/Rewind/App/AppEntryPoint.swift new file mode 100644 index 0000000..4b04b63 --- /dev/null +++ b/Rewind/App/AppEntryPoint.swift @@ -0,0 +1,23 @@ +import SwiftUI + +@main +enum AppEntryPoint { + static func main() { + #if DEBUG + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { + UnitTestApp.main() + return + } + #endif + + RewindApp.main() + } +} + +#if DEBUG +private struct UnitTestApp: App { + var body: some Scene { + WindowGroup {} + } +} +#endif diff --git a/Rewind/App/RewindApp.swift b/Rewind/App/RewindApp.swift index d071442..6c1ffac 100644 --- a/Rewind/App/RewindApp.swift +++ b/Rewind/App/RewindApp.swift @@ -7,7 +7,6 @@ import SwiftUI -@main struct RewindApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate diff --git a/Rewind/Screens/Comparison/ComparisonScreen.swift b/Rewind/Screens/Comparison/ComparisonScreen.swift index 978e147..3fcb868 100644 --- a/Rewind/Screens/Comparison/ComparisonScreen.swift +++ b/Rewind/Screens/Comparison/ComparisonScreen.swift @@ -158,68 +158,6 @@ private struct ComparisonViewRepresentable: UIViewControllerRepresentable { } } -private struct SavedBanner: View { - var savesCount: Int - - var body: some View { - HStack { - Image(systemName: "photo.badge.checkmark") - Text("Saved to Photos") - } - .padding() - .blurBackground(in: Capsule()) - .padding(.top, 5) - .modifier(AnimatedTopBanner(trigger: savesCount)) - } -} - -private struct AnimatedTopBanner: ViewModifier { - var trigger: T - - enum Phase: CaseIterable { - case initial - case shown - case pause - case hidden - - var yOffset: CGFloat { - switch self { - case .initial, .hidden: -200 - case .shown: 0 - case .pause: -5 // 🐞 if values are the same, they get ignored - } - } - - var opacity: CGFloat { - switch self { - case .initial, .hidden: 0 - case .shown, .pause: 1 - } - } - } - - func body(content: Content) -> some View { - content - .phaseAnimator( - Phase.allCases, - trigger: trigger, - content: { content, currentPhase in - content - .offset(y: currentPhase.yOffset) - .opacity(currentPhase.opacity) - }, - animation: { nextPhase in - switch nextPhase { - case .initial: nil - case .shown: .smooth - case .pause: .smooth(duration: 2) - case .hidden: .smooth - } - }, - ) - } -} - extension ComparisonState.Style: Identifiable { var id: Self { self } @@ -296,20 +234,4 @@ private let shutterButtonSize: CGFloat = 80 ComparisonScreen(deps: deps) } -#Preview("saved banner") { - @Previewable @State - var savesCount = 0 - - ZStack { - Image(.cat).resizable().ignoresSafeArea() - - VStack { - SavedBanner(savesCount: savesCount) - Spacer() - Button("Simulate save") { - savesCount += 1 - }.buttonStyle(.borderedProminent) - } - } -} #endif diff --git a/Rewind/Screens/ImageDetails/ImageDetailsModel.swift b/Rewind/Screens/ImageDetails/ImageDetailsModel.swift index 2f159bc..5036bac 100644 --- a/Rewind/Screens/ImageDetails/ImageDetailsModel.swift +++ b/Rewind/Screens/ImageDetails/ImageDetailsModel.swift @@ -41,7 +41,7 @@ struct ImageDetailsState { var uiImage: UIImage? var cachedLowResImage: UIImage? - var isImageSaved: Bool + var imageSaveCount: Int var openSource: String var isFavorite: Bool var mapOptionsPresented: Bool @@ -140,7 +140,7 @@ func makeImageDetailsModel( details: nil, uiImage: nil, cachedLowResImage: nil, - isImageSaved: false, + imageSaveCount: 0, openSource: openSource, isFavorite: favoriteModel.state.wrappedValue, mapOptionsPresented: false, @@ -384,7 +384,7 @@ func makeImageDetailsModel( }) case .imageSaved: UINotificationFeedbackGenerator().notificationOccurred(.success) - state.isImageSaved = true + state.imageSaveCount += 1 case let .shareSheetLoaded(vc): state.shareVC = Identified(value: vc) case let .detailsLoaded(details): @@ -422,6 +422,10 @@ func makeImageDetailsModel( ) } +extension ImageDetailsState { + var isImageSaved: Bool { imageSaveCount > 0 } +} + func pastVuURL(cid: Int) -> URL? { var components = URLComponents() components.scheme = "https" diff --git a/Rewind/Screens/ImageDetails/ImageDetailsView.swift b/Rewind/Screens/ImageDetails/ImageDetailsView.swift index 6a1007b..9bfdd51 100644 --- a/Rewind/Screens/ImageDetails/ImageDetailsView.swift +++ b/Rewind/Screens/ImageDetails/ImageDetailsView.swift @@ -61,6 +61,7 @@ struct ImageDetailsView: View { content: { identifiedImage in ZoomableImageScreen( image: identifiedImage.value, + savesCount: viewStore.imageSaveCount, saveImage: { viewStore(.fullscreenPreview(.saveImage)) }, ) .navigationTransition(.zoom(sourceID: TransitionSource.titleImage, in: namespace)) diff --git a/Rewind/Screens/ImageDetails/ZoomableImageScreen.swift b/Rewind/Screens/ImageDetails/ZoomableImageScreen.swift index cdf3014..04caac7 100644 --- a/Rewind/Screens/ImageDetails/ZoomableImageScreen.swift +++ b/Rewind/Screens/ImageDetails/ZoomableImageScreen.swift @@ -10,6 +10,7 @@ import UIKit struct ZoomableImageScreen: View { var image: UIImage + var savesCount: Int var saveImage: () -> Void @Environment(\.dismiss) @@ -38,11 +39,15 @@ struct ZoomableImageScreen: View { } } + SavedBanner(savesCount: savesCount) + HStack { BackButton() Spacer() OverlayButton( - iconName: "square.and.arrow.down", + iconName: savesCount > 0 + ? "square.and.arrow.down.badge.checkmark" + : "square.and.arrow.down", action: saveImage, ) } @@ -73,9 +78,13 @@ struct ZoomableImageView: View { } #Preview { + @Previewable @State + var savesCount = 0 + ZoomableImageScreen( image: UIImage(named: "cat")!, - saveImage: {}, + savesCount: savesCount, + saveImage: { savesCount += 1 }, ) } diff --git a/Rewind/Shared/Views/SavedBanner.swift b/Rewind/Shared/Views/SavedBanner.swift new file mode 100644 index 0000000..036093e --- /dev/null +++ b/Rewind/Shared/Views/SavedBanner.swift @@ -0,0 +1,89 @@ +// +// SavedBanner.swift +// Rewind +// +// Created by Aleksei Sherstnev on 19. 7. 2026. +// + +import SwiftUI + +struct SavedBanner: View { + var savesCount: Int + + var body: some View { + HStack { + Image(systemName: "photo.badge.checkmark") + Text("Saved to Photos") + } + .padding() + .blurBackground(in: Capsule()) + .padding(.top, 5) + .modifier(AnimatedTopBanner(trigger: savesCount)) + } +} + +private struct AnimatedTopBanner: ViewModifier { + var trigger: T + + enum Phase: CaseIterable { + case initial + case shown + case pause + case hidden + + var yOffset: CGFloat { + switch self { + case .initial, .hidden: -200 + case .shown: 0 + case .pause: -5 // 🐞 if values are the same, they get ignored + } + } + + var opacity: CGFloat { + switch self { + case .initial, .hidden: 0 + case .shown, .pause: 1 + } + } + } + + func body(content: Content) -> some View { + content + .phaseAnimator( + Phase.allCases, + trigger: trigger, + content: { content, currentPhase in + content + .offset(y: currentPhase.yOffset) + .opacity(currentPhase.opacity) + }, + animation: { nextPhase in + switch nextPhase { + case .initial: nil + case .shown: .smooth + case .pause: .smooth(duration: 2) + case .hidden: .smooth + } + }, + ) + } +} + +#if DEBUG +#Preview("saved banner") { + @Previewable @State + var savesCount = 0 + + ZStack { + Image(.cat).resizable().ignoresSafeArea() + + VStack { + SavedBanner(savesCount: savesCount) + Spacer() + Button("Simulate save") { + savesCount += 1 + }.buttonStyle(.borderedProminent) + } + } +} +#endif