diff --git a/DarockBili.xcodeproj/project.pbxproj b/DarockBili.xcodeproj/project.pbxproj index 636a6660d..9746aefae 100644 --- a/DarockBili.xcodeproj/project.pbxproj +++ b/DarockBili.xcodeproj/project.pbxproj @@ -215,7 +215,6 @@ B2B813972CC3D22800C69D17 /* BiliBiliAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B813962CC3D22800C69D17 /* BiliBiliAPIService.swift */; }; B2B813982CC3D22800C69D17 /* BiliBiliAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B813962CC3D22800C69D17 /* BiliBiliAPIService.swift */; }; B2B813992CC3D22800C69D17 /* BiliBiliAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B813962CC3D22800C69D17 /* BiliBiliAPIService.swift */; }; - B2B8139D2CC3D37500C69D17 /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8139A2CC3D37500C69D17 /* TrendingView.swift */; }; B4DAF0DD2B80725800755F0C /* LinkDetectText.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DAF0DB2B80725800755F0C /* LinkDetectText.swift */; }; /* End PBXBuildFile section */ @@ -433,7 +432,6 @@ B2B8138B2CC3D0F300C69D17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B2B8138D2CC3D0F300C69D17 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B2B813962CC3D22800C69D17 /* BiliBiliAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiliBiliAPIService.swift; sourceTree = ""; }; - B2B8139A2CC3D37500C69D17 /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; B4468A152B4FC24A002CCEB2 /* Dynamic_Feed_All.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Dynamic_Feed_All.json; sourceTree = ""; }; B4468A162B4FC24A002CCEB2 /* Search_With_UP_V2.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Search_With_UP_V2.json; sourceTree = ""; }; B4DAF0DB2B80725800755F0C /* LinkDetectText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkDetectText.swift; sourceTree = ""; }; @@ -817,7 +815,6 @@ children = ( B2B813832CC3D0ED00C69D17 /* MeowWidgetBundle.swift */, B2B813872CC3D0ED00C69D17 /* MeowWidget.swift */, - B2B8139A2CC3D37500C69D17 /* TrendingView.swift */, B2B8138B2CC3D0F300C69D17 /* Assets.xcassets */, B2B8138D2CC3D0F300C69D17 /* Info.plist */, ); @@ -1403,7 +1400,6 @@ buildActionMask = 2147483647; files = ( B2B813842CC3D0ED00C69D17 /* MeowWidgetBundle.swift in Sources */, - B2B8139D2CC3D37500C69D17 /* TrendingView.swift in Sources */, B2B813882CC3D0ED00C69D17 /* MeowWidget.swift in Sources */, B2B813992CC3D22800C69D17 /* BiliBiliAPIService.swift in Sources */, ); diff --git a/DarockBili.xcodeproj/xcshareddata/xcschemes/Alternative Destribution.xcscheme b/DarockBili.xcodeproj/xcshareddata/xcschemes/Alternative Destribution.xcscheme index 8e551bb00..8245c0519 100644 --- a/DarockBili.xcodeproj/xcshareddata/xcschemes/Alternative Destribution.xcscheme +++ b/DarockBili.xcodeproj/xcshareddata/xcschemes/Alternative Destribution.xcscheme @@ -1,7 +1,7 @@ + version = "1.8"> diff --git a/MeowWidget/MeowWidget.swift b/MeowWidget/MeowWidget.swift index adcd6137a..6dfec1788 100644 --- a/MeowWidget/MeowWidget.swift +++ b/MeowWidget/MeowWidget.swift @@ -16,6 +16,162 @@ // //===----------------------------------------------------------------------===// +import WidgetKit +import SwiftUI + +struct MeowWidgetEntry: TimelineEntry { + let date: Date + let video: Video +} + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> MeowWidgetEntry { + MeowWidgetEntry(date: Date(), video: Video(id: 0, title: "miku miku oo ee oo", description: "https://twitter.com/i/status/1697029186777706544 channel(twi:_CASTSTATION)", authorName: "未来de残像", viewCount: 0, likeCount: 0, coinCount: 0, shareCount: 0, danmakuCount: 0)) + } + + func getSnapshot(in context: Context, completion: @escaping (MeowWidgetEntry) -> Void) { + let placeholder = MeowWidgetEntry(date: Date(), video: Video(id: 0, title: "miku miku oo ee o", description: "https://twitter.com/i/status/1697029186777706544 channel(twi:_CASTSTATION)", authorName: "未来de残像", viewCount: 0, likeCount: 0, coinCount: 0, shareCount: 0, danmakuCount: 0)) + completion(placeholder) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + BiliBiliAPIService.shared.fetchPopularVideos { videos in + let entries: [MeowWidgetEntry] = videos.enumerated().map { index, video in + let interval = 10 * 60 // 每10分钟更新 + let date = Calendar.current.date(byAdding: .second, value: interval * index, to: Date()) ?? Date() + return MeowWidgetEntry(date: date, video: video) + } + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } + } +} + +struct MeowWidgetView: View { + @Environment(\.widgetFamily) var family + var entry: MeowWidgetEntry + + var body: some View { + let widgetURL = URL(string: "wget://openURL/\(entry.video.id)") + switch family { + case .accessoryInline: + Text(entry.video.title) + .widgetURL(widgetURL) + case .accessoryCircular: + VStack { + Image(systemName: "play.circle.fill") + .foregroundColor(Color("WidgetTitleColor")) + Text(entry.video.title) + .font(.caption) + } + .widgetURL(widgetURL) + case .accessoryRectangular: + VStack(alignment: .leading) { + Text(entry.video.title) + .font(.headline) + Text(entry.video.authorName) + .font(.subheadline) + } + .widgetURL(widgetURL) + case .systemSmall: + VStack { + Text(entry.video.title) + .font(.headline) + HStack { + Image(systemName: "person.fill") + .foregroundColor(Color("WidgetTitleColor")) + Text(entry.video.authorName) + .font(.caption) + } + } + .widgetURL(widgetURL) + case .systemMedium: + HStack { + VStack(alignment: .leading) { + Text(entry.video.title) + .font(.headline) + Text(entry.video.description) + .font(.caption) + .lineLimit(2) + } + Spacer() + VStack(alignment: .leading) { + HStack { + Image(systemName: "play.rectangle") + .foregroundColor(Color("WidgetTitleColor")) + Text("\(entry.video.viewCount)") + .font(.caption) + } + HStack { + Image(systemName: "heart.fill") + .foregroundColor(Color("WidgetTitleColor")) + Text("\(entry.video.likeCount)") + .font(.caption) + } + } + } + .widgetURL(widgetURL) + case .systemLarge: + VStack(alignment: .leading) { + Text(entry.video.title) + .font(.title) + Text(entry.video.description) + .font(.body) + .lineLimit(3) + Spacer() + HStack { + HStack { + Image(systemName: "play.rectangle.fill") + .foregroundColor(Color("WidgetTitleColor")) + Text("\(entry.video.viewCount)") + .font(.footnote) + } + Spacer() + HStack { + Image(systemName: "heart.fill") + .foregroundColor(Color("WidgetTitleColor")) + Text("\(entry.video.likeCount)") + .font(.footnote) + } + } + Spacer() + Text("在喵哩喵哩查看视频") + .font(.footnote) + .foregroundColor(.gray) + } + .widgetURL(widgetURL) + default: + Text("Unsupported Widget Family") + } + } +} + + private var families: [WidgetFamily] { + #if os(watchOS) + return [.accessoryInline, .accessoryCircular, .accessoryRectangular] + #else + return [.systemSmall, .systemMedium, .systemLarge, .accessoryCircular, .accessoryRectangular] + #endif + } + +struct MeowWidget: Widget { + let kind: String = "MeowWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + MeowWidgetView(entry: entry) + } + .configurationDisplayName("MeowWidget") + .description("热门或推荐的视频内容") + .supportedFamilies([.accessoryInline, .accessoryCircular, .accessoryRectangular, .systemSmall, .systemMedium, .systemLarge]) + } +} + + +/* + +! The older version for reference in few future updates, will be removed soon after more UI and function intergration + import WidgetKit import SwiftUI import Intents @@ -163,3 +319,4 @@ struct MeowWidget: Widget { #endif } } +*/ diff --git a/MeowWidget/MeowWidgetBundle.swift b/MeowWidget/MeowWidgetBundle.swift index a71ad6756..d2dfa6635 100644 --- a/MeowWidget/MeowWidgetBundle.swift +++ b/MeowWidget/MeowWidgetBundle.swift @@ -21,6 +21,7 @@ import SwiftUI @main struct MeowWidgetBundle: WidgetBundle { + @WidgetBundleBuilder var body: some Widget { MeowWidget() } diff --git a/MeowWidget/TrendingView.swift b/MeowWidget/TrendingView.swift deleted file mode 100644 index 315941fb5..000000000 --- a/MeowWidget/TrendingView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// -// TrendingView.swift -// DarockBili -// -// Created by feng on 10/19/24. -// -//===----------------------------------------------------------------------===// -// -// This source file is part of the MeowBili open source project -// -// Copyright (c) 2024 Darock Studio and the MeowBili project authors -// Licensed under GNU General Public License v3 -// -// See https://darock.top/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -import SwiftUI - -struct TrendingView: View { - @State private var trendingVideos: [(title: String, description: String, author: String, views: String)] = [] - @State private var errorMessage: String? - - var body: some View { - VStack { - if let errorMessage = errorMessage { - Text(errorMessage) - .foregroundColor(.red) - } else { - List(trendingVideos, id: \.title) { video in - VStack(alignment: .leading) { - Text(video.title) - .font(.headline) - Text(video.description) - .font(.subheadline) - Text("作者: \(video.author) | 播放量: \(video.views)") - .font(.footnote) - } - } - } - } - .onAppear { - fetchTrendingVideos() - } - } - - private func fetchTrendingVideos() { - Task { - let result = await BiliBiliAPIService().fetchBiliBiliData(for: .trending, limit: 5) - switch result { - case .success(let videos): - trendingVideos = videos - case .failure(let error): - errorMessage = error.localizedDescription - } - } - } -} - diff --git a/SharedCode/BiliBiliAPIService.swift b/SharedCode/BiliBiliAPIService.swift index 9dcfd85f7..93f2a54a5 100644 --- a/SharedCode/BiliBiliAPIService.swift +++ b/SharedCode/BiliBiliAPIService.swift @@ -18,52 +18,114 @@ import Foundation +// 定义 Video 数据模型 +struct Video: Identifiable { + let id: Int + let title: String + let description: String + let authorName: String + let viewCount: Int + let likeCount: Int + let coinCount: Int + let shareCount: Int + let danmakuCount: Int +} + class BiliBiliAPIService { - enum ContentType { - case trending, recommendations + static let shared = BiliBiliAPIService() + private init() {} + + // MARK: - 数据获取 + + /// 获取热门内容 + func fetchPopularVideos(completion: @escaping ([Video]) -> Void) { + let url = "https://api.bilibili.com/x/web-interface/popular" + fetchVideos(from: url, completion: completion) } - - /// 获取BiliBili数据 - /// - Parameters: - /// - type: 内容类型(热门或推荐) - /// - limit: 限制返回的视频数量,默认是5 - func fetchBiliBiliData(for type: ContentType, limit: Int = 5) async -> Result<[(title: String, description: String, author: String, views: String)], Error> { - do { - let urlString: String - switch type { - case .trending: - urlString = "https://api.bilibili.com/x/web-interface/popular?ps=\(limit)" - case .recommendations: - urlString = "https://api.bilibili.com/x/web-interface/index/top/rcmd?ps=1" + + /// 获取推荐内容 + func fetchRecommendedVideos(completion: @escaping ([Video]) -> Void) { + let url = "https://api.bilibili.com/x/web-interface/index/top/rcmd" + fetchVideos(from: url, completion: completion) + } + + // MARK: - 数据解析 + + /// 从 URL 获取视频数据 + private func fetchVideos(from urlString: String, completion: @escaping ([Video]) -> Void) { + guard let url = URL(string: urlString) else { + print("Invalid URL") + completion([]) + return + } + + let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in + if let error = error { + print("Error fetching data: \(error.localizedDescription)") + completion([]) + return } - guard let url = URL(string: urlString) else { - throw URLError(.badURL) + guard let data = data else { + print("No data received") + completion([]) + return } - let (data, _) = try await URLSession.shared.data(from: url) + do { + // 解析 JSON 数据 + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let data = json["data"] as? [String: Any], + let list = data["list"] as? [[String: Any]] { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let dataDict = json["data"] as? [String: Any], - let itemList = dataDict["item"] as? [[String: Any]] { + let videos = list.compactMap { self?.parseVideo(from: $0) } - let videos = itemList.prefix(limit).map { video -> (String, String, String, String) in - let videoTitle = video["title"] as? String ?? "无标题" - let videoDesc = video["desc"] as? String ?? "无描述" - let videoAuthor = (video["owner"] as? [String: Any])?["name"] as? String ?? "未知作者" - let videoViews = (video["stat"] as? [String: Any])?["view"] as? Int ?? 0 - return (title: videoTitle, description: videoDesc, author: videoAuthor, views: "\(videoViews)") + DispatchQueue.main.async { + completion(videos) + } + } else { + print("Invalid JSON structure") + completion([]) } - - return .success(videos) - } else { - return .failure(NSError(domain: "BiliBiliAPIService", code: -1, userInfo: [NSLocalizedDescriptionKey: "数据格式错误"])) + } catch { + print("Error parsing JSON: \(error.localizedDescription)") + completion([]) } - - } catch { - return .failure(error) } + + task.resume() } + /// 解析单个视频条目 + private func parseVideo(from dict: [String: Any]) -> Video? { + guard + let id = dict["aid"] as? Int, + let title = dict["title"] as? String, + let description = dict["desc"] as? String, + let owner = dict["owner"] as? [String: Any], + let authorName = owner["name"] as? String, + let stat = dict["stat"] as? [String: Any], + let viewCount = stat["view"] as? Int, + let likeCount = stat["like"] as? Int, + let coinCount = stat["coin"] as? Int, + let shareCount = stat["share"] as? Int, + let danmakuCount = stat["danmaku"] as? Int + else { + print("Missing data in video entry") + return nil + } + + return Video( + id: id, + title: title, + description: description, + authorName: authorName, + viewCount: viewCount, + likeCount: likeCount, + coinCount: coinCount, + shareCount: shareCount, + danmakuCount: danmakuCount + ) + } }