rename: Jarvis -> Jervis target, scheme, and folder throughout
The bundle-ID and display-name renames weren't enough — the actual Xcode target/product name was still "Jarvis", which drives CFBundleName, Xcode's Organizer archive list, the .xcodeproj filename, and the scheme name. Renamed all the way through: - project.yml: top-level name, target key, PRODUCT_NAME, source/info paths - Jarvis/ -> Jervis/ (source folder, git-tracked as renames, no content diffs on the moved files) - .gitignore, CI workflows, README, CONTRIBUTING: Jarvis.xcodeproj / scheme Jarvis -> Jervis.xcodeproj / scheme Jervis Verified: xcodebuild -scheme Jervis succeeds, produces Jervis.app, CFBundleName/CFBundleExecutable/CFBundleDisplayName all read "Jervis". CFBundleIdentifier intentionally stays com.kisani.jarvis (per prior decision — bundle ID isn't user-facing anywhere including Organizer). Swift type names (JarvisApp, JarvisWordmark, etc.) and the Gitea repo name/URL are unchanged — pure internal source identifiers, not user or developer-facing product identity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
131
Jervis/Views/Home/StoryRowView.swift
Normal file
131
Jervis/Views/Home/StoryRowView.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
// StoryRowView.swift
|
||||
// Jarvis — one row in the signal feed. Everything fades with the signal score.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StoryRowView: View {
|
||||
let story: StorySummary
|
||||
/// True when the story's articles are cached in SwiftData (offline-ready).
|
||||
var isCached: Bool = false
|
||||
/// True once the story has been opened — drops the orange stripe and dims it.
|
||||
var isRead: Bool = false
|
||||
/// Slim single-line presentation, used for the "Read" shelf.
|
||||
var compact: Bool = false
|
||||
|
||||
private var sourceNames: [String] { story.sources.map(\.name) }
|
||||
|
||||
var body: some View {
|
||||
if compact { compactBody } else { fullBody }
|
||||
}
|
||||
|
||||
private var compactBody: some View {
|
||||
HStack(spacing: 0) {
|
||||
SignalStripe(score: story.signalScore, muted: true)
|
||||
Text(story.headline)
|
||||
.font(.system(size: 15, weight: .semibold, design: .default))
|
||||
.foregroundStyle(Palette.secondaryText)
|
||||
.lineLimit(1)
|
||||
.padding(.leading, 14)
|
||||
.padding(.vertical, 11)
|
||||
Spacer(minLength: 12)
|
||||
Text("\(story.signalScore)")
|
||||
.font(.system(size: 14, weight: .heavy, design: .monospaced))
|
||||
.foregroundStyle(Color(hex: "474747"))
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Palette.background)
|
||||
.overlay(alignment: .bottom) { Rectangle().fill(Palette.hairline).frame(height: 0.5) }
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private var fullBody: some View {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
if isRead {
|
||||
// No stripe for read stories — remove the orange signal indicator entirely.
|
||||
Color.clear.frame(width: 3)
|
||||
} else {
|
||||
SignalStripe(score: story.signalScore)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
Text(story.headline)
|
||||
.font(.system(size: 19, weight: .bold, design: .default))
|
||||
.foregroundStyle(Signal.titleColor(story.signalScore))
|
||||
.lineLimit(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if !story.summary.isEmpty {
|
||||
Text(story.summary)
|
||||
.font(.system(size: 15, weight: .regular, design: .default))
|
||||
.foregroundStyle(Palette.bodyText)
|
||||
.lineLimit(5)
|
||||
.lineSpacing(3)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if !sourceNames.isEmpty {
|
||||
SourceChips(names: sourceNames, total: story.sourceCount)
|
||||
}
|
||||
|
||||
Text(metaLine)
|
||||
.font(.system(size: 11, weight: .regular, design: .monospaced))
|
||||
.foregroundStyle(Palette.tertiaryText)
|
||||
}
|
||||
.padding(.leading, 14)
|
||||
.padding(.vertical, 16)
|
||||
|
||||
Spacer(minLength: 12)
|
||||
|
||||
// Signal cluster: score + offline dot, compact and aligned
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text("\(story.signalScore)")
|
||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||
.foregroundStyle(Signal.scoreColor(story.signalScore))
|
||||
if isCached {
|
||||
CachedDot(size: 6)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.top, 18)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Palette.background)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle().fill(Palette.hairline).frame(height: 0.5)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private var metaLine: String {
|
||||
let sources = "\(story.sourceCount) source\(story.sourceCount == 1 ? "" : "s")"
|
||||
// firstSeenAt is when the cluster appeared — updatedAt bumps on every
|
||||
// article the backend attaches, so it can read "just now" on stories
|
||||
// that are actually weeks old.
|
||||
let age = (story.firstSeenAt ?? story.updatedAt).timeAgoShort()
|
||||
return "\(sources) · \(Topic.label(story.topic)) · \(age) ago"
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let sources = [
|
||||
StorySource(id: "1", name: "Daily Monitor", url: "", publishedAt: Date(), isBreaking: true),
|
||||
StorySource(id: "2", name: "NilePost", url: "", publishedAt: Date(), isBreaking: false),
|
||||
StorySource(id: "3", name: "The EastAfrican", url: "", publishedAt: Date(), isBreaking: false),
|
||||
]
|
||||
let breakdown = ScoreBreakdown(sourceAuthority: 28, freshness: 22, localRelevance: 15, crossSourceConfirmation: 16, topicImportance: 10)
|
||||
return VStack(spacing: 0) {
|
||||
ForEach([97, 64, 33, 14], id: \.self) { score in
|
||||
StoryRowView(
|
||||
story: StorySummary(
|
||||
id: "\(score)", headline: "Bank of Uganda cuts interest rates by 50 basis points",
|
||||
summary: "", topic: "finance", tags: ["finance"], signalScore: score, scoreBreakdown: breakdown,
|
||||
sourceCount: 7, sources: sources, consensus: nil, conflict: nil,
|
||||
updatedAt: Date().addingTimeInterval(-3600), createdAt: Date(),
|
||||
firstSeenAt: Date().addingTimeInterval(-3600)),
|
||||
isCached: score == 64
|
||||
)
|
||||
}
|
||||
}
|
||||
.background(Palette.background)
|
||||
}
|
||||
Reference in New Issue
Block a user