Compare commits

...

4 Commits

Author SHA1 Message Date
eddc8a35ff bumped build version 2025-10-14 14:22:41 +02:00
446be3424e docs: Improve release notes with user-friendly language and better formatting
Release notes improvements:
- Rewrote technical descriptions for better user understanding
- Replaced technical jargon with clear benefits
- Added blank lines after section headers for better readability
- Focused on what users gain instead of implementation details

View improvements:
- Use AttributedString with proper markdown parsing
- Enable text selection for copying content
- Better markdown rendering with .interpretedSyntax option

Content updates:
- AppStore link and introduction added
- User-focused feature descriptions
- Clear benefit-oriented language
- Acknowledgment for community contributions
2025-10-14 14:20:39 +02:00
b8e5766cb1 feat: Add release notes system with auto-popup on version updates
Implement comprehensive release notes feature:
- RELEASE_NOTES.md with version 1.0 and 1.1 content in English
- VersionManager to track app versions and detect updates
- ReleaseNotesView with native markdown rendering
- Auto-popup sheet on first launch after version update
- Manual access via "What's New" button in General Settings

Features:
- Markdown-based release notes stored in app bundle
- Automatic version detection using CFBundleShortVersionString
- UserDefaults tracking of last seen version
- Dismissable sheet with "Done" button
- Settings button shows current version number

Technical implementation:
- VersionManager singleton for version tracking
- Sheet presentation in MainTabView on new version
- Settings integration with sparkles icon
- Native SwiftUI Text markdown rendering
- Bundle resource loading for RELEASE_NOTES.md

Release notes content:
- Version 1.1: iOS 26 features, floating buttons, progress tracking
- Version 1.0: Initial release features and capabilities
2025-10-14 14:04:28 +02:00
e61dbc7d72 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
2025-10-14 13:53:31 +02:00
9 changed files with 372 additions and 84 deletions

View File

@ -437,7 +437,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 29;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -470,7 +470,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 29;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -625,7 +625,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 29;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;
@ -669,7 +669,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 29;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;

View File

@ -0,0 +1,87 @@
# Release Notes
Thanks for using the Readeck iOS app! Below are the release notes for each version.
**AppStore:** The App is now in the App Store! [Get it here](https://apps.apple.com/de/app/readeck/id6748764703) for all TestFlight users. If you wish a more stable Version, please download it from there. Or you can continue using TestFlight for the latest features.
## Version 1.1
There is a lot of feature reqeusts and improvements in this release which are based on your feedback. Thank you so much for that! If you like the new features, please consider leaving a review on the App Store to support further development.
### Modern Reading Experience (iOS 26+)
- **Completely rebuilt article view** for the latest iOS version
- Smoother scrolling and faster page loading
- Better battery life and memory usage
- Native iOS integration for the best experience
### Quick Actions
- **Smart action buttons** appear automatically when you're almost done reading
- Beautiful, modern design that blends with your content
- Quickly favorite or archive articles without scrolling back up
- Buttons fade away elegantly when you scroll back
- Your progress bar now reflects the entire article length
### Beautiful Article Images
- **Article header images now display properly** without awkward cropping
- Full images with a subtle blurred background
- Tap to view images in full screen
### Smoother Performance
- **Dramatically improved scrolling** - no more stuttering or lag
- Faster article loading times
- Better handling of long articles with many images
- Overall snappier app experience
### Open Links Your Way
- **Choose your preferred browser** for opening links
- Open in Safari or in-app browser
- Thanks to christian-putzke for this contribution!
### Fixes & Improvements
- Articles no longer overflow the screen width
- Fixed spacing issues in article view
- Improved progress calculation accuracy
- Better handling of article content
- Fixed issues with label names containing spaces
---
## Version 1.0 (Initial Release)
### Core Features
- Browse and read saved articles
- Bookmark management with labels
- Full article view with custom fonts
- Text-to-speech support (Beta)
- Archive and favorite functionality
- Choose different Layouts (Compact, Magazine, Natural)
### Reading Experience
- Clean, distraction-free reading interface
- Customizable font settings
- Header Image viewer with zoom support
- Progress tracking per article
- Dark mode support
### Organization
- Label system for categorization (multi-select)
- Search
- Archive completed articles
- Jump to last read position
### Share Extension
- Save articles from other apps
- Quick access to save and label bookmarks
- Save Bookmarks offline if your server is not reachable and sync later

View File

@ -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) {

View File

@ -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

View File

@ -5,21 +5,37 @@ struct MainTabView: View {
@State private var selectedTab: SidebarTab = .unread
@State var selectedBookmark: Bookmark?
@StateObject private var playerUIState = PlayerUIState()
@State private var showReleaseNotes = false
// sizeClass
@Environment(\.horizontalSizeClass)
var horizontalSizeClass
@Environment(\.verticalSizeClass)
var verticalSizeClass
var body: some View {
if UIDevice.isPhone {
PhoneTabView()
.environmentObject(playerUIState)
} else {
PadSidebarView()
.environmentObject(playerUIState)
Group {
if UIDevice.isPhone {
PhoneTabView()
.environmentObject(playerUIState)
} else {
PadSidebarView()
.environmentObject(playerUIState)
}
}
.sheet(isPresented: $showReleaseNotes) {
ReleaseNotesView()
}
.onAppear {
checkForNewVersion()
}
}
private func checkForNewVersion() {
if VersionManager.shared.isNewVersion {
showReleaseNotes = true
VersionManager.shared.markVersionAsSeen()
}
}
}

View File

@ -0,0 +1,48 @@
import SwiftUI
struct ReleaseNotesView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
if let attributedString = loadReleaseNotes() {
Text(attributedString)
.textSelection(.enabled)
.padding()
} else {
Text("Unable to load release notes")
.foregroundColor(.secondary)
.padding()
}
}
}
.navigationTitle("What's New")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
}
}
private func loadReleaseNotes() -> AttributedString? {
guard let url = Bundle.main.url(forResource: "RELEASE_NOTES", withExtension: "md"),
let markdownContent = try? String(contentsOf: url),
let attributedString = try? AttributedString(
markdown: markdownContent,
options: .init(interpretedSyntax: .full)
) else {
return nil
}
return attributedString
}
}
#Preview {
ReleaseNotesView()
}

View File

@ -9,11 +9,12 @@ import SwiftUI
struct SettingsGeneralView: View {
@State private var viewModel: SettingsGeneralViewModel
@State private var showReleaseNotes = false
init(viewModel: SettingsGeneralViewModel = SettingsGeneralViewModel()) {
self.viewModel = viewModel
}
var body: some View {
VStack(spacing: 20) {
SectionHeader(title: "General Settings".localized, icon: "gear")
@ -22,6 +23,24 @@ struct SettingsGeneralView: View {
VStack(alignment: .leading, spacing: 12) {
Text("General")
.font(.headline)
// What's New Button
Button(action: {
showReleaseNotes = true
}) {
HStack {
Label("What's New", systemImage: "sparkles")
Spacer()
Text("Version \(VersionManager.shared.currentVersion)")
.font(.caption)
.foregroundColor(.secondary)
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
.buttonStyle(.plain)
Toggle("Read Aloud Feature", isOn: $viewModel.enableTTS)
.toggleStyle(.switch)
.onChange(of: viewModel.enableTTS) {
@ -98,6 +117,9 @@ struct SettingsGeneralView: View {
#endif
}
.sheet(isPresented: $showReleaseNotes) {
ReleaseNotesView()
}
.task {
await viewModel.loadGeneralSettings()
}

View File

@ -51,3 +51,58 @@ struct readeckApp: App {
}
}
}
struct TestView: View {
var body: some View {
if #available(iOS 26.0, *) {
Text("hello")
.toolbar {
ToolbarSpacer(.flexible)
ToolbarItem {
Button {
} label: {
Label("Favorite", systemImage: "share")
.symbolVariant(.none)
}
}
ToolbarSpacer(.fixed)
ToolbarItemGroup {
Button {
} label: {
Label("Favorite", systemImage: "heart")
.symbolVariant(.none)
}
Button("Info", systemImage: "info") {
}
}
ToolbarItemGroup(placement: .bottomBar) {
Spacer()
Button {
} label: {
Label("Favorite", systemImage: "heart")
.symbolVariant(.none)
}
Button("Info", systemImage: "info") {
}
}
}
.toolbar(removing: .title)
.ignoresSafeArea(edges: .top)
} else {
Text("hello1")
}
}
}

View File

@ -0,0 +1,38 @@
import Foundation
class VersionManager {
static let shared = VersionManager()
private let lastSeenVersionKey = "lastSeenAppVersion"
private let userDefaults = UserDefaults.standard
var currentVersion: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
}
var currentBuild: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
}
var fullVersion: String {
"\(currentVersion) (\(currentBuild))"
}
var lastSeenVersion: String? {
userDefaults.string(forKey: lastSeenVersionKey)
}
var isNewVersion: Bool {
guard let lastSeen = lastSeenVersion else {
// First launch
return true
}
return lastSeen != currentVersion
}
func markVersionAsSeen() {
userDefaults.set(currentVersion, forKey: lastSeenVersionKey)
}
private init() {}
}