From e61dbc7d727c8f05f8b639bda57842fe73cb128f Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Tue, 14 Oct 2025 13:53:31 +0200 Subject: [PATCH] feat: Add iOS 26 native WebView with floating action buttons and improved header BookmarkDetailView2 enhancements: - Implement floating action buttons with iOS 26 GlassEffect - Buttons appear at 90% reading progress with slide-up animation - Use GlassEffectContainer with liquid glass interaction effect - Position buttons in bottom-right corner with spring animation - Auto-hide when scrolling back above 90% Header image improvements: - Use aspect fit with blurred background for better image display - Prevents random cropping of header images - Maintains full image visibility while filling header space Debug-only features: - Add #if DEBUG wrapper for view toggle buttons - Toggle between legacy and native WebView only in debug builds Technical details: - GlassEffectContainer with 52pt buttons and 31pt icons - Spring animation (response: 0.6, damping: 0.8) - Combined move and opacity transitions - Full screen ScrollView with bottom safe area extension - Blurred background layer for non-filling images --- .../BookmarkDetailLegacyView.swift | 2 + .../BookmarkDetail/BookmarkDetailView2.swift | 158 ++++++++++-------- 2 files changed, 91 insertions(+), 69 deletions(-) diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailLegacyView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailLegacyView.swift index 674cabd..34c4154 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailLegacyView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailLegacyView.swift @@ -197,6 +197,7 @@ struct BookmarkDetailLegacyView: View { .frame(maxWidth: .infinity) .navigationBarTitleDisplayMode(.inline) .toolbar { + #if DEBUG // Toggle button (left) ToolbarItem(placement: .navigationBarLeading) { if #available(iOS 26.0, *) { @@ -208,6 +209,7 @@ struct BookmarkDetailLegacyView: View { } } } + #endif // Top toolbar (right) ToolbarItem(placement: .navigationBarTrailing) { diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift index 93a1034..b37f463 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift @@ -39,6 +39,44 @@ struct BookmarkDetailView2: View { } private var mainView: some View { + content + .navigationBarTitleDisplayMode(.inline) + .toolbar { + toolbarContent + } + .sheet(isPresented: $showingFontSettings) { + fontSettingsSheet + } + .sheet(isPresented: $showingLabelsSheet) { + BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels) + } + .sheet(isPresented: $showingImageViewer) { + ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl) + } + .onChange(of: showingFontSettings) { _, isShowing in + if !isShowing { + Task { + await viewModel.loadBookmarkDetail(id: bookmarkId) + } + } + } + .onChange(of: showingLabelsSheet) { _, isShowing in + if !isShowing { + Task { + await viewModel.refreshBookmarkDetail(id: bookmarkId) + } + } + } + .onChange(of: viewModel.readProgress) { _, progress in + showJumpToProgressButton = progress > 0 && progress < 100 + } + .task { + await viewModel.loadBookmarkDetail(id: bookmarkId) + await viewModel.loadArticleContent(id: bookmarkId) + } + } + + private var content: some View { VStack(spacing: 0) { // Progress bar at top ProgressView(value: readingProgress) @@ -47,42 +85,50 @@ struct BookmarkDetailView2: View { // Main scroll content scrollViewContent - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - toolbarContent - } - .toolbarBackgroundVisibility(.hidden, for: .bottomBar) - .sheet(isPresented: $showingFontSettings) { - fontSettingsSheet - } - .sheet(isPresented: $showingLabelsSheet) { - BookmarkLabelsView(bookmarkId: bookmarkId, initialLabels: viewModel.bookmarkDetail.labels) - } - .sheet(isPresented: $showingImageViewer) { - ImageViewerView(imageUrl: viewModel.bookmarkDetail.imageUrl) - } - .onChange(of: showingFontSettings) { _, isShowing in - if !isShowing { - Task { - await viewModel.loadBookmarkDetail(id: bookmarkId) + .overlay(alignment: .bottomTrailing) { + if viewModel.isLoadingArticle == false && viewModel.isLoading == false { + if readingProgress >= 0.9 { + floatingActionButtons + .transition(.move(edge: .bottom).combined(with: .opacity)) + } + } } + .animation(.spring(response: 0.6, dampingFraction: 0.8), value: readingProgress >= 0.9) + } + } + + private var floatingActionButtons: some View { + GlassEffectContainer(spacing: 52.0) { + HStack(spacing: 52.0) { + Button(action: { + Task { + await viewModel.toggleFavorite(id: bookmarkId) + } + }) { + Image(systemName: viewModel.bookmarkDetail.isMarked ? "star.fill" : "star") + .foregroundStyle(viewModel.bookmarkDetail.isMarked ? .yellow : .primary) + .frame(width: 52.0, height: 52.0) + .font(.system(size: 31)) + } + .disabled(viewModel.isLoading) + .glassEffect() + + Button(action: { + Task { + await viewModel.archiveBookmark(id: bookmarkId, isArchive: !viewModel.bookmarkDetail.isArchived) + } + }) { + Image(systemName: viewModel.bookmarkDetail.isArchived ? "checkmark.circle" : "archivebox") + .frame(width: 52.0, height: 52.0) + .font(.system(size: 31)) + } + .disabled(viewModel.isLoading) + .glassEffect() + .offset(x: -52.0, y: 0.0) } } - .onChange(of: showingLabelsSheet) { _, isShowing in - if !isShowing { - Task { - await viewModel.refreshBookmarkDetail(id: bookmarkId) - } - } - } - .onChange(of: viewModel.readProgress) { _, progress in - showJumpToProgressButton = progress > 0 && progress < 100 - } - .task { - await viewModel.loadBookmarkDetail(id: bookmarkId) - await viewModel.loadArticleContent(id: bookmarkId) - } + .padding(.trailing, 1) + .padding(.bottom, 10) } private var scrollViewContent: some View { @@ -136,7 +182,7 @@ struct BookmarkDetailView2: View { } .coordinateSpace(name: "scrollView") .clipped() - .ignoresSafeArea(edges: .top) + .ignoresSafeArea(edges: [.top, .bottom]) .scrollPosition($scrollPosition) .onPreferenceChange(ContentHeightPreferenceKey.self) { endPosition in contentEndPosition = endPosition @@ -171,9 +217,10 @@ struct BookmarkDetailView2: View { let reachedEnd = progress >= 1.0 && lastSentProgress < 1.0 let shouldUpdate = abs(progress - lastSentProgress) >= threshold || reachedEnd + readingProgress = progress + if shouldUpdate { lastSentProgress = progress - readingProgress = progress viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: progress, anchor: nil) } } @@ -214,40 +261,6 @@ struct BookmarkDetailView2: View { } } } - - #if DEBUG - // Bottom toolbar - Archive section - if viewModel.isLoadingArticle == false && viewModel.isLoading == false { - ToolbarItemGroup(placement: .bottomBar) { - - Spacer() - - Button(action: { - Task { - await viewModel.toggleFavorite(id: bookmarkId) - } - }) { - Label( - viewModel.bookmarkDetail.isMarked ? "Favorited" : "Favorite", - systemImage: viewModel.bookmarkDetail.isMarked ? "star.fill" : "star" - ) - } - .disabled(viewModel.isLoading) - - Button(action: { - Task { - await viewModel.archiveBookmark(id: bookmarkId, isArchive: !viewModel.bookmarkDetail.isArchived) - } - }) { - Label( - viewModel.bookmarkDetail.isArchived ? "Unarchive" : "Archive", - systemImage: viewModel.bookmarkDetail.isArchived ? "checkmark.circle" : "archivebox" - ) - } - .disabled(viewModel.isLoading) - } - } - #endif } private var fontSettingsSheet: some View { @@ -278,11 +291,18 @@ struct BookmarkDetailView2: View { private func headerView(width: CGFloat) -> some View { if !viewModel.bookmarkDetail.imageUrl.isEmpty { ZStack(alignment: .bottomTrailing) { + // Background blur for images that don't fill CachedAsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) .aspectRatio(contentMode: .fill) .frame(width: width, height: headerHeight) + .blur(radius: 30) .clipped() + // Main image with fit + CachedAsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) + .aspectRatio(contentMode: .fit) + .frame(width: width, height: headerHeight) + // Zoom icon Button(action: { showingImageViewer = true