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
1 change: 1 addition & 0 deletions .agents/skills/repo-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ 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.

## 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 = 667;
CURRENT_PROJECT_VERSION = 668;
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 = 667;
CURRENT_PROJECT_VERSION = 668;
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 = 667;
CURRENT_PROJECT_VERSION = 668;
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 = 667;
CURRENT_PROJECT_VERSION = 668;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = M777UHWZA4;
GENERATE_INFOPLIST_FILE = YES;
Expand Down
21 changes: 21 additions & 0 deletions KMReader/Assets.xcassets/TrackerAniList.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "anilist.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions KMReader/Assets.xcassets/TrackerBangumi.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "bangumi.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions KMReader/Assets.xcassets/TrackerMangaBaka.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "mangabaka.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions KMReader/Assets.xcassets/TrackerMyAnimeList.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "mal.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions KMReader/Features/Tracker/Models/TrackerProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ nonisolated enum TrackerProvider: String, CaseIterable, Codable, Sendable {
}
}

var imageName: String {
switch self {
case .anilist: return "TrackerAniList"
case .mal: return "TrackerMyAnimeList"
case .bangumi: return "TrackerBangumi"
case .mangabaka: return "TrackerMangaBaka"
}
}

func entryURL(trackId: String) -> URL? {
switch self {
case .anilist: return URL(string: "https://anilist.co/manga/\(trackId)")
Expand Down
4 changes: 3 additions & 1 deletion KMReader/Features/Tracker/Views/TrackerAccountRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ struct TrackerAccountRow: View {
let onLogout: () -> Void

var body: some View {
HStack {
HStack(spacing: 12) {
TrackerLogo(provider: provider)
.frame(width: 28, height: 28)
Text(provider.displayName)
Spacer()
if isPending {
Expand Down
38 changes: 38 additions & 0 deletions KMReader/Features/Tracker/Views/TrackerLinkChip.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// TrackerLinkChip.swift
//

import SwiftUI

struct TrackerLinkChip: View {
let link: TrackerLink
let state: TrackerRemoteState?
let requiresLogin: Bool

var body: some View {
HStack(spacing: 6) {
TrackerLogo(provider: link.provider)
.frame(width: 16, height: 16)
Text(link.provider.displayName)
if requiresLogin {
Circle()
.fill(.red)
.frame(width: 6, height: 6)
.accessibilityLabel(Text(String(localized: "Login Required")))
} else if let status = state?.status {
Text(status.displayName)
.foregroundStyle(.secondary)
}
}
.font(.caption.weight(.medium))
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(LayoutConfig.neutralFillColor, in: Capsule())
.overlay {
Capsule()
.strokeBorder(Color.secondary.opacity(0.2))
}
.contentShape(Capsule())
}
}
17 changes: 17 additions & 0 deletions KMReader/Features/Tracker/Views/TrackerLogo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// TrackerLogo.swift
//

import SwiftUI

struct TrackerLogo: View {
let provider: TrackerProvider

var body: some View {
Image(provider.imageName)
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 3))
.accessibilityHidden(true)
}
}
76 changes: 53 additions & 23 deletions KMReader/Features/Tracker/Views/TrackerSeriesSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// TrackerSeriesSection.swift
//

import Flow
import SwiftUI

struct TrackerSeriesSection: View {
Expand All @@ -10,36 +11,42 @@ struct TrackerSeriesSection: View {
let seriesTitle: String

@State private var links: [TrackerLink] = []
@State private var states: [TrackerProvider: TrackerRemoteState] = [:]
@State private var providersRequiringLogin: Set<TrackerProvider> = []
@State private var editingLink: TrackerLink?
@State private var showManage = false

var body: some View {
#if os(iOS) || os(macOS)
if TrackerCapabilityStore.shared.isSupported,
TrackerCapabilityStore.shared.allows(libraryId: libraryId),
!links.isEmpty
TrackerCapabilityStore.shared.allows(libraryId: libraryId)
{
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "Trackers"))
.font(.headline)
Button {
showManage = true
} label: {
HStack {
ForEach(links) { link in
Text(link.provider.displayName)
.font(.caption)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(LayoutConfig.neutralFillColor, in: Capsule())
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.secondary)
HFlow(itemSpacing: 8) {
ForEach(links) { link in
Button {
editingLink = link
} label: {
TrackerLinkChip(
link: link,
state: states[link.provider],
requiresLogin: providersRequiringLogin.contains(link.provider)
)
}
.contentShape(Rectangle())
.adaptiveButtonStyle(.plain)
}
if links.count < TrackerProvider.allCases.count {
Button {
showManage = true
} label: {
Label(String(localized: "Add Tracker"), systemImage: "plus")
.font(.caption.weight(.medium))
.lineLimit(1)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.contentShape(Capsule())
}
.adaptiveButtonStyle(.plain)
}
.adaptiveButtonStyle(.plain)
}
.task(id: seriesId) {
await load()
Expand All @@ -56,14 +63,37 @@ struct TrackerSeriesSection: View {
seriesTitle: seriesTitle
)
}
.sheet(item: $editingLink) { link in
TrackerLinkEditSheet(
seriesId: seriesId,
seriesTitle: seriesTitle,
link: link
) {
await load()
}
}
}
#endif
}

private func load() async {
guard !AppConfig.isOffline else { return }
do {
links = try await TrackerService.getLinks(seriesId: seriesId)
let loadedLinks = try await TrackerService.getLinks(seriesId: seriesId)
guard !Task.isCancelled else { return }
links = loadedLinks
states = [:]
providersRequiringLogin = []
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
} catch APIError.unauthorized {
providersRequiringLogin.insert(link.provider)
} catch {}
}
} catch {
links = []
}
Expand Down
18 changes: 17 additions & 1 deletion 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 onLinksChanged: (() async -> Void)?

@AppStorage("currentAccount") private var current: Current = .init()
@Environment(\.dismiss) private var dismiss
Expand All @@ -18,6 +19,18 @@ struct TrackerSeriesSheet: View {
@State private var isLoading = false
@State private var editingLink: TrackerLink?

init(
seriesId: String,
libraryId: String,
seriesTitle: String,
onLinksChanged: (() async -> Void)? = nil
) {
self.seriesId = seriesId
self.libraryId = libraryId
self.seriesTitle = seriesTitle
self.onLinksChanged = onLinksChanged
}

private var webURL: URL? {
URL(string: current.serverURL + "/account/trackers")
}
Expand Down Expand Up @@ -55,7 +68,9 @@ struct TrackerSeriesSheet: View {
Button {
editingLink = link
} label: {
HStack {
HStack(spacing: 12) {
TrackerLogo(provider: link.provider)
.frame(width: 28, height: 28)
VStack(alignment: .leading, spacing: 3) {
Text(link.provider.displayName)
Text(link.displayTitle)
Expand Down Expand Up @@ -132,6 +147,7 @@ struct TrackerSeriesSheet: View {
link: link
) {
await load()
await onLinksChanged?()
}
}
}
Expand Down
Loading