custom explore page + api

This commit is contained in:
leminlimez committed 2025-07-15 15:00:42 -04:00
1 parent 5a94ea73d9
commit 4d6b093ffd
10 files changed
+415 -49

No files matched your search

@@ -6,41 +6,209 @@
//
import SwiftUI
import CachedAsyncImage
let MIN_SIZE: CGFloat = 165
struct ExploreView: View {
@State private var isLoading = true
@State private var error: Error? = nil
@ObservedObject var cowabungaAPI = CowabungaAPI.shared
#if DEBUG
@AppStorage("exploreLink") var exploreLink: String = DownloadManager.exploreLink
#endif
// lazyvgrid
@State private var gridItemLayout = [GridItem(.adaptive(minimum: MIN_SIZE))]
@State private var wallpapers: [DownloadableWallpaper] = []
@State var wallpaperTypeSelected = 0
@State var wallpaperTypeShown = DownloadableWallpaper.WallpaperType.custom
@State var filterType: FilterType = .random
@State var searchTerm: String = ""
var body: some View {
ZStack {
#if DEBUG
let websiteURL = URL(string: exploreLink)
#else
let websiteURL = URL(string: DownloadManager.exploreLink)
#endif
if let error = error {
NavigationView {
ScrollView {
PullToRefresh(coordinateSpaceName: "pullToRefresh") {
// refresh
wallpapers.removeAll()
Haptic.shared.play(.light)
//URLCache.imageCache.removeAllCachedResponses()
loadWallpapers()
}
VStack {
Text(error.localizedDescription)
.foregroundStyle(.red)
Picker("", selection: $wallpaperTypeSelected) {
Text("Custom").tag(0)
Text("Apple").tag(1)
}
.pickerStyle(.segmented)
}
.padding(.horizontal, 24)
.padding(.bottom, 10)
if wallpapers.isEmpty {
ProgressView()
.scaleEffect(1.75)
.navigationTitle("Explore")
} else {
LazyVGrid(columns: gridItemLayout) {
ForEach(wallpapers) { wallpaper in
if searchTerm == "" || wallpaper.name.lowercased().contains(searchTerm.lowercased()) || wallpaper.authors.lowercased().contains(searchTerm.lowercased()) {
Button(action: {
DownloadManager.shared.startTendiesDownload(for: cowabungaAPI.getDownloadURLForWallpaper(wallpaper: wallpaper))
}) {
VStack(spacing: 0) {
CachedAsyncImage(url: cowabungaAPI.getPreviewURLForWallpaper(wallpaper: wallpaper), urlCache: .imageCache) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity)
.cornerRadius(10, corners: .topLeft)
.cornerRadius(10, corners: .topRight)
} placeholder: {
Color.gray
.frame(height: MIN_SIZE)
}
HStack {
VStack(spacing: 4) {
HStack {
Text(wallpaper.name)
.foregroundStyle(Color(uiColor: .label))
.minimumScaleFactor(0.5)
Spacer()
}
HStack {
Text(wallpaper.authors)
.foregroundColor(.secondary)
.font(.caption)
.minimumScaleFactor(0.5)
Spacer()
}
}
.lineLimit(1)
Spacer()
Image(systemName: "arrow.down.circle")
.foregroundColor(.blue)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(height: 58)
}
}
.frame(minWidth: MIN_SIZE)
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(10)
.padding(4)
}
}
}
.padding()
}
}
.searchable(text: $searchTerm)
.coordinateSpace(name: "pullToRefresh")
.navigationTitle("Explore")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isLoading = true
self.error = nil
showFilterChangerPopup()
}) {
Text("Retry")
Image(systemName: "line.3.horizontal.decrease.circle")
}
}
} else if let url = websiteURL {
WallpaperWebView(url, isLoading: $isLoading, error: $error)
if isLoading {
ProgressView()
}
} else {
Text("Failed to load explore page.")
}
}
.onAppear {
loadWallpapers()
}
.onChange(of: wallpaperTypeSelected) { newValue in
let map = [0: DownloadableWallpaper.WallpaperType.custom, 1: .apple, 2: .template]
wallpaperTypeShown = map[newValue]!
wallpapers.removeAll()
loadWallpapers()
}
}
func loadWallpapers() {
Task {
do {
wallpapers = try await cowabungaAPI.fetchWallpapers(type: wallpaperTypeShown)
wallpapers = cowabungaAPI.filterWallpapers(wallpapers: wallpapers, filterType: filterType)
} catch {
UIApplication.shared.alert(title: NSLocalizedString("Failed to fetch wallpapers", comment: ""), body: error.localizedDescription)
}
}
}
func showFilterChangerPopup() {
// create and configure alert controller
let alert = UIAlertController(title: NSLocalizedString("Filter Wallpapers", comment: ""), message: "", preferredStyle: .actionSheet)
// create the actions
for type in FilterType.allCases {
let newAction = UIAlertAction(title: type.rawValue, style: .default) { (action) in
// apply the filter type
filterType = type
wallpapers.removeAll()
loadWallpapers()
}
if filterType == type {
// add a check mark
newAction.setValue(true, forKey: "checked")
}
alert.addAction(newAction)
}
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { (action) in
// cancels the action
}
// add the actions
alert.addAction(cancelAction)
let view: UIView = UIApplication.shared.windows.first!.rootViewController!.view
// present popover for iPads
alert.popoverPresentationController?.sourceView = view // prevents crashing on iPads
alert.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY, width: 0, height: 0) // show up at center bottom on iPads
// present the alert
UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true)
}
}
struct PullToRefresh: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
.scaleEffect(1.75)
.onAppear {
Haptic.shared.play(.light)
}
} else {
Text("")
}
Spacer()
}
}.padding(.top, -50)
}
}