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:
192
Jervis/Views/SecondaryTabs.swift
Normal file
192
Jervis/Views/SecondaryTabs.swift
Normal file
@@ -0,0 +1,192 @@
|
||||
// SecondaryTabs.swift
|
||||
// Jarvis — Latest / Saved / Search. Required by RootTabView; they reuse the
|
||||
// signal-feed components rather than introducing new surfaces.
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
// Shared destinations so taps work inside each tab's own NavigationStack.
|
||||
private extension View {
|
||||
func jarvisDestinations() -> some View {
|
||||
self
|
||||
.navigationDestination(for: StorySummary.self) { StoryDetailView(story: $0) }
|
||||
.navigationDestination(for: ArticleRoute.self) { ArticleReaderView(route: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
private struct TabHeader: View {
|
||||
let title: String
|
||||
var body: some View {
|
||||
HStack {
|
||||
JarvisWordmark(size: 26)
|
||||
Text(title)
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundStyle(Palette.tertiaryText)
|
||||
.padding(.top, 6)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
}
|
||||
|
||||
private struct StoryList: View {
|
||||
let stories: [StorySummary]
|
||||
let cachedIds: Set<String>
|
||||
var readIds: Set<String> = []
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(stories) { story in
|
||||
NavigationLink(value: story) {
|
||||
StoryRowView(story: story,
|
||||
isCached: cachedIds.contains(story.id),
|
||||
isRead: readIds.contains(story.id))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Latest (most recently formed stories first)
|
||||
|
||||
struct LatestView: View {
|
||||
@EnvironmentObject var store: StoryStore
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
|
||||
private var latest: [StorySummary] {
|
||||
store.stories.sorted { $0.createdAt > $1.createdAt }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
Palette.background.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
TabHeader(title: "Latest")
|
||||
Divider().overlay(Palette.hairline)
|
||||
StoryList(stories: latest, cachedIds: Set(cachedArticles.map(\.storyId)),
|
||||
readIds: Set(readStories.map(\.id)))
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.jarvisDestinations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Saved (offline cache)
|
||||
|
||||
struct SavedView: View {
|
||||
@Query private var cachedStories: [CachedStory]
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
@Query private var savedStories: [SavedStory]
|
||||
|
||||
private var stories: [StorySummary] {
|
||||
let savedIds = Set(savedStories.map(\.id))
|
||||
return cachedStories
|
||||
.filter { savedIds.contains($0.id) }
|
||||
.map(StorySummary.init(cached:))
|
||||
.sorted(by: StorySummary.feedOrder)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
Palette.background.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
TabHeader(title: "Saved")
|
||||
Divider().overlay(Palette.hairline)
|
||||
if stories.isEmpty {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "bookmark")
|
||||
.font(.system(size: 28)).foregroundStyle(Palette.mutedText)
|
||||
Text("Nothing saved yet")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundStyle(Palette.tertiaryText)
|
||||
Text("Swipe a story left to save it")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(Palette.mutedText)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
StoryList(stories: stories, cachedIds: Set(cachedArticles.map(\.storyId)),
|
||||
readIds: Set(readStories.map(\.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.jarvisDestinations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Search
|
||||
|
||||
struct SearchView: View {
|
||||
@EnvironmentObject var store: StoryStore
|
||||
@Query private var cachedArticles: [CachedArticle]
|
||||
@Query private var readStories: [ReadStory]
|
||||
@State private var query = ""
|
||||
|
||||
private var results: [StorySummary] {
|
||||
guard !query.isEmpty else { return [] }
|
||||
return store.stories
|
||||
.filter { $0.headline.localizedCaseInsensitiveContains(query)
|
||||
|| $0.summary.localizedCaseInsensitiveContains(query) }
|
||||
.sorted(by: StorySummary.feedOrder)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
Palette.background.ignoresSafeArea()
|
||||
VStack(spacing: 0) {
|
||||
TabHeader(title: "Search")
|
||||
searchBar
|
||||
Divider().overlay(Palette.hairline)
|
||||
if query.isEmpty {
|
||||
placeholder("Search stories by headline")
|
||||
} else if results.isEmpty {
|
||||
placeholder("No matches for “\(query)”")
|
||||
} else {
|
||||
StoryList(stories: results, cachedIds: Set(cachedArticles.map(\.storyId)),
|
||||
readIds: Set(readStories.map(\.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.jarvisDestinations()
|
||||
}
|
||||
}
|
||||
|
||||
private var searchBar: some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.system(size: 14)).foregroundStyle(Palette.tertiaryText)
|
||||
TextField("", text: $query,
|
||||
prompt: Text("Search").foregroundColor(Palette.tertiaryText))
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(Palette.primaryText)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.frame(height: 44)
|
||||
.background(Palette.surface2)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
|
||||
private func placeholder(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundStyle(Palette.tertiaryText)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user