Skip to content
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
21 changes: 21 additions & 0 deletions Modules/Sources/APIClient/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public struct APIClient: Sendable {
public var searchUsers: @Sendable (_ request: SearchUsersRequest) async throws -> SearchUsersResponse

// DevDB
public var deviceBrands: @Sendable (_ type: DeviceType) async throws -> DeviceVendorsList
public var deviceVendor: @Sendable (_ name: String, _ type: DeviceType) async throws -> DeviceVendor
public var deviceSpecifications: @Sendable (_ tag: String, _ subTag: String) async throws -> DeviceSpecifications

// STREAMS
Expand Down Expand Up @@ -568,6 +570,19 @@ extension APIClient: DependencyKey {

// MARK: - Device Specs

deviceBrands: { type in
let command = DeviceCommand.type(typeCode: type.transferType)
let response = try await api.send(command)
return try await parser.parseDeviceBrands(response)
},
deviceVendor: { name, type in
let command = DeviceCommand.vendor(
typeCode: type.transferType,
vendorCode: name
)
let response = try await api.send(command)
return try await parser.parseDeviceVendor(response)
},
deviceSpecifications: { tag, subTag in
let command = DeviceCommand.entry(tag: tag, subTag: subTag)
let response = try await api.send(command)
Expand Down Expand Up @@ -734,6 +749,12 @@ extension APIClient: DependencyKey {
searchUsers: { _ in
return .mock
},
deviceBrands: { _ in
return .mock
},
deviceVendor: { _, _ in
return .mock
},
deviceSpecifications: { _, _ in
return .mock
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// DeviceType+Extension.swift
// ForPDA
//
// Created by Xialtal on 2.04.26.
//

import PDAPI
import Models

extension DeviceType {
var transferType: DeviceCommand.DeviceType {
switch self {
case .phone: .phone
case .ebook: .ebook
case .pad: .pad
case .smartWatch: .smartWatch
}
}
}
28 changes: 28 additions & 0 deletions Modules/Sources/AnalyticsClient/Events/DeviceTypeEvent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// DeviceTypeEvent.swift
// ForPDA
//
// Created by Xialtal on 13.04.26.
//

public enum DeviceTypeEvent: Event {

case typeTapped(String)
case deviceTapped(String)
case vendorTapped(String, type: String)

public var name: String {
return "DeviceType " + eventName(for: self).inProperCase
}

public var properties: [String: String]? {
switch self {
case .deviceTapped(let tag):
return ["tag": name]
case .typeTapped(let type):
return ["type": type]
case .vendorTapped(let name, let type):
return ["type": type, "name": name]
}
}
}
14 changes: 12 additions & 2 deletions Modules/Sources/AppFeature/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Combine
import SearchResultFeature
import CacheClient
import DeviceSpecificationsFeature
import DeviceTypeFeature

@Reducer
public struct AppFeature: Reducer, Sendable {
Expand Down Expand Up @@ -638,8 +639,17 @@ 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 .device(goTo):
screen = switch goTo {
case .index:
.devDB(.type(DeviceTypeFeature.State(content: .index)))
case .vendorsList(let type):
.devDB(.type(DeviceTypeFeature.State(content: .vendorsList(type))))
case .vendor(let vendorName, let type):
.devDB(.type(DeviceTypeFeature.State(content: .vendor(vendorName, type: type))))
case .device(let tag, let subTag):
.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
6 changes: 6 additions & 0 deletions Modules/Sources/AppFeature/Navigation/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ArticleFeature
import ArticlesListFeature
import DeveloperFeature
import DeviceSpecificationsFeature
import DeviceTypeFeature
import FavoritesRootFeature
import FavoritesFeature
import ForumFeature
Expand Down Expand Up @@ -49,6 +50,7 @@ public enum Path {

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

Expand Down Expand Up @@ -149,6 +151,10 @@ extension Path {
@MainActor @ViewBuilder
private static func DevDBViews(_ store: Store<Path.DevDB.State, Path.DevDB.Action>) -> some View {
switch store.case {
case let .type(store):
DeviceTypeScreen(store: store)
.tracking(for: DeviceTypeScreen.self)
Comment thread
SubvertDev marked this conversation as resolved.

case let .specifications(store):
DeviceSpecificationsScreen(store: store)
.tracking(for: DeviceSpecificationsScreen.self)
Expand Down
23 changes: 21 additions & 2 deletions Modules/Sources/AppFeature/Navigation/StackTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import AuthFeature
import SearchFeature
import SearchResultFeature
import DeviceSpecificationsFeature
import DeviceTypeFeature

@Reducer
public struct StackTab: Reducer, Sendable {
Expand Down Expand Up @@ -192,6 +193,15 @@ public struct StackTab: Reducer, Sendable {

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

case let .type(.delegate(.openVendorsList(type))):
state.path.append(.devDB(.type(DeviceTypeFeature.State(content: .vendorsList(type)))))

case let .type(.delegate(.openVendor(code, type))):
state.path.append(.devDB(.type(DeviceTypeFeature.State(content: .vendor(code, type: type)))))

case let .specifications(.delegate(.openDevice(tag, subTag))):
state.path.append(.devDB(.specifications(DeviceSpecificationsFeature.State(tag: tag, subTag: subTag))))

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

case let .device(tag, subTag):
state.path.append(.devDB(.specifications(DeviceSpecificationsFeature.State(tag: tag, subTag: subTag))))
case let .device(goTo):
switch goTo {
case .index:
state.path.append(.devDB(.type(DeviceTypeFeature.State(content: .index))))
case .vendorsList(let type):
state.path.append(.devDB(.type(DeviceTypeFeature.State(content: .vendorsList(type)))))
case .vendor(let vendorName, let type):
state.path.append(.devDB(.type(DeviceTypeFeature.State(content: .vendor(vendorName, type: type)))))
case .device(let tag, let 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)
Expand Down
17 changes: 12 additions & 5 deletions Modules/Sources/DeeplinkHandler/DeeplinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum Deeplink {
case user(id: Int)
case qms(id: Int)
case search(SearchResult)
case device(tag: String, subTag: String)
case device(DeviceGoTo)
}

public struct DeeplinkHandler {
Expand Down Expand Up @@ -129,16 +129,23 @@ public struct DeeplinkHandler {

if url.pathComponents.contains("devdb") {
if url.pathComponents.count == 4 { // /devdb/phones/apple
// TODO: vendor deeplink
guard let type = DeviceType(rawValue: String(url.pathComponents[2])) else {
throw .unknownType(type: "DeviceType", for: url.absoluteString)
}
let tag = String(url.pathComponents[3])

return .device(.vendor(tag: tag, type: type))
} else if url.pathComponents.count == 3, !url.pathComponents[2].isEmpty {
if let _ = DeviceType(rawValue: url.pathComponents[2]) { // /devdb/phones
// TODO: deviceType deeplink
if let type = DeviceType(rawValue: url.pathComponents[2]) { // /devdb/phones
return .device(.vendorsList(type))
} 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)
return .device(.device(tag: tags.first!, subTag: subTag))
}
} else if url.pathComponents.count == 2, url.pathComponents[1] == "devdb" {
return .device(.index)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,25 +147,26 @@ public struct DeviceSpecificationsScreen: View {

@ViewBuilder
private func HeaderImages(_ images: [DeviceSpecifications.DeviceImage]) -> some View {
HStack(spacing: 8) {
HStack(spacing: 0) {
ForEach(Array(images.enumerated()), id: \.element) { index, image in
LazyImage(url: image.url) { state in
Group {
if let image = state.image {
image.resizable().scaledToFill()
image.resizable().scaledToFit()
} else {
Color(.systemBackground)
}
}
.skeleton(with: state.isLoading, shape: .rectangle)
}
.frame(width: 37, height: 75)
.frame(width: 75, height: 75)
.clipped()
.onTapGesture {
send(.headerImageTapped(index))
}
}
}
.padding(.top, 8)
}

@ViewBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// DeviceTypeFeature+Analytics.swift
// ForPDA
//
// Created by Xialtal on 13.04.2026.
//

import ComposableArchitecture
import AnalyticsClient

extension DeviceTypeFeature {

struct Analytics: Reducer {
typealias State = DeviceTypeFeature.State
typealias Action = DeviceTypeFeature.Action

@Dependency(\.analyticsClient) var analytics

var body: some Reducer<State, Action> {
Reduce<State, Action> { state, action in
switch action {
case .view(.deviceButtonTapped(let tag)):
analytics.log(DeviceTypeEvent.deviceTapped(tag))

case .view(.typeButtonTapped(let type)):
analytics.log(DeviceTypeEvent.typeTapped(type.rawValue))

case .view(.vendorButtonTapped(let name, let type)):
analytics.log(DeviceTypeEvent.vendorTapped(name, type: type.rawValue))

case .delegate, .internal, .view:
break
}
return .none
}
}
}
}
Loading
Loading