Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
efc29fd
Add device specifications endpoint
Xialtal Dec 23, 2025
62bdd68
Add field to device specs model
Xialtal Dec 23, 2025
6be8cd8
[WIP] DevDB Specifications
Xialtal Dec 25, 2025
29172e3
Make profile devices clickable
Xialtal Dec 27, 2025
0abb565
Merge remote-tracking branch 'origin/develop' into feature/devdb-entry
Xialtal Mar 27, 2026
03f1bd7
Post-merge fix
Xialtal Mar 27, 2026
a3730d1
Extract listSectionSpacing backport to SharedUI
Xialtal Mar 27, 2026
8c50c49
[WIP] Device Specifications
Xialtal Mar 27, 2026
7064067
Add error handling to device specifications
Xialtal Mar 27, 2026
3fed48c
Add navigationTitle for DeviceSpecifications
Xialtal Mar 27, 2026
2092687
Add device status change for DeviceSpecifications
Xialtal Mar 27, 2026
e3cc371
Improve navigationTitle for DeviceSpecifications
Xialtal Mar 27, 2026
35ad0cc
Fix localizable
Xialtal Mar 27, 2026
358d1c9
[WIP] DeviceSpecifications
Xialtal Mar 27, 2026
d15d83d
Fix device editions parser
Xialtal Mar 27, 2026
5366428
Improve device name in header for DeviceSpecifications
Xialtal Mar 27, 2026
b966970
Add device editions to DeviceSpecifications
Xialtal Mar 27, 2026
a5ace13
Add context menu to DeviceSpecifications
Xialtal Mar 27, 2026
cf0f1ca
Fix device update request
Xialtal Mar 27, 2026
4da3088
Add device update animations
Xialtal Mar 27, 2026
17e78aa
Fix device update progress spinner
Xialtal Mar 27, 2026
ae4d079
Add device limit check
Xialtal Mar 27, 2026
55e16f2
Add images gallery to DeviceSpecifications
Xialtal Mar 27, 2026
4ddf5e7
Improve devices section in profile
Xialtal Mar 27, 2026
419b35e
Improve UI for long entry
Xialtal Mar 27, 2026
c77a64d
Improve namings
Xialtal Mar 27, 2026
90272b5
Merge branch 'develop' into feature/devdb-specs
Xialtal Mar 29, 2026
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
14 changes: 14 additions & 0 deletions Modules/Sources/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public struct APIClient: Sendable {
public var search: @Sendable (_ request: SearchRequest) async throws -> SearchResponse
public var searchUsers: @Sendable (_ request: SearchUsersRequest) async throws -> SearchUsersResponse

// DevDB
public var deviceSpecifications: @Sendable (_ tag: String, _ subTag: String) async throws -> DeviceSpecifications

// STREAMS
public var connectionState: @Sendable () -> AsyncStream<ConnectionState> = { .finished }
public var notificationStream: @Sendable () -> AsyncStream<String> = { .finished }
Expand Down Expand Up @@ -563,6 +566,14 @@ extension APIClient: DependencyKey {
return try await parser.parseSearchUsers(response)
},

// MARK: - Device Specs

deviceSpecifications: { tag, subTag in
let command = DeviceCommand.entry(tag: tag, subTag: subTag)
let response = try await api.send(command)
return try await parser.parseDeviceSpecifications(response: response)
},

// MARK: - Streams

connectionState: {
Expand Down Expand Up @@ -723,6 +734,9 @@ extension APIClient: DependencyKey {
searchUsers: { _ in
return .mock
},
deviceSpecifications: { _, _ in
return .mock
},
connectionState: {
return .finished
},
Expand Down
3 changes: 3 additions & 0 deletions Modules/Sources/AnalyticsClient/Events/ProfileEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum ProfileEvent: Event {
case reputationTapped
case searchTopicsTapped
case searchRepliesTapped
case deviceButtonTapped(String)
case userLoaded(Int)
case userLoadingFailed
case achievementTapped
Expand All @@ -33,6 +34,8 @@ public enum ProfileEvent: Event {
switch self {
case let .userLoaded(userId):
return ["userId": String(userId)]
case let .deviceButtonTapped(tag):
return ["deviceTag": tag]
case let .curatedTopicTapped(topicId):
return ["topicId": String(topicId)]
default:
Expand Down
3 changes: 3 additions & 0 deletions Modules/Sources/AppFeature/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import ToastClient
import Combine
import SearchResultFeature
import CacheClient
import DeviceSpecificationsFeature

@Reducer
public struct AppFeature: Reducer, Sendable {
Expand Down Expand Up @@ -637,6 +638,8 @@ public struct AppFeature: Reducer, Sendable {
screen = .articles(.article(ArticleFeature.State(articlePreview: preview, scrollToId: scrollToId)))
case let .announcement(id):
screen = .forum(.announcement(AnnouncementFeature.State(id: id)))
case let .device(tag, subTag):
screen = .devDB(.specifications(DeviceSpecificationsFeature.State(tag: tag, subTag: subTag)))
case let .topic(id, goTo):
screen = .forum(.topic(TopicFeature.State(topicId: id!, goTo: goTo)))
case let .forum(id, page):
Expand Down
20 changes: 20 additions & 0 deletions Modules/Sources/AppFeature/Navigation/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import AnnouncementFeature
import ArticleFeature
import ArticlesListFeature
import DeveloperFeature
import DeviceSpecificationsFeature
import FavoritesRootFeature
import FavoritesFeature
import ForumFeature
Expand All @@ -31,6 +32,7 @@ import AuthFeature
@Reducer
public enum Path {
case articles(Articles.Body = Articles.body)
case devDB(DevDB.Body = DevDB.body)
case favorites(FavoritesFeature)
case forum(Forum.Body = Forum.body)
case profile(Profile.Body = Profile.body)
Expand All @@ -45,6 +47,11 @@ public enum Path {
case article(ArticleFeature)
}

@Reducer
public enum DevDB {
case specifications(DeviceSpecificationsFeature)
}

@Reducer
public enum Profile {
case profile(ProfileFeature)
Expand Down Expand Up @@ -84,6 +91,7 @@ public enum Path {

extension Path.State: Equatable {}
extension Path.Articles.State: Equatable {}
extension Path.DevDB.State: Equatable {}
extension Path.Profile.State: Equatable {}
extension Path.Forum.State: Equatable {}
extension Path.Settings.State: Equatable {}
Expand All @@ -97,6 +105,9 @@ extension Path {
case let .articles(path):
ArticlesViews(path)

case let .devDB(path):
DevDBViews(path)

case let .favorites(store):
FavoritesScreen(store: store)
.tracking(for: FavoritesScreen.self)
Expand Down Expand Up @@ -135,6 +146,15 @@ extension Path {
}
}

@MainActor @ViewBuilder
private static func DevDBViews(_ store: Store<Path.DevDB.State, Path.DevDB.Action>) -> some View {
switch store.case {
case let .specifications(store):
DeviceSpecificationsScreen(store: store)
.tracking(for: DeviceSpecificationsScreen.self)
}
}

@MainActor @ViewBuilder
private static func ProfileViews(_ store: Store<Path.Profile.State, Path.Profile.Action>) -> some View {
switch store.case {
Expand Down
24 changes: 23 additions & 1 deletion Modules/Sources/AppFeature/Navigation/StackTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import ReputationFeature
import AuthFeature
import SearchFeature
import SearchResultFeature
import DeviceSpecificationsFeature

@Reducer
public struct StackTab: Reducer, Sendable {
Expand Down Expand Up @@ -138,6 +139,9 @@ public struct StackTab: Reducer, Sendable {
case let .articles(action):
return handleArticlesPathNavigation(action: action, state: &state)

case let .devDB(action):
return handleDevDBPathNavigation(action: action, state: &state)

case let .favorites(action):
return handleFavoritesPathNavigation(action: action, state: &state)

Expand Down Expand Up @@ -184,6 +188,19 @@ public struct StackTab: Reducer, Sendable {
return .none
}

// MARK: - DevDB

private func handleDevDBPathNavigation(action: Path.DevDB.Action, state: inout State) -> Effect<Action> {
switch action {
case let .specifications(.delegate(.openDevice(tag, subTag))):
state.path.append(.devDB(.specifications(DeviceSpecificationsFeature.State(tag: tag, subTag: subTag))))

default:
break
}
return .none
}

// MARK: - Favorites

private func handleFavoritesPathNavigation(action: FavoritesFeature.Action, state: inout State) -> Effect<Action> {
Expand Down Expand Up @@ -288,6 +305,8 @@ public struct StackTab: Reducer, Sendable {
case .profile(.delegate(.openSettings)):
state.path.append(.settings(.settings(SettingsFeature.State())))

case let .profile(.delegate(.openDevice(tag))):
state.path.append(.devDB(.specifications(DeviceSpecificationsFeature.State(tag: tag, subTag: ""))))
case let .profile(.delegate(.openTopic(id))):
state.path.append(.forum(.topic(TopicFeature.State(topicId: id))))

Expand Down Expand Up @@ -461,7 +480,10 @@ public struct StackTab: Reducer, Sendable {
case let .search(options: options):
state.path.append(.search(.searchResult(SearchResultFeature.State(search: options))))

case let .article(id: id, title: title, imageUrl: imageUrl, scrollToId: scrollToId):
case let .device(tag, subTag):
state.path.append(.devDB(.specifications(DeviceSpecificationsFeature.State(tag: tag, subTag: subTag))))

case let .article(id: id, title: title, imageUrl: imageUrl, scrollToId):
let preview = ArticlePreview.outerDeeplink(id: id, imageUrl: imageUrl, title: title)
state.path.append(.articles(.article(ArticleFeature.State(articlePreview: preview, scrollToId: scrollToId))))
}
Expand Down
18 changes: 18 additions & 0 deletions Modules/Sources/DeeplinkHandler/DeeplinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public enum Deeplink {
case user(id: Int)
case qms(id: Int)
case search(SearchResult)
case device(tag: String, subTag: String)
}

public struct DeeplinkHandler {
Expand Down Expand Up @@ -124,6 +125,23 @@ public struct DeeplinkHandler {

guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { throw .noUrlComponents(in: url) }

// devdb

if url.pathComponents.contains("devdb") {
if url.pathComponents.count == 4 { // /devdb/phones/apple
// TODO: vendor deeplink
} else if url.pathComponents.count == 3, !url.pathComponents[2].isEmpty {
if let _ = DeviceType(rawValue: url.pathComponents[2]) { // /devdb/phones
// TODO: deviceType deeplink
} else { // /devdb/apple_iphone_13
let tags = url.pathComponents[2].components(separatedBy: ":")
let subTag = tags.first == tags.last ? "" : tags.last!

return .device(tag: tags.first!, subTag: subTag)
}
}
}

guard let queryItems = components.queryItems else { throw .noQueryItems(in: url) }

// site search
Expand Down
Loading
Loading