Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Rewind/App/AppEntryPoint.swift
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion Rewind/App/RewindApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import SwiftUI

@main
struct RewindApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
var appDelegate
Expand Down
78 changes: 0 additions & 78 deletions Rewind/Screens/Comparison/ComparisonScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Equatable>: 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 }

Expand Down Expand Up @@ -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
10 changes: 7 additions & 3 deletions Rewind/Screens/ImageDetails/ImageDetailsModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -140,7 +140,7 @@ func makeImageDetailsModel(
details: nil,
uiImage: nil,
cachedLowResImage: nil,
isImageSaved: false,
imageSaveCount: 0,
openSource: openSource,
isFavorite: favoriteModel.state.wrappedValue,
mapOptionsPresented: false,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -422,6 +422,10 @@ func makeImageDetailsModel(
)
}

extension ImageDetailsState {
var isImageSaved: Bool { imageSaveCount > 0 }
}

func pastVuURL(cid: Int) -> URL? {
var components = URLComponents()
components.scheme = "https"
Expand Down
1 change: 1 addition & 0 deletions Rewind/Screens/ImageDetails/ImageDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
13 changes: 11 additions & 2 deletions Rewind/Screens/ImageDetails/ZoomableImageScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import UIKit

struct ZoomableImageScreen: View {
var image: UIImage
var savesCount: Int
var saveImage: () -> Void

@Environment(\.dismiss)
Expand Down Expand Up @@ -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,
)
}
Expand Down Expand Up @@ -73,9 +78,13 @@ struct ZoomableImageView: View {
}

#Preview {
@Previewable @State
var savesCount = 0

ZoomableImageScreen(
image: UIImage(named: "cat")!,
saveImage: {},
savesCount: savesCount,
saveImage: { savesCount += 1 },
)
}

Expand Down
89 changes: 89 additions & 0 deletions Rewind/Shared/Views/SavedBanner.swift
Original file line number Diff line number Diff line change
@@ -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<T: Equatable>: 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
Loading