Compare commits

..

No commits in common. "eddc8a35ff82990b8fb5bde9f8d81937f4823ed3" and "f302f8800f92decf5da8477cca3920d09b3fda6b" have entirely different histories.

9 changed files with 83 additions and 371 deletions

View File

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

View File

@ -1,87 +0,0 @@
# 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,7 +197,6 @@ struct BookmarkDetailLegacyView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
#if DEBUG
// Toggle button (left) // Toggle button (left)
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
if #available(iOS 26.0, *) { if #available(iOS 26.0, *) {
@ -209,7 +208,6 @@ struct BookmarkDetailLegacyView: View {
} }
} }
} }
#endif
// Top toolbar (right) // Top toolbar (right)
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {

View File

@ -39,11 +39,20 @@ struct BookmarkDetailView2: View {
} }
private var mainView: some View { private var mainView: some View {
content VStack(spacing: 0) {
// Progress bar at top
ProgressView(value: readingProgress)
.progressViewStyle(LinearProgressViewStyle())
.frame(height: 3)
// Main scroll content
scrollViewContent
}
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
toolbarContent toolbarContent
} }
.toolbarBackgroundVisibility(.hidden, for: .bottomBar)
.sheet(isPresented: $showingFontSettings) { .sheet(isPresented: $showingFontSettings) {
fontSettingsSheet fontSettingsSheet
} }
@ -76,61 +85,6 @@ struct BookmarkDetailView2: View {
} }
} }
private var content: some View {
VStack(spacing: 0) {
// Progress bar at top
ProgressView(value: readingProgress)
.progressViewStyle(LinearProgressViewStyle())
.frame(height: 3)
// Main scroll content
scrollViewContent
.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)
}
}
.padding(.trailing, 1)
.padding(.bottom, 10)
}
private var scrollViewContent: some View { private var scrollViewContent: some View {
GeometryReader { geometry in GeometryReader { geometry in
ScrollView { ScrollView {
@ -182,7 +136,7 @@ struct BookmarkDetailView2: View {
} }
.coordinateSpace(name: "scrollView") .coordinateSpace(name: "scrollView")
.clipped() .clipped()
.ignoresSafeArea(edges: [.top, .bottom]) .ignoresSafeArea(edges: .top)
.scrollPosition($scrollPosition) .scrollPosition($scrollPosition)
.onPreferenceChange(ContentHeightPreferenceKey.self) { endPosition in .onPreferenceChange(ContentHeightPreferenceKey.self) { endPosition in
contentEndPosition = endPosition contentEndPosition = endPosition
@ -217,10 +171,9 @@ struct BookmarkDetailView2: View {
let reachedEnd = progress >= 1.0 && lastSentProgress < 1.0 let reachedEnd = progress >= 1.0 && lastSentProgress < 1.0
let shouldUpdate = abs(progress - lastSentProgress) >= threshold || reachedEnd let shouldUpdate = abs(progress - lastSentProgress) >= threshold || reachedEnd
readingProgress = progress
if shouldUpdate { if shouldUpdate {
lastSentProgress = progress lastSentProgress = progress
readingProgress = progress
viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: progress, anchor: nil) viewModel.debouncedUpdateReadProgress(id: bookmarkId, progress: progress, anchor: nil)
} }
} }
@ -261,6 +214,40 @@ 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 { private var fontSettingsSheet: some View {
@ -291,18 +278,11 @@ struct BookmarkDetailView2: View {
private func headerView(width: CGFloat) -> some View { private func headerView(width: CGFloat) -> some View {
if !viewModel.bookmarkDetail.imageUrl.isEmpty { if !viewModel.bookmarkDetail.imageUrl.isEmpty {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
// Background blur for images that don't fill
CachedAsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) CachedAsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl))
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: width, height: headerHeight) .frame(width: width, height: headerHeight)
.blur(radius: 30)
.clipped() .clipped()
// Main image with fit
CachedAsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl))
.aspectRatio(contentMode: .fit)
.frame(width: width, height: headerHeight)
// Zoom icon // Zoom icon
Button(action: { Button(action: {
showingImageViewer = true showingImageViewer = true

View File

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

View File

@ -1,48 +0,0 @@
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,7 +9,6 @@ import SwiftUI
struct SettingsGeneralView: View { struct SettingsGeneralView: View {
@State private var viewModel: SettingsGeneralViewModel @State private var viewModel: SettingsGeneralViewModel
@State private var showReleaseNotes = false
init(viewModel: SettingsGeneralViewModel = SettingsGeneralViewModel()) { init(viewModel: SettingsGeneralViewModel = SettingsGeneralViewModel()) {
self.viewModel = viewModel self.viewModel = viewModel
@ -23,24 +22,6 @@ struct SettingsGeneralView: View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text("General") Text("General")
.font(.headline) .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) Toggle("Read Aloud Feature", isOn: $viewModel.enableTTS)
.toggleStyle(.switch) .toggleStyle(.switch)
.onChange(of: viewModel.enableTTS) { .onChange(of: viewModel.enableTTS) {
@ -117,9 +98,6 @@ struct SettingsGeneralView: View {
#endif #endif
} }
.sheet(isPresented: $showReleaseNotes) {
ReleaseNotesView()
}
.task { .task {
await viewModel.loadGeneralSettings() await viewModel.loadGeneralSettings()
} }

View File

@ -51,58 +51,3 @@ 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

@ -1,38 +0,0 @@
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() {}
}