Compare commits

...

5 Commits

Author SHA1 Message Date
534ceddad4 bumped build version 2025-09-17 22:39:42 +02:00
dcbe0515fc fix: Share extension title extraction and theme persistence
- Enable text support in share extension to extract page titles
- Extract titles from attributedTitle and attributedContentText
- Prevent titles from being used as URLs with proper validation
- Fix theme settings persistence using SettingsRepository instead of UserDefaults
- Theme changes now properly notify the app for immediate updates
2025-09-17 22:27:52 +02:00
ba74430d10 feat: Improve label input functionality
- Split label input on space to create multiple labels at once
- Disable autocapitalization in tag search field
- Prevent duplicate labels when adding multiple at once
2025-09-17 13:36:36 +02:00
fbf840888a bumped build version 2025-09-05 21:59:18 +02:00
c13fc107b1 fix: Card width consistency and layout loading in search
- Fixed natural layout width using screen bounds instead of infinity
- Added card layout settings loading in SearchBookmarksView
- Consistent card width across all views prevents overflow
2025-09-05 21:58:24 +02:00
9 changed files with 103 additions and 23 deletions

View File

@ -8,6 +8,8 @@
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>

View File

@ -67,8 +67,22 @@ class ShareBookmarkViewModel: ObservableObject {
logger.warning("No extension context available for content extraction")
return
}
var extractedUrl: String?
var extractedTitle: String?
for item in extensionContext.inputItems {
guard let inputItem = item as? NSExtensionItem else { continue }
// Use the inputItem's attributedTitle or attributedContentText as potential title
if let attributedTitle = inputItem.attributedTitle?.string, !attributedTitle.isEmpty {
extractedTitle = attributedTitle
logger.info("Extracted title from input item: \(attributedTitle)")
} else if let attributedContent = inputItem.attributedContentText?.string, !attributedContent.isEmpty {
extractedTitle = attributedContent
logger.info("Extracted title from content text: \(attributedContent)")
}
for attachment in inputItem.attachments ?? [] {
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (url, error) in
@ -76,6 +90,12 @@ class ShareBookmarkViewModel: ObservableObject {
if let url = url as? URL {
self?.url = url.absoluteString
self?.logger.info("Extracted URL from shared content: \(url.absoluteString)")
// Set title if we extracted one and current title is empty
if let title = extractedTitle, self?.title.isEmpty == true {
self?.title = title
self?.logger.info("Set title from shared content: \(title)")
}
} else if let error = error {
self?.logger.error("Failed to extract URL: \(error.localizedDescription)")
}
@ -85,9 +105,18 @@ class ShareBookmarkViewModel: ObservableObject {
if attachment.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
attachment.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (text, error) in
DispatchQueue.main.async {
if let text = text as? String, let url = URL(string: text) {
self?.url = url.absoluteString
self?.logger.info("Extracted URL from shared text: \(url.absoluteString)")
if let text = text as? String {
// Only treat as URL if it's a valid URL and we don't have one yet
if self?.url == nil, let url = URL(string: text), url.scheme != nil {
self?.url = url.absoluteString
self?.logger.info("Extracted URL from shared text: \(url.absoluteString)")
} else {
// If not a valid URL or we already have a URL, treat as potential title
if self?.title.isEmpty == true {
self?.title = text
self?.logger.info("Set title from shared text: \(text)")
}
}
} else if let error = error {
self?.logger.error("Failed to extract text: \(error.localizedDescription)")
}

View File

@ -15,9 +15,9 @@
```mermaid
graph TD
UI["UI Layer\n(View, ViewModel)"]
Domain["Domain Layer\n(Use Cases, Models, Repository Protocols)"]
Data["Data Layer\n(Repository implementations, Database, Entities, API)"]
UI["UI Layer (View, ViewModel)"]
Domain["Domain Layer (Use Cases, Models, Repository Protocols)"]
Data["Data Layer (Repository implementations, Database, Entities, API)"]
UI --> Domain
Domain --> Data
```

View File

@ -436,7 +436,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -469,7 +469,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -624,7 +624,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;
@ -668,7 +668,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;

View File

@ -73,10 +73,15 @@ class BookmarkLabelsViewModel {
@MainActor
func addLabel(to bookmarkId: String, label: String) async {
let trimmedLabel = label.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedLabel.isEmpty else { return }
let individualLabels = label
.components(separatedBy: " ")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.filter { !currentLabels.contains($0) }
await addLabels(to: bookmarkId, labels: [trimmedLabel])
guard !individualLabels.isEmpty else { return }
await addLabels(to: bookmarkId, labels: individualLabels)
newLabelText = ""
searchText = ""
}

View File

@ -275,8 +275,9 @@ struct BookmarkCardView: View {
VStack(alignment: .leading, spacing: 8) {
ZStack(alignment: .bottomTrailing) {
CachedAsyncImage(url: imageURL)
.aspectRatio(contentMode: .fit)
.frame(minHeight: 180)
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width - 32)
.clipped()
.clipShape(RoundedRectangle(cornerRadius: 8))
if bookmark.readProgress > 0 && bookmark.isArchived == false && bookmark.isMarked == false {

View File

@ -133,6 +133,7 @@ struct TagManagementView: View {
.textFieldStyle(CustomTextFieldStyle())
.keyboardType(.default)
.autocorrectionDisabled(true)
.autocapitalization(.none)
.onSubmit {
onAddCustomTag()
}

View File

@ -7,6 +7,7 @@ struct SearchBookmarksView: View {
@Binding var selectedBookmark: Bookmark?
@Namespace private var namespace
@State private var isFirstAppearance = true
@State private var cardLayoutStyle: CardLayoutStyle = .magazine
var body: some View {
VStack(spacing: 0) {
@ -61,10 +62,22 @@ struct SearchBookmarksView: View {
}
}
}) {
BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in })
BookmarkCardView(
bookmark: bookmark,
currentState: .all,
layout: cardLayoutStyle,
onArchive: {_ in },
onDelete: {_ in },
onToggleFavorite: {_ in }
)
}
.buttonStyle(PlainButtonStyle())
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.listRowInsets(EdgeInsets(
top: cardLayoutStyle == .compact ? 8 : 12,
leading: 16,
bottom: cardLayoutStyle == .compact ? 8 : 12,
trailing: 16
))
.listRowSeparator(.hidden)
.listRowBackground(Color(R.color.bookmark_list_bg))
}
@ -98,6 +111,22 @@ struct SearchBookmarksView: View {
searchFieldIsFocused = true
isFirstAppearance = false
}
loadCardLayoutStyle()
}
.onReceive(NotificationCenter.default.publisher(for: .cardLayoutChanged)) { notification in
if let layout = notification.object as? CardLayoutStyle {
cardLayoutStyle = layout
}
}
}
private func loadCardLayoutStyle() {
Task {
let loadCardLayoutUseCase = DefaultUseCaseFactory.shared.makeLoadCardLayoutUseCase()
let layout = await loadCardLayoutUseCase.execute()
await MainActor.run {
cardLayoutStyle = layout
}
}
}
}

View File

@ -6,10 +6,12 @@ struct AppearanceSettingsView: View {
private let loadCardLayoutUseCase: PLoadCardLayoutUseCase
private let saveCardLayoutUseCase: PSaveCardLayoutUseCase
private let settingsRepository: PSettingsRepository
init(factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
self.loadCardLayoutUseCase = factory.makeLoadCardLayoutUseCase()
self.saveCardLayoutUseCase = factory.makeSaveCardLayoutUseCase()
self.settingsRepository = SettingsRepository()
}
var body: some View {
@ -58,18 +60,29 @@ struct AppearanceSettingsView: View {
}
private func loadSettings() {
// Load theme setting
let themeString = UserDefaults.standard.string(forKey: "selectedTheme") ?? "system"
selectedTheme = Theme(rawValue: themeString) ?? .system
// Load card layout setting
Task {
// Load both theme and card layout from repository
if let settings = try? await settingsRepository.loadSettings() {
await MainActor.run {
selectedTheme = settings.theme ?? .system
}
}
selectedCardLayout = await loadCardLayoutUseCase.execute()
}
}
private func saveThemeSettings() {
UserDefaults.standard.set(selectedTheme.rawValue, forKey: "selectedTheme")
Task {
// Load current settings, update theme, and save back
var settings = (try? await settingsRepository.loadSettings()) ?? Settings()
settings.theme = selectedTheme
try? await settingsRepository.saveSettings(settings)
// Notify app about theme change
await MainActor.run {
NotificationCenter.default.post(name: .settingsChanged, object: nil)
}
}
}
private func saveCardLayoutSettings() {