Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fullscreen Image #1499

Merged
merged 12 commits into from
Dec 16, 2024
6 changes: 5 additions & 1 deletion Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
CD09BA7F2CB4698E00C93926 /* OledPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD09BA7E2CB4698600C93926 /* OledPalette.swift */; };
CD0E06F72C0E739F00445849 /* PostType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0E06F62C0E739F00445849 /* PostType+Extensions.swift */; };
CD0F280A2C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */; };
CD10CC182D0A0A650006C20F /* MediaState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD10CC172D0A0A5C0006C20F /* MediaState.swift */; };
CD10FA772C7A8622008985AD /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD10FA762C7A8622008985AD /* ImageSaver.swift */; };
CD13CC572C582DD8001AF428 /* DynamicMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC562C582DD8001AF428 /* DynamicMediaView.swift */; };
CD13CC592C583C7A001AF428 /* WebsitePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */; };
Expand Down Expand Up @@ -707,6 +708,7 @@
CD0E06F62C0E739F00445849 /* PostType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostType+Extensions.swift"; sourceTree = "<group>"; };
CD0E07002C12707700445849 /* ToolbarEllipsisMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarEllipsisMenu.swift; sourceTree = "<group>"; };
CD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+IsAtTopSubscriber.swift"; sourceTree = "<group>"; };
CD10CC172D0A0A5C0006C20F /* MediaState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaState.swift; sourceTree = "<group>"; };
CD10FA762C7A8622008985AD /* ImageSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = "<group>"; };
CD13CC562C582DD8001AF428 /* DynamicMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicMediaView.swift; sourceTree = "<group>"; };
CD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1666,13 +1668,14 @@
CD4D59212B87BD0800B82964 /* Definitions */ = {
isa = PBXGroup;
children = (
CDCA44AE2C17672900C092B3 /* HapticManager */,
CD4D58B22B86BFD400B82964 /* AccountsTracker.swift */,
036CC3AE2B8145C30098B6A1 /* AppState.swift */,
0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */,
CD10CC172D0A0A5C0006C20F /* MediaState.swift */,
CD317D4B2BE97FED008F63E2 /* Palette.swift */,
CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */,
CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */,
CDCA44AE2C17672900C092B3 /* HapticManager */,
);
path = Definitions;
sourceTree = "<group>";
Expand Down Expand Up @@ -2338,6 +2341,7 @@
CD7DB9762C4D6C0A00DCC542 /* FeedCommentView.swift in Sources */,
03D3A1EF2BB9CA1D009DE55E /* MenuButton.swift in Sources */,
CD0507732C6AA0C8008B1505 /* FeedSelection.swift in Sources */,
CD10CC182D0A0A650006C20F /* MediaState.swift in Sources */,
039F58862C7A810100C61658 /* ExpandedPostView+Logic.swift in Sources */,
031E2D512BEF961D0003BC45 /* SubscriptionListView.swift in Sources */,
CDD4A09E2C8B69FC0001AD1A /* Button.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions Mlem/App/Globals/Definitions/MediaState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// MediaState.swift
// Mlem
//
// Created by Eric Andrews on 2024-12-11.
//

import SwiftUI

@Observable
class MediaState {
var url: URL?
}
93 changes: 87 additions & 6 deletions Mlem/App/Views/Pages/ImageViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,106 @@ import SwiftUI

struct ImageViewer: View {
@Environment(Palette.self) var palette
@Environment(MediaState.self) var mediaState

let url: URL

let duration: CGFloat = 0.25
let screenHeight: CGFloat = UIScreen.main.bounds.height

@GestureState var dragState: Bool = false

@State var isZoomed: Bool = false
@State var offset: CGFloat = 0
@State var isDismissing: Bool = false
@State var opacity: CGFloat = 0

init(url: URL) {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.queryItems = components.queryItems?.filter { $0.name != "thumbnail" }
self.url = components.url!
}

var body: some View {
ZoomableContainer {
DynamicMediaView(url: url, playImmediately: true)
ZoomableContainer(isZoomed: $isZoomed) {
DynamicMediaView(url: url, cornerRadius: 0, playImmediately: true)
}
.offset(y: offset)
.background(.black)
.opacity(opacity)
.overlay(alignment: .topTrailing) {
if offset == 0 {
Button {
fadeDismiss()
} label: {
Image(systemName: Icons.close)
.resizable()
.frame(width: 15, height: 15)
.foregroundStyle(.white)
.padding([.top, .trailing], Constants.main.standardSpacing)
.padding([.bottom, .leading], Constants.main.doubleSpacing)
.contentShape(.rect)
}
.padding(Constants.main.standardSpacing)
}
}
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
.updating($dragState) { value, state, _ in
state = true
if !isZoomed, !isDismissing {
offset = value.translation.height
opacity = 1.0 - (abs(value.translation.height) / screenHeight)
}
}
)
.onAppear {
updateOpacity(1.0)
}
.onChange(of: dragState) {
if !dragState {
if abs(offset) > 100 {
swipeDismiss(finalOffset: offset > 0 ? screenHeight : -screenHeight)
} else {
updateDragDistance(0)
}
}
}
}

private func fadeDismiss() {
isDismissing = true
updateOpacity(0) {
mediaState.url = nil
}
}

private func swipeDismiss(finalOffset: CGFloat = UIScreen.main.bounds.height) {
isDismissing = true
updateDragDistance(finalOffset) {
mediaState.url = nil
}
}

private func updateOpacity(_ newOpacity: CGFloat, callback: (() -> Void)? = nil) {
withAnimation(.easeOut(duration: duration)) {
opacity = newOpacity
}
if let callback {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
callback()
}
}
}

private func updateDragDistance(_ newDistance: CGFloat, callback: (() -> Void)? = nil) {
withAnimation(.easeOut(duration: duration)) {
offset = newDistance
opacity = 1.0 - (abs(newDistance) / screenHeight)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
CloseButtonView()
if let callback {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
callback()
}
}
.background(palette.background)
}
}
11 changes: 11 additions & 0 deletions Mlem/App/Views/Root/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct ContentView: View {
var palette: Palette { .main }
var tabReselectTracker: TabReselectTracker { .main }
var navigationModel: NavigationModel { .main }
var mediaState: MediaState = .init()

@State var avatarImage: UIImage?
@State var selectedAvatarImage: UIImage?
Expand Down Expand Up @@ -102,6 +103,7 @@ struct ContentView: View {
}
}
}
.environment(mediaState)
.environment(AppState.main)
}
}
Expand Down Expand Up @@ -170,5 +172,14 @@ struct ContentView: View {
location: .top
)
}
.overlay {
if let url = mediaState.url {
NavigationLayerView(
layer: .init(root: .imageViewer(url),
model: navigationModel,
hasNavigationStack: false),
hasSheetModifiers: false)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private struct AnimationControlLayer: ViewModifier {
}
}
}
.overlay(alignment: .topTrailing) {
.overlay(alignment: .bottomTrailing) {
if let muted {
Image(systemName: muted.wrappedValue ? Icons.muted : Icons.unmuted)
.resizable()
Expand Down
9 changes: 3 additions & 6 deletions Mlem/App/Views/Shared/Images/Wrappers/LargeImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import SwiftUI
// and anything else that can't go in thumbnail views etc.

struct LargeImageView: View {
@Environment(MediaState.self) var mediaState

@Environment(NavigationLayer.self) private var navigation
@Setting(\.blurNsfw) var blurNsfw

Expand Down Expand Up @@ -55,12 +57,7 @@ struct LargeImageView: View {
onTapActions()
}
if let loading, loading == .done, let url {
// Sheets don't cover the whole screen on iPad, so use a fullScreenCover instead
if UIDevice.isPad {
navigation.showFullScreenCover(.imageViewer(url))
} else {
navigation.openSheet(.imageViewer(url))
}
mediaState.url = url
}
}
.onPreferenceChange(MediaLoadingPreferenceKey.self, perform: { loading = $0 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SwiftUI
struct ThumbnailImageView: View {
@Environment(Palette.self) var palette
@Environment(NavigationLayer.self) var navigation
@Environment(MediaState.self) var mediaState
@Environment(\.openURL) var openURL

@State var loading: MediaLoadingState?
Expand Down Expand Up @@ -55,12 +56,7 @@ struct ThumbnailImageView: View {
if let loading, loading == .done || loading == .proxyFailed {
post.markRead()

// Sheets don't cover the whole screen on iPad, so use a fullScreenCover instead
if UIDevice.isPad {
navigation.showFullScreenCover(.imageViewer(url))
} else {
navigation.openSheet(.imageViewer(url))
}
mediaState.url = url
}
}
.contextMenu {
Expand Down
2 changes: 2 additions & 0 deletions Mlem/App/Views/Shared/Navigation/NavigationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class NavigationModel {
var showingFilePicker: Bool = false
var filePickerCallback: ((URL) -> Void)?

var mediaUrl: URL?

private func openSheet(_ page: NavigationPage, hasNavigationStack: Bool? = nil, isFullScreenCover: Bool) {
layers.append(
.init(
Expand Down
7 changes: 6 additions & 1 deletion Mlem/App/Views/Shared/ZoomableContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ struct ZoomableContainer<Content: View>: View {
let content: Content
@State private var currentScale: CGFloat = 1.0
@State private var tapLocation: CGPoint = .zero
@Binding var isZoomed: Bool

init(@ViewBuilder content: () -> Content) {
init(isZoomed: Binding<Bool> = .constant(false), @ViewBuilder content: () -> Content) {
self.content = content()
self._isZoomed = isZoomed
}

func doubleTapAction(location: CGPoint) {
Expand All @@ -32,6 +34,9 @@ struct ZoomableContainer<Content: View>: View {
content
}
.onTapGesture(count: 2, perform: doubleTapAction)
.onChange(of: currentScale) {
isZoomed = currentScale != 1.0
}
}

fileprivate struct ZoomableScrollView<ScollContent: View>: UIViewRepresentable {
Expand Down
Loading