完整的修改我这两个新建的文件:
// NewCoverImageContainerView.swift
// SurveillanceHome
//
// Created by MaCong on 2025/12/8.
// Copyright © 2025 tplink. All rights reserved.
//
import UIKit
import SnapKit
class NewCoverImageContainerView: UIView {
// MARK: - Subviews
var coverImageView: UIImageView!
var maskImageView: UIImageView!
var maskLabel: UILabel!
var helpButton: UIButton?
var helpLabel: UILabel!
var collectButton: UIButton!
var maskLabelCenterYConstraint: NSLayoutConstraint?
// Constraints
private var maskLabelCenterYConstraint: Constraint?
// Data & Actions
private var isCollected: Bool = false {
didSet {
let image = isCollected ? TPImageLiteral("device_have_collect_card") : TPImageLiteral("device_not_collect_card")
collectButton.setImage(image, for: .normal)
}
}
var collectAction: ((Bool) -> Void)?
var helpAction: (() -> Void)?
// Status Icons (for future extension if needed)
private var statusImageViews = [UIImageView]()
private let numberOfStatusImages = 0
private let needHideMask = 1
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
setupViews()
setupConstraints()
}
// MARK: - Setup Views
private func setupViews() {
backgroundColor = .clear
layer.cornerRadius = 2
layer.masksToBounds = true
// === Cover Image View ===
coverImageView = UIImageView()
coverImageView.contentMode = .scaleToFill
coverImageView.clipsToBounds = true
coverImageView.isUserInteractionEnabled = false
// === Mask Image View ===
maskImageView = UIImageView()
maskImageView.contentMode = .scaleToFill
maskImageView.clipsToBounds = true
maskImageView.isUserInteractionEnabled = false
maskImageView.tag = 3
// === Mask Label ===
maskLabel = UILabel()
maskLabel.tag = 4
maskLabel.text = LocalizedString(key: deviceListNoSuchDevice)
maskLabel.font = UIFont(name: "PingFangSC-Regular", size: 12)
maskLabel.textColor = UIColor(white: 1.0, alpha: 1.0)
maskLabel.textAlignment = .center
maskLabel.backgroundColor = .clear
maskLabel.numberOfLines = 1
maskLabel.adjustsFontSizeToFitWidth = false
maskLabel.baselineAdjustment = .alignBaselines
// === Help Button (transparent clickable area) ===
helpButton = UIButton(type: .custom)
helpButton?.backgroundColor = .clear
helpButton?.layer.cornerRadius = 14
helpButton?.clipsToBounds = true
helpButton?.layer.borderWidth = 1
helpButton?.layer.borderColor = UIColor.tpbTextDisabled.cgColor
helpButton?.addTarget(self, action: #selector(helpButtonClicked), for: .touchUpInside)
// === Help Label (inside helpButton) ===
helpLabel = UILabel()
helpLabel.text = LocalizedString(key: deviceListOfflineNeedHelp)
helpLabel.font = UIFont(name: "PingFangSC-Regular", size: 12)
helpLabel.textColor = UIColor(white: 1.0, alpha: 1.0)
helpLabel.textAlignment = .center
helpLabel.backgroundColor = .clear
helpButton?.addSubview(helpLabel)
// === Collect Button ===
collectButton = UIButton(type: .custom)
collectButton.setImage(TPImageLiteral("device_not_collect_card"), for: .normal)
collectButton.addTarget(self, action: #selector(collectButtonClicked), for: .touchUpInside)
collectButton.isHidden = true
// Add to hierarchy
addSubview(coverImageView)
addSubview(maskImageView)
addSubview(maskLabel)
addSubview(collectButton)
addSubview(helpButton!)
}
// MARK: - Setup Constraints with SnapKit
private func setupConstraints() {
coverImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
maskImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
maskLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.leading.greaterThanOrEqualToSuperview().offset(16)
make.trailing.lessThanOrEqualToSuperview().offset(-16)
let centerY = make.centerY.equalToSuperview().offset(0).priority(.defaultLow).constraint
maskLabelCenterYConstraint = centerY
}
collectButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(8)
make.trailing.equalToSuperview().offset(-8)
make.width.height.equalTo(26)
}
helpButton?.snp.makeConstraints { make in
make.centerY.equalToSuperview().offset(20)
make.trailing.equalToSuperview().offset(-8)
make.width.greaterThanOrEqualTo(30)
}
helpLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10))
}
// Setup status image views (if enabled later)
for i in 0..<numberOfStatusImages {
let imageView = UIImageView()
imageView.isHidden = true
imageView.isUserInteractionEnabled = false
statusImageViews.append(imageView)
addSubview(imageView)
}
}
// MARK: - Actions
@objc private func collectButtonClicked() {
isCollected.toggle()
collectAction?(isCollected)
}
@objc private func helpButtonClicked() {
helpAction?()
}
// MARK: - Public Configuration Methods
func shouldShowCollectButton(device: TPSSDeviceForDeviceList, channel: TPSSChannelInfo? = nil, isCard: Bool) {
let isVms = TPAppContextFactory.shared().isVmsLogin
collectButton.isHidden = (device.displayType == .solar || device.listType == .local || !isCard || (TPSSAppContext.shared.loginType == .personal ? false : TPSSAppContext.shared.isVmsDeviceListFavoriteOldVersion))
if device.deviceType == .IPC {
isCollected = isVms ? device.isVMSFavorited : device.isCollected
} else if device.deviceType == .NVR {
if let channel = channel {
isCollected = isVms ? channel.isVMSFavorited : device.isCollected && device.collectChannels.contains(channel.channelId as NSNumber)
} else {
isCollected = isVms ? device.isVMSFavorited : device.isCollected
}
}
}
func configureCoverWith(size: TPSSDeviceForDeviceList.CoverImageSize,
device: TPSSDeviceForDeviceList,
channel: TPSSChannelInfo? = nil,
hideMaskIfNeeded: Bool = false,
isCard: Bool = false,
folded: Bool = false,
leadingConst: NSLayoutConstraint? = nil,
trailConst: NSLayoutConstraint? = nil,
isSmall: Bool = false,
isModelCover: Bool = false,
noDevice: Bool = false,
isHideLogo: Bool = false) {
var hasNoChannels = true
if let channel = channel {
hasNoChannels = !device.activeChannelsInfo.contains(channel)
}
shouldShowCollectButton(device: device, channel: channel, isCard: isCard)
var coverImage: UIImage? = device.displayType == .solar
? TPSSDeviceForDeviceList.defaultSolarCoverImage(of: size)
: TPSSDeviceForDeviceList.defaultCoverImage(of: size)
if isModelCover {
coverImage = TPSSDeviceForDeviceList.getDeviceModelImage(type: .IPC, model: noDevice ? "" : channel?.deviceModel ?? "")
}
if !hasNoChannels {
coverImage = device.coverImage(of: size, channelID: channel?.channelId.intValue ?? -1, listType: device.listType)
}
if hideMaskIfNeeded && !maskImageView.isHidden {
maskImageView.isHidden = maskImageView.tag == needHideMask && coverImage != nil
}
coverImageView.image = coverImage ?? (device.displayType == .solar
? TPSSDeviceForDeviceList.defaultSolarCoverImage(of: size)
: (isModelCover
? TPSSDeviceForDeviceList.getDeviceModelImage(type: .IPC, model: noDevice ? "" : channel?.deviceModel ?? "")
: TPSSDeviceForDeviceList.defaultCoverImage(of: size)))
// Handle fisheye / pano logic
var isFisheye = false
if TPSSAppContext.shared.isVmsLogin && device.listType == .remote {
isFisheye = coverImage != nil && device.coverImageIsFisheye(of: size, channelID: channel?.channelId.intValue ?? -1, listType: device.listType)
} else if let channelInfo = channel, device.displayType == .NVR {
isFisheye = coverImage != nil && channelInfo.supportsFisheye
} else {
isFisheye = coverImage != nil && device.supportFishEye
}
var isPano = false
if let channelInfo = channel, device.displayType == .NVR {
isPano = coverImage != nil && channelInfo.isSupportSplicingSetting
} else {
isPano = coverImage != nil && device.supportPano
}
let imgSize = coverImage?.size ?? CGSize(width: 1, height: 1)
if (!isFisheye && imgSize.width > 1.1 && abs(imgSize.width - imgSize.height) < 1) {
isFisheye = true
}
coverImageView.contentMode = isFisheye ? (isCard ? .scaleAspectFit : .scaleAspectFill) : .scaleToFill
isPano = isPano || (imgSize.width / imgSize.height >= 3)
backgroundColor = isFisheye ? .tpbFisheyeBackground : .clear
if device.displayType != .solar {
if let oldImg = coverImageView.image,
let newImg = cropFishScaleImg(img: oldImg, isFishEye: isFisheye),
oldImg != newImg {
coverImageView.image = newImg
coverImageView.contentMode = isCard ? .scaleAspectFit : .scaleAspectFill
backgroundColor = .tpbFisheyeBackground
}
if isPano {
coverImageView.contentMode = .scaleAspectFill
backgroundColor = .clear
}
if let oldImg = coverImageView.image,
let newImg = cropFishScaleImg(img: oldImg, isFishEye: isFisheye),
isCard && (isFisheye || oldImg != newImg) && (!folded || device.deviceType == .IPC) {
coverImageView.contentMode = .scaleAspectFit
}
}
if device.displayType == .solar || (isModelCover && (noDevice || coverImage == nil)) {
coverImageView.contentMode = .scaleAspectFit
coverImageView.backgroundColor = .tpbBackground
} else {
coverImageView.backgroundColor = .clear
}
if isHideLogo && coverImage == nil {
coverImageView.isHidden = true
} else {
coverImageView.isHidden = false
}
guard let finalImage = coverImageView.image, !isFisheye, finalImage.size.height > finalImage.size.width else { return }
backgroundColor = .tpbFisheyeBackground
coverImageView.contentMode = .scaleAspectFill
}
func configureWith(cloudStorageEnabled: Bool = false,
supportsAlarm: Bool = false,
alarm: Bool = true,
noDevice: Bool = false,
channelOffline: Bool = false,
channelHidden: Bool = false,
channelUnAuth: Bool = false,
channelUninitialized: Bool = false,
offline: Bool = false,
nvrOffline: Bool = false,
ipcOffline: Bool = false,
offlineTime: String = "",
notInSharePeriod: Bool = false,
notShareEnable: Bool = false,
notConnected: Bool = false,
supportConnectionInfo: Bool = false,
connectedViaWifi: Bool = false,
wifiRssi: Int = 0,
showHelp: Bool = false,
playButtonHidden: Bool = false,
size: TPSSDeviceForDeviceList.CoverImageSize,
showText: Bool = true,
showIcons: Bool = true,
hasNoVmsLicense: Bool = false,
isChannel: Bool = false) {
let showAlarm = supportsAlarm && !noDevice
let showWifiStatus = supportConnectionInfo && connectedViaWifi && !(offline || nvrOffline || ipcOffline)
let showCloudStorage = cloudStorageEnabled
let images: [UIImage] = [
showAlarm ? (alarm ? TPImageLiteral("devicelist_indicator_alarm_on") : TPImageLiteral("devicelist_indicator_alarm_off")) : nil,
showWifiStatus ? UIImage(named: "common_wifi_indicator\(wifiRssi)") : nil,
showCloudStorage ? TPImageLiteral("cloud_label") : nil
].compactMap { $0 }
for i in 0..<min(statusImageViews.count, images.count) {
statusImageViews[i].image = images[i]
statusImageViews[i].isHidden = !showIcons
}
for i in images.count..<statusImageViews.count {
statusImageViews[i].isHidden = true
}
let deviceOffline = offline || nvrOffline || ipcOffline || channelOffline
let basicMaskVisible = noDevice || deviceOffline || notInSharePeriod || notShareEnable || notConnected || channelHidden || hasNoVmsLicense
maskImageView.isHidden = !basicMaskVisible
maskLabel.isHidden = !basicMaskVisible && !playButtonHidden
maskImageView.image = nil
maskImageView.backgroundColor = basicMaskVisible ? .tpbDeviceMask : .clear
maskImageView.contentMode = .scaleAspectFit
maskImageView.tag = basicMaskVisible ? 0 : needHideMask
maskLabel.numberOfLines = basicMaskVisible ? (offlineTime.isEmpty ? 1 : 0) : 1
if deviceOffline {
if isChannel && noDevice {
maskLabel.text = LocalizedString(key: deviceListNoSuchDevice)
} else if isChannel && channelUnAuth {
maskLabel.text = LocalizedString(key: NVRAddIPCDeviceNotCertified)
} else if isChannel && channelUninitialized {
maskLabel.text = LocalizedString(key: multiWindowPlayerUninitialized)
} else {
let firstLine = notInSharePeriod ? LocalizedString(key: deviceListNotInSharePeriod) : LocalizedString(key: deviceListDeviceNotOnline)
if !offlineTime.isEmpty {
let secondLine = String(format: LocalizedString(key: deviceListOfflineTimeFormat), offlineTime)
let attributedText = NSMutableAttributedString(string: "\(firstLine)\n\(secondLine)")
let style = NSMutableParagraphStyle()
style.lineSpacing = 8
style.alignment = .center
attributedText.addAttributes([
.font: UIFont.projectFont(ofSize: 12),
.paragraphStyle: style
], range: NSMakeRange(0, firstLine.count))
attributedText.addAttributes([
.font: UIFont.projectFont(ofSize: 10)
], range: NSMakeRange(firstLine.count + 1, secondLine.count))
maskLabel.attributedText = attributedText
} else {
maskLabel.text = firstLine
}
}
} else if noDevice {
maskLabel.text = LocalizedString(key: deviceListNoSuchDevice)
} else if notShareEnable {
maskLabel.text = LocalizedString(key: deviceListNotShareEnable)
} else if notInSharePeriod && !deviceOffline {
maskLabel.text = LocalizedString(key: deviceListNotInSharePeriod)
} else if channelUnAuth {
maskLabel.text = LocalizedString(key: NVRAddIPCDeviceNotCertified)
} else if channelHidden {
maskLabel.text = LocalizedString(key: deviceListChannelHidden)
} else if notConnected {
maskLabel.text = LocalizedString(key: wirelessDirectNotConnected)
} else if hasNoVmsLicense {
maskLabel.text = LocalizedString(key: deviceListCloudVmsWithoutLicenseMaskLabel)
} else {
maskImageView.isHidden = playButtonHidden
maskLabel.isHidden = true
switch size {
case .small:
maskImageView.image = TPImageLiteral("devicelist_preview_small")
maskImageView.contentMode = .scaleAspectFit
case .medium:
maskImageView.image = TPImageLiteral("devicelist_preview_medium")
maskImageView.contentMode = .center
default:
maskImageView.image = TPImageLiteral("devicelist_preview_large")
maskImageView.contentMode = .center
}
maskImageView.backgroundColor = .clear
maskImageView.tag = needHideMask
}
if !showText {
maskLabel.text = ""
}
// Update help button visibility and label position
helpButton?.isHidden = !showHelp
helpLabel.isHidden = !showHelp
maskLabelCenterYConstraint?.update(offset: showHelp ? -22 : 0)
}
// Helper method (assumed to exist)
private func cropFishScaleImg(img: UIImage?, isFishEye: Bool) -> UIImage? {
// Implement or keep existing version
return img
}
}
// MARK: - Extension: Configure Overloads
extension NewCoverImageContainerView {
enum CoverType {
case play
case selection
}
func configure(ipc: TPSSDeviceForDeviceList, size: TPSSDeviceForDeviceList.CoverImageSize = .medium, type: CoverType = .play, isCard: Bool = false, folded: Bool = false) {
shouldShowCollectButton(device: ipc, isCard: isCard)
let showAccessory = size != .small
let showDetailText = showAccessory && size != .medium
let showPlayButton = (ipc.displayType != .solar && type == .play)
if ipc.listType == .remote {
configureWith(
cloudStorageEnabled: ipc.cloudStorageService()?.serviceState.isValid ?? false,
supportsAlarm: ipc.supportMessage && ipc.displayOnline && !ipc.isSharedDevice,
alarm: ipc.messageEnable,
ipcOffline: !ipc.displayOnline,
offlineTime: showDetailText ? ipc.offlineTime : "",
notInSharePeriod: (ipc.isSharedDevice && !ipc.isInSharePeriod),
notShareEnable: (ipc.isSharedDevice && !ipc.isShareEnable),
supportConnectionInfo: ipc.supportConnectInfo,
connectedViaWifi: ipc.linkType == .wifi,
wifiRssi: ipc.linkRssi,
showHelp: showAccessory && !ipc.displayOnline && !ipc.isSharedDevice,
playButtonHidden: !showPlayButton,
size: size,
showText: showAccessory,
showIcons: showAccessory,
hasNoVmsLicense: ipc.isLicenseNotActive
)
} else {
configureWith(
ipcOffline: !ipc.displayOnline,
offlineTime: showDetailText ? ipc.offlineTime : "",
supportConnectionInfo: ipc.supportConnectInfo,
connectedViaWifi: ipc.linkType == .wifi,
wifiRssi: ipc.linkRssi,
showHelp: showAccessory && !ipc.displayOnline && !ipc.isSharedDevice,
playButtonHidden: !showPlayButton,
size: size,
showText: showAccessory,
showIcons: showAccessory
)
}
configureCoverWith(size: size, device: ipc, channel: ipc.channelsInfo.first, hideMaskIfNeeded: true, isCard: isCard, folded: folded)
statusImageViews.forEach { $0.isHidden = true }
}
func configure(channel: TPSSChannelInfo, nvr: TPSSDeviceForDeviceList, size: TPSSDeviceForDeviceList.CoverImageSize = .medium, type: CoverType = .play, isCard: Bool = false, folded: Bool = false, leadingConst: NSLayoutConstraint? = nil, trialConst: NSLayoutConstraint? = nil, isSmall: Bool = false, isSigleChannel: Bool = false, isHideLogo: Bool = false) {
let showAccessory = size != .small
let showPlayButton = type == .play && !isSigleChannel
shouldShowCollectButton(device: nvr, channel: channel, isCard: isCard)
if nvr.listType == .remote {
configureWith(
cloudStorageEnabled: nvr.cloudStorageService(channel: channel)?.serviceState.isValid ?? false,
supportsAlarm: channel.supportsMessagePush && channel.online && !nvr.isSharedDevice,
alarm: channel.messageEnable,
noDevice: !nvr.activeChannelsInfo.contains(channel),
channelOffline: !channel.online,
channelUnAuth: channel.authResult == 1,
channelUninitialized: channel.authResult == 3,
notInSharePeriod: (nvr.isSharedDevice && !channel.isInSharePeriod),
notShareEnable: (nvr.isSharedDevice && !channel.isShareEnable),
showHelp: !channel.online && channel.authResult == 0 && channel.active,
playButtonHidden: !showPlayButton,
size: size,
showText: showAccessory,
showIcons: showAccessory,
hasNoVmsLicense: channel.isLicenseNotActive,
isChannel: true
)
} else {
configureWith(
noDevice: !nvr.activeChannelsInfo.contains(channel),
channelOffline: !channel.online,
channelUnAuth: channel.authResult == 1,
channelUninitialized: channel.authResult == 3,
showHelp: !channel.online && channel.authResult == 0,
playButtonHidden: !showPlayButton,
size: size,
showText: showAccessory,
showIcons: showAccessory,
isChannel: true
)
}
configureCoverWith(size: size, device: nvr, channel: channel, hideMaskIfNeeded: true, isCard: isCard, folded: folded, leadingConst: leadingConst, trailConst: trialConst, isSmall: isSmall, isHideLogo: !isCard && isHideLogo)
statusImageViews.forEach { $0.isHidden = true }
}
func configure(nvr: TPSSDeviceForDeviceList, size: TPSSDeviceForDeviceList.CoverImageSize = .medium, type: CoverType = .play) {
let showAccessory = size != .small
let showPlayButton = type == .play
let showDetailText = showAccessory && size != .medium
let channelHidden = nvr.channelsInfo.count == 0 && nvr.allChannelsInfo.count != 0
var noDevice = nvr.activeChannelsInfo.count == 0 && !channelHidden
if nvr.isSharedFromOthers {
for channel in nvr.activeChannelsInfo {
if !channel.isSharedFromOthers {
noDevice = true
} else {
noDevice = false
break
}
}
}
if nvr.listType == .remote {
configureWith(
noDevice: noDevice,
channelHidden: channelHidden,
nvrOffline: !nvr.displayOnline,
offlineTime: showDetailText ? nvr.offlineTime : "",
showHelp: showAccessory && !nvr.displayOnline && !nvr.isSharedDevice,
playButtonHidden: !showPlayButton,
size: size,
showText: showAccessory,
showIcons: showAccessory,
)
} else {
configureWith(
noDevice: noDevice,
channelHidden: channelHidden,
nvrOffline: !nvr.displayOnline,
offlineTime: showDetailText ? nvr.offlineTime : "",
showHelp: showAccessory && !nvr.displayOnline && !nvr.isSharedDevice,
playButtonHidden: !showPlayButton,
size: size,
showText: showAccessory,
showIcons: showAccessory,
)
}
configureCoverWith(size: size, device: nvr, channel: nil, hideMaskIfNeeded: true)
}
func showEmpty(size: TPSSDeviceForDeviceList.CoverImageSize = .medium) {
configureWith(playButtonHidden: true, size: size, showText: false, showIcons: false)
coverImageView.image = TPSSDeviceForDeviceList.defaultCoverImage(of: size)
}
}
// MARK: - Device Setting Extension
extension NewCoverImageContainerView {
func configure(device: TPSSDeviceForSetting, channel: TPSSChannelInfo?, size: TPSSDeviceForDeviceList.CoverImageSize = .medium, model: String = "") {
configureWith(playButtonHidden: true, size: size, showText: false, showIcons: false)
let channelID = channel?.channelId.intValue ?? -1
let coverImage = device.coverImage(at: channelID, listType: device.deviceListType)
coverImageView.image = coverImage ?? device.defaultCoverImage(at: channelID, size: size, model: model)
var isFisheye = false
if TPSSAppContext.shared.isVmsLogin && device.deviceListType == .remote {
isFisheye = coverImage != nil && device.coverImageIsFisheye(at: channelID, listType: device.deviceListType)
} else {
isFisheye = coverImage != nil && (channel?.supportsFisheye ?? device.isFishEye)
}
if isFisheye {
coverImageView.contentMode = .scaleAspectFill
} else if device.deviceType == .solar || coverImage == nil {
coverImageView.contentMode = .scaleAspectFit
coverImageView.backgroundColor = .tpbProgressBackground
coverImageView.layer.cornerRadius = 8
} else {
let mode = (coverImage?.size.height ?? 0) > (coverImage?.size.width ?? 0) ? UIView.ContentMode.scaleAspectFit : .scaleAspectFill
if let ch = channel, ch.supportCorridor {
coverImageView.contentMode = mode
coverImageView.backgroundColor = .tpbProgressBackground
} else if device.supportCorridor {
coverImageView.contentMode = mode
coverImageView.backgroundColor = .tpbProgressBackground
} else if let ch = channel, ch.isSupportSplicingSetting {
coverImageView.contentMode = .scaleAspectFill
} else if device.isSupportSplicingSetting {
coverImageView.contentMode = .scaleAspectFill
} else {
coverImageView.contentMode = .scaleToFill
}
}
backgroundColor = isFisheye ? .tpbFisheyeBackground : .clear
}
}
//
// NewCoverImageBigContainerView.swift
// SurveillanceHome
//
// Created by MaCong on 2025/12/8.
// Copyright © 2025 tplink. All rights reserved.
//
import UIKit
import SnapKit
class NewCoverImageBigContainerView: NewCoverImageContainerView {
// MARK: - Only Add NEW Subviews (not existed in parent)
let nvrChannelNameContainer = UIView()
let nvrChannelNameLabel = UILabel()
private let gradientView = GradientView()
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
setupConstraints()
setupAppearance()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupSubviews()
setupConstraints()
setupAppearance()
}
// MARK: - Private Setup Methods
private func setupSubviews() {
// Configure gradient background
gradientView.startColor = UIColor(white: 0, alpha: 0)
gradientView.endColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.7)
gradientView.horizontalMode = false
gradientView.startLocation = 0.0
gradientView.endLocation = 1.0
nvrChannelNameContainer.addSubview(gradientView)
nvrChannelNameContainer.addSubview(nvrChannelNameLabel)
addSubview(nvrChannelNameContainer)
// ✅ 使用父类已有的 helpButton 和 helpLabel
// 不需要 addSubview(helpButton),它已经在父类中被添加了!
// 但我们需要调整它的约束(如果之前是基于 maskLabel 的话)
}
private func setupConstraints() {
// Layout nvr channel name container at bottom
nvrChannelNameContainer.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.height.equalTo(24)
}
gradientView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
nvrChannelNameLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(10)
make.trailing.equalToSuperview().offset(-6)
make.centerY.equalToSuperview().offset(2)
}
// ✅ 修正 helpButton 和 helpLabel 的约束(脱离对 maskLabel 的依赖)
helpButton?.snp.removeConstraints() // 移除父类可能设置的老约束
helpButton?.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(20)
make.size.equalTo(CGSize(width: 68, height: 29))
}
helpLabel.snp.removeConstraints() // 同样移除旧约束
helpLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(20)
make.leading.greaterThanOrEqualToSuperview().offset(20)
make.trailing.lessThanOrEqualToSuperview().offset(-20)
}
}
private func setupAppearance() {
layer.cornerRadius = 2
clipsToBounds = true
nvrChannelNameContainer.layer.cornerRadius = 2
nvrChannelNameContainer.clipsToBounds = true
nvrChannelNameLabel.font = UIFont(name: "PingFangSC-Regular", size: 13)
nvrChannelNameLabel.textColor = .white
nvrChannelNameLabel.textAlignment = .left
nvrChannelNameLabel.text = "Label"
helpButton?.layer.cornerRadius = 14
helpButton?.clipsToBounds = true
helpButton?.backgroundColor = .clear // 保持透明背景
helpLabel.font = UIFont(name: "PingFangSC-Regular", size: 12)
helpLabel.textColor = .white
helpLabel.textAlignment = .center
helpLabel.text = LocalizedString(key: "deviceListOfflineNeedHelp")
helpLabel.isUserInteractionEnabled = false
}
// MARK: - Public APIs
func showHelpSection(visible: Bool) {
helpButton?.isHidden = !visible
helpLabel.isHidden = !visible
// 可选:触发 maskLabel 位置更新
if let constraint = self.labelToCenterConstraint {
constraint.constant = visible ? -22 : 0
UIView.animate(withDuration: 0.25) {
self.layoutIfNeeded()
}
}
}
func setChannelName(_ name: String?) {
guard let name = name, !name.isEmpty else {
nvrChannelNameContainer.isHidden = true
return
}
nvrChannelNameLabel.text = name
nvrChannelNameContainer.isHidden = false
}
}