Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/repo-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ Subsystem conventions and invariants for KMReader. `AGENTS.md` holds repo-wide r

- Trackers (`/api/v1/trackers/*`) are kmrs-private per-user integrations, not admin features and absent on Komga. `TrackerCapabilityStore` probes preferences with independent 24h negative capability records, clears preferences on account switches, and gates series actions by both support and per-user library preferences. Account and series management are online-only on iOS/macOS; tvOS has no tracker UI.
- Native OAuth is intentionally not implemented: kmrs's current callback requires a browser session, while KMReader uses API keys. Disconnected providers link to `/account/trackers` in the server web UI, never to a relay carrying the KMReader credential. Connected account status and logout use the kmrs endpoints; provider tokens remain server-side.
- `TrackerSeriesSheet` lists existing per-series bindings, adds a provider only when connected, and opens `TrackerLinkEditSheet` for search, binding mode/offset, and sparse remote updates. Empty remote fields are never submitted, binding-only saves do not call remote update, and an Unlink never edits the provider entry. Server-side automatic sync owns progress aggregation; the client does not mirror it. Grouped bindings and the manual-update ledger use `PaginationState`; the ledger is recent manual updates, not a full bound-series catalog.
- Supported, enabled series detail pages always expose the tracker flow below the metadata chips: branded binding chips show remote reading status, open their own `TrackerLinkEditSheet` on tap, and a persistent Add Tracker button opens `TrackerSeriesSheet`; the toolbar and card context-menu entries remain. The chips use `TrackerLogo`, the same provider artwork as kmweb, also shown in tracker account rows. No extra section title or enclosing card.
- `TrackerSeriesSheet` lists existing per-series bindings, adds a provider only when connected, and opens `TrackerLinkEditSheet` for automatic initial search, switchable series alternate titles, binding mode/offset, and sparse remote updates. Empty remote fields are never submitted, binding-only saves do not call remote update, and an Unlink never edits the provider entry. Server-side automatic sync owns progress aggregation; the client does not mirror it. Grouped bindings and the manual-update ledger use `PaginationState`; the ledger is recent manual updates, not a full bound-series catalog.
- Supported, enabled series detail pages always expose the tracker flow below the metadata chips: branded binding chips show remote reading status, open their own `TrackerLinkEditSheet` on tap, and a persistent Add Tracker button opens `TrackerSeriesSheet`; the card and row context-menu entries remain. The chips use `TrackerLogo`, the same provider artwork as kmweb, also shown in tracker account rows. No extra section title or enclosing card.

## Browse & Dashboard

Expand Down
8 changes: 4 additions & 4 deletions KMReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "KMReader/KMReader-macOS.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 668;
CURRENT_PROJECT_VERSION = 669;
DEVELOPMENT_TEAM = M777UHWZA4;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
Expand Down Expand Up @@ -502,7 +502,7 @@
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "KMReader/KMReader-macOS.entitlements";
CODE_SIGN_IDENTITY = "Apple Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 668;
CURRENT_PROJECT_VERSION = 669;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = M777UHWZA4;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = M777UHWZA4;
Expand Down Expand Up @@ -563,7 +563,7 @@
CODE_SIGN_ENTITLEMENTS = KMReaderWidgets/KMReaderWidgets.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 668;
CURRENT_PROJECT_VERSION = 669;
DEVELOPMENT_TEAM = M777UHWZA4;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = KMReaderWidgets/Info.plist;
Expand Down Expand Up @@ -597,7 +597,7 @@
CODE_SIGN_IDENTITY = "Apple Distribution";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 668;
CURRENT_PROJECT_VERSION = 669;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = M777UHWZA4;
GENERATE_INFOPLIST_FILE = YES;
Expand Down
3 changes: 2 additions & 1 deletion KMReader/Features/Series/Views/SeriesCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ struct SeriesCardView: View {
TrackerSeriesSheet(
seriesId: item.seriesId,
libraryId: item.series.libraryId,
seriesTitle: item.metaTitle
seriesTitle: item.metaTitle,
alternateTitles: (item.series.metadata.alternateTitles ?? []).map(\.title)
)
}
.sheet(isPresented: $showKomfIdentify) {
Expand Down
3 changes: 2 additions & 1 deletion KMReader/Features/Series/Views/SeriesDetailContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ struct SeriesDetailContentView<Actions: View>: View {
TrackerSeriesSection(
seriesId: series.id,
libraryId: series.libraryId,
seriesTitle: series.metadata.title
seriesTitle: series.metadata.title,
alternateTitles: (series.metadata.alternateTitles ?? []).map(\.title)
)

SeriesAlternateTitlesView(series: series)
Expand Down
23 changes: 0 additions & 23 deletions KMReader/Features/Series/Views/SeriesDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ struct SeriesDetailView: View {
@State private var showSavedFilters = false
@State private var showKomfIdentify = false
@State private var showKomfResetConfirmation = false
@State private var showTrackers = false
@State private var readingTargetBook: Book?
@State private var readingTargetInstanceId: String?
@State private var readingTargetIsOffline: Bool?
Expand Down Expand Up @@ -269,15 +268,6 @@ struct SeriesDetailView: View {
}
}
}
.sheet(isPresented: $showTrackers) {
if let series {
TrackerSeriesSheet(
seriesId: series.id,
libraryId: series.libraryId,
seriesTitle: series.metadata.title
)
}
}
.sheet(isPresented: $showKomfIdentify) {
if let series {
KomfIdentifySheet(series: series)
Expand Down Expand Up @@ -603,19 +593,6 @@ extension SeriesDetailView {
}
#endif

#if os(iOS) || os(macOS)
if TrackerCapabilityStore.shared.isSupported, let series,
TrackerCapabilityStore.shared.allows(libraryId: series.libraryId)
{
Button {
deferMenuActionPresentation { showTrackers = true }
} label: {
Label("Trackers", systemImage: "arrow.triangle.2.circlepath")
}
Divider()
}
#endif

if current.isAdmin {
Button {
deferMenuActionPresentation { showEditSheet = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ struct SeriesDetailWideLayoutView<Actions: View>: View {
TrackerSeriesSection(
seriesId: series.id,
libraryId: series.libraryId,
seriesTitle: series.metadata.title
seriesTitle: series.metadata.title,
alternateTitles: (series.metadata.alternateTitles ?? []).map(\.title)
)
SeriesAlternateTitlesView(series: series)
}
Expand Down
3 changes: 2 additions & 1 deletion KMReader/Features/Series/Views/SeriesRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ struct SeriesRowView: View {
TrackerSeriesSheet(
seriesId: item.seriesId,
libraryId: item.series.libraryId,
seriesTitle: item.metaTitle
seriesTitle: item.metaTitle,
alternateTitles: (item.series.metadata.alternateTitles ?? []).map(\.title)
)
}
.sheet(isPresented: $showKomfIdentify) {
Expand Down
46 changes: 46 additions & 0 deletions KMReader/Features/Tracker/Views/TrackerLinkEditSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// TrackerLinkEditSheet.swift
//

import Flow
import SwiftUI

struct TrackerLinkEditSheet: View {
let seriesId: String
let seriesTitle: String
let alternateTitles: [String]
let link: TrackerLink
let onSaved: () async -> Void

Expand All @@ -33,11 +35,13 @@ struct TrackerLinkEditSheet: View {
init(
seriesId: String,
seriesTitle: String,
alternateTitles: [String],
link: TrackerLink,
onSaved: @escaping () async -> Void
) {
self.seriesId = seriesId
self.seriesTitle = seriesTitle
self.alternateTitles = alternateTitles
self.link = link
self.onSaved = onSaved
_query = State(initialValue: link.title ?? seriesTitle)
Expand All @@ -58,6 +62,17 @@ struct TrackerLinkEditSheet: View {
)
}

private var searchTitles: [String] {
var titles: [String] = []
for title in [seriesTitle] + alternateTitles {
let title = title.trimmingCharacters(in: .whitespacesAndNewlines)
if !title.isEmpty, !titles.contains(title) {
titles.append(title)
}
}
return titles
}

var body: some View {
SheetView(
title: link.provider.displayName,
Expand All @@ -82,6 +97,34 @@ struct TrackerLinkEditSheet: View {
} else {
TextField(String(localized: "Search Title or URL"), text: $query)
.onChange(of: query) { _, _ in scheduleSearch() }
if searchTitles.count > 1 {
HFlow(itemSpacing: 8) {
ForEach(searchTitles, id: \.self) { title in
Button {
query = title
scheduleSearch()
} label: {
Text(title)
.font(.caption)
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
query == title ? Color.accentColor.opacity(0.15) : LayoutConfig.neutralFillColor,
in: Capsule()
)
.overlay {
Capsule()
.strokeBorder(
query == title ? Color.accentColor.opacity(0.4) : Color.secondary.opacity(0.2)
)
}
.contentShape(Capsule())
}
.adaptiveButtonStyle(.plain)
}
}
}
if isSearching {
ProgressView()
}
Expand Down Expand Up @@ -156,6 +199,9 @@ struct TrackerLinkEditSheet: View {
remoteLoadTask = Task {
await loadRemoteState()
}
if selectedItem == nil, link.trackId.isEmpty {
scheduleSearch()
}
}
.onDisappear {
searchTask?.cancel()
Expand Down
26 changes: 17 additions & 9 deletions KMReader/Features/Tracker/Views/TrackerSeriesSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct TrackerSeriesSection: View {
let seriesId: String
let libraryId: String
let seriesTitle: String
let alternateTitles: [String]

@State private var links: [TrackerLink] = []
@State private var states: [TrackerProvider: TrackerRemoteState] = [:]
Expand Down Expand Up @@ -60,13 +61,15 @@ struct TrackerSeriesSection: View {
TrackerSeriesSheet(
seriesId: seriesId,
libraryId: libraryId,
seriesTitle: seriesTitle
seriesTitle: seriesTitle,
alternateTitles: alternateTitles
)
}
.sheet(item: $editingLink) { link in
TrackerLinkEditSheet(
seriesId: seriesId,
seriesTitle: seriesTitle,
alternateTitles: alternateTitles,
link: link
) {
await load()
Expand All @@ -81,21 +84,26 @@ struct TrackerSeriesSection: View {
do {
let loadedLinks = try await TrackerService.getLinks(seriesId: seriesId)
guard !Task.isCancelled else { return }
links = loadedLinks
states = [:]
providersRequiringLogin = []
if links != loadedLinks {
withAnimation(.appCurve()) {
links = loadedLinks
}
}
for link in loadedLinks {
do {
let state = try await TrackerService.getState(
provider: link.provider, trackId: link.trackId)
guard !Task.isCancelled else { return }
states[link.provider] = state
withAnimation(.appCurve()) {
states[link.provider] = state
providersRequiringLogin.remove(link.provider)
}
} catch APIError.unauthorized {
providersRequiringLogin.insert(link.provider)
_ = withAnimation(.appCurve()) {
providersRequiringLogin.insert(link.provider)
}
} catch {}
}
} catch {
links = []
}
} catch {}
}
}
4 changes: 4 additions & 0 deletions KMReader/Features/Tracker/Views/TrackerSeriesSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ struct TrackerSeriesSheet: View {
let seriesId: String
let libraryId: String
let seriesTitle: String
let alternateTitles: [String]
let onLinksChanged: (() async -> Void)?

@AppStorage("currentAccount") private var current: Current = .init()
Expand All @@ -23,11 +24,13 @@ struct TrackerSeriesSheet: View {
seriesId: String,
libraryId: String,
seriesTitle: String,
alternateTitles: [String] = [],
onLinksChanged: (() async -> Void)? = nil
) {
self.seriesId = seriesId
self.libraryId = libraryId
self.seriesTitle = seriesTitle
self.alternateTitles = alternateTitles
self.onLinksChanged = onLinksChanged
}

Expand Down Expand Up @@ -144,6 +147,7 @@ struct TrackerSeriesSheet: View {
TrackerLinkEditSheet(
seriesId: seriesId,
seriesTitle: seriesTitle,
alternateTitles: alternateTitles,
link: link
) {
await load()
Expand Down