From 5d6812732f95b11949b0dfc592cda54fd2feb42f Mon Sep 17 00:00:00 2001 From: everpcpc Date: Thu, 29 Oct 2026 16:51:11 +0800 Subject: [PATCH 1/2] feat(trackers): surface series tracker links --- .agents/skills/repo-conventions/SKILL.md | 1 + .../TrackerAniList.imageset/Contents.json | 21 +++++ .../TrackerAniList.imageset/anilist.png | Bin 0 -> 1238 bytes .../TrackerBangumi.imageset/Contents.json | 21 +++++ .../TrackerBangumi.imageset/bangumi.png | Bin 0 -> 8495 bytes .../TrackerMangaBaka.imageset/Contents.json | 21 +++++ .../TrackerMangaBaka.imageset/mangabaka.png | Bin 0 -> 11243 bytes .../TrackerMyAnimeList.imageset/Contents.json | 21 +++++ .../TrackerMyAnimeList.imageset/mal.png | Bin 0 -> 4553 bytes .../Tracker/Models/TrackerProvider.swift | 9 +++ .../Tracker/Views/TrackerAccountRow.swift | 4 +- .../Tracker/Views/TrackerLinkChip.swift | 38 +++++++++ .../Features/Tracker/Views/TrackerLogo.swift | 17 ++++ .../Tracker/Views/TrackerSeriesSection.swift | 76 ++++++++++++------ .../Tracker/Views/TrackerSeriesSheet.swift | 18 ++++- KMReader/Localizable.xcstrings | 64 +++++++++++++++ 16 files changed, 286 insertions(+), 25 deletions(-) create mode 100644 KMReader/Assets.xcassets/TrackerAniList.imageset/Contents.json create mode 100644 KMReader/Assets.xcassets/TrackerAniList.imageset/anilist.png create mode 100644 KMReader/Assets.xcassets/TrackerBangumi.imageset/Contents.json create mode 100644 KMReader/Assets.xcassets/TrackerBangumi.imageset/bangumi.png create mode 100644 KMReader/Assets.xcassets/TrackerMangaBaka.imageset/Contents.json create mode 100644 KMReader/Assets.xcassets/TrackerMangaBaka.imageset/mangabaka.png create mode 100644 KMReader/Assets.xcassets/TrackerMyAnimeList.imageset/Contents.json create mode 100644 KMReader/Assets.xcassets/TrackerMyAnimeList.imageset/mal.png create mode 100644 KMReader/Features/Tracker/Views/TrackerLinkChip.swift create mode 100644 KMReader/Features/Tracker/Views/TrackerLogo.swift diff --git a/.agents/skills/repo-conventions/SKILL.md b/.agents/skills/repo-conventions/SKILL.md index 0e3ef8199..192d624ab 100644 --- a/.agents/skills/repo-conventions/SKILL.md +++ b/.agents/skills/repo-conventions/SKILL.md @@ -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 diff --git a/KMReader/Assets.xcassets/TrackerAniList.imageset/Contents.json b/KMReader/Assets.xcassets/TrackerAniList.imageset/Contents.json new file mode 100644 index 000000000..8e7954109 --- /dev/null +++ b/KMReader/Assets.xcassets/TrackerAniList.imageset/Contents.json @@ -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 + } +} diff --git a/KMReader/Assets.xcassets/TrackerAniList.imageset/anilist.png b/KMReader/Assets.xcassets/TrackerAniList.imageset/anilist.png new file mode 100644 index 0000000000000000000000000000000000000000..266af07cbb4e430ad238a4b68ff623b0a1266bca GIT binary patch literal 1238 zcmV;{1S$K8P)Px#mQYMoMHwM2|Ns6JGDrZL=jG<-7$q|ThR6Q?{s5}~ zH$F*{m74&R-W4rD@$vHc`uoJj$RH>*E;T?VE;*W=qR!CLQ(0nPWNh&G{0C{A*xB2J zh>k@|Rc>~FOT+)4-~Qe6{u{Obv$eSiu>Yl~tbo-2%kKVT%l`nQ^$S^pF}we{=>8K$ zW#HlB1bem@Dmwt3?32OUx7q2O#KXA&00VnTL_t(|+U=WbQ{q4nMR^299zzlmP!rHa zSaelf|NpOBWPOC4o*5EDy0_|{k5W{f#GB;yOlY+L0000000000000000000000000 z0Ouecn8b%qdgD%k7eUU^Dll0^`%j*Bqi%uz)mV&lu9b<-_s-pmf7ppR*_ooqTD|^- z!GjRzX-G9y5awQqz<51t*;w?AY`FYM_c^C3aHw{kdB`H&zz2%OSp zJ|svll$)(U>1gG4HuE7sI@ca)BOd~!Go?!AgJ0TG|BI*kQJnNld(ZUcFKN<4rhqW+tQ(cXxXChEUVv}Y9HmL}?dnwtl5xs)VL)c=I|<8eNi zr3dCooy>{Tf1D3yX`=pL$5hadMgd-Fs{XIZTgp+@x9fj1=zpauK^1&%h4TqU^?YEE* zPH8F~vyUQ`I`Y9NO$~?X)LvP?Mny(xs{SWm^-tSJ0Y+(JINX<|mLnerB-5*m$L{8ma%~@^RO^H2G>+kPJzlE9IkpX|n#W zOWbJMo5rQd`d?V^@L#m#3x?;FChLE3!NY%hyj(UcP1paV)Pu;IkGiGFSHtwY)J@%+ zkGiF)`rl6Da*xl+vQg=%WI8PQxM@_H91cI1(ofC@@dD}@rOEpLwUk~{KI)aG>;JHH zk@e)G-oiu{JZCzpx6KU!A@QrSAoE=Z)R$hn7DoMtn#ZjH#ng8p zkl*T97z_Ei5dZ)H000000000000000000000093je*kl!rcL)xpI-m~09SfcSaech zcOY z6Cgx@G{a;ABePT>%h=S&#LUDT#0SfONT5nC0O}VJbn-$ql>h($07*qoM6N<$f{is> A#{d8T literal 0 HcmV?d00001 diff --git a/KMReader/Assets.xcassets/TrackerBangumi.imageset/Contents.json b/KMReader/Assets.xcassets/TrackerBangumi.imageset/Contents.json new file mode 100644 index 000000000..f10dc034e --- /dev/null +++ b/KMReader/Assets.xcassets/TrackerBangumi.imageset/Contents.json @@ -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 + } +} diff --git a/KMReader/Assets.xcassets/TrackerBangumi.imageset/bangumi.png b/KMReader/Assets.xcassets/TrackerBangumi.imageset/bangumi.png new file mode 100644 index 0000000000000000000000000000000000000000..f94fb0e7eaef489e59011299270ea183e6ca9505 GIT binary patch literal 8495 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 z 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 URL? { switch self { case .anilist: return URL(string: "https://anilist.co/manga/\(trackId)") diff --git a/KMReader/Features/Tracker/Views/TrackerAccountRow.swift b/KMReader/Features/Tracker/Views/TrackerAccountRow.swift index 54290710c..25e6fb590 100644 --- a/KMReader/Features/Tracker/Views/TrackerAccountRow.swift +++ b/KMReader/Features/Tracker/Views/TrackerAccountRow.swift @@ -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 { diff --git a/KMReader/Features/Tracker/Views/TrackerLinkChip.swift b/KMReader/Features/Tracker/Views/TrackerLinkChip.swift new file mode 100644 index 000000000..c2df44512 --- /dev/null +++ b/KMReader/Features/Tracker/Views/TrackerLinkChip.swift @@ -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()) + } +} diff --git a/KMReader/Features/Tracker/Views/TrackerLogo.swift b/KMReader/Features/Tracker/Views/TrackerLogo.swift new file mode 100644 index 000000000..708bcc749 --- /dev/null +++ b/KMReader/Features/Tracker/Views/TrackerLogo.swift @@ -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) + } +} diff --git a/KMReader/Features/Tracker/Views/TrackerSeriesSection.swift b/KMReader/Features/Tracker/Views/TrackerSeriesSection.swift index 5b22d6be0..14fa2247b 100644 --- a/KMReader/Features/Tracker/Views/TrackerSeriesSection.swift +++ b/KMReader/Features/Tracker/Views/TrackerSeriesSection.swift @@ -2,6 +2,7 @@ // TrackerSeriesSection.swift // +import Flow import SwiftUI struct TrackerSeriesSection: View { @@ -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 = [] + @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() @@ -56,6 +63,15 @@ struct TrackerSeriesSection: View { seriesTitle: seriesTitle ) } + .sheet(item: $editingLink) { link in + TrackerLinkEditSheet( + seriesId: seriesId, + seriesTitle: seriesTitle, + link: link + ) { + await load() + } + } } #endif } @@ -63,7 +79,21 @@ struct TrackerSeriesSection: View { 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 = [] } diff --git a/KMReader/Features/Tracker/Views/TrackerSeriesSheet.swift b/KMReader/Features/Tracker/Views/TrackerSeriesSheet.swift index 7b55c2a7b..c41036b14 100644 --- a/KMReader/Features/Tracker/Views/TrackerSeriesSheet.swift +++ b/KMReader/Features/Tracker/Views/TrackerSeriesSheet.swift @@ -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 @@ -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") } @@ -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) @@ -132,6 +147,7 @@ struct TrackerSeriesSheet: View { link: link ) { await load() + await onLinksChanged?() } } } diff --git a/KMReader/Localizable.xcstrings b/KMReader/Localizable.xcstrings index 48c84c0ea..aa3714954 100644 --- a/KMReader/Localizable.xcstrings +++ b/KMReader/Localizable.xcstrings @@ -46876,6 +46876,70 @@ } } }, + "Login Required" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anmeldung erforderlich" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Login Required" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inicio de sesión requerido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connexion requise" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accesso richiesto" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ログインが必要です" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "로그인 필요" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Требуется вход" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要登录" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要登入" + } + } + } + }, "Logout" : { "localizations" : { "de" : { From 5aeff1a6b3e41a1e88fc3e37bccea674eeea78b3 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Thu, 29 Oct 2026 16:55:02 +0800 Subject: [PATCH 2/2] chore: incr build ver to 668 --- KMReader.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/KMReader.xcodeproj/project.pbxproj b/KMReader.xcodeproj/project.pbxproj index c85e0fde0..66c591556 100644 --- a/KMReader.xcodeproj/project.pbxproj +++ b/KMReader.xcodeproj/project.pbxproj @@ -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; @@ -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; @@ -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; @@ -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;