Commit e0ed7074 authored by Alex朱枝文's avatar Alex朱枝文

社区相关功能模块开发

parent 59b41db5
......@@ -254,7 +254,8 @@ NSString *const TUILogoutFailNotification = @"TUILogoutFailNotification";
[NSNotificationCenter.defaultCenter postNotificationName:TUILoginSuccessNotification object:nil];
}
fail:^(int code, NSString *desc) {
self.loginWithInit = NO;
__strong __typeof(weakSelf) strongSelf = weakSelf;
strongSelf.loginWithInit = NO;
if (fail) {
fail(code, desc);
}
......
This diff is collapsed.
......@@ -39,7 +39,7 @@ class YHNavigationController: UINavigationController {
if let lastVC = viewControllers.last {
let className = String(describing: type(of: lastVC))
if !className.hasPrefix("TUI") { // 模糊匹配类名,使得腾讯IM页面不用隐藏NavigationBar
var needAnimated = false
var needAnimated = animated
let lastSecondCount = viewControllers.count - 2
if lastSecondCount >= 0 {
let lastSecondVC = viewControllers[lastSecondCount]
......
//
// YHCircleAddPhotoCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/26.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
class YHCircleAddPhotoCell: UICollectionViewCell {
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.systemGray6
return view
}()
private lazy var plusImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "circle_plus_icon")
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
contentView.addSubview(containerView)
containerView.addSubview(plusImageView)
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
plusImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(40)
}
}
}
//
// YHCircleMediaCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
import AVFoundation
class YHCircleMediaCell: UICollectionViewCell {
var deleteCallback: (() -> Void)?
// MARK: - UI Components
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.backgroundColor = UIColor.brandGrayColor2
return imageView
}()
private lazy var deleteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
button.isHidden = true
return button
}()
private lazy var playIcon: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "circle_play_icon")
imageView.isHidden = true
return imageView
}()
private lazy var durationLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12, weight: .medium)
label.textColor = .white
label.backgroundColor = UIColor.black.withAlphaComponent(0.5)
label.textAlignment = .center
label.layer.cornerRadius = 8
label.clipsToBounds = true
label.isHidden = true
return label
}()
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup UI
private func setupUI() {
contentView.addSubview(imageView)
contentView.addSubview(playIcon)
contentView.addSubview(durationLabel)
contentView.addSubview(deleteButton)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
playIcon.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(40)
}
durationLabel.snp.makeConstraints { make in
make.bottom.right.equalToSuperview().inset(8)
make.height.equalTo(20)
make.width.greaterThanOrEqualTo(35)
}
deleteButton.snp.makeConstraints { make in
make.top.right.equalToSuperview().inset(4)
make.width.height.equalTo(20)
}
}
// MARK: - Configure Cell
func configure(with mediaItem: YHSelectMediaItem) {
resetCell()
switch mediaItem.type {
case .image:
configureForImage(mediaItem)
case .video:
configureForVideo(mediaItem)
}
}
private func resetCell() {
imageView.image = nil
playIcon.isHidden = true
durationLabel.isHidden = true
durationLabel.text = nil
}
private func configureForImage(_ mediaItem: YHSelectMediaItem) {
imageView.image = mediaItem.image
playIcon.isHidden = true
durationLabel.isHidden = true
}
private func configureForVideo(_ mediaItem: YHSelectMediaItem) {
// 显示视频相关UI
playIcon.isHidden = false
// 生成视频缩略图
if let videoURL = mediaItem.videoURL {
generateVideoThumbnail(from: videoURL) { [weak self] thumbnail in
DispatchQueue.main.async {
self?.imageView.image = thumbnail
}
}
}
// 显示视频时长
// if let duration = mediaItem.duration {
// durationLabel.text = formatDuration(duration)
// durationLabel.isHidden = false
// } else if let videoURL = mediaItem.videoURL {
// // 如果没有时长信息,尝试获取
// getVideoDuration(from: videoURL) { [weak self] duration in
// DispatchQueue.main.async {
// if duration > 0 {
// self?.durationLabel.text = self?.formatDuration(duration)
// self?.durationLabel.isHidden = false
// }
// }
// }
// }
}
// MARK: - Video Helpers
private func generateVideoThumbnail(from url: URL, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let asset = AVAsset(url: url)
let imageGenerator = AVAssetImageGenerator(asset: asset)
imageGenerator.appliesPreferredTrackTransform = true
imageGenerator.maximumSize = CGSize(width: 300, height: 300)
let time = CMTime(seconds: 0.0, preferredTimescale: 600)
do {
let cgImage = try imageGenerator.copyCGImage(at: time, actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
completion(thumbnail)
} catch {
print("生成视频缩略图失败: \(error)")
completion(nil)
}
}
}
private func getVideoDuration(from url: URL, completion: @escaping (TimeInterval) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let asset = AVAsset(url: url)
let duration = CMTimeGetSeconds(asset.duration)
completion(duration.isNaN ? 0 : duration)
}
}
private func formatDuration(_ duration: TimeInterval) -> String {
let totalSeconds = Int(duration)
let minutes = totalSeconds / 60
let seconds = totalSeconds % 60
if minutes > 0 {
return String(format: "%d:%02d", minutes, seconds)
} else {
return String(format: "0:%02d", seconds)
}
}
// MARK: - Actions
@objc private func deleteButtonTapped() {
deleteCallback?()
}
}
//
// YHCirclePhotoCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/26.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
// MARK: - Custom Cells
class YHCirclePhotoCell: UICollectionViewCell {
var deleteCallback: (() -> Void)?
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
private lazy var deleteButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
button.isHidden = true
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
contentView.addSubview(imageView)
contentView.addSubview(deleteButton)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
deleteButton.snp.makeConstraints { make in
make.top.right.equalToSuperview().inset(4)
make.width.height.equalTo(20)
}
}
func configure(with image: UIImage) {
imageView.image = image
}
@objc private func deleteButtonTapped() {
deleteCallback?()
}
}
......@@ -405,8 +405,31 @@ extension YHCertificateUploadSheetView: (UIImagePickerControllerDelegate & UINav
return false
}
private func getCaptureDeviceAuthorization(notDeterminedBlock: (() -> Void)?) -> Bool? {
let cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch cameraAuthStatus {
case .authorized:
// 已授权,直接使用相机
return true
case .notDetermined:
// 未请求过权限,主动请求
AVCaptureDevice.requestAccess(for: .video) { _ in
notDeterminedBlock?()
}
return nil
case .denied, .restricted:
// 权限被拒绝或受限
break
@unknown default:
break
}
return false
}
func takePhoto() {
guard let authorization = getPhotoLibraryAuthorization(notDeterminedBlock: {
guard let authorization = getCaptureDeviceAuthorization(notDeterminedBlock: {
DispatchQueue.main.async {
self.takePhoto()
}
......@@ -415,7 +438,7 @@ extension YHCertificateUploadSheetView: (UIImagePickerControllerDelegate & UINav
}
if !authorization {
YHHUD.flash(message: "请在设置中打开相权限")
YHHUD.flash(message: "请在设置中打开相权限")
return
}
......
......@@ -310,8 +310,31 @@ extension YHDocumentUploadView: (UIImagePickerControllerDelegate & UINavigationC
return false
}
private func getCaptureDeviceAuthorization(notDeterminedBlock: (() -> Void)?) -> Bool? {
let cameraAuthStatus = AVCaptureDevice.authorizationStatus(for: .video)
switch cameraAuthStatus {
case .authorized:
// 已授权,直接使用相机
return true
case .notDetermined:
// 未请求过权限,主动请求
AVCaptureDevice.requestAccess(for: .video) { _ in
notDeterminedBlock?()
}
return nil
case .denied, .restricted:
// 权限被拒绝或受限
break
@unknown default:
break
}
return false
}
func takePhoto() {
guard let authorization = getPhotoLibraryAuthorization(notDeterminedBlock: {
guard let authorization = getCaptureDeviceAuthorization(notDeterminedBlock: {
DispatchQueue.main.async {
self.takePhoto()
}
......@@ -320,7 +343,7 @@ extension YHDocumentUploadView: (UIImagePickerControllerDelegate & UINavigationC
}
if !authorization {
YHHUD.flash(message: "请在设置中打开相权限")
YHHUD.flash(message: "请在设置中打开相权限")
return
}
......
//
// YHMediaBrowserViewController.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import Kingfisher
import UIKit
import JXPhotoBrowser
import Photos
import PhotosUI
class YHMediaBrowserViewController: JXPhotoBrowser {
var deleteMediaBlock: ((Int) -> Void)?
var dismissBlock: (() -> Void)?
lazy var navBar: UIView = {
let v = UIView()
let backBtn = UIButton()
backBtn.setImage(UIImage(named: "nav_back_white"), for: .normal)
backBtn.addTarget(self, action: #selector(didBackBtnClicked), for: .touchUpInside)
v.addSubview(backBtn)
let deleteBtn = UIButton()
let img = UIImage(named: "media_brower_delete")
let templateImage = img?.withRenderingMode(.alwaysTemplate)
deleteBtn.setImage(templateImage, for: .normal)
deleteBtn.imageView?.tintColor = .white
deleteBtn.addTarget(self, action: #selector(didDeleteBtnClicked), for: .touchUpInside)
v.addSubview(deleteBtn)
backBtn.snp.makeConstraints { make in
make.width.height.equalTo(44)
make.left.equalToSuperview()
make.bottom.equalToSuperview()
}
deleteBtn.snp.makeConstraints { make in
make.width.height.equalTo(44)
make.right.equalToSuperview()
make.bottom.equalToSuperview()
}
return v
}()
@objc func didBackBtnClicked() {
dismiss()
}
@objc func didDeleteBtnClicked() {
let index = self.browserView.pageIndex
deleteMediaBlock?(index)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(navBar)
navBar.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.height.equalTo(k_Height_NavigationtBarAndStatuBar)
}
}
deinit {
printLog("YHMediaBrowserViewController deinit")
}
override func dismiss() {
super.dismiss()
dismissBlock?()
}
}
//
// YHPreviewMediaItem.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
// 预览媒体项 - 专门用于预览的数据模型
struct YHPreviewMediaItem {
let type: YHMediaType
let source: YHMediaSource
enum YHMediaSource {
case remoteURL(String) // 远程URL
case localImage(UIImage) // 本地图片
case localVideoURL(URL) // 本地视频文件URL
var displayURL: String {
switch self {
case .remoteURL(let url):
return url
case .localImage:
return "" // 本地图片不需要URL
case .localVideoURL(let url):
return url.absoluteString
}
}
}
// 从远程URL创建
init(remoteURL: String, type: YHMediaType = .image) {
self.type = type
self.source = .remoteURL(remoteURL)
}
// 从本地图片创建
init(localImage: UIImage) {
self.type = .image
self.source = .localImage(localImage)
}
// 从本地视频URL创建
init(localVideoURL: URL) {
self.type = .video
self.source = .localVideoURL(localVideoURL)
}
// 从 YHSelectMediaItem 创建
init(from selectMediaItem: YHSelectMediaItem) {
self.type = selectMediaItem.type
switch selectMediaItem.type {
case .image:
if let image = selectMediaItem.image {
self.source = .localImage(image)
} else {
// 如果没有本地图片,使用占位符
self.source = .localImage(UIImage(named: "global_default_image") ?? UIImage())
}
case .video:
if let videoURL = selectMediaItem.videoURL {
self.source = .localVideoURL(videoURL)
} else {
// 异常情况,创建一个空的远程URL
self.source = .remoteURL("")
}
}
}
}
//
// YHSelectMediaItem.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import UIKit
class YHSelectMediaItem {
var name: String
var type: YHMediaType
var image: UIImage?
var videoURL: URL?
var duration: TimeInterval?
init(name: String = "", type: YHMediaType = .image, image: UIImage? = nil, videoURL: URL? = nil, duration: TimeInterval? = nil) {
self.name = name
self.type = type
self.image = image
self.videoURL = videoURL
self.duration = duration
}
}
enum YHMediaType {
case image
case video
}
//
// YHVideoCell.swift
// galaxy
//
// Created by alexzzw on 2025/9/27.
// Copyright © 2025 https://www.galaxy-immi.com. All rights reserved.
//
import Foundation
import UIKit
import AVFoundation
import JXPhotoBrowser
import SnapKit
class YHVideoCell: UIView, JXPhotoBrowserCell {
weak var photoBrowser: JXPhotoBrowser?
lazy var player = AVPlayer()
lazy var playerLayer = AVPlayerLayer(player: player)
// 播放状态图标
lazy var playImageView: UIImageView = {
let imageView = UIImageView()
let playImage = UIImage(named: "circle_play_icon")?.withRenderingMode(.alwaysOriginal)
imageView.image = playImage
return imageView
}()
// 进度滑块
lazy var progressSlider: UISlider = {
let slider = UISlider()
slider.minimumTrackTintColor = .white
slider.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3)
slider.thumbTintColor = .white
slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderTouchDown), for: .touchDown)
slider.addTarget(self, action: #selector(sliderTouchUp), for: .touchUpInside)
slider.addTarget(self, action: #selector(sliderTouchUp), for: .touchUpOutside)
return slider
}()
// 时间标签
lazy var timeLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 12)
label.textColor = .white
label.text = "00:00 / 00:00"
label.textAlignment = .right
return label
}()
private var timeObserver: Any?
private var isPlaying = false
private var isSeeking = false
static func generate(with browser: JXPhotoBrowser) -> Self {
let instance = Self.init(frame: .zero)
instance.photoBrowser = browser
return instance
}
required override init(frame: CGRect) {
super.init(frame: .zero)
setupUI()
setupGestures()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
removeTimeObserver()
NotificationCenter.default.removeObserver(self)
}
private func setupUI() {
backgroundColor = .black
layer.addSublayer(playerLayer)
addSubview(playImageView)
addSubview(progressSlider)
addSubview(timeLabel)
setupConstraints()
setupNotifications()
}
private func setupConstraints() {
playImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.width.height.equalTo(48)
}
progressSlider.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(16)
make.bottom.equalToSuperview().offset(-60)
make.height.equalTo(30)
}
timeLabel.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-16)
make.top.equalTo(progressSlider.snp.bottom).offset(8)
}
}
private func setupGestures() {
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tap)
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap))
doubleTap.numberOfTapsRequired = 2
addGestureRecognizer(doubleTap)
tap.require(toFail: doubleTap)
}
private func setupNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(playerDidFinishPlaying),
name: .AVPlayerItemDidPlayToEndTime,
object: nil
)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
// MARK: - Actions
@objc private func handleTap() {
// 点击整个页面播放或暂停
if isPlaying {
pauseVideo()
} else {
playVideo()
}
// 显示/隐藏控制器
toggleControlsVisibility()
}
@objc private func handleDoubleTap() {
photoBrowser?.dismiss()
}
@objc private func sliderValueChanged(_ sender: UISlider) {
guard let item = player.currentItem else { return }
let duration = CMTimeGetSeconds(item.duration)
if duration.isFinite && duration > 0 {
let targetTime = Double(sender.value) * duration
let currentTimeString = formatTime(targetTime)
let durationString = formatTime(duration)
timeLabel.text = "\(currentTimeString) / \(durationString)"
}
}
@objc private func sliderTouchDown() {
isSeeking = true
}
@objc private func sliderTouchUp() {
guard let item = player.currentItem else { return }
let duration = CMTimeGetSeconds(item.duration)
if duration.isFinite && duration > 0 {
let targetTime = Double(progressSlider.value) * duration
let seekTime = CMTime(seconds: targetTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player.seek(to: seekTime) { [weak self] _ in
self?.isSeeking = false
}
} else {
isSeeking = false
}
}
@objc private func playerDidFinishPlaying() {
isPlaying = false
playImageView.isHidden = false
player.seek(to: CMTime.zero)
progressSlider.value = 0
updatePlayButtonImage()
}
// MARK: - Video Control
func loadVideo(from url: URL) {
let playerItem = AVPlayerItem(url: url)
player.replaceCurrentItem(with: playerItem)
addTimeObserver()
updatePlayButtonImage()
}
func loadVideo(from urlString: String) {
guard let url = URL(string: urlString) else { return }
loadVideo(from: url)
}
func playVideo() {
player.play()
isPlaying = true
playImageView.isHidden = true
updatePlayButtonImage()
}
private func pauseVideo() {
player.pause()
isPlaying = false
playImageView.isHidden = false
updatePlayButtonImage()
}
func stopVideo() {
player.pause()
player.seek(to: CMTime.zero)
isPlaying = false
playImageView.isHidden = false
progressSlider.value = 0
updatePlayButtonImage()
}
private func updatePlayButtonImage() {
let imageName = isPlaying ? "circle_pause_icon" : "circle_play_icon"
let image = UIImage(named: imageName)?.withRenderingMode(.alwaysOriginal)
playImageView.image = image
}
private func toggleControlsVisibility() {
let isHidden = playImageView.alpha == 0
UIView.animate(withDuration: 0.3) {
self.playImageView.alpha = isHidden ? 1.0 : 0.0
self.progressSlider.alpha = isHidden ? 1.0 : 0.0
self.timeLabel.alpha = isHidden ? 1.0 : 0.0
} completion: { _ in
self.playImageView.isHidden = !isHidden && self.isPlaying
self.progressSlider.isHidden = !isHidden
self.timeLabel.isHidden = !isHidden
}
}
// MARK: - Time Observer
private func addTimeObserver() {
let timeInterval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = player.addPeriodicTimeObserver(forInterval: timeInterval, queue: .main) { [weak self] _ in
self?.updateProgress()
}
}
private func removeTimeObserver() {
if let timeObserver = timeObserver {
player.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
private func updateProgress() {
guard let item = player.currentItem, !isSeeking else { return }
let currentTime = CMTimeGetSeconds(player.currentTime())
let duration = CMTimeGetSeconds(item.duration)
if duration.isFinite && duration > 0 {
progressSlider.value = Float(currentTime / duration)
let currentTimeString = formatTime(currentTime)
let durationString = formatTime(duration)
timeLabel.text = "\(currentTimeString) / \(durationString)"
}
}
private func formatTime(_ time: Double) -> String {
if time.isInfinite || time.isNaN {
return "00:00"
}
let minutes = Int(time) / 60
let seconds = Int(time) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "circle_pause_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "circle_pause_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "circle_play_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "circle_play_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "circle_plus_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "circle_plus_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "media_brower_delete@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "media_brower_delete@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment