Compare commits
4 Commits
f302f8800f
...
eddc8a35ff
| Author | SHA1 | Date | |
|---|---|---|---|
| eddc8a35ff | |||
| 446be3424e | |||
| b8e5766cb1 | |||
| e61dbc7d72 |
@ -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;
|
||||
|
||||
87
readeck/Resources/RELEASE_NOTES.md
Normal file
87
readeck/Resources/RELEASE_NOTES.md
Normal 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
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
readeck/UI/Settings/ReleaseNotesView.swift
Normal file
48
readeck/UI/Settings/ReleaseNotesView.swift
Normal 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()
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
readeck/Utils/VersionManager.swift
Normal file
38
readeck/Utils/VersionManager.swift
Normal 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() {}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user