From 7d326ee05025332f741ae5b8d70e9eb226e7dbc0 Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:22:05 -0500 Subject: [PATCH 1/8] ios 26 build target --- Django Files.xcodeproj/project.pbxproj | 12 ++++++------ Django FilesUITests/Django_FilesUITests.swift | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Django Files.xcodeproj/project.pbxproj b/Django Files.xcodeproj/project.pbxproj index ab51e7a..6f7b3f8 100644 --- a/Django Files.xcodeproj/project.pbxproj +++ b/Django Files.xcodeproj/project.pbxproj @@ -466,7 +466,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -513,7 +513,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -585,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -642,7 +642,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -660,7 +660,7 @@ CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ZY6BPTGK47; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 0.0; PRODUCT_BUNDLE_IDENTIFIER = blastsoftstudios.djangofilesTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -679,7 +679,7 @@ CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ZY6BPTGK47; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 0.0; PRODUCT_BUNDLE_IDENTIFIER = blastsoftstudios.djangofilesTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Django FilesUITests/Django_FilesUITests.swift b/Django FilesUITests/Django_FilesUITests.swift index fd380cf..99dcaf4 100644 --- a/Django FilesUITests/Django_FilesUITests.swift +++ b/Django FilesUITests/Django_FilesUITests.swift @@ -44,7 +44,7 @@ final class Django_FilesUITests: XCTestCase { // Skip performance testing in simulator as it's not reliable return #else - if #available(iOS 18.0, *) { + if #available(iOS 26.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTApplicationLaunchMetric()]) { XCUIApplication().launch() From e01697d39f021e4d38cea85cbebd3dcb3431a2a8 Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:23:10 -0500 Subject: [PATCH 2/8] xcode 26 --- .github/workflows/publish.yml | 2 +- .github/workflows/tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ba20c28..e36dd26 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,7 +42,7 @@ jobs: - name: "Install iOS 18.3 Platform" run: | - sudo xcode-select --switch /Applications/Xcode_16.4.app + sudo xcode-select --switch /Applications/Xcode_26.0.app # sudo xcodebuild -downloadPlatform iOS # sudo xcodebuild -downloadPlatform iOS -platform iOS -version 18.4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1aa293d..5f21255 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,7 @@ jobs: - name: "Install iOS 18.3 Platform" run: | - sudo xcode-select --switch /Applications/Xcode_16.4.app + sudo xcode-select --switch /Applications/Xcode_26.0.app # sudo xcodebuild -downloadPlatform iOS # sudo xcodebuild -downloadPlatform iOS -platform iOS -version 18.4 From 6de1f1403e83c415c9d91af66a46897f922084ba Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sun, 28 Sep 2025 15:16:21 -0500 Subject: [PATCH 3/8] make preview toolbars conform to ios --- Django Files/Views/Preview/Preview.swift | 359 ++++++++++------------- 1 file changed, 159 insertions(+), 200 deletions(-) diff --git a/Django Files/Views/Preview/Preview.swift b/Django Files/Views/Preview/Preview.swift index 1e97a82..6cb717c 100644 --- a/Django Files/Views/Preview/Preview.swift +++ b/Django Files/Views/Preview/Preview.swift @@ -64,6 +64,7 @@ struct ContentPreview: View { mimeType: mimeType, fileName: fileURL.lastPathComponent ) + .background(.black) } private var imagePreview: some View { @@ -83,6 +84,7 @@ struct ContentPreview: View { } } .ignoresSafeArea() + .background(.black) } // Video Preview @@ -100,12 +102,14 @@ struct ContentPreview: View { } } } + .background(.black) } // Audio Preview private var audioPreview: some View { AudioPlayerView(url: fileURL) .padding() + .background(.black) } // PDF Preview @@ -130,6 +134,7 @@ struct ContentPreview: View { .foregroundColor(.secondary) } .padding() + .background(.black) } private func loadContent() { @@ -492,227 +497,181 @@ struct FilePreviewView: View { } var body: some View { - GeometryReader { geometry in - ZStack { - if redirectURLs[file.raw] == nil { - LoadingView() - .frame(width: 100, height: 100) - .onAppear { - Task { - await preloadFiles() + NavigationStack{ + GeometryReader { geometry in + ZStack { + if redirectURLs[file.raw] == nil { + LoadingView() + .frame(width: 100, height: 100) + .onAppear { + Task { + await preloadFiles() + } } } } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - ZStack { - PageViewController( - files: allFiles, - currentIndex: currentIndex, - redirectURLs: redirectURLs, - showFileInfo: $showFileInfo, - selectedFileDetails: $selectedFileDetails, - onPageChange: { newIndex in - onNavigate(newIndex) - Task { - await preloadFiles() - } - }, - onLoadMore: onLoadMore - ) - .ignoresSafeArea() - .onTapGesture { - withAnimation(.easeInOut(duration: 0.2)) { - isOverlayVisible.toggle() - } - } - .background( - FileDialogs( - showingDeleteConfirmation: $showingDeleteConfirmation, - fileIDsToDelete: $fileIDsToDelete, - fileNameToDelete: $fileNameToDelete, - showingExpirationDialog: $showingExpirationDialog, - expirationText: $expirationText, - fileToExpire: $fileToExpire, - showingPasswordDialog: $showingPasswordDialog, - passwordText: $passwordText, - fileToPassword: $fileToPassword, - showingRenameDialog: $showingRenameDialog, - fileNameText: $fileNameText, - fileToRename: $fileToRename, - onDelete: { fileIDs in - if await deleteFiles(fileIDs: fileIDs) { - showingPreview = false - return true + .frame(maxWidth: .infinity, maxHeight: .infinity) + ZStack { + PageViewController( + files: allFiles, + currentIndex: currentIndex, + redirectURLs: redirectURLs, + showFileInfo: $showFileInfo, + selectedFileDetails: $selectedFileDetails, + onPageChange: { newIndex in + onNavigate(newIndex) + Task { + await preloadFiles() } - return false - }, - onSetExpiration: { file, expr in - await setFileExpr(file: file, expr: expr) - }, - onSetPassword: { file, password in - await setFilePassword(file: file, password: password) }, - onRename: { file, name in - await renameFile(file: file, name: name) - } + onLoadMore: onLoadMore ) - ) - - ZStack(alignment: .top) { - if isOverlayVisible { - VStack { - HStack{ - Button(action: { + .ignoresSafeArea() + .background(.black) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.2)) { + isOverlayVisible.toggle() + } + } + .background( + FileDialogs( + showingDeleteConfirmation: $showingDeleteConfirmation, + fileIDsToDelete: $fileIDsToDelete, + fileNameToDelete: $fileNameToDelete, + showingExpirationDialog: $showingExpirationDialog, + expirationText: $expirationText, + fileToExpire: $fileToExpire, + showingPasswordDialog: $showingPasswordDialog, + passwordText: $passwordText, + fileToPassword: $fileToPassword, + showingRenameDialog: $showingRenameDialog, + fileNameText: $fileNameText, + fileToRename: $fileToRename, + onDelete: { fileIDs in + if await deleteFiles(fileIDs: fileIDs) { showingPreview = false - }) { - Image(systemName: "xmark") - .font(.system(size: 17)) - .foregroundColor(.blue) - .padding() - } - .background(.ultraThinMaterial) - .frame(width: 32, height: 32) - .cornerRadius(16) - .padding(.leading, 15) - Spacer() - Text(file.name) - .padding(5) - .font(.headline) - .lineLimit(1) - .foregroundColor(file.mime.starts(with: "text") ? .primary : .white) - .shadow(color: .black, radius: file.mime.starts(with: "text") ? 0 : 3) - Spacer() - if !isDeepLinkPreview { - Menu { - fileContextMenu(for: file, isPreviewing: true, isPrivate: file.private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText) - .padding() - } label: { - Image(systemName: "ellipsis") - .font(.system(size: 20)) - .padding() - } - .menuStyle(.button) - .background(.ultraThinMaterial) - .frame(width: 32, height: 32) - .cornerRadius(16) - .padding(.trailing, 10) + return true } + return false + }, + onSetExpiration: { file, expr in + await setFileExpr(file: file, expr: expr) + }, + onSetPassword: { file, password in + await setFilePassword(file: file, password: password) + }, + onRename: { file, name in + await renameFile(file: file, name: name) } - .padding(.vertical, 8) - .frame(maxWidth: .infinity) - .background { - if file.mime.starts(with: "text") || file.mime.starts(with: "application") { - Rectangle() - .fill(.ultraThinMaterial) - .ignoresSafeArea() - } + ) + ) + } + .offset(y: dragOffset.height) + .gesture( + DragGesture() + .updating($dragState) { value, state, _ in + state = .dragging(translation: value.translation) + } + .onChanged { value in + let translation = value.translation + // Only allow vertical dragging with initial resistance + let dampingFactor: CGFloat = 0.5 // Increase this value for more resistance + let dampenedHeight = pow(translation.height, dampingFactor) * 8 + dragOffset = CGSize(width: 0, height: max(0, dampenedHeight)) + } + .onEnded { value in + let translation = value.translation + let velocity = CGSize( + width: value.predictedEndLocation.x - value.location.x, + height: value.predictedEndLocation.y - value.location.y + ) + + // Only handle vertical gestures for dismissal + let progress = translation.height / geometry.size.height + let velocityThreshold: CGFloat = 300 + let progressThreshold: CGFloat = 0.3 + + if progress > progressThreshold || velocity.height > velocityThreshold { + withAnimation(.easeOut(duration: 0.2)) { + dragOffset = CGSize(width: 0, height: geometry.size.height) + showingPreview = false } - Spacer() - if !file.mime.starts(with: "video/") { - HStack { - Spacer() - Button(action: { - showFileInfo = true - }) { - Image(systemName: "info.circle") - .font(.system(size: 20)) - .padding(8) - } - .buttonStyle(.borderless) - - Menu { - fileShareMenu(for: file) - } label: { - Image(systemName: "link.icloud") - .font(.system(size: 20)) - .padding(8) - } - .menuStyle(.button) - - Button(action: { - showingShareSheet = true - }) { - Image(systemName: "square.and.arrow.up") - .font(.system(size: 20)) - .offset(y: -2) - .padding(8) - } - .buttonStyle(.borderless) - .padding(.leading, 1) - .sheet(isPresented: $showingShareSheet) { - if let url = URL(string: file.url) { - ShareSheet(url: url) - .presentationDetents([.medium]) - } - } - Spacer() - } - .background(.ultraThinMaterial) - .frame(width: 155, height: 44) - .cornerRadius(20) + } else { + // Reset position if not dismissed + withAnimation(.spring()) { + dragOffset = .zero } } } + ) + } + .onChange(of: currentIndex) { _, _ in + // Preload files when current index changes externally + Task { + await preloadFiles() } - } - .offset(y: dragOffset.height) - .gesture( - DragGesture() - .updating($dragState) { value, state, _ in - state = .dragging(translation: value.translation) + } + .sheet(isPresented: $showFileInfo) { + if let details = selectedFileDetails { + PreviewFileInfo(file: details) + .presentationBackground(.ultraThinMaterial) + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) + } else { + PreviewFileInfo(file: file) + .presentationBackground(.ultraThinMaterial) + .presentationDetents([.medium]) + .presentationDragIndicator(.visible) } - .onChanged { value in - let translation = value.translation - // Only allow vertical dragging with initial resistance - let dampingFactor: CGFloat = 0.5 // Increase this value for more resistance - let dampenedHeight = pow(translation.height, dampingFactor) * 8 - dragOffset = CGSize(width: 0, height: max(0, dampenedHeight)) + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(action: { + showingPreview = false + }) { + Image(systemName: "xmark") + } } - .onEnded { value in - let translation = value.translation - let velocity = CGSize( - width: value.predictedEndLocation.x - value.location.x, - height: value.predictedEndLocation.y - value.location.y - ) - - // Only handle vertical gestures for dismissal - let progress = translation.height / geometry.size.height - let velocityThreshold: CGFloat = 300 - let progressThreshold: CGFloat = 0.3 - - if progress > progressThreshold || velocity.height > velocityThreshold { - withAnimation(.easeOut(duration: 0.2)) { - dragOffset = CGSize(width: 0, height: geometry.size.height) - showingPreview = false + ToolbarItem(placement: .principal) { + Text(file.name) + .shadow(color: .black, radius: file.mime.starts(with: "text") ? 0 : 3) + } + ToolbarItem { + if !isDeepLinkPreview { + Menu { + fileContextMenu(for: file, isPreviewing: true, isPrivate: file.private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText) + } label: { + Image(systemName: "ellipsis") } - } else { - // Reset position if not dismissed - withAnimation(.spring()) { - dragOffset = .zero + } + } + ToolbarItemGroup(placement: .bottomBar) { + Button(action: { + showFileInfo = true + }) { + Image(systemName: "info.circle") + } + Menu { + fileShareMenu(for: file) + } label: { + Image(systemName: "link.icloud") + } + + Button(action: { + showingShareSheet = true + }) { + Image(systemName: "square.and.arrow.up") + } + .sheet(isPresented: $showingShareSheet) { + if let url = URL(string: file.url) { + ShareSheet(url: url) + .presentationDetents([.medium]) } } } - ) - } - .onChange(of: currentIndex) { _, _ in - // Preload files when current index changes externally - Task { - await preloadFiles() - } - } - .sheet(isPresented: $showFileInfo) { - if let details = selectedFileDetails { - PreviewFileInfo(file: details) - .presentationBackground(.ultraThinMaterial) - .presentationDetents([.medium]) - .presentationDragIndicator(.visible) - } else { - PreviewFileInfo(file: file) - .presentationBackground(.ultraThinMaterial) - .presentationDetents([.medium]) - .presentationDragIndicator(.visible) } + .toolbarVisibility(isOverlayVisible ? .visible : .hidden, for: .navigationBar) + .toolbarVisibility(isOverlayVisible ? .visible : .hidden, for: .bottomBar) } } From 35d2cbffdfbac6dad33968b187548266ddd562df Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sun, 28 Sep 2025 16:49:13 -0500 Subject: [PATCH 4/8] fix file details appearance in ios26 --- Django Files/Views/Preview/Info.swift | 4 ++-- Django Files/Views/Preview/Preview.swift | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Django Files/Views/Preview/Info.swift b/Django Files/Views/Preview/Info.swift index f2892cc..2e58d3a 100644 --- a/Django Files/Views/Preview/Info.swift +++ b/Django Files/Views/Preview/Info.swift @@ -106,7 +106,7 @@ struct PreviewFileInfo: View { // Photo Information Section if let dateTime = file.exif?["DateTimeOriginal"]?.value as? String { HStack { - Image(systemName: "camera") + Image(systemName: "calendar.badge.clock") .frame(width: 15, height: 15) .font(.caption) Text("Captured: \(formatExifDate(dateTime))") @@ -142,7 +142,7 @@ struct PreviewFileInfo: View { let make = file.exif?["Make"]?.value as? String ?? "" let cameraName = make.isEmpty || model.contains(make) ? model : "\(make) \(model)" HStack { - Image(systemName: "camera.aperture") + Image(systemName: "camera") .frame(width: 15, height: 15) Text("Camera: \(cameraName)") .font(.caption) diff --git a/Django Files/Views/Preview/Preview.swift b/Django Files/Views/Preview/Preview.swift index 6cb717c..55c1c61 100644 --- a/Django Files/Views/Preview/Preview.swift +++ b/Django Files/Views/Preview/Preview.swift @@ -614,12 +614,10 @@ struct FilePreviewView: View { .sheet(isPresented: $showFileInfo) { if let details = selectedFileDetails { PreviewFileInfo(file: details) - .presentationBackground(.ultraThinMaterial) .presentationDetents([.medium]) .presentationDragIndicator(.visible) } else { PreviewFileInfo(file: file) - .presentationBackground(.ultraThinMaterial) .presentationDetents([.medium]) .presentationDragIndicator(.visible) } From 052ca68d5556ca9375f24e8d15ca10469c595730 Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:26:50 -0500 Subject: [PATCH 5/8] fix preview drag dismissal, text preview loading --- Django Files/Views/Lists/FileList.swift | 2 +- Django Files/Views/Preview/Preview.swift | 98 ++++++++------------ Django Files/Views/Preview/TextPreview.swift | 14 ++- 3 files changed, 54 insertions(+), 60 deletions(-) diff --git a/Django Files/Views/Lists/FileList.swift b/Django Files/Views/Lists/FileList.swift index 61e7c26..52ad071 100644 --- a/Django Files/Views/Lists/FileList.swift +++ b/Django Files/Views/Lists/FileList.swift @@ -205,7 +205,7 @@ struct FileListView: View { @State private var showFileInfo: Bool = false @State private var showingUserFilter: Bool = false @State private var users: [DFUser] = [] - + // Add computed property for selected username private var selectedUsername: String? { if let userID = filterUserID { diff --git a/Django Files/Views/Preview/Preview.swift b/Django Files/Views/Preview/Preview.swift index 55c1c61..e707322 100644 --- a/Django Files/Views/Preview/Preview.swift +++ b/Django Files/Views/Preview/Preview.swift @@ -3,6 +3,12 @@ import AVKit import HighlightSwift import UIKit +extension UIPageViewController { + var scrollView: UIScrollView? { + return view.subviews.first { $0 is UIScrollView } as? UIScrollView + } +} + struct ContentPreview: View { let mimeType: String @@ -62,7 +68,9 @@ struct ContentPreview: View { TextPreview( content: content, mimeType: mimeType, - fileName: fileURL.lastPathComponent + fileName: fileURL.lastPathComponent, + isLoading: isLoading, + error: error ) .background(.black) } @@ -205,6 +213,7 @@ struct PageViewController: UIViewControllerRepresentable { @Binding var selectedFileDetails: DFFile? var onPageChange: (Int) -> Void var onLoadMore: (() async -> Void)? + var isDragging: Bool func makeCoordinator() -> Coordinator { Coordinator(self) @@ -228,6 +237,9 @@ struct PageViewController: UIViewControllerRepresentable { func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { context.coordinator.parent = self + // Disable scrolling when dragging is active + pageViewController.scrollView?.isScrollEnabled = !isDragging + // Only update if the current index has changed and we're not in the middle of a transition if let currentVC = pageViewController.viewControllers?.first as? UIHostingController, let currentFileIndex = files.firstIndex(where: { $0.id == currentVC.rootView.file.id }), @@ -411,7 +423,7 @@ struct FilePreviewView: View { @State private var redirectURLs: [String: String] = [:] @State private var dragOffset = CGSize.zero - @GestureState private var dragState = DragState.inactive + @State private var isDragging = false @State private var selectedFileDetails: DFFile? @State private var showingDeleteConfirmation = false @@ -438,20 +450,6 @@ struct FilePreviewView: View { fileListDelegate == nil } - private enum DragState { - case inactive - case dragging(translation: CGSize) - - var translation: CGSize { - switch self { - case .inactive: - return .zero - case .dragging(let translation): - return translation - } - } - } - @MainActor private func preloadFiles() async { // Load the current file and preload adjacent files @@ -497,7 +495,7 @@ struct FilePreviewView: View { } var body: some View { - NavigationStack{ + NavigationStack { GeometryReader { geometry in ZStack { if redirectURLs[file.raw] == nil { @@ -524,7 +522,8 @@ struct FilePreviewView: View { await preloadFiles() } }, - onLoadMore: onLoadMore + onLoadMore: onLoadMore, + isDragging: isDragging ) .ignoresSafeArea() .background(.black) @@ -566,45 +565,6 @@ struct FilePreviewView: View { ) ) } - .offset(y: dragOffset.height) - .gesture( - DragGesture() - .updating($dragState) { value, state, _ in - state = .dragging(translation: value.translation) - } - .onChanged { value in - let translation = value.translation - // Only allow vertical dragging with initial resistance - let dampingFactor: CGFloat = 0.5 // Increase this value for more resistance - let dampenedHeight = pow(translation.height, dampingFactor) * 8 - dragOffset = CGSize(width: 0, height: max(0, dampenedHeight)) - } - .onEnded { value in - let translation = value.translation - let velocity = CGSize( - width: value.predictedEndLocation.x - value.location.x, - height: value.predictedEndLocation.y - value.location.y - ) - - // Only handle vertical gestures for dismissal - let progress = translation.height / geometry.size.height - let velocityThreshold: CGFloat = 300 - let progressThreshold: CGFloat = 0.3 - - if progress > progressThreshold || velocity.height > velocityThreshold { - withAnimation(.easeOut(duration: 0.2)) { - dragOffset = CGSize(width: 0, height: geometry.size.height) - showingPreview = false - } - } else { - // Reset position if not dismissed - withAnimation(.spring()) { - dragOffset = .zero - } - } - } - ) - } .onChange(of: currentIndex) { _, _ in // Preload files when current index changes externally Task { @@ -670,7 +630,31 @@ struct FilePreviewView: View { } .toolbarVisibility(isOverlayVisible ? .visible : .hidden, for: .navigationBar) .toolbarVisibility(isOverlayVisible ? .visible : .hidden, for: .bottomBar) + } } + .offset(dragOffset) + .simultaneousGesture( + DragGesture() + .onChanged { gesture in + if gesture.translation.height > 10 { + dragOffset = gesture.translation + isDragging = true + } + } + .onEnded { gesture in + isDragging = false + if gesture.translation.height > 150 { + withAnimation(.spring()) { + showingPreview = false + } + } else { + // Otherwise, animate the view back to its original position + withAnimation(.spring()) { + dragOffset = .zero + } + } + } + ) } @MainActor diff --git a/Django Files/Views/Preview/TextPreview.swift b/Django Files/Views/Preview/TextPreview.swift index 0af2b2d..d9c252b 100644 --- a/Django Files/Views/Preview/TextPreview.swift +++ b/Django Files/Views/Preview/TextPreview.swift @@ -12,15 +12,25 @@ struct TextPreview: View { let content: Data? let mimeType: String let fileName: String + let isLoading: Bool + let error: Error? var body: some View { ScrollView(showsIndicators: true) { ZStack { - if let content = content, let text = String(data: content, encoding: .utf8) { + if isLoading { + HStack { + Spacer() + LoadingView() + .frame(width: 100, height: 100) + Spacer() + } + } else if let content = content, let text = String(data: content, encoding: .utf8) { CodeText(text) .highlightLanguage(determineLanguage(from: mimeType, fileName: fileName)) .padding() - } else { + } else if error != nil { + // Only show error message if there's an actual error Text("Unable to decode text content") .foregroundColor(.red) } From cf3b18dde93640b87e73da2632288a9fd64a04fd Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sun, 28 Sep 2025 18:51:07 -0500 Subject: [PATCH 6/8] patch overeager dismissing --- Django Files/Views/Preview/Image.swift | 19 +++++++ Django Files/Views/Preview/Preview.swift | 44 +++++++++++++---- Django Files/Views/Preview/TextPreview.swift | 52 +++++++++++++------- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/Django Files/Views/Preview/Image.swift b/Django Files/Views/Preview/Image.swift index 3d06aab..83e20de 100644 --- a/Django Files/Views/Preview/Image.swift +++ b/Django Files/Views/Preview/Image.swift @@ -9,6 +9,7 @@ import SwiftUI struct ImageScrollView: UIViewRepresentable { let image: UIImage + @Binding var isContentScrolling: Bool func makeCoordinator() -> Coordinator { Coordinator(self) @@ -120,6 +121,24 @@ struct ImageScrollView: UIViewRepresentable { imageView.frame = frameToCenter } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + parent.isContentScrolling = true + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.parent.isContentScrolling = false + } + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.parent.isContentScrolling = false + } + } } } diff --git a/Django Files/Views/Preview/Preview.swift b/Django Files/Views/Preview/Preview.swift index e707322..a5b3325 100644 --- a/Django Files/Views/Preview/Preview.swift +++ b/Django Files/Views/Preview/Preview.swift @@ -16,6 +16,7 @@ struct ContentPreview: View { let file: DFFile var showFileInfo: Binding @Binding var selectedFileDetails: DFFile? + @Binding var isContentScrolling: Bool @State private var content: Data? @State private var isLoading = true @@ -70,7 +71,8 @@ struct ContentPreview: View { mimeType: mimeType, fileName: fileURL.lastPathComponent, isLoading: isLoading, - error: error + error: error, + isContentScrolling: $isContentScrolling ) .background(.black) } @@ -82,7 +84,7 @@ struct ContentPreview: View { AnimatedImageScrollView(data: content) .frame(width: geometry.size.width, height: geometry.size.height) } else if let uiImage = UIImage(data: content) { - ImageScrollView(image: uiImage) + ImageScrollView(image: uiImage, isContentScrolling: $isContentScrolling) .frame(width: geometry.size.width, height: geometry.size.height) } else { Text("Unable to load image") @@ -211,6 +213,7 @@ struct PageViewController: UIViewControllerRepresentable { var redirectURLs: [String: String] var showFileInfo: Binding @Binding var selectedFileDetails: DFFile? + @Binding var isContentScrolling: Bool var onPageChange: (Int) -> Void var onLoadMore: (() async -> Void)? var isDragging: Bool @@ -304,7 +307,8 @@ struct PageViewController: UIViewControllerRepresentable { fileURL: URL(string: parent.redirectURLs[file.raw] ?? file.raw)!, file: file, showFileInfo: parent.showFileInfo, - selectedFileDetails: parent.$selectedFileDetails + selectedFileDetails: parent.$selectedFileDetails, + isContentScrolling: parent.$isContentScrolling ) let vc = UIHostingController(rootView: contentPreview) vc.view.backgroundColor = .clear @@ -445,6 +449,7 @@ struct FilePreviewView: View { @State private var showingShareSheet = false @State private var isOverlayVisible = true + @State private var isContentScrolling = false private var isDeepLinkPreview: Bool { fileListDelegate == nil @@ -516,6 +521,7 @@ struct FilePreviewView: View { redirectURLs: redirectURLs, showFileInfo: $showFileInfo, selectedFileDetails: $selectedFileDetails, + isContentScrolling: $isContentScrolling, onPageChange: { newIndex in onNavigate(newIndex) Task { @@ -634,21 +640,41 @@ struct FilePreviewView: View { } .offset(dragOffset) .simultaneousGesture( - DragGesture() + DragGesture(minimumDistance: 100) .onChanged { gesture in - if gesture.translation.height > 10 { - dragOffset = gesture.translation - isDragging = true + // Don't trigger dismiss if content is currently scrolling + guard !isContentScrolling else { return } + + // Only trigger dismiss if gesture is clearly a dismiss intent + // Check if this is a vertical downward gesture with minimal horizontal movement + let isVerticalGesture = abs(gesture.translation.height) > abs(gesture.translation.width) * 2 + let isDownwardGesture = gesture.translation.height > 0 + let hasMinimalHorizontalMovement = abs(gesture.translation.width) < 20 + + if isVerticalGesture && isDownwardGesture && hasMinimalHorizontalMovement && gesture.translation.height > 15 { + dragOffset = gesture.translation + isDragging = true } } .onEnded { gesture in isDragging = false - if gesture.translation.height > 150 { + + // Don't dismiss if content was scrolling + guard !isContentScrolling else { + dragOffset = .zero + return + } + + // Only dismiss if gesture was clearly a dismiss intent + let isVerticalGesture = abs(gesture.translation.height) > abs(gesture.translation.width) * 2 + let isDownwardGesture = gesture.translation.height > 0 + let hasMinimalHorizontalMovement = abs(gesture.translation.width) < 30 + + if isVerticalGesture && isDownwardGesture && hasMinimalHorizontalMovement && gesture.translation.height > 120 { withAnimation(.spring()) { showingPreview = false } } else { - // Otherwise, animate the view back to its original position withAnimation(.spring()) { dragOffset = .zero } diff --git a/Django Files/Views/Preview/TextPreview.swift b/Django Files/Views/Preview/TextPreview.swift index d9c252b..79b6723 100644 --- a/Django Files/Views/Preview/TextPreview.swift +++ b/Django Files/Views/Preview/TextPreview.swift @@ -14,31 +14,45 @@ struct TextPreview: View { let fileName: String let isLoading: Bool let error: Error? + @Binding var isContentScrolling: Bool var body: some View { - ScrollView(showsIndicators: true) { - ZStack { - if isLoading { - HStack { - Spacer() - LoadingView() - .frame(width: 100, height: 100) - Spacer() + ScrollViewReader { proxy in + ScrollView(showsIndicators: true) { + ZStack { + if isLoading { + HStack { + Spacer() + LoadingView() + .frame(width: 100, height: 100) + Spacer() + } + } else if let content = content, let text = String(data: content, encoding: .utf8) { + CodeText(text) + .highlightLanguage(determineLanguage(from: mimeType, fileName: fileName)) + .padding() + } else if error != nil { + // Only show error message if there's an actual error + Text("Unable to decode text content") + .foregroundColor(.red) } - } else if let content = content, let text = String(data: content, encoding: .utf8) { - CodeText(text) - .highlightLanguage(determineLanguage(from: mimeType, fileName: fileName)) - .padding() - } else if error != nil { - // Only show error message if there's an actual error - Text("Unable to decode text content") - .foregroundColor(.red) } + .padding(.top, 40) } - .padding(.top, 40) + .refreshable(action: {}) // Empty refreshable to disable pull-to-refresh + .scrollDisabled(false) // Explicitly enable scrolling + .simultaneousGesture( + DragGesture() + .onChanged { _ in + isContentScrolling = true + } + .onEnded { _ in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + isContentScrolling = false + } + } + ) } - .refreshable(action: {}) // Empty refreshable to disable pull-to-refresh - .scrollDisabled(false) // Explicitly enable scrolling } // Helper function to determine the highlight language based on file type From eaa09f772335b829ddf9199f25edcf6a5fd35fa7 Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:52:16 -0500 Subject: [PATCH 7/8] support ios 18, preview cleanup, misc fixes --- Django Files.xcodeproj/project.pbxproj | 12 +- .../Views/ContextMenus/FileContextMenu.swift | 28 ++-- Django Files/Views/Preview/Preview.swift | 122 ++++++++++++++++-- 3 files changed, 128 insertions(+), 34 deletions(-) diff --git a/Django Files.xcodeproj/project.pbxproj b/Django Files.xcodeproj/project.pbxproj index 6f7b3f8..3c370b0 100644 --- a/Django Files.xcodeproj/project.pbxproj +++ b/Django Files.xcodeproj/project.pbxproj @@ -466,7 +466,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -513,7 +513,7 @@ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -585,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -642,7 +642,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -660,7 +660,7 @@ CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ZY6BPTGK47; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; MARKETING_VERSION = 0.0; PRODUCT_BUNDLE_IDENTIFIER = blastsoftstudios.djangofilesTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -679,7 +679,7 @@ CURRENT_PROJECT_VERSION = 0; DEVELOPMENT_TEAM = ZY6BPTGK47; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; MARKETING_VERSION = 0.0; PRODUCT_BUNDLE_IDENTIFIER = blastsoftstudios.djangofilesTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Django Files/Views/ContextMenus/FileContextMenu.swift b/Django Files/Views/ContextMenus/FileContextMenu.swift index 72a9785..dff1942 100644 --- a/Django Files/Views/ContextMenus/FileContextMenu.swift +++ b/Django Files/Views/ContextMenus/FileContextMenu.swift @@ -25,22 +25,22 @@ struct FileContextMenuButtons: View { } label: { Label("Open Preview", systemImage: "arrow.up.forward.app") } - - - Button { - onCopyShareLink() - notifyClipboard() - } label: { - Label("Copy Share Link", systemImage: "link") - } - Button { - onCopyRawLink() - notifyClipboard() - } label: { - Label("Copy Raw Link", systemImage: "link.circle") - } } + Button { + onCopyShareLink() + notifyClipboard() + } label: { + Label("Copy Share Link", systemImage: "link") + } + + Button { + onCopyRawLink() + notifyClipboard() + } label: { + Label("Copy Raw Link", systemImage: "link.circle") + } +// } Button { openRawBrowser() diff --git a/Django Files/Views/Preview/Preview.swift b/Django Files/Views/Preview/Preview.swift index a5b3325..3a62e33 100644 --- a/Django Files/Views/Preview/Preview.swift +++ b/Django Files/Views/Preview/Preview.swift @@ -207,6 +207,12 @@ struct ContentPreview: View { } } +func textWidth(text: String, font: UIFont) -> CGFloat { + let attributes: [NSAttributedString.Key: Any] = [.font: font] + let size = text.size(withAttributes: attributes) + return size.width +} + struct PageViewController: UIViewControllerRepresentable { var files: [DFFile] var currentIndex: Int @@ -402,6 +408,17 @@ struct PageViewController: UIViewControllerRepresentable { } } +extension View { + @ViewBuilder + func adaptiveSystemButtonStyle() -> some View { + if #available(iOS 26.0, *) { + self.buttonStyle(.glass) + } else { + self.buttonStyle(.bordered) + } + } +} + struct FilePreviewView: View { @Binding var file: DFFile let server: Binding @@ -451,10 +468,32 @@ struct FilePreviewView: View { @State private var isOverlayVisible = true @State private var isContentScrolling = false + @State private var marqueeOffset = 3.0 + @State private var animationID = UUID() + private var isDeepLinkPreview: Bool { fileListDelegate == nil } + private func resetMarqueeAnimation() { + // Generate new animation ID to cancel previous animations + animationID = UUID() + + // Immediately reset the offset to initial position without animation + marqueeOffset = 3.0 + + // Start animation if filename is long enough + if file.name.count > 26 { + // Use a slight delay to ensure the reset happens first + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + let baseAnimation = Animation.linear(duration: 8).delay(6).repeatForever() + withAnimation(baseAnimation) { + marqueeOffset = -textWidth(text: file.name, font: UIFont.systemFont(ofSize: 5)) + } + } + } + } + @MainActor private func preloadFiles() async { // Load the current file and preload adjacent files @@ -597,30 +636,66 @@ struct FilePreviewView: View { } } ToolbarItem(placement: .principal) { - Text(file.name) - .shadow(color: .black, radius: file.mime.starts(with: "text") ? 0 : 3) + Button(action: { + showFileInfo = true + }) { + VStack { + Text(file.name) + .hidden() + .overlay(alignment: file.name.count > 26 ? .leading : .center) { + Text(file.name) + .lineLimit(1) + .fixedSize() + .font(.subheadline) + + } + .offset(x: file.name.count > 26 ? marqueeOffset : 0) + .id(animationID) + .onAppear { + resetMarqueeAnimation() + } + .onChange(of: file.name) { _, _ in + resetMarqueeAnimation() + } + .mask( + LinearGradient( + gradient: Gradient(stops: [ + .init(color: .clear, location: 0.0), // Fades in from left + .init(color: .black, location: file.name.count < 26 ? 0.0 : 0.04), // Fully visible area starts + .init(color: .black, location: file.name.count < 26 ? 1.0 : 0.96), // Fully visible area ends + .init(color: .clear, location: 1.0) // Fades out on right + ]), + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(maxWidth: .infinity) + + ZStack { + Text(file.formattedDate()) + .font(.caption) + } +// if let gpsArea = file.meta?["GPSArea"]?.value as? String { +// Text(gpsArea) +// .font(.custom("SF Pro", size: 11, relativeTo: .caption)) +// } + } + } + .frame(minWidth: 200) + .adaptiveSystemButtonStyle() } - ToolbarItem { + ToolbarItem(placement: .topBarTrailing) { if !isDeepLinkPreview { Menu { fileContextMenu(for: file, isPreviewing: true, isPrivate: file.private, expirationText: $expirationText, passwordText: $passwordText, fileNameText: $fileNameText) } label: { Image(systemName: "ellipsis") + .foregroundColor(Color.white) } + } } ToolbarItemGroup(placement: .bottomBar) { - Button(action: { - showFileInfo = true - }) { - Image(systemName: "info.circle") - } - Menu { - fileShareMenu(for: file) - } label: { - Image(systemName: "link.icloud") - } - Button(action: { showingShareSheet = true }) { @@ -632,6 +707,25 @@ struct FilePreviewView: View { .presentationDetents([.medium]) } } + Spacer() +// Menu { +// fileShareMenu(for: file) +// } label: { +// Image(systemName: "link.icloud") +// } +// Button(action: { +// showFileInfo = true +// }) { +// Image(systemName: "info.circle") +// } +// Spacer() + Button(action: { + fileIDsToDelete = [file.id] + fileNameToDelete = file.name + showingDeleteConfirmation = true + }) { + Image(systemName: "trash") + } } } .toolbarVisibility(isOverlayVisible ? .visible : .hidden, for: .navigationBar) From c05e5b37234d3bc33f39bf54d75a48462c947f01 Mon Sep 17 00:00:00 2001 From: Ralph Luaces <16159770+raluaces@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:23:35 -0500 Subject: [PATCH 8/8] fix swipe between text files --- Django Files/Views/Preview/Preview.swift | 1 - Django Files/Views/Preview/TextPreview.swift | 127 ++++++++++++++----- 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/Django Files/Views/Preview/Preview.swift b/Django Files/Views/Preview/Preview.swift index 3a62e33..7ceffa7 100644 --- a/Django Files/Views/Preview/Preview.swift +++ b/Django Files/Views/Preview/Preview.swift @@ -49,7 +49,6 @@ struct ContentPreview: View { pdfPreview } else if mimeType.starts(with: "text/") || (mimeType.starts(with: "application/") && mimeType.contains("json")) { textPreview - .padding(.top, 60) } else if mimeType.starts(with: "image/") { imagePreview } else if mimeType.starts(with: "video/") { diff --git a/Django Files/Views/Preview/TextPreview.swift b/Django Files/Views/Preview/TextPreview.swift index 79b6723..6153d03 100644 --- a/Django Files/Views/Preview/TextPreview.swift +++ b/Django Files/Views/Preview/TextPreview.swift @@ -17,41 +17,23 @@ struct TextPreview: View { @Binding var isContentScrolling: Bool var body: some View { - ScrollViewReader { proxy in - ScrollView(showsIndicators: true) { - ZStack { - if isLoading { - HStack { - Spacer() - LoadingView() - .frame(width: 100, height: 100) - Spacer() - } - } else if let content = content, let text = String(data: content, encoding: .utf8) { - CodeText(text) - .highlightLanguage(determineLanguage(from: mimeType, fileName: fileName)) - .padding() - } else if error != nil { - // Only show error message if there's an actual error - Text("Unable to decode text content") - .foregroundColor(.red) - } - } - .padding(.top, 40) + if isLoading { + HStack { + Spacer() + LoadingView() + .frame(width: 100, height: 100) + Spacer() } - .refreshable(action: {}) // Empty refreshable to disable pull-to-refresh - .scrollDisabled(false) // Explicitly enable scrolling - .simultaneousGesture( - DragGesture() - .onChanged { _ in - isContentScrolling = true - } - .onEnded { _ in - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - isContentScrolling = false - } - } + } else if let content = content, let text = String(data: content, encoding: .utf8) { + TextScrollView( + text: text, + language: determineLanguage(from: mimeType, fileName: fileName), + isContentScrolling: $isContentScrolling ) + .ignoresSafeArea() + } else if error != nil { + Text("Unable to decode text content") + .foregroundColor(.red) } } @@ -138,3 +120,82 @@ struct TextPreview: View { } } } + +struct TextScrollView: UIViewRepresentable { + let text: String + let language: HighlightLanguage + @Binding var isContentScrolling: Bool + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> UIScrollView { + let scrollView = UIScrollView() + scrollView.delegate = context.coordinator + scrollView.showsVerticalScrollIndicator = true + scrollView.showsHorizontalScrollIndicator = false + + // Create a hosting controller for the SwiftUI CodeText + let hostingController = UIHostingController(rootView: AnyView( + CodeText(text) + .highlightLanguage(language) + )) + hostingController.view.backgroundColor = .clear + hostingController.view.layoutMargins = .zero + hostingController.view.insetsLayoutMarginsFromSafeArea = false + + // Add the hosting controller's view to the scroll view + scrollView.addSubview(hostingController.view) + context.coordinator.hostingController = hostingController + + // Set up constraints with negative top margin to eliminate padding + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: -35), + hostingController.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + hostingController.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + ]) + + return scrollView + } + + func updateUIView(_ scrollView: UIScrollView, context: Context) { + // Update the text content if needed + if let hostingController = context.coordinator.hostingController { + hostingController.rootView = AnyView( + CodeText(text) + .highlightLanguage(language) + ) + } + } + + class Coordinator: NSObject, UIScrollViewDelegate { + let parent: TextScrollView + weak var hostingController: UIHostingController? + + init(_ parent: TextScrollView) { + self.parent = parent + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + parent.isContentScrolling = true + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.parent.isContentScrolling = false + } + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.parent.isContentScrolling = false + } + } + } +}