Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: router #10

Merged
merged 6 commits into from
Jan 8, 2025
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
2 changes: 2 additions & 0 deletions NeoDB/NeoDB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
membershipExceptions = (
Info.plist,
References/Cursor/auth_and_profile.md,
References/Cursor/router.md,
References/Cursor/shelf_feature.md,
References/Cursor/timeline_local.md,
References/IceCubes/Router.md,
References/Mastodon/timelines.md,
References/NeoDB/api.md,
References/NeoDB/openapi/openapi.yaml,
Expand Down
21 changes: 21 additions & 0 deletions NeoDB/NeoDB/Assets.xcassets/neodb-logo.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "logo_square.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
193 changes: 193 additions & 0 deletions NeoDB/NeoDB/Assets.xcassets/neodb-logo.imageset/logo_square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 97 additions & 7 deletions NeoDB/NeoDB/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,38 @@ import SwiftUI

struct ContentView: View {
@EnvironmentObject var authService: AuthService
@StateObject private var router = Router()

var body: some View {
TabView {
NavigationStack {
NavigationStack(path: $router.path) {
HomeView(authService: authService)
.navigationDestination(for: RouterDestination.self) { destination in
switch destination {
case .itemDetail(let id):
Text("Item Detail: \(id)") // TODO: Implement ItemDetailView
case .itemDetailWithItem(let item):
Text("Item Detail: \(item.displayTitle)") // TODO: Implement ItemDetailView
case .shelfDetail(let type):
Text("Shelf: \(type.displayName)") // TODO: Implement ShelfDetailView
case .userShelf(let userId, let type):
Text("User Shelf: \(userId) - \(type.displayName)") // TODO: Implement UserShelfView
case .userProfile(let id):
Text("User Profile: \(id)") // TODO: Implement UserProfileView
case .userProfileWithUser(let user):
Text("User Profile: \(user.displayName)") // TODO: Implement UserProfileView
case .statusDetail(let id):
Text("Status: \(id)") // TODO: Implement StatusDetailView
case .statusDetailWithStatus(let status):
Text("Status: \(status.id)") // TODO: Implement StatusDetailView
case .hashTag(let tag):
Text("Tag: #\(tag)") // TODO: Implement HashTagView
case .followers(let id):
Text("Followers: \(id)") // TODO: Implement FollowersView
case .following(let id):
Text("Following: \(id)") // TODO: Implement FollowingView
}
}
}
.tabItem {
Label("Home", systemImage: "house.fill")
Expand All @@ -27,26 +54,89 @@ struct ContentView: View {
Label("Search", systemImage: "magnifyingglass")
}

NavigationStack {
NavigationStack(path: $router.path) {
LibraryView(authService: authService)
.navigationDestination(for: RouterDestination.self) { destination in
switch destination {
case .itemDetail(let id):
Text("Item Detail: \(id)") // TODO: Implement ItemDetailView
case .itemDetailWithItem(let item):
Text("Item Detail: \(item.displayTitle)") // TODO: Implement ItemDetailView
case .shelfDetail(let type):
Text("Shelf: \(type.displayName)") // TODO: Implement ShelfDetailView
case .userShelf(let userId, let type):
Text("User Shelf: \(userId) - \(type.displayName)") // TODO: Implement UserShelfView
case .userProfile(let id):
Text("User Profile: \(id)") // TODO: Implement UserProfileView
case .userProfileWithUser(let user):
Text("User Profile: \(user.displayName)") // TODO: Implement UserProfileView
case .statusDetail(let id):
Text("Status: \(id)") // TODO: Implement StatusDetailView
case .statusDetailWithStatus(let status):
Text("Status: \(status.id)") // TODO: Implement StatusDetailView
case .hashTag(let tag):
Text("Tag: #\(tag)") // TODO: Implement HashTagView
case .followers(let id):
Text("Followers: \(id)") // TODO: Implement FollowersView
case .following(let id):
Text("Following: \(id)") // TODO: Implement FollowingView
}
}
}
.tabItem {
Label("Library", systemImage: "books.vertical.fill")
}

NavigationStack {
NavigationStack(path: $router.path) {
ProfileView(authService: authService)
.navigationDestination(for: RouterDestination.self) { destination in
switch destination {
case .itemDetail(let id):
Text("Item Detail: \(id)") // TODO: Implement ItemDetailView
case .itemDetailWithItem(let item):
Text("Item Detail: \(item.displayTitle)") // TODO: Implement ItemDetailView
case .shelfDetail(let type):
Text("Shelf: \(type.displayName)") // TODO: Implement ShelfDetailView
case .userShelf(let userId, let type):
Text("User Shelf: \(userId) - \(type.displayName)") // TODO: Implement UserShelfView
case .userProfile(let id):
Text("User Profile: \(id)") // TODO: Implement UserProfileView
case .userProfileWithUser(let user):
Text("User Profile: \(user.displayName)") // TODO: Implement UserProfileView
case .statusDetail(let id):
Text("Status: \(id)") // TODO: Implement StatusDetailView
case .statusDetailWithStatus(let status):
Text("Status: \(status.id)") // TODO: Implement StatusDetailView
case .hashTag(let tag):
Text("Tag: #\(tag)") // TODO: Implement HashTagView
case .followers(let id):
Text("Followers: \(id)") // TODO: Implement FollowersView
case .following(let id):
Text("Following: \(id)") // TODO: Implement FollowingView
}
}
}
.tabItem {
Label("Profile", systemImage: "person.fill")
}
}
.tint(.accentColor)
.environmentObject(router)
.sheet(item: $router.presentedSheet) { sheet in
switch sheet {
case .newStatus:
Text("New Status") // TODO: Implement StatusEditorView
case .editStatus(let status):
Text("Edit Status: \(status.id)") // TODO: Implement StatusEditorView
case .replyToStatus(let status):
Text("Reply to: \(status.id)") // TODO: Implement StatusEditorView
case .addToShelf(let item):
Text("Add to Shelf: \(item.displayTitle)") // TODO: Implement ShelfEditorView
case .editShelfItem(let mark):
Text("Edit Shelf Item: \(mark.item.displayTitle)") // TODO: Implement ShelfEditorView
}
}
}

#if DEBUG
@ObserveInjection var forceRedraw
#endif
}

#Preview {
Expand Down
22 changes: 17 additions & 5 deletions NeoDB/NeoDB/NeoDBApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,37 @@ import SwiftUI
@main
struct NeoDBApp: App {
@StateObject private var authService = AuthService()
@StateObject private var router = Router()

var body: some Scene {
WindowGroup {
Group {
if authService.isAuthenticated {
ContentView()
.environmentObject(authService)
.environmentObject(router)
} else {
LoginView()
.environmentObject(authService)
}
}
.onOpenURL { url in
Task {
do {
try await authService.handleCallback(url: url)
} catch {
print("Authentication error: \(error)")
// First try to handle OAuth callback
if url.scheme == "neodb" && url.host == "oauth" {
Task {
do {
try await authService.handleCallback(url: url)
} catch {
print("Authentication error: \(error)")
}
}
return
}

// Then try to handle deep links
if !router.handleURL(url) {
// If the router didn't handle the URL, open it in the default browser
UIApplication.shared.open(url)
}
}
}
Expand Down
67 changes: 67 additions & 0 deletions NeoDB/NeoDB/References/Cursor/auth_and_profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ graph TD
B --> C[ProfileViewModel]
C --> D[ProfileView]
A --> D
E[Router] --> D
```

### State Management
Expand All @@ -20,6 +21,10 @@ graph TD
// Profile-level state
@Published var user: User?
@Published var isLoading: Bool

// Navigation state
@Published var path: [RouterDestination]
@Published var presentedSheet: SheetDestination?
```

## Key Components
Expand Down Expand Up @@ -55,6 +60,51 @@ class UserService {
}
```

### Router Integration
```swift
// Profile-related destinations
case userProfile(id: String)
case userProfileWithUser(user: User)
case followers(id: String)
case following(id: String)

// Navigation in views
Button {
router.navigate(to: .userProfile(id: account.id))
} label: {
KFImage(URL(string: account.avatar))
.placeholder { ... }
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 44, height: 44)
.clipShape(Circle())
}

// For User type
Button {
router.navigate(to: .userProfileWithUser(user: user))
} label: {
UserAvatarView(user: user)
}
```

### Navigation Examples
```swift
// In StatusView
Button {
router.navigate(to: .userProfile(id: status.account.id))
} label: {
// Avatar or username view
}

// In ProfileView
Button {
router.navigate(to: .followers(id: user.id))
} label: {
Text("Followers")
}
```

### Cache Strategy
```swift
// Cache key format
Expand Down Expand Up @@ -100,6 +150,13 @@ KFImage(URL(string: user.avatar))
.clipShape(Circle())
```

### Navigation Features
- Profile view navigation
- Followers/Following lists
- Deep linking support
- Back navigation
- Sheet presentations

### Image Loading Features
- Automatic caching
- Placeholder support
Expand Down Expand Up @@ -149,11 +206,14 @@ enum AuthError {
- [ ] Background sync
- [ ] Rate limiting
- [ ] Error retry mechanism
- [ ] Analytics tracking
- [ ] Deep linking enhancements

### Performance Optimizations
- Preload avatar images
- Cache size limits
- Background data prefetch
- Navigation state persistence

## API Endpoints

Expand All @@ -173,4 +233,11 @@ Response: {
avatar: string
username: string
}
```

### Navigation
```
/users/{id}
/users/{id}/followers
/users/{id}/following
```
Loading