diff --git a/BeeReel.xcodeproj/project.pbxproj b/BeeReel.xcodeproj/project.pbxproj index cf09808..290e672 100644 --- a/BeeReel.xcodeproj/project.pbxproj +++ b/BeeReel.xcodeproj/project.pbxproj @@ -205,6 +205,10 @@ F398558B2E37792700E2D28D /* Date+BRAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398558A2E37792200E2D28D /* Date+BRAdd.swift */; }; F398558E2E37857D00E2D28D /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = F398558D2E37857D00E2D28D /* FirebaseMessaging */; }; F39855902E37862200E2D28D /* BRSettingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F398558F2E37861D00E2D28D /* BRSettingAPI.swift */; }; + F39855922E37999900E2D28D /* BRVideoDetailRecommendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855912E37999900E2D28D /* BRVideoDetailRecommendView.swift */; }; + F39855942E379D9600E2D28D /* BRVideoDetailRecommendCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */; }; + F39855962E38A27500E2D28D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F39855952E38A27500E2D28D /* GoogleService-Info.plist */; }; + F39855982E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -420,6 +424,11 @@ F39855882E37732600E2D28D /* BRGuideViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRGuideViewController.swift; sourceTree = ""; }; F398558A2E37792200E2D28D /* Date+BRAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+BRAdd.swift"; sourceTree = ""; }; F398558F2E37861D00E2D28D /* BRSettingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRSettingAPI.swift; sourceTree = ""; }; + F39855912E37999900E2D28D /* BRVideoDetailRecommendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendView.swift; sourceTree = ""; }; + F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendCell.swift; sourceTree = ""; }; + F39855952E38A27500E2D28D /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BRVideoDetailRecommendTransformer.swift; sourceTree = ""; }; + F39855992E38CBE800E2D28D /* BeeReel.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BeeReel.entitlements; sourceTree = ""; }; F70FA1F4169364C4C53534CE /* Pods-BeeReel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BeeReel.release.xcconfig"; path = "Target Support Files/Pods-BeeReel/Pods-BeeReel.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -644,6 +653,7 @@ BF692AEA2E0A475D00A5C2DA /* BeeReel */ = { isa = PBXGroup; children = ( + F39855992E38CBE800E2D28D /* BeeReel.entitlements */, BF692AF32E0A47B500A5C2DA /* Delegate */, BF692AF42E0A47CA00A5C2DA /* Base */, BF692AF52E0A47D400A5C2DA /* Class */, @@ -657,6 +667,7 @@ BF692AF22E0A478D00A5C2DA /* Sources */ = { isa = PBXGroup; children = ( + F39855952E38A27500E2D28D /* GoogleService-Info.plist */, F39855432E33840500E2D28D /* AaHouDiHei-Regular.ttf */, BF692AE22E0A475D00A5C2DA /* Assets.xcassets */, BF692AE32E0A475D00A5C2DA /* Info.plist */, @@ -898,6 +909,9 @@ BF02B7F22E2E571600172177 /* BRRateSelectorCell.swift */, F398554D2E34699F00E2D28D /* BRVideoLockView.swift */, F39855512E347BDE00E2D28D /* BRVideoRechargeView.swift */, + F39855912E37999900E2D28D /* BRVideoDetailRecommendView.swift */, + F39855932E379D9600E2D28D /* BRVideoDetailRecommendCell.swift */, + F39855972E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift */, ); path = View; sourceTree = ""; @@ -1249,6 +1263,7 @@ files = ( BF692AEE2E0A475D00A5C2DA /* Assets.xcassets in Resources */, BF692AF02E0A475D00A5C2DA /* LaunchScreen.storyboard in Resources */, + F39855962E38A27500E2D28D /* GoogleService-Info.plist in Resources */, F39855442E33840500E2D28D /* AaHouDiHei-Regular.ttf in Resources */, BF692B442E0A910E00A5C2DA /* Localizable.xcstrings in Resources */, ); @@ -1325,10 +1340,12 @@ BFC6768D2E123D6E006659E5 /* AttributedString+BRAdd.swift in Sources */, BF02B8392E30B30400172177 /* AlignedCollectionViewFlowLayout.swift in Sources */, BF3A56882E30E0DD009E5CF9 /* BREmpty.swift in Sources */, + F39855942E379D9600E2D28D /* BRVideoDetailRecommendCell.swift in Sources */, F39855542E34A49500E2D28D /* BRPayDataRequest.swift in Sources */, BF3338F52E1616B200B10F76 /* BRExploreControlView.swift in Sources */, F398552D2E33126D00E2D28D /* BRMineCoinItemView.swift in Sources */, BF692B132E0A7B9000A5C2DA /* BRUserInfo.swift in Sources */, + F39855982E38BB3500E2D28D /* BRVideoDetailRecommendTransformer.swift in Sources */, BF692B042E0A76D200A5C2DA /* BRLoginManager.swift in Sources */, BFC6769D2E129794006659E5 /* BRHomeTop10ViewController.swift in Sources */, F398556C2E3717A500E2D28D /* BRWalletHeaderItemView.swift in Sources */, @@ -1475,6 +1492,7 @@ BF692B7A2E0D3BD300A5C2DA /* BRShortModel.swift in Sources */, F39855822E376CB000E2D28D /* BROpenAppModel.swift in Sources */, BFC676712E0E9234006659E5 /* BRSpotlightViewViewController.swift in Sources */, + F39855922E37999900E2D28D /* BRVideoDetailRecommendView.swift in Sources */, BF3338F02E15569600B10F76 /* BRExploreViewController.swift in Sources */, BF0DBDD12E0D4E150035F6B4 /* BRTabBar.swift in Sources */, BF692B562E0AA92100A5C2DA /* BRCollectionViewCell.swift in Sources */, @@ -1522,6 +1540,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = BeeReel/BeeReel.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8NNUR9HPV3; @@ -1561,6 +1580,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = BeeReel/BeeReel.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 8NNUR9HPV3; diff --git a/BeeReel/Base/Extension/UIColor+BRAdd.swift b/BeeReel/Base/Extension/UIColor+BRAdd.swift index 112bd4e..f650c27 100644 --- a/BeeReel/Base/Extension/UIColor+BRAdd.swift +++ b/BeeReel/Base/Extension/UIColor+BRAdd.swift @@ -206,4 +206,12 @@ extension UIColor { static func color7ECD5E(alpha: CGFloat = 1) -> UIColor { return UIColor(rgb: 0x7ECD5E, alpha: alpha) } + + static func color576300(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x576300, alpha: alpha) + } + + static func color499C20(alpha: CGFloat = 1) -> UIColor { + return UIColor(rgb: 0x499C20, alpha: alpha) + } } diff --git a/BeeReel/Base/Network/API/BRVideoAPI.swift b/BeeReel/Base/Network/API/BRVideoAPI.swift index 00bcd6a..048534e 100644 --- a/BeeReel/Base/Network/API/BRVideoAPI.swift +++ b/BeeReel/Base/Network/API/BRVideoAPI.swift @@ -32,6 +32,17 @@ class BRVideoAPI { } } + ///视频推荐 + static func requestDetailsRecommand(completer: ((_ list: [BRShortModel]?) -> Void)?) { + + var param = BRNetworkParameters(path: "/getDetailsRecommand") + param.method = .get + + BRNetwork.request(parameters: param) { (response: BRNetworkResponse>) in + completer?(response.data?.list) + } + } + ///收藏 static func requestFavorite(isFavorite: Bool, shortPlayId: String, videoId: String?, isLoding: Bool = true, success: (() -> Void)?, failure: (() -> Void)? = nil) { diff --git a/BeeReel/BeeReel.entitlements b/BeeReel/BeeReel.entitlements new file mode 100644 index 0000000..f67c43c --- /dev/null +++ b/BeeReel/BeeReel.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + keychain-access-groups + + + diff --git a/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift b/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift index d6633f8..c7c56a3 100644 --- a/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift +++ b/BeeReel/Class/Player/Controller/BRVideoDetailViewController.swift @@ -23,6 +23,10 @@ class BRVideoDetailViewController: BRPlayerListViewController { private var detailArr: [BRVideoDetailModel] = [] + ///是否展示推荐 + private lazy var isAllowShowRecommand = false + private var recommandTimer: Timer? + ///上一次上报播放时长的节点 private var lastUploadTime: TimeInterval = 0 @@ -30,7 +34,7 @@ class BRVideoDetailViewController: BRPlayerListViewController { private lazy var backButton: UIButton = { let button = UIButton(type: .custom) button.setImage(UIImage(named: "nav_back_icon_01"), for: .normal) - button.addTarget(self, action: #selector(handleNavBack), for: .touchUpInside) + button.addTarget(self, action: #selector(handleBackButton), for: .touchUpInside) return button }() @@ -44,6 +48,7 @@ class BRVideoDetailViewController: BRPlayerListViewController { self.fd_interactivePopDisabled = true self.requestDetailData() + self.viewModel.asyncGetRecommandDataArr() br_setupUI() } @@ -86,9 +91,24 @@ class BRVideoDetailViewController: BRPlayerListViewController { } } - override func handleNavBack() { - super.handleNavBack() + @objc private func handleBackButton() { + if !self.viewModel.recommandList.isEmpty, isAllowShowRecommand { + self.pause() + let view = BRVideoDetailRecommendView() + view.videoList = self.viewModel.recommandList + view.didDismissBlock = { [weak self] in + self?.handleNavBack() + } + view.didSelectedVideo = { [weak self] model in + guard let self = self else { return } + self.shortPlayId = model.short_play_id + self.requestDetailData() + } + view.present(in: nil) + } else { + self.handleNavBack() + } } } @@ -102,7 +122,13 @@ extension BRVideoDetailViewController { make.top.equalToSuperview().offset(UIScreen.statusBarHeight + 10) } } +} + +extension BRVideoDetailViewController { + @objc private func handleRecommandTimer() { + self.isAllowShowRecommand = true + } } //MARK: -------------- BRPlayerViewModelDelegate -------------- @@ -161,6 +187,13 @@ extension BRVideoDetailViewController: BRPlayerListViewControllerDataSource, BRP cell.videoInfo = model.episodeList?[indexPath.row] cell.shortModel = model.shortPlayInfo + let upRow = indexPath.row - 1 + if upRow >= 0, let videoInfo = model.episodeList?[upRow], videoInfo.is_lock == true { + cell.hasLastEpisodeUnlocked = true + } else { + cell.hasLastEpisodeUnlocked = false + } + return cell } @@ -188,8 +221,11 @@ extension BRVideoDetailViewController { private func requestDetailData(indexPath: IndexPath? = nil) { guard let shortPlayId = shortPlayId else { return } - + isAllowShowRecommand = false + recommandTimer?.invalidate() + recommandTimer = nil + recommandTimer = Timer.scheduledTimer(timeInterval: 6, target: YYTextWeakProxy(target: self), selector: #selector(handleRecommandTimer), userInfo: nil, repeats: false) BRHUD.show(containerView: self.view) BRVideoAPI.requestVideoDetail(shortPlayId: shortPlayId, activityId: activityId) { [weak self] model in diff --git a/BeeReel/Class/Player/View/BRDetailControlView.swift b/BeeReel/Class/Player/View/BRDetailControlView.swift index 1c3e6ea..008ab0f 100644 --- a/BeeReel/Class/Player/View/BRDetailControlView.swift +++ b/BeeReel/Class/Player/View/BRDetailControlView.swift @@ -37,6 +37,12 @@ class BRDetailControlView: BRPlayerControlView { } } + override var hasLastEpisodeUnlocked: Bool { + didSet { + self.videoLockView.hasLastEpisodeUnlocked = hasLastEpisodeUnlocked + } + } + override var progress: CGFloat { didSet { progressView.progress = progress diff --git a/BeeReel/Class/Player/View/BRPlayerControlView.swift b/BeeReel/Class/Player/View/BRPlayerControlView.swift index ae18072..fdbbff1 100644 --- a/BeeReel/Class/Player/View/BRPlayerControlView.swift +++ b/BeeReel/Class/Player/View/BRPlayerControlView.swift @@ -29,6 +29,8 @@ class BRPlayerControlView: UIView, BRPlayerControlProtocol { } } + var hasLastEpisodeUnlocked: Bool = false + ///滑动进度条 var panProgressFinishBlock: ((_ progress: CGFloat) -> Void)? diff --git a/BeeReel/Class/Player/View/BRPlayerListCell.swift b/BeeReel/Class/Player/View/BRPlayerListCell.swift index 46d4327..240a36f 100644 --- a/BeeReel/Class/Player/View/BRPlayerListCell.swift +++ b/BeeReel/Class/Player/View/BRPlayerListCell.swift @@ -30,6 +30,12 @@ class BRPlayerListCell: BRCollectionViewCell, BRPlayerProtocol { } } + var hasLastEpisodeUnlocked: Bool = false { + didSet { + self.controlView.hasLastEpisodeUnlocked = hasLastEpisodeUnlocked + } + } + var isCurrent: Bool = false { didSet { self.controlView.isCurrent = isCurrent diff --git a/BeeReel/Class/Player/View/BRVideoDetailRecommendCell.swift b/BeeReel/Class/Player/View/BRVideoDetailRecommendCell.swift new file mode 100644 index 0000000..ef7b45f --- /dev/null +++ b/BeeReel/Class/Player/View/BRVideoDetailRecommendCell.swift @@ -0,0 +1,115 @@ +// +// BRVideoDetailRecommendCell.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/28. +// + +import UIKit +import FSPagerView + +class BRVideoDetailRecommendCell: FSPagerViewCell { + + var model: BRShortModel? { + didSet { + coverImageView.br_setImage(url: model?.image_url) + categoryLabel.text = model?.category?.first + self.player.coverImageView?.br_setImage(url: model?.image_url) + player.setPlayUrl(url: model?.video_url ?? "") + } + } + + var isCurrent: Bool = false + + private lazy var bgView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "Rectangle 80")) + return imageView + }() + + private lazy var player: BRPlayer = { + let player = BRPlayer(controlView: nil) + player.playerView = self.playerView +// player.delegate = self + + return player + }() + + private lazy var playerView: UIView = { + let view = UIView() + view.layer.cornerRadius = 10 + view.layer.masksToBounds = true + view.layer.borderColor = UIColor.colorFFFFFF().cgColor + view.layer.borderWidth = 2 + return view + }() + + private lazy var coverImageView: BRImageView = { + let imageView = BRImageView() + imageView.layer.cornerRadius = 10 + imageView.layer.masksToBounds = true + imageView.layer.borderColor = UIColor.colorFFFFFF().cgColor + imageView.layer.borderWidth = 2 + imageView.isHidden = true + return imageView + }() + + private lazy var categoryLabel: UILabel = { + let label = UILabel() + label.font = .fontMedium(ofSize: 10) + label.textColor = .color576300() + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + br_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func play() { + self.player.start() + } + + func pause() { + self.player.pause() + } +} + +extension BRVideoDetailRecommendCell { + + private func br_setupUI() { + contentView.addSubview(bgView) + contentView.addSubview(playerView) + contentView.addSubview(coverImageView) + contentView.addSubview(categoryLabel) + + + bgView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + playerView.snp.makeConstraints { make in + make.left.equalToSuperview().offset(5) + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-5) + make.top.equalToSuperview().offset(19) + } + + coverImageView.snp.makeConstraints { make in + make.edges.equalTo(playerView) + } + + categoryLabel.snp.makeConstraints { make in + make.left.equalToSuperview().offset(7) + make.top.equalToSuperview().offset(4) + make.width.lessThanOrEqualTo(65) + } + + + } + +} diff --git a/BeeReel/Class/Player/View/BRVideoDetailRecommendTransformer.swift b/BeeReel/Class/Player/View/BRVideoDetailRecommendTransformer.swift new file mode 100644 index 0000000..f655945 --- /dev/null +++ b/BeeReel/Class/Player/View/BRVideoDetailRecommendTransformer.swift @@ -0,0 +1,214 @@ +// +// BRVideoDetailRecommendTransformer.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/29. +// + +import UIKit +import FSPagerView + +class BRVideoDetailRecommendTransformer: FSPagerViewTransformer { + + + override func applyTransform(to attributes: FSPagerViewLayoutAttributes) { + guard let pagerView = self.pagerView else { + return + } + let position = attributes.position + let scrollDirection = pagerView.scrollDirection + let itemSpacing = (scrollDirection == .horizontal ? attributes.bounds.width : attributes.bounds.height) + self.proposedInteritemSpacing() + switch self.type { + case .crossFading: + var zIndex = 0 + var alpha: CGFloat = 0 + var transform = CGAffineTransform.identity + switch scrollDirection { + case .horizontal: + transform.tx = -itemSpacing * position + case .vertical: + transform.ty = -itemSpacing * position + } + if (abs(position) < 1) { // [-1,1] + // Use the default slide transition when moving to the left page + alpha = 1 - abs(position) + zIndex = 1 + } else { // (1,+Infinity] + // This page is way off-screen to the right. + alpha = 0 + zIndex = Int.min + } + attributes.alpha = alpha + attributes.transform = transform + attributes.zIndex = zIndex + case .zoomOut: + var alpha: CGFloat = 0 + var transform = CGAffineTransform.identity + switch position { + case -CGFloat.greatestFiniteMagnitude ..< -1 : // [-Infinity,-1) + // This page is way off-screen to the left. + alpha = 0 + case -1 ... 1 : // [-1,1] + // Modify the default slide transition to shrink the page as well + let scaleFactor = max(self.minimumScale, 1 - abs(position)) + transform.a = scaleFactor + transform.d = scaleFactor + switch scrollDirection { + case .horizontal: + let vertMargin = attributes.bounds.height * (1 - scaleFactor) / 2; + let horzMargin = itemSpacing * (1 - scaleFactor) / 2; + transform.tx = position < 0 ? (horzMargin - vertMargin*2) : (-horzMargin + vertMargin*2) + case .vertical: + let horzMargin = attributes.bounds.width * (1 - scaleFactor) / 2; + let vertMargin = itemSpacing * (1 - scaleFactor) / 2; + transform.ty = position < 0 ? (vertMargin - horzMargin*2) : (-vertMargin + horzMargin*2) + } + // Fade the page relative to its size. + alpha = self.minimumAlpha + (scaleFactor-self.minimumScale)/(1-self.minimumScale)*(1-self.minimumAlpha) + case 1 ... CGFloat.greatestFiniteMagnitude : // (1,+Infinity] + // This page is way off-screen to the right. + alpha = 0 + default: + break + } + attributes.alpha = alpha + attributes.transform = transform + case .depth: + var transform = CGAffineTransform.identity + var zIndex = 0 + var alpha: CGFloat = 0.0 + switch position { + case -CGFloat.greatestFiniteMagnitude ..< -1: // [-Infinity,-1) + // This page is way off-screen to the left. + alpha = 0 + zIndex = 0 + case -1 ... 0: // [-1,0] + // Use the default slide transition when moving to the left page + alpha = 1 + transform.tx = 0 + transform.a = 1 + transform.d = 1 + zIndex = 1 + case 0 ..< 1: // (0,1) + // Fade the page out. + alpha = CGFloat(1.0) - position + // Counteract the default slide transition + switch scrollDirection { + case .horizontal: + transform.tx = itemSpacing * -position + case .vertical: + transform.ty = itemSpacing * -position + } + // Scale the page down (between minimumScale and 1) + let scaleFactor = self.minimumScale + + (1.0 - self.minimumScale) * (1.0 - abs(position)); + transform.a = scaleFactor + transform.d = scaleFactor + zIndex = 0 + case 1 ... CGFloat.greatestFiniteMagnitude: // [1,+Infinity) + // This page is way off-screen to the right. + alpha = 0 + zIndex = 0 + default: + break + } + attributes.alpha = alpha + attributes.transform = transform + attributes.zIndex = zIndex + case .overlap,.linear: + guard scrollDirection == .horizontal else { + // This type doesn't support vertical mode + return + } + let scale = max(1 - (1-self.minimumScale) * abs(position), self.minimumScale) + let translate = 1 - (scale - self.minimumScale) / (1 - self.minimumScale); + + var transform = CATransform3DIdentity + transform = CATransform3DTranslate(transform, 0, 28 * translate, 0); + transform = CATransform3DScale(transform, scale, scale, 1.0); + attributes.transform3D = transform + + let alpha = (self.minimumAlpha + (1-abs(position))*(1-self.minimumAlpha)) + attributes.alpha = alpha + let zIndex = (1-abs(position)) * 10 + attributes.zIndex = Int(zIndex) + case .coverFlow: + guard scrollDirection == .horizontal else { + // This type doesn't support vertical mode + return + } + let position = min(max(-position,-1) ,1) + let rotation = sin(position*(.pi)*0.5)*(.pi)*0.25*1.5 + let translationZ = -itemSpacing * 0.5 * abs(position) + var transform3D = CATransform3DIdentity + transform3D.m34 = -0.002 + transform3D = CATransform3DRotate(transform3D, rotation, 0, 1, 0) + transform3D = CATransform3DTranslate(transform3D, 0, 0, translationZ) + attributes.zIndex = 100 - Int(abs(position)) + attributes.transform3D = transform3D + case .ferrisWheel, .invertedFerrisWheel: + guard scrollDirection == .horizontal else { + // This type doesn't support vertical mode + return + } + // http://ronnqvi.st/translate-rotate-translate/ + var zIndex = 0 + var transform = CGAffineTransform.identity + switch position { + case -5 ... 5: + let itemSpacing = attributes.bounds.width+self.proposedInteritemSpacing() + let count: CGFloat = 14 + let circle: CGFloat = .pi * 2.0 + let radius = itemSpacing * count / circle + let ty = radius * (self.type == .ferrisWheel ? 1 : -1) + let theta = circle / count + let rotation = position * theta * (self.type == .ferrisWheel ? 1 : -1) + transform = transform.translatedBy(x: -position*itemSpacing, y: ty) + transform = transform.rotated(by: rotation) + transform = transform.translatedBy(x: 0, y: -ty) + zIndex = Int((4.0-abs(position)*10)) + default: + break + } + attributes.alpha = abs(position) < 0.5 ? 1 : self.minimumAlpha + attributes.transform = transform + attributes.zIndex = zIndex + case .cubic: + switch position { + case -CGFloat.greatestFiniteMagnitude ... -1: + attributes.alpha = 0 + case -1 ..< 1: + attributes.alpha = 1 + attributes.zIndex = Int((1-position) * CGFloat(10)) + let direction: CGFloat = position < 0 ? 1 : -1 + let theta = position * .pi * 0.5 * (scrollDirection == .horizontal ? 1 : -1) + let radius = scrollDirection == .horizontal ? attributes.bounds.width : attributes.bounds.height + var transform3D = CATransform3DIdentity + transform3D.m34 = -0.002 + switch scrollDirection { + case .horizontal: + // ForwardX -> RotateY -> BackwardX + attributes.center.x += direction*radius*0.5 // ForwardX + transform3D = CATransform3DRotate(transform3D, theta, 0, 1, 0) // RotateY + transform3D = CATransform3DTranslate(transform3D,-direction*radius*0.5, 0, 0) // BackwardX + case .vertical: + // ForwardY -> RotateX -> BackwardY + attributes.center.y += direction*radius*0.5 // ForwardY + transform3D = CATransform3DRotate(transform3D, theta, 1, 0, 0) // RotateX + transform3D = CATransform3DTranslate(transform3D,0, -direction*radius*0.5, 0) // BackwardY + } + attributes.transform3D = transform3D + case 1 ... CGFloat.greatestFiniteMagnitude: + attributes.alpha = 0 + default: + attributes.alpha = 0 + attributes.zIndex = 0 + } + } + } + + + open override func proposedInteritemSpacing() -> CGFloat { + return pagerView?.interitemSpacing ?? 0 + } +} diff --git a/BeeReel/Class/Player/View/BRVideoDetailRecommendView.swift b/BeeReel/Class/Player/View/BRVideoDetailRecommendView.swift new file mode 100644 index 0000000..afbb4c2 --- /dev/null +++ b/BeeReel/Class/Player/View/BRVideoDetailRecommendView.swift @@ -0,0 +1,194 @@ +// +// BRVideoDetailRecommendView.swift +// BeeReel +// +// Created by 长沙鸿瑶 on 2025/7/28. +// + +import UIKit +import FSPagerView + +class BRVideoDetailRecommendView: BRPanModalContentView { + + var didDismissBlock: (() -> Void)? + + var didSelectedVideo: ((_ model: BRShortModel) -> Void)? + + var videoList: [BRShortModel] = [] { + didSet { + + self.bannerView.reloadData() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in + self?.updateCurrentData() + } + } + } + + private var currentCell: BRVideoDetailRecommendCell? { + didSet { + oldValue?.isCurrent = false + oldValue?.pause() + + currentCell?.isCurrent = true + currentCell?.play() + } + } + + private var isClickWatchButton = false + + private lazy var videoNameLabel: UILabel = { + let label = UILabel() + label.font = .fontBold(ofSize: 18) + label.textColor = .colorFFFFFF() + return label + }() + + private lazy var bannerView: FSPagerView = { +// let transformer = BRVideoDetailRecommendTransformer(type: .linear) +// transformer.minimumScale = 0.8 + + let view = FSPagerView() + view.transformer = BRVideoDetailRecommendTransformer(type: .linear) + view.transformer?.minimumScale = 0.75 + view.transformer?.minimumAlpha = 1 +// view.decelerationDistance = FSPagerView.automaticDistance + view.itemSize = .init(width: 180, height: 244) + view.isInfinite = true + view.delegate = self + view.dataSource = self + view.interitemSpacing = 25 + view.register(BRVideoDetailRecommendCell.self, forCellWithReuseIdentifier: "cell") + return view + }() + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "箭头")) + return imageView + }() + + private lazy var watchButton: UIButton = { + let button = UIButton(type: .custom) + button.setBackgroundImage(UIImage(named: "bg 1"), for: .normal) + button.layer.cornerRadius = 24 + button.layer.borderWidth = 1 + button.layer.borderColor = UIColor.color499C20(alpha: 0.6).cgColor + button.layer.shadowOpacity = 1 + button.layer.shadowRadius = 15 + button.layer.shadowOffset = .init(width: 0, height: 0) + button.setTitle("Watch Now".localized, for: .normal) + button.setTitleColor(.color1C1C1C(), for: .normal) + button.titleLabel?.font = .fontMedium(ofSize: 15) + button.addTarget(self, action: #selector(handleWatchButton), for: .touchUpInside) + return button + }() + + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.contentHeight = 433 + UIScreen.tabbarSafeBottomMargin + NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActiveNotification), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(willResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil) + + br_setupUI() + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func panModalDidDismissed() { + super.panModalDidDismissed() + if !isClickWatchButton { + self.didDismissBlock?() + } + } + + ///更新当前数据 + private func updateCurrentData() { + guard let cell = self.bannerView.cellForItem(at: self.bannerView.currentIndex) as? BRVideoDetailRecommendCell else { return } + + self.currentCell = cell + let model = cell.model + self.videoNameLabel.text = model?.name + } + + @objc private func handleWatchButton() { + self.isClickWatchButton = true + self.dismiss(animated: true) { + + } + self.didSelectedVideo?(videoList[self.bannerView.currentIndex]) + } +} + +extension BRVideoDetailRecommendView { + + private func br_setupUI() { + + addSubview(videoNameLabel) + addSubview(bannerView) + addSubview(iconImageView) + addSubview(watchButton) + + videoNameLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.right.lessThanOrEqualToSuperview().offset(-15) + make.top.equalToSuperview().offset(26) + } + + bannerView.snp.makeConstraints { make in + make.left.right.equalToSuperview() + make.top.equalTo(74) + make.height.equalTo(244) + } + + iconImageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalTo(bannerView.snp.bottom).offset(5) + } + + watchButton.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalToSuperview().offset(-(UIScreen.tabbarSafeBottomMargin + 6)) + make.width.equalTo(260) + make.height.equalTo(48) + } + + } + + +} + +extension BRVideoDetailRecommendView: FSPagerViewDelegate, FSPagerViewDataSource { + + func numberOfItems(in pagerView: FSPagerView) -> Int { + return videoList.count + } + + func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell { + let cell = pagerView.dequeueReusableCell(withReuseIdentifier: "cell", at: index) as! BRVideoDetailRecommendCell + cell.model = videoList[index] + return cell + } + + func pagerViewDidEndDecelerating(_ pagerView: FSPagerView) { + self.updateCurrentData() + } + + +} + +extension BRVideoDetailRecommendView { + @objc func didBecomeActiveNotification() { + self.currentCell?.play() + } + + @objc func willResignActiveNotification() { + self.currentCell?.pause() + } +} diff --git a/BeeReel/Class/Player/View/BRVideoLockView.swift b/BeeReel/Class/Player/View/BRVideoLockView.swift index 646fc0d..316e08a 100644 --- a/BeeReel/Class/Player/View/BRVideoLockView.swift +++ b/BeeReel/Class/Player/View/BRVideoLockView.swift @@ -9,6 +9,7 @@ import UIKit class BRVideoLockView: UIView { + var clickUnlockButton: (() -> Void)? var videoInfo: BRVideoInfoModel? { didSet { @@ -16,7 +17,12 @@ class BRVideoLockView: UIView { } } - var clickUnlockButton: (() -> Void)? + var hasLastEpisodeUnlocked = false { + didSet { + lockButton.setNeedsUpdateConfiguration() + } + } + private lazy var lockIconView: UIImageView = { let view = UIImageView(image: UIImage(named: "Frame 4")) @@ -47,7 +53,13 @@ class BRVideoLockView: UIView { button.configurationUpdateHandler = { [weak self] button in guard let self = self else { return } - let title = "Unlocking costs ## Coins".localizedReplace(text: "\(videoInfo?.coins ?? 0)") + let title: String + if hasLastEpisodeUnlocked { + title = "beereel_video_lock_tip_text".localized + } else { + title = "Unlocking costs ## Coins".localizedReplace(text: "\(videoInfo?.coins ?? 0)") + } + button.configuration?.attributedTitle = AttributedString.br_createAttributedString(string: title, color: .color1C1C1C(), font: .fontRegular(ofSize: 14)) } diff --git a/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift b/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift index 908a31e..bc068f7 100644 --- a/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift +++ b/BeeReel/Class/Player/ViewModel/BRPlayerViewModel.swift @@ -31,6 +31,9 @@ class BRPlayerViewModel: NSObject { weak var playerListVC: BRPlayerListViewController? + ///推荐列表 + private(set) lazy var recommandList: [BRShortModel] = [] + @objc dynamic var isPlaying: Bool = true var currentIndexPath = IndexPath(row: 0, section: 0) @@ -192,4 +195,14 @@ extension BRPlayerViewModel { BRVideoAPI.requestUploadPlayTime(shortPlayId: shortPlayId, videoId: videoId, seconds: Int(time) * 1000) } + + ///获取推荐数据 + func asyncGetRecommandDataArr() { + BRVideoAPI.requestDetailsRecommand { [weak self] list in + guard let self = self else { return } + if let list = list { + self.recommandList = list + } + } + } } diff --git a/BeeReel/Delegate/SceneDelegate.swift b/BeeReel/Delegate/SceneDelegate.swift index 384744a..76e2819 100644 --- a/BeeReel/Delegate/SceneDelegate.swift +++ b/BeeReel/Delegate/SceneDelegate.swift @@ -70,9 +70,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate { private func startApp() { -// let hasOpenApp = UserDefaults.standard.object(forKey: kBRHasBeenOpenedAPPDefaultsKey) as? Bool -// -// if hasOpenApp != true { + let hasOpenApp = UserDefaults.standard.object(forKey: kBRHasBeenOpenedAPPDefaultsKey) as? Bool + + if hasOpenApp != true { let guideVc = BRGuideViewController() guideVc.clickStartButton = { [weak self] in guard let self = self else { return } @@ -81,9 +81,9 @@ extension SceneDelegate { } window?.rootViewController = guideVc window?.makeKeyAndVisible() -// } else { -// openApp() -// } + } else { + openApp() + } } private func openApp() { diff --git a/BeeReel/Lib/Player/BRPlayer.swift b/BeeReel/Lib/Player/BRPlayer.swift index 247f57d..fc976fe 100644 --- a/BeeReel/Lib/Player/BRPlayer.swift +++ b/BeeReel/Lib/Player/BRPlayer.swift @@ -61,6 +61,8 @@ class BRPlayer: NSObject { return self.player.presentView.placeholderImageView } + var isLoop = false + ///精确到秒 var duration: TimeInterval { return self.player.duration @@ -142,7 +144,11 @@ extension BRPlayer { //播放完成回调 self.player.playbackObserver.playbackDidFinishExeBlock = { [weak self] player in guard let self = self else { return } - self.delegate?.br_playerDidPlayFinish?(self) + if self.isLoop { + self.replay() + } else { + self.delegate?.br_playerDidPlayFinish?(self) + } } //播放状态改变 self.player.playbackObserver.playbackStatusDidChangeExeBlock = { [weak self] player in diff --git a/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Contents.json b/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Contents.json new file mode 100644 index 0000000..1c40821 --- /dev/null +++ b/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Rectangle 80@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Rectangle 80@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Rectangle 80@2x.png b/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Rectangle 80@2x.png new file mode 100644 index 0000000..78fa4c7 Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Rectangle 80@2x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Rectangle 80@3x.png b/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Rectangle 80@3x.png new file mode 100644 index 0000000..2de5d9f Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/Rectangle 80.imageset/Rectangle 80@3x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/Contents.json b/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/Contents.json new file mode 100644 index 0000000..8d14046 --- /dev/null +++ b/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "箭头@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "箭头@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/箭头@2x.png b/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/箭头@2x.png new file mode 100644 index 0000000..17d11a8 Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/箭头@2x.png differ diff --git a/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/箭头@3x.png b/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/箭头@3x.png new file mode 100644 index 0000000..b35a04f Binary files /dev/null and b/BeeReel/Sources/Assets.xcassets/icon/箭头.imageset/箭头@3x.png differ diff --git a/BeeReel/Sources/GoogleService-Info.plist b/BeeReel/Sources/GoogleService-Info.plist new file mode 100644 index 0000000..5564e1f --- /dev/null +++ b/BeeReel/Sources/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyDpxwKO6oHYZEvxJOnJVfdCJbBDyUAZzBU + GCM_SENDER_ID + 75119644836 + PLIST_VERSION + 1 + BUNDLE_ID + com.breeltv.beereel + PROJECT_ID + beereel-6843e + STORAGE_BUCKET + beereel-6843e.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:75119644836:ios:216e375865f2e011a25631 + + \ No newline at end of file diff --git a/BeeReel/Sources/Info.plist b/BeeReel/Sources/Info.plist index fd05924..8e0d2b5 100644 --- a/BeeReel/Sources/Info.plist +++ b/BeeReel/Sources/Info.plist @@ -2,6 +2,17 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + beereelapp + + + FirebaseAppDelegateProxyEnabled UIAppFonts @@ -25,5 +36,10 @@ + UIBackgroundModes + + remote-notification + fetch + diff --git a/BeeReel/Sources/Localizable.xcstrings b/BeeReel/Sources/Localizable.xcstrings index 35a2d59..727f30c 100644 --- a/BeeReel/Sources/Localizable.xcstrings +++ b/BeeReel/Sources/Localizable.xcstrings @@ -102,6 +102,18 @@ } } }, + "beereel_video_lock_tip_text" : { + "comment" : "请解锁上一集", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please unlock the previous episode" + } + } + } + }, "Browse Genres" : { "extractionState" : "manual", "localizations" : { @@ -806,6 +818,17 @@ } } }, + "Watch Now" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Watch Now" + } + } + } + }, "week" : { "extractionState" : "manual", "localizations" : { diff --git a/BeeReel/Thirdparty/WMZBanner/WMZBannerFlowLayout.m b/BeeReel/Thirdparty/WMZBanner/WMZBannerFlowLayout.m index e85813e..1abfc2c 100644 --- a/BeeReel/Thirdparty/WMZBanner/WMZBannerFlowLayout.m +++ b/BeeReel/Thirdparty/WMZBanner/WMZBannerFlowLayout.m @@ -82,7 +82,16 @@ attributes.transform3D = CATransform3DMakeScale(1.0, 1.0, 1.0); } }else{ - attributes.transform3D = CATransform3DMakeScale(1.0, zoom, 1.0); + CGFloat scaleFactor = 1 - self.param.wScaleFactor; + CGFloat translate = 1 - (zoom - scaleFactor) / (1 - scaleFactor); + + CATransform3D transform = CATransform3DIdentity; + transform = CATransform3DTranslate(transform, 0, 45 * translate, 0); + transform = CATransform3DScale(transform, zoom, zoom, 1.0); + attributes.transform3D = transform; +// CATransform3D transform3D = CATransform3DMakeScale(zoom, zoom, 1.0); +// +// attributes.transform3D = CATransform3DTranslate(transform3D, 0, 30, 0); } if (self.param.wAlpha<1) { CGFloat collectionCenter = self.collectionView.frame.size.width / 2 ;