Compare commits
No commits in common. "eecee74af27171855059ae831896697cf7a38f84" and "8517c5afc3405222f7a8cee0e3039df5100f6ddc" have entirely different histories.
eecee74af2
...
8517c5afc3
14
TODO.md
14
TODO.md
@ -1,14 +0,0 @@
|
|||||||
## 增加接口
|
|
||||||
|
|
||||||
### 1. 获取用户的关注数
|
|
||||||
GET: /api/follow_num?user_id=$user_id
|
|
||||||
|
|
||||||
### 2. 保存用户的设备token
|
|
||||||
url: /api/device_token
|
|
||||||
method: post
|
|
||||||
params:
|
|
||||||
{
|
|
||||||
user_id: string,
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -27,7 +27,6 @@
|
|||||||
C85F58C02D64D10F00D761E9 /* dimensionhub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dimensionhub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
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; };
|
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; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@ -89,7 +88,6 @@
|
|||||||
C85F58B72D64D10F00D761E9 = {
|
C85F58B72D64D10F00D761E9 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C8E6E2052DAF4A130014CDB3 /* TODO.md */,
|
|
||||||
C85F58C22D64D10F00D761E9 /* dimensionhub */,
|
C85F58C22D64D10F00D761E9 /* dimensionhub */,
|
||||||
C85F58D52D64D11000D761E9 /* dimensionhubTests */,
|
C85F58D52D64D11000D761E9 /* dimensionhubTests */,
|
||||||
C85F58DF2D64D11000D761E9 /* dimensionhubUITests */,
|
C85F58DF2D64D11000D761E9 /* dimensionhubUITests */,
|
||||||
|
|||||||
@ -10,23 +10,5 @@
|
|||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -61,13 +61,6 @@ struct API {
|
|||||||
return await doRequest(request: request, as: T.self)
|
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> {
|
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)")!)
|
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存文件到缓存
|
// 保存文件到缓存
|
||||||
func saveCacheFile(filename: String, data: Data) throws {
|
private func saveCacheFile(filename: String, data: Data) throws {
|
||||||
let fileURL = cacheDir.appendingPathComponent(filename)
|
let fileURL = cacheDir.appendingPathComponent(filename)
|
||||||
try data.write(to: fileURL)
|
try data.write(to: fileURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载图片
|
// 下载图片
|
||||||
func downloadImage(from urlString: String) async throws -> Data? {
|
private func downloadImage(from urlString: String) async throws -> Data? {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -106,10 +106,6 @@ final class CacheManager {
|
|||||||
return sha256(str: urlString) + "." + url.pathExtension.lowercased()
|
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 {
|
private func sha256(str: String) -> String {
|
||||||
let data = Data(str.utf8)
|
let data = Data(str.utf8)
|
||||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||||
|
|||||||
@ -28,9 +28,11 @@ struct FlexImage: View {
|
|||||||
|
|
||||||
let cacheManager = CacheManager.shared
|
let cacheManager = CacheManager.shared
|
||||||
if let data = cacheManager.readFileContents(urlString: urlString) {
|
if let data = cacheManager.readFileContents(urlString: urlString) {
|
||||||
//print("url: \(urlString), hit cache")
|
|
||||||
self.mode = .local(data)
|
self.mode = .local(data)
|
||||||
} else {
|
} else {
|
||||||
|
Task.detached {
|
||||||
|
await cacheManager.preloadImage(url: urlString)
|
||||||
|
}
|
||||||
self.mode = .remote(urlString)
|
self.mode = .remote(urlString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,7 +40,7 @@ struct FlexImage: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
switch self.mode {
|
switch self.mode {
|
||||||
case .remote(let url):
|
case .remote(let url):
|
||||||
FlexAsyncImage(url: URL(string: url)) { phase in
|
AsyncImage(url: URL(string: url)) { phase in
|
||||||
switch phase {
|
switch phase {
|
||||||
case .empty:
|
case .empty:
|
||||||
ProgressView()
|
ProgressView()
|
||||||
@ -48,7 +50,7 @@ struct FlexImage: View {
|
|||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: width, height: height)
|
.frame(width: width, height: height)
|
||||||
.clipped()
|
.clipped()
|
||||||
case .failure:
|
default:
|
||||||
Image(placeholder)
|
Image(placeholder)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
@ -73,61 +75,6 @@ struct FlexImage: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
#Preview {
|
||||||
FlexImage(urlString: "https://lain.bgm.tv/pic/cover/l/60/fe/358801_WuBx6.jpg", width: 80, height: 80, placeholder: "ph_img_big")
|
//FlexImage()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
//
|
|
||||||
// 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,27 +12,21 @@ import Observation
|
|||||||
final class FollowListModel {
|
final class FollowListModel {
|
||||||
|
|
||||||
struct DramaItem: Codable {
|
struct DramaItem: Codable {
|
||||||
|
struct Episode: Codable, Identifiable {
|
||||||
struct Channel: Codable {
|
let id = UUID().uuidString
|
||||||
struct Episode: Codable, Identifiable {
|
|
||||||
let id = UUID().uuidString
|
|
||||||
let name: String
|
|
||||||
let thumb: String
|
|
||||||
let num_name: String
|
|
||||||
let play: String
|
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
|
||||||
case name, thumb, num_name, play
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
let episodes: [Episode]
|
let thumb: String
|
||||||
|
let num_name: String
|
||||||
|
let play: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case name, thumb, num_name, play
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let id: Int
|
let id: Int
|
||||||
let title: String
|
let title: String
|
||||||
let channels: [Channel]
|
let episodes: [Episode]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FavorResponse: Codable {
|
struct FavorResponse: Codable {
|
||||||
@ -61,10 +55,7 @@ final class FollowListModel {
|
|||||||
private func preloadImages(dramas: [DramaItem]) {
|
private func preloadImages(dramas: [DramaItem]) {
|
||||||
let cacheManager = CacheManager.shared
|
let cacheManager = CacheManager.shared
|
||||||
dramas.forEach { dramaItem in
|
dramas.forEach { dramaItem in
|
||||||
let urls = dramaItem.channels.flatMap { channel in
|
let urls = dramaItem.episodes.map { $0.thumb }
|
||||||
channel.episodes.map { $0.thumb }
|
|
||||||
}
|
|
||||||
|
|
||||||
if urls.count > 0 {
|
if urls.count > 0 {
|
||||||
Task.detached(priority: .medium) {
|
Task.detached(priority: .medium) {
|
||||||
try? await cacheManager.preloadImages(urls: urls)
|
try? await cacheManager.preloadImages(urls: urls)
|
||||||
|
|||||||
@ -25,7 +25,7 @@ struct FollowListView: View {
|
|||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
LazyVStack(alignment: .center) {
|
LazyVStack(alignment: .center) {
|
||||||
ForEach(followModel.dramas, id: \.id) { drama in
|
ForEach(followModel.dramas, id: \.id) { drama in
|
||||||
DramaCellView(dramaModel: FollowDramaModel(drama: drama))
|
DramaCellView(dramaItem: drama)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,34 +54,21 @@ extension FollowListView {
|
|||||||
|
|
||||||
// 显示剧集的列表信息
|
// 显示剧集的列表信息
|
||||||
struct DramaCellView: View {
|
struct DramaCellView: View {
|
||||||
let dramaModel: FollowDramaModel
|
let dramaItem: FollowListModel.DramaItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
NavigationLink(destination: DetailView(id: dramaModel.drama.id)) {
|
NavigationLink(destination: DetailView(id: dramaItem.id)) {
|
||||||
Text(dramaModel.drama.title)
|
Text(dramaItem.title)
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
.foregroundColor(Color(hex: "#333333"))
|
.foregroundColor(Color(hex: "#333333"))
|
||||||
.lineLimit(1)
|
.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) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(alignment: .center, spacing: 5) {
|
LazyHStack(alignment: .center, spacing: 5) {
|
||||||
ForEach(dramaModel.episodes) { item in
|
ForEach(dramaItem.episodes) { item in
|
||||||
DramaCellEpisodeView(item: item)
|
DramaCellEpisodeView(item: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,7 +78,7 @@ extension FollowListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct DramaCellEpisodeView: View {
|
struct DramaCellEpisodeView: View {
|
||||||
let item: FollowListModel.DramaItem.Channel.Episode
|
let item: FollowListModel.DramaItem.Episode
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
|
|||||||
@ -34,13 +34,13 @@ struct IndexMainView: View {
|
|||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Text("亚次元")
|
Text("亚次元")
|
||||||
.font(.system(size: 20, weight: .bold))
|
.font(.system(size: 18, weight: .bold))
|
||||||
.padding([.top, .bottom], 5)
|
.padding([.top, .bottom], 5)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("♡ \(indexModel.follow_num)")
|
Text("♡ \(indexModel.follow_num)")
|
||||||
.font(.system(size: 18))
|
.font(.system(size: 17))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.padding([.top, .bottom], 5)
|
.padding([.top, .bottom], 5)
|
||||||
.padding(.leading, 10)
|
.padding(.leading, 10)
|
||||||
@ -52,9 +52,6 @@ struct IndexMainView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
.task {
|
|
||||||
await indexModel.reloadFollowNum(userId: userId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding([.leading, .trailing], 15)
|
.padding([.leading, .trailing], 15)
|
||||||
.frame(height: 50)
|
.frame(height: 50)
|
||||||
@ -68,15 +65,15 @@ struct IndexMainView: View {
|
|||||||
LazyVStack(alignment: .center, spacing: 10) {
|
LazyVStack(alignment: .center, spacing: 10) {
|
||||||
ForEach(Array(indexModel.dramaGroupElements.enumerated()), id: \.offset) { idx, item in
|
ForEach(Array(indexModel.dramaGroupElements.enumerated()), id: \.offset) { idx, item in
|
||||||
switch item {
|
switch item {
|
||||||
case .label(let groupId, let groupName):
|
case .label(groupId: let groupId, groupName: let groupName):
|
||||||
DramaGroupLabelView(group_name: groupName) {
|
DramaGroupLabelView(group_name: groupName) {
|
||||||
selectGroupId = groupId
|
selectGroupId = groupId
|
||||||
indexModel.selectedDate = groupId
|
indexModel.selectedDate = groupId
|
||||||
showDateNavPopover = true
|
showDateNavPopover = true
|
||||||
}
|
}
|
||||||
case .item(let groupId, let item):
|
case .item(groupId: let groupId, item: let item):
|
||||||
DramaGroupItemView(groupId: groupId, item: item)
|
DramaGroupItemView(groupId: groupId, item: item)
|
||||||
.id(idx)
|
.id(item.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,10 +51,8 @@ final class IndexModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum GroupElement {
|
enum GroupElement {
|
||||||
// groupId, groupName
|
case label(groupId: String, groupName: String)
|
||||||
case label(String, String)
|
case item(groupId: String, item: UpdateDramaGroup.Item)
|
||||||
// groupId, UpdateDramaGroup.Item
|
|
||||||
case item(String, UpdateDramaGroup.Item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedDate: String
|
var selectedDate: String
|
||||||
@ -96,13 +94,17 @@ final class IndexModel {
|
|||||||
|
|
||||||
self.cancel = self.scrollIDPublisher
|
self.cancel = self.scrollIDPublisher
|
||||||
.sink { userId, scrollID in
|
.sink { userId, scrollID in
|
||||||
// 更新group的label的部分
|
|
||||||
switch self.dramaGroupElements[scrollID] {
|
self.dramaGroupElements.forEach { element in
|
||||||
case .label(_, _):
|
switch element {
|
||||||
()
|
case .label(_, _):
|
||||||
case .item(let groupId, let item):
|
()
|
||||||
DispatchQueue.main.async {
|
case .item(let groupId, let item):
|
||||||
self.setFixedDrameGroup(groupId: groupId)
|
if item.id == scrollID {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setFixedDrameGroup(groupId: groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,19 +144,6 @@ final class IndexModel {
|
|||||||
self.isLoaded = true
|
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) {
|
func setFixedDrameGroup(groupId: String) {
|
||||||
if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}),
|
if let newFixedDramaGroup = self.updateDramaGroups.first(where: {$0.group_id == groupId}),
|
||||||
newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id {
|
newFixedDramaGroup.group_id != self.fixedDramaGroup?.group_id {
|
||||||
@ -169,7 +158,7 @@ final class IndexModel {
|
|||||||
|
|
||||||
// 按照id来判断不一定正确,需要借助其他值
|
// 按照id来判断不一定正确,需要借助其他值
|
||||||
let dramaIds = self.getDramaIds(self.updateDramaGroups)
|
let dramaIds = self.getDramaIds(self.updateDramaGroups)
|
||||||
//print("current ids: \(dramaIds)")
|
print("current ids: \(dramaIds)")
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .prev:
|
case .prev:
|
||||||
@ -218,9 +207,9 @@ final class IndexModel {
|
|||||||
private func transformUpdateDramaGroups(groups: [UpdateDramaGroup]) -> [GroupElement] {
|
private func transformUpdateDramaGroups(groups: [UpdateDramaGroup]) -> [GroupElement] {
|
||||||
var groupElements: [GroupElement] = []
|
var groupElements: [GroupElement] = []
|
||||||
for group in groups {
|
for group in groups {
|
||||||
groupElements.append(.label(group.group_id, group.group_name))
|
groupElements.append(.label(groupId: group.group_id, groupName: group.group_name))
|
||||||
for item in group.items {
|
for item in group.items {
|
||||||
groupElements.append(.item(group.group_id, item))
|
groupElements.append(.item(groupId: group.group_id, item: item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return groupElements
|
return groupElements
|
||||||
|
|||||||
79
dimensionhub/Views/Search/SearchBar.swift
Normal file
79
dimensionhub/Views/Search/SearchBar.swift
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
//
|
||||||
|
// 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()
|
||||||
|
//}
|
||||||
47
dimensionhub/Views/Search/SearchDramaGroupView.swift
Normal file
47
dimensionhub/Views/Search/SearchDramaGroupView.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// 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,146 +10,58 @@ import SwiftData
|
|||||||
|
|
||||||
struct SearchView: View {
|
struct SearchView: View {
|
||||||
@Environment(\.modelContext) var modelContext
|
@Environment(\.modelContext) var modelContext
|
||||||
|
@State var showHistoryNum: Int = 2
|
||||||
|
|
||||||
@AppStorage("userId") private var userId: String = Utils.defaultUserId()
|
@AppStorage("userId") private var userId: String = Utils.defaultUserId()
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State var searchText: String = ""
|
@State var searchText: String = ""
|
||||||
|
|
||||||
@State var searchModel = SearchModel()
|
@State var searchModel = SearchModel()
|
||||||
|
|
||||||
@FocusState private var isFocused: Bool
|
|
||||||
@State private var isSearching = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack {
|
||||||
HStack(spacing: 12) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
Button(action: { dismiss() }) {
|
Button(action: { dismiss() }) {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
.font(.system(size: 20, weight: .medium))
|
.font(.system(size: 20, weight: .medium))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
Spacer()
|
||||||
TextField("搜索...", text: $searchText)
|
|
||||||
.focused($isFocused)
|
SearchBar(searchText: $searchText, placeholder: "搜索...") {
|
||||||
.textFieldStyle(PlainTextFieldStyle())
|
let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
.padding(8)
|
if !trimmedSearchText.isEmpty {
|
||||||
.background(Color(.systemGray6))
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
.cornerRadius(8)
|
Task.detached {
|
||||||
.submitLabel(.search)
|
await searchModel.search(userId: userId, name: trimmedSearchText)
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
.disableAutocorrection(true)
|
|
||||||
.keyboardType(.default)
|
|
||||||
.onSubmit {
|
|
||||||
doSearch()
|
|
||||||
}
|
}
|
||||||
.overlay(alignment: .trailing) {
|
|
||||||
Button(action: {
|
// let historyModel = SearchHistory(keyword: trimmedSearchText, timestamp: Date())
|
||||||
searchText = ""
|
// modelContext.insert(historyModel)
|
||||||
}) {
|
|
||||||
Image(systemName: "multiply.circle.fill")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
.padding(.trailing, 8)
|
|
||||||
}
|
|
||||||
.opacity(isSearching ? 1 : 0)
|
|
||||||
.disabled(!isSearching)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
.frame(height: 50)
|
||||||
.padding(.top, 12)
|
.padding([.top, .bottom], 8)
|
||||||
|
|
||||||
if searchModel.dramaGroups.isEmpty {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
Spacer()
|
// 基于日期的更新列表
|
||||||
Text("什么都没有找到")
|
LazyVStack(alignment: .center, spacing: 10) {
|
||||||
.font(.system(size: 18))
|
ForEach(searchModel.dramaGroups, id: \.group_id) { group in
|
||||||
.foregroundColor(Color(hex: "#333333"))
|
SearchDramaGroupView(group: group)
|
||||||
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("")
|
.navigationTitle("")
|
||||||
.navigationBarBackButtonHidden(true)
|
.navigationBarBackButtonHidden(true)
|
||||||
.padding(.horizontal, 12)
|
.padding(8)
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom) // 避免键盘遮挡
|
.ignoresSafeArea(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 {
|
extension SearchView {
|
||||||
|
|||||||
@ -68,9 +68,7 @@ struct dimensionhubApp: App {
|
|||||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||||
Task.detached {
|
registerForPushNotifications()
|
||||||
await self.registerForPushNotifications()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -146,9 +144,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
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])
|
completionHandler([.banner, .sound])
|
||||||
@ -173,19 +171,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||||||
// MARK: 消息处理
|
// MARK: 消息处理
|
||||||
|
|
||||||
private func handleRemoteNotification(userInfo: [AnyHashable: Any]) {
|
private func handleRemoteNotification(userInfo: [AnyHashable: Any]) {
|
||||||
guard let customData = userInfo["custom_data"] as? [String: AnyObject],
|
if let customData = userInfo["custom_data"] as? [String: AnyObject],
|
||||||
let target = customData["target"] as? String,
|
let dramaId = customData["drama_id"] as? Int {
|
||||||
let params = customData["params"] as? [String : AnyObject] else {
|
AppNavigation.shared.append(dest: .detail(id: dramaId))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch target {
|
|
||||||
case "detail":
|
|
||||||
if let dramaId = params["drama_id"] as? Int {
|
|
||||||
AppNavigation.shared.append(dest: .detail(id: dramaId))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user