Files
Adobe-Downloader/Adobe Downloader/ContentView.swift

299 lines
10 KiB
Swift

import SwiftUI
struct ContentView: View {
@EnvironmentObject private var networkManager: NetworkManager
@State private var isRefreshing = false
@State private var errorMessage: String?
@State private var showDownloadManager = false
@State private var searchText = ""
@State private var useDefaultLanguage = true
@State private var useDefaultDirectory = true
@AppStorage("defaultLanguage") private var defaultLanguage: String = "zh_CN"
@AppStorage("defaultDirectory") private var defaultDirectory: String = ""
@State private var showLanguagePicker = false
private var filteredProducts: [Product] {
let products = networkManager.products.values
.filter { !$0.hidden && !$0.versions.isEmpty }
.sorted { $0.displayName < $1.displayName }
if searchText.isEmpty {
return Array(products)
}
return products.filter {
$0.displayName.localizedCaseInsensitiveContains(searchText) ||
$0.sapCode.localizedCaseInsensitiveContains(searchText)
}
}
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 20) {
HStack {
Text("Adobe Downloader")
.font(.title2)
.fontWeight(.bold)
}
.frame(minWidth: 200)
HStack {
SettingsView(
useDefaultLanguage: $useDefaultLanguage,
useDefaultDirectory: $useDefaultDirectory,
onSelectLanguage: selectLanguage,
onSelectDirectory: selectDirectory
)
}
.frame(maxWidth: .infinity)
HStack(spacing: 8) {
SearchField(text: $searchText)
.frame(width: 160)
Button(action: refreshData) {
Image(systemName: "arrow.clockwise")
.imageScale(.large)
}
.disabled(isRefreshing)
.buttonStyle(.borderless)
Button(action: { showDownloadManager.toggle() }) {
Image(systemName: "arrow.down.circle")
.imageScale(.large)
}
.buttonStyle(.borderless)
.overlay(
Group {
if !networkManager.downloadTasks.isEmpty {
Text("\(networkManager.downloadTasks.count)")
.font(.caption2)
.padding(4)
.background(Color.blue)
.clipShape(Circle())
.foregroundColor(.white)
.offset(x: 10, y: -10)
}
}
)
}
.frame(width: 220)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(NSColor.windowBackgroundColor))
ZStack {
Color(NSColor.windowBackgroundColor)
.ignoresSafeArea()
switch networkManager.loadingState {
case .idle, .loading:
ProgressView("正在加载...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .failed(let error):
VStack(spacing: 20) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 48))
.foregroundColor(.red)
Text("加载失败")
.font(.title2)
.fontWeight(.medium)
Text(error.localizedDescription)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: 300)
.padding(.bottom, 10)
Button(action: {
networkManager.retryFetchData()
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
Text("重试")
}
.padding(.horizontal, 20)
.padding(.vertical, 8)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .success:
if filteredProducts.isEmpty {
ContentUnavailableView(
"没有找到产品",
systemImage: "magnifyingglass",
description: Text("尝试使用不同的搜索关键词")
)
} else {
ScrollView {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 250))],
spacing: 20
) {
ForEach(filteredProducts) { product in
AppCardView(product: product)
}
}
.padding()
}
}
}
}
}
.sheet(isPresented: $showLanguagePicker) {
LanguagePickerView(languages: AppStatics.supportedLanguages) { language in
defaultLanguage = language
showLanguagePicker = false
}
}
.sheet(isPresented: $showDownloadManager) {
DownloadManagerView()
.environmentObject(networkManager)
}
.onAppear {
if networkManager.products.isEmpty {
refreshData()
}
}
}
private func refreshData() {
isRefreshing = true
errorMessage = nil
Task {
await networkManager.fetchProducts()
await MainActor.run {
isRefreshing = false
}
}
}
private func selectLanguage() {
showLanguagePicker = true
}
private func selectDirectory() {
let panel = NSOpenPanel()
panel.title = "选择默认下载目录"
panel.canCreateDirectories = true
panel.canChooseDirectories = true
panel.canChooseFiles = false
if panel.runModal() == .OK {
defaultDirectory = panel.url?.path ?? ""
}
}
}
struct SearchField: View {
@Binding var text: String
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.secondary)
TextField("搜索应用", text: $text)
.textFieldStyle(PlainTextFieldStyle())
if !text.isEmpty {
Button(action: { text = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(8)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
#Preview {
let networkManager = NetworkManager()
let mockProducts: [String: Product] = [
"PHSP": Product(
id: "PHSP",
hidden: false,
displayName: "Photoshop",
sapCode: "PHSP",
versions: [
"25.0.0": Product.ProductVersion(
sapCode: "PHSP",
baseVersion: "25.0.0",
productVersion: "25.0.0",
apPlatform: "macuniversal",
dependencies: [],
buildGuid: ""
)
],
icons: [
Product.ProductIcon(
size: "192x192",
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/PHSP/25.0.0/192x192.png"
)
]
),
"ILST": Product(
id: "ILST",
hidden: false,
displayName: "Illustrator",
sapCode: "ILST",
versions: [
"28.0.0": Product.ProductVersion(
sapCode: "ILST",
baseVersion: "28.0.0",
productVersion: "28.0.0",
apPlatform: "macuniversal",
dependencies: [],
buildGuid: ""
)
],
icons: [
Product.ProductIcon(
size: "192x192",
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/ILST/28.0.0/192x192.png"
)
]
),
"AEFT": Product(
id: "AEFT",
hidden: false,
displayName: "After Effects",
sapCode: "AEFT",
versions: [
"24.0.0": Product.ProductVersion(
sapCode: "AEFT",
baseVersion: "24.0.0",
productVersion: "24.0.0",
apPlatform: "macuniversal",
dependencies: [],
buildGuid: ""
)
],
icons: [
Product.ProductIcon(
size: "192x192",
url: "https://ffc-static-cdn.oobesaas.adobe.com/icons/AEFT/24.0.0/192x192.png"
)
]
)
]
Task { @MainActor in
networkManager.products = mockProducts
networkManager.loadingState = .success
}
return ContentView()
.environmentObject(networkManager)
.frame(width: 850, height: 700)
}