Compare commits
10 Commits
8517c5afc3
...
eecee74af2
| Author | SHA1 | Date | |
|---|---|---|---|
| eecee74af2 | |||
| 4c0a63a1f4 | |||
| 73c4f260d3 | |||
| 1d12ff5625 | |||
| 3463e9b7b2 | |||
| d0209d298b | |||
| 6b3273ad2f | |||
| 9f6f3eb6b2 | |||
| 260bf6017f | |||
| f4939c0099 |
14
TODO.md
Normal file
14
TODO.md
Normal file
@ -0,0 +1,14 @@
|
||||
## 增加接口
|
||||
|
||||
### 1. 获取用户的关注数
|
||||
GET: /api/follow_num?user_id=$user_id
|
||||
|
||||
### 2. 保存用户的设备token
|
||||
url: /api/device_token
|
||||
method: post
|
||||
params:
|
||||
{
|
||||
user_id: string,
|
||||
token: string
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
C85F58C02D64D10F00D761E9 /* dimensionhub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dimensionhub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C85F58D22D64D11000D761E9 /* dimensionhubTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = dimensionhubTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C85F58DC2D64D11000D761E9 /* dimensionhubUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = dimensionhubUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C8E6E2052DAF4A130014CDB3 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@ -88,6 +89,7 @@
|
||||
C85F58B72D64D10F00D761E9 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C8E6E2052DAF4A130014CDB3 /* TODO.md */,
|
||||
C85F58C22D64D10F00D761E9 /* dimensionhub */,
|
||||
C85F58D52D64D11000D761E9 /* dimensionhubTests */,
|
||||
C85F58DF2D64D11000D761E9 /* dimensionhubUITests */,
|
||||
|
||||
@ -10,5 +10,23 @@
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>C85F58BF2D64D10F00D761E9</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>C85F58D12D64D11000D761E9</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>C85F58DB2D64D11000D761E9</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -61,6 +61,13 @@ struct API {
|
||||
return await doRequest(request: request, as: T.self)
|
||||
}
|
||||
|
||||
// 获取用户的关注数
|
||||
static func getFlowNum<T: Codable>(userId: String, as: T.Type) async -> APIResponse<T> {
|
||||
let request = URLRequest(url: URL(string: baseUrl + "/api/follow_num?user_id=\(userId)")!)
|
||||
|
||||
return await doRequest(request: request, as: T.self)
|
||||
}
|
||||
|
||||
// 前后刷新获取数据
|
||||
static func loadMoreUpdateDramas<T: Codable>(userId: String, mode: LoadMode, id: Int, as: T.Type) async -> APIResponse<T> {
|
||||
let request = URLRequest(url: URL(string: baseUrl + "/api/load_more_dramas?user_id=\(userId)&mode=\(mode.rawValue)&id=\(id)")!)
|
||||
|
||||
@ -77,13 +77,13 @@ final class CacheManager {
|
||||
}
|
||||
|
||||
// 保存文件到缓存
|
||||
private func saveCacheFile(filename: String, data: Data) throws {
|
||||
func saveCacheFile(filename: String, data: Data) throws {
|
||||
let fileURL = cacheDir.appendingPathComponent(filename)
|
||||
try data.write(to: fileURL)
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
private func downloadImage(from urlString: String) async throws -> Data? {
|
||||
func downloadImage(from urlString: String) async throws -> Data? {
|
||||
guard let url = URL(string: urlString) else {
|
||||
return nil
|
||||
}
|
||||
@ -106,6 +106,10 @@ final class CacheManager {
|
||||
return sha256(str: urlString) + "." + url.pathExtension.lowercased()
|
||||
}
|
||||
|
||||
func getCacheFileName(url: URL) -> String {
|
||||
return sha256(str: url.absoluteString) + "." + url.pathExtension.lowercased()
|
||||
}
|
||||
|
||||
private func sha256(str: String) -> String {
|
||||
let data = Data(str.utf8)
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
|
||||
@ -28,11 +28,9 @@ struct FlexImage: View {
|
||||
|
||||
let cacheManager = CacheManager.shared
|
||||
if let data = cacheManager.readFileContents(urlString: urlString) {
|
||||
//print("url: \(urlString), hit cache")
|
||||
self.mode = .local(data)
|
||||
} else {
|
||||
Task.detached {
|
||||
await cacheManager.preloadImage(url: urlString)
|
||||
}
|
||||
self.mode = .remote(urlString)
|
||||
}
|
||||
}
|
||||
@ -40,7 +38,7 @@ struct FlexImage: View {
|
||||
var body: some View {
|
||||
switch self.mode {
|
||||
case .remote(let url):
|
||||
AsyncImage(url: URL(string: url)) { phase in
|
||||
FlexAsyncImage(url: URL(string: url)) { phase in
|
||||
switch phase {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
@ -50,7 +48,7 @@ struct FlexImage: View {
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: width, height: height)
|
||||
.clipped()
|
||||
default:
|
||||
case .failure:
|
||||
Image(placeholder)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
@ -75,6 +73,61 @@ struct FlexImage: View {
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
//FlexImage()
|
||||
enum FlexImagePhase {
|
||||
case empty
|
||||
case success(Image)
|
||||
case failure
|
||||
}
|
||||
|
||||
struct FlexAsyncImage<Content: View>: View {
|
||||
let url: URL?
|
||||
@State private var phase: FlexImagePhase = .empty
|
||||
@ViewBuilder let content: (FlexImagePhase) -> Content
|
||||
|
||||
init(url: URL?, @ViewBuilder content: @escaping (FlexImagePhase) -> Content) {
|
||||
self.url = url
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
content(phase)
|
||||
.task(id: url) {
|
||||
guard let url else {
|
||||
return
|
||||
}
|
||||
|
||||
let cacheManager = CacheManager.shared
|
||||
|
||||
phase = .empty
|
||||
do {
|
||||
if let data = cacheManager.readFileContents(urlString: url.absoluteString),
|
||||
let cachedImage = await decodeImageData(data) {
|
||||
phase = .success(Image(uiImage: cachedImage))
|
||||
return
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
if let image = await decodeImageData(data) {
|
||||
let cacheFilename = cacheManager.getCacheFileName(url: url)
|
||||
try? cacheManager.saveCacheFile(filename: cacheFilename, data: data)
|
||||
|
||||
phase = .success(Image(uiImage: image))
|
||||
} else {
|
||||
phase = .empty
|
||||
}
|
||||
} catch {
|
||||
phase = .failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeImageData(_ data: Data) async -> UIImage? {
|
||||
await Task.detached {
|
||||
UIImage(data: data)
|
||||
}.value
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FlexImage(urlString: "https://lain.bgm.tv/pic/cover/l/60/fe/358801_WuBx6.jpg", width: 80, height: 80, placeholder: "ph_img_big")
|
||||
}
|
||||
|
||||
40
dimensionhub/Views/FollowList/FollowDramaModel.swift
Normal file
40
dimensionhub/Views/FollowList/FollowDramaModel.swift
Normal file
@ -0,0 +1,40 @@
|
||||
//
|
||||
// FollowListModel.swift
|
||||
// dimensionhub
|
||||
//
|
||||
// Created by 安礼成 on 2025/4/8.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
final class FollowDramaModel {
|
||||
|
||||
@ObservationIgnored
|
||||
var drama: FollowListModel.DramaItem
|
||||
|
||||
// 选中的频道信息
|
||||
var checkedChannelId: Int = 0
|
||||
var episodes: [FollowListModel.DramaItem.Channel.Episode]
|
||||
|
||||
init(drama: FollowListModel.DramaItem) {
|
||||
self.drama = drama
|
||||
self.checkedChannelId = 0
|
||||
if let channel = drama.channels.first {
|
||||
self.episodes = channel.episodes
|
||||
} else {
|
||||
self.episodes = []
|
||||
}
|
||||
}
|
||||
|
||||
func changeChannel(channelId: Int) {
|
||||
self.checkedChannelId = channelId
|
||||
if drama.channels.indices.contains(channelId) {
|
||||
self.episodes = drama.channels[channelId].episodes
|
||||
} else {
|
||||
self.episodes = []
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,6 +12,8 @@ import Observation
|
||||
final class FollowListModel {
|
||||
|
||||
struct DramaItem: Codable {
|
||||
|
||||
struct Channel: Codable {
|
||||
struct Episode: Codable, Identifiable {
|
||||
let id = UUID().uuidString
|
||||
let name: String
|
||||
@ -24,9 +26,13 @@ final class FollowListModel {
|
||||
}
|
||||
}
|
||||
|
||||
let name: String
|
||||
let episodes: [Episode]
|
||||
}
|
||||
|
||||
let id: Int
|
||||
let title: String
|
||||
let episodes: [Episode]
|
||||
let channels: [Channel]
|
||||
}
|
||||
|
||||
struct FavorResponse: Codable {
|
||||
@ -55,7 +61,10 @@ final class FollowListModel {
|
||||
private func preloadImages(dramas: [DramaItem]) {
|
||||
let cacheManager = CacheManager.shared
|
||||
dramas.forEach { dramaItem in
|
||||
let urls = dramaItem.episodes.map { $0.thumb }
|
||||
let urls = dramaItem.channels.flatMap { channel in
|
||||
channel.episodes.map { $0.thumb }
|
||||
}
|
||||
|
||||
if urls.count > 0 {
|
||||
Task.detached(priority: .medium) {
|
||||
try? await cacheManager.preloadImages(urls: urls)
|
||||
|
||||
@ -25,7 +25,7 @@ struct FollowListView: View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVStack(alignment: .center) {
|
||||
ForEach(followModel.dramas, id: \.id) { drama in
|
||||
DramaCellView(dramaItem: drama)
|
||||
DramaCellView(dramaModel: FollowDramaModel(drama: drama))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -54,21 +54,34 @@ extension FollowListView {
|
||||
|
||||
// 显示剧集的列表信息
|
||||
struct DramaCellView: View {
|
||||
let dramaItem: FollowListModel.DramaItem
|
||||
let dramaModel: FollowDramaModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
|
||||
NavigationLink(destination: DetailView(id: dramaItem.id)) {
|
||||
Text(dramaItem.title)
|
||||
NavigationLink(destination: DetailView(id: dramaModel.drama.id)) {
|
||||
Text(dramaModel.drama.title)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(Color(hex: "#333333"))
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
// 渠道列表
|
||||
HStack(alignment: .center, spacing: 15) {
|
||||
ForEach(Array(dramaModel.drama.channels.enumerated()), id: \.offset) { idx, channel in
|
||||
Text(channel.name)
|
||||
.font(.system(size: 13, weight: idx == dramaModel.checkedChannelId ? .medium : .regular))
|
||||
.foregroundColor(idx == dramaModel.checkedChannelId ? Color(hex: "#202020") : Color(hex: "#666666"))
|
||||
.onTapGesture {
|
||||
dramaModel.changeChannel(channelId: idx)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(alignment: .center, spacing: 5) {
|
||||
ForEach(dramaItem.episodes) { item in
|
||||
ForEach(dramaModel.episodes) { item in
|
||||
DramaCellEpisodeView(item: item)
|
||||
}
|
||||
}
|
||||
@ -78,7 +91,7 @@ extension FollowListView {
|
||||
}
|
||||
|
||||
struct DramaCellEpisodeView: View {
|
||||
let item: FollowListModel.DramaItem.Episode
|
||||
let item: FollowListModel.DramaItem.Channel.Episode
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
|
||||
@ -34,13 +34,13 @@ struct IndexMainView: View {
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Text("亚次元")
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.font(.system(size: 20, weight: .bold))
|
||||
.padding([.top, .bottom], 5)
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Text("♡ \(indexModel.follow_num)")
|
||||
.font(.system(size: 17))
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.black)
|
||||
.padding([.top, .bottom], 5)
|
||||
.padding(.leading, 10)
|
||||
@ -52,6 +52,9 @@ struct IndexMainView: View {
|
||||
}
|
||||
)
|
||||
.zIndex(1)
|
||||
.task {
|
||||
await indexModel.reloadFollowNum(userId: userId)
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing], 15)
|
||||
.frame(height: 50)
|
||||
@ -65,15 +68,15 @@ struct IndexMainView: View {
|
||||
LazyVStack(alignment: .center, spacing: 10) {
|
||||
ForEach(Array(indexModel.dramaGroupElements.enumerated()), id: \.offset) { idx, item in
|
||||
switch item {
|
||||
case .label(groupId: let groupId, groupName: let groupName):
|
||||
case .label(let groupId, let groupName):
|
||||
DramaGroupLabelView(group_name: groupName) {
|
||||
selectGroupId = groupId
|
||||
indexModel.selectedDate = groupId
|
||||
showDateNavPopover = true
|
||||
}
|
||||
case .item(groupId: let groupId, item: let item):
|
||||
case .item(let groupId, let item):
|
||||
DramaGroupItemView(groupId: groupId, item: item)
|
||||
.id(item.id)
|
||||
.id(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,8 +51,10 @@ final class IndexModel {
|
||||
}
|
||||
|
||||
enum GroupElement {
|
||||
case label(groupId: String, groupName: String)
|
||||
case item(groupId: String, item: UpdateDramaGroup.Item)
|
||||
// groupId, groupName
|
||||
case label(String, String)
|
||||
// groupId, UpdateDramaGroup.Item
|
||||
case item(String, UpdateDramaGroup.Item)
|
||||
}
|
||||
|
||||
var selectedDate: String
|
||||
@ -94,19 +96,15 @@ final class IndexModel {
|
||||
|
||||
self.cancel = self.scrollIDPublisher
|
||||
.sink { userId, scrollID in
|
||||
|
||||
self.dramaGroupElements.forEach { element in
|
||||
switch element {
|
||||
// 更新group的label的部分
|
||||
switch self.dramaGroupElements[scrollID] {
|
||||
case .label(_, _):
|
||||
()
|
||||
case .item(let groupId, let item):
|
||||
if item.id == scrollID {
|
||||
DispatchQueue.main.async {
|
||||
self.setFixedDrameGroup(groupId: groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断更新周期
|
||||
let ids = self.getDramaIds(self.updateDramaGroups).suffix(6)
|
||||
@ -144,6 +142,19 @@ final class IndexModel {
|
||||
self.isLoaded = true
|
||||
}
|
||||
|
||||
func reloadFollowNum(userId: String) async {
|
||||
let response = await API.getFlowNum(userId: userId, as: Int.self)
|
||||
switch response {
|
||||
case .error(let code, let message):
|
||||
print("reloadFollowNum get error: \(code), message: \(message)")
|
||||
case .result(let follow_num):
|
||||
await MainActor.run {
|
||||
self.follow_num = follow_num >= 100 ? "99+" : "\(follow_num)"
|
||||
}
|
||||
}
|
||||
self.isLoaded = true
|
||||
}
|
||||
|
||||
func setFixedDrameGroup(groupId: String) {
|
||||
if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}),
|
||||
newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id {
|
||||
@ -158,7 +169,7 @@ final class IndexModel {
|
||||
|
||||
// 按照id来判断不一定正确,需要借助其他值
|
||||
let dramaIds = self.getDramaIds(self.updateDramaGroups)
|
||||
print("current ids: \(dramaIds)")
|
||||
//print("current ids: \(dramaIds)")
|
||||
|
||||
switch mode {
|
||||
case .prev:
|
||||
@ -207,9 +218,9 @@ final class IndexModel {
|
||||
private func transformUpdateDramaGroups(groups: [UpdateDramaGroup]) -> [GroupElement] {
|
||||
var groupElements: [GroupElement] = []
|
||||
for group in groups {
|
||||
groupElements.append(.label(groupId: group.group_id, groupName: group.group_name))
|
||||
groupElements.append(.label(group.group_id, group.group_name))
|
||||
for item in group.items {
|
||||
groupElements.append(.item(groupId: group.group_id, item: item))
|
||||
groupElements.append(.item(group.group_id, item))
|
||||
}
|
||||
}
|
||||
return groupElements
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
//
|
||||
// SearchBar.swift
|
||||
// dimensionhub
|
||||
//
|
||||
// Created by 安礼成 on 2025/4/8.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// 搜索框
|
||||
struct SearchBar: View {
|
||||
@State private var isSearching = false
|
||||
|
||||
@Binding<String> var searchText: String
|
||||
var placeholder: String
|
||||
var onSearch: () -> Void
|
||||
|
||||
@FocusState private var isFocused
|
||||
|
||||
var body: some View {
|
||||
// 自定义搜索栏
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
// 输入框
|
||||
TextField(placeholder, text: $searchText)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.keyboardType(.default)
|
||||
.focused($isFocused)
|
||||
.submitLabel(.search)
|
||||
.onSubmit {
|
||||
onSearch()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(8)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.overlay(alignment: .trailing) {
|
||||
HStack {
|
||||
if isSearching {
|
||||
Button(action: {
|
||||
searchText = ""
|
||||
}) {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.gray)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
// 搜索按钮
|
||||
Button {
|
||||
onSearch()
|
||||
} label: {
|
||||
Text("搜索")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
.disabled(searchText.isEmpty)
|
||||
}
|
||||
.onChange(of: searchText) {
|
||||
if searchText.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
isSearching = false
|
||||
} else {
|
||||
isSearching = true
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
DispatchQueue.main.async {
|
||||
isFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// SearchBar()
|
||||
//}
|
||||
@ -1,47 +0,0 @@
|
||||
//
|
||||
// SearchDramaGroupView.swift
|
||||
// dimensionhub
|
||||
//
|
||||
// Created by 安礼成 on 2025/4/8.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// 显示分组信息
|
||||
struct SearchDramaGroupView: View {
|
||||
let group: SearchModel.DramaGroup
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(alignment: .center, spacing: 10) {
|
||||
ForEach(group.items, id: \.id) { item in
|
||||
NavigationLink(destination: DetailView(id: item.id)) {
|
||||
FlexImage(urlString: item.thumb, width: 370, height: 180, placeholder: "ph_img_big")
|
||||
.frame(width: 370, height: 180)
|
||||
.overlay(alignment: .topLeading) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(item.name)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(item.status)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(5)
|
||||
.background(
|
||||
Color.black.opacity(0.6)
|
||||
)
|
||||
.cornerRadius(5)
|
||||
.padding(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#Preview {
|
||||
// SearchDramaGroupView()
|
||||
//}
|
||||
@ -10,58 +10,146 @@ import SwiftData
|
||||
|
||||
struct SearchView: View {
|
||||
@Environment(\.modelContext) var modelContext
|
||||
@State var showHistoryNum: Int = 2
|
||||
|
||||
@AppStorage("userId") private var userId: String = Utils.defaultUserId()
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var searchText: String = ""
|
||||
|
||||
@State var searchModel = SearchModel()
|
||||
|
||||
@FocusState private var isFocused: Bool
|
||||
@State private var isSearching = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
VStack(spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
SearchBar(searchText: $searchText, placeholder: "搜索...") {
|
||||
let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmedSearchText.isEmpty {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
Task.detached {
|
||||
await searchModel.search(userId: userId, name: trimmedSearchText)
|
||||
HStack(spacing: 8) {
|
||||
TextField("搜索...", text: $searchText)
|
||||
.focused($isFocused)
|
||||
.textFieldStyle(PlainTextFieldStyle())
|
||||
.padding(8)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.submitLabel(.search)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.default)
|
||||
.onSubmit {
|
||||
doSearch()
|
||||
}
|
||||
.overlay(alignment: .trailing) {
|
||||
Button(action: {
|
||||
searchText = ""
|
||||
}) {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.gray)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
.opacity(isSearching ? 1 : 0)
|
||||
.disabled(!isSearching)
|
||||
}
|
||||
|
||||
// let historyModel = SearchHistory(keyword: trimmedSearchText, timestamp: Date())
|
||||
// modelContext.insert(historyModel)
|
||||
Button {
|
||||
if !searchText.isEmpty {
|
||||
doSearch()
|
||||
isFocused = false // 关闭键盘
|
||||
}
|
||||
} label: {
|
||||
Text("搜索")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.trailing, 10)
|
||||
.disabled(searchText.isEmpty)
|
||||
}
|
||||
.onChange(of: searchText) {
|
||||
isSearching = !searchText.trimmingCharacters(in: .whitespaces).isEmpty
|
||||
}
|
||||
}
|
||||
.frame(height: 50)
|
||||
.padding([.top, .bottom], 8)
|
||||
.padding(.top, 12)
|
||||
|
||||
if searchModel.dramaGroups.isEmpty {
|
||||
Spacer()
|
||||
Text("什么都没有找到")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(Color(hex: "#333333"))
|
||||
Spacer()
|
||||
} else {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
// 基于日期的更新列表
|
||||
LazyVStack(alignment: .center, spacing: 10) {
|
||||
ForEach(searchModel.dramaGroups, id: \.group_id) { group in
|
||||
SearchDramaGroupView(group: group)
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.padding(8)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.padding(.horizontal, 12)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom) // 避免键盘遮挡
|
||||
.onAppear {
|
||||
// 避免过早触发系统输入错误
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
isFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func doSearch() {
|
||||
let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
Task.detached {
|
||||
await searchModel.search(userId: userId, name: trimmed)
|
||||
}
|
||||
// 可选:添加历史记录
|
||||
// let history = SearchHistory(keyword: trimmed, timestamp: Date())
|
||||
// modelContext.insert(history)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 显示分组信息
|
||||
struct SearchDramaGroupView: View {
|
||||
let group: SearchModel.DramaGroup
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(alignment: .center, spacing: 10) {
|
||||
ForEach(group.items, id: \.id) { item in
|
||||
NavigationLink(destination: DetailView(id: item.id)) {
|
||||
FlexImage(urlString: item.thumb, width: 370, height: 180, placeholder: "ph_img_big")
|
||||
.frame(width: 370, height: 180)
|
||||
.overlay(alignment: .topLeading) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(item.name)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(item.status)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(5)
|
||||
.background(
|
||||
Color.black.opacity(0.6)
|
||||
)
|
||||
.cornerRadius(5)
|
||||
.padding(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SearchView {
|
||||
|
||||
@ -68,7 +68,9 @@ struct dimensionhubApp: App {
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
registerForPushNotifications()
|
||||
Task.detached {
|
||||
await self.registerForPushNotifications()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -144,9 +146,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
|
||||
//let userInfo = notification.request.content.userInfo
|
||||
let userInfo = notification.request.content.userInfo
|
||||
// 处理通知数据
|
||||
//handleRemoteNotification(userInfo: userInfo)
|
||||
handleRemoteNotification(userInfo: userInfo)
|
||||
|
||||
// 设置如何显示通知
|
||||
completionHandler([.banner, .sound])
|
||||
@ -171,10 +173,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
// MARK: 消息处理
|
||||
|
||||
private func handleRemoteNotification(userInfo: [AnyHashable: Any]) {
|
||||
if let customData = userInfo["custom_data"] as? [String: AnyObject],
|
||||
let dramaId = customData["drama_id"] as? Int {
|
||||
guard let customData = userInfo["custom_data"] as? [String: AnyObject],
|
||||
let target = customData["target"] as? String,
|
||||
let params = customData["params"] as? [String : AnyObject] else {
|
||||
return
|
||||
}
|
||||
|
||||
switch target {
|
||||
case "detail":
|
||||
if let dramaId = params["drama_id"] as? Int {
|
||||
AppNavigation.shared.append(dest: .detail(id: dramaId))
|
||||
}
|
||||
default:
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleDeepLink(_ link: String) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user