mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Adjusting layout and design, fixing fullscreen PiP and other tweaks
This commit is contained in:
@@ -21,7 +21,7 @@ public final class AnimatedCountView: UIView {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.foregroundGradientLayer.type = .radial
|
||||
self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
// self.foregroundGradientLayer.colors = [pink.cgColor, purple.cgColor, purple.cgColor]
|
||||
self.foregroundGradientLayer.locations = [0.0, 0.85, 1.0]
|
||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||
@@ -46,18 +46,34 @@ public final class AnimatedCountView: UIView {
|
||||
self.foregroundView.frame = CGRect(origin: CGPoint.zero, size: bounds.size)// .insetBy(dx: -40, dy: -40)
|
||||
self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60)
|
||||
self.maskingView.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
countLabel.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 8 : bounds.height - 12, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20)
|
||||
|
||||
let subtitleHeight: CGFloat = subtitleLabel.intrinsicContentSize.height// 18
|
||||
// let counterInset: CGFloat = 8
|
||||
// let counterBottomOffset: CGFloat = subtitleHeight + counterInset
|
||||
|
||||
countLabel.frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: bounds.height))
|
||||
subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - subtitleHeight / 2 : bounds.height - subtitleHeight, width: subtitleLabel.intrinsicContentSize.width + 20, height: subtitleHeight)
|
||||
// backgroundColor = .white.withAlphaComponent(0.3)
|
||||
// countLabel.backgroundColor = .red.withAlphaComponent(0.2)
|
||||
// subtitleLabel.backgroundColor = .blue.withAlphaComponent(0.2)
|
||||
}
|
||||
|
||||
func update(countString: String, subtitle: String, fontSize: CGFloat = 48.0) {
|
||||
func update(countString: String, subtitle: String, fontSize: CGFloat = 48.0, gradientColors: [CGColor] = [pink.cgColor, purple.cgColor, purple.cgColor]) {
|
||||
self.setupGradientAnimations()
|
||||
|
||||
let backgroundGradientColors: [CGColor]
|
||||
if gradientColors.count == 1 {
|
||||
backgroundGradientColors = [gradientColors[0], gradientColors[0]]
|
||||
} else {
|
||||
backgroundGradientColors = gradientColors
|
||||
}
|
||||
self.foregroundGradientLayer.colors = backgroundGradientColors
|
||||
|
||||
let text: String = countString
|
||||
self.countLabel.fontSize = fontSize
|
||||
self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: fontSize, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white)
|
||||
|
||||
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, attributes: [.font: UIFont.systemFont(ofSize: max(floor(fontSize / 3), 12), weight: .semibold)])
|
||||
self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, attributes: [.font: UIFont.systemFont(ofSize: max(floor((fontSize + 4.0) / 3.0), 12.0), weight: .semibold)])
|
||||
self.subtitleLabel.isHidden = subtitle.isEmpty
|
||||
}
|
||||
|
||||
@@ -152,7 +168,7 @@ class AnimatedCountLabel: UILabel {
|
||||
private let containerView = UIView()
|
||||
|
||||
var itemWidth: CGFloat { 36 * fontSize / 60 }
|
||||
var commaWidthForSpacing: CGFloat { 8 * fontSize / 60 }
|
||||
var commaWidthForSpacing: CGFloat { 12 * fontSize / 60 }
|
||||
var commaFrameWidth: CGFloat { 36 * fontSize / 60 }
|
||||
var interItemSpacing: CGFloat { 0 * fontSize / 60 }
|
||||
var didBegin = false
|
||||
@@ -180,9 +196,9 @@ class AnimatedCountLabel: UILabel {
|
||||
}
|
||||
if characters.count > index && characters[index].string == "," {
|
||||
if index > 0, ["1", "7"].contains(characters[index - 1].string) {
|
||||
offset -= commaWidthForSpacing * 0.7
|
||||
offset -= commaWidthForSpacing * 0.5
|
||||
} else {
|
||||
offset -= commaWidthForSpacing / 3
|
||||
offset -= commaWidthForSpacing / 6// 3
|
||||
}
|
||||
}
|
||||
return offset
|
||||
@@ -199,6 +215,7 @@ class AnimatedCountLabel: UILabel {
|
||||
let offset = offsetForChar(at: index)
|
||||
char.frame.origin.x = offset
|
||||
char.frame.origin.y = 0
|
||||
char.frame.size.height = containerView.bounds.height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
var isFullscreen: Bool = false
|
||||
var videoSize: CGSize?
|
||||
var prevFullscreenOrientation: UIDeviceOrientation?
|
||||
var didAutoDismissForPiP: Bool = false
|
||||
|
||||
private(set) var canManageCall: Bool = false
|
||||
// TODO: also handle pictureInPicturePossible
|
||||
@@ -169,11 +170,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
|
||||
var updated = false
|
||||
// TODO: remove debug timer
|
||||
// Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
|
||||
var shouldReplaceNoViewersWithOne: Bool { true }
|
||||
|
||||
strongSelf.infoThrottler.publish(shouldReplaceNoViewersWithOne ? max(members.totalCount, 1) : members.totalCount /*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in
|
||||
// let _ = members.totalCount
|
||||
let membersCount = Int.random(in: 0..<10000000) // members.totalCount
|
||||
strongSelf.infoThrottler.publish(shouldReplaceNoViewersWithOne ? max(membersCount, 1) : membersCount) { [weak strongSelf] latestCount in
|
||||
let _ = members.totalCount
|
||||
guard let strongSelf = strongSelf else { return }
|
||||
var updated = false
|
||||
let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: latestCount)
|
||||
@@ -185,7 +186,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
strongSelf.updated(transition: .immediate)
|
||||
}
|
||||
}
|
||||
// }.fire()
|
||||
}.fire()
|
||||
if state.canManageCall != strongSelf.canManageCall {
|
||||
strongSelf.canManageCall = state.canManageCall
|
||||
updated = true
|
||||
@@ -228,6 +229,8 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
|
||||
strongSelf.deactivatePictureInPictureIfVisible.invoke(Void())
|
||||
})
|
||||
} else {
|
||||
// MARK: TODO: fullscreen ui toggle
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -318,6 +321,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
return
|
||||
}
|
||||
if controller.view.window == nil {
|
||||
if state.didAutoDismissForPiP {
|
||||
state.updated(transition: .easeInOut(duration: 3))
|
||||
deactivatePictureInPicture.invoke(Void())
|
||||
// call.accountContext.sharedContext.mainWindow?.inCallNavigate?()
|
||||
}
|
||||
return
|
||||
}
|
||||
state.updated(transition: .easeInOut(duration: 3))
|
||||
@@ -349,10 +357,10 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
let videoHeight: CGFloat = forceFullScreenInLandscape
|
||||
? (context.availableSize.width - videoInset * 2) / 16 * 9
|
||||
: context.state.videoSize?.height ?? (min(context.availableSize.width, context.availableSize.height) - videoInset * 2) / 16 * 9
|
||||
let bottomPadding = 40 + environment.safeInsets.bottom
|
||||
let bottomPadding = 32.0 + environment.safeInsets.bottom
|
||||
let requiredSheetHeight: CGFloat = isFullscreen
|
||||
? context.availableSize.height
|
||||
: (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding)
|
||||
: (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding + 8)
|
||||
|
||||
let safeAreaTopInView: CGFloat
|
||||
if #available(iOS 16.0, *) {
|
||||
@@ -374,7 +382,8 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
// (controller() as? MediaStreamComponentController)?.prefersOnScreenNavigationHidden = isFullscreen
|
||||
// (controller() as? MediaStreamComponentController)?.window?.invalidatePrefersOnScreenNavigationHidden()
|
||||
let video = video.update(
|
||||
component: MediaStreamVideoComponent(
|
||||
call: context.component.call,
|
||||
@@ -432,12 +441,17 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
)))
|
||||
]
|
||||
)),
|
||||
action: {
|
||||
action: { [weak state] in
|
||||
guard let state, state.videoIsPlayable else { return }
|
||||
|
||||
activatePictureInPicture.invoke(Action {
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
return
|
||||
}
|
||||
controller.dismiss(closing: false, manual: true)
|
||||
if state.displayUI {
|
||||
state.toggleDisplayUI()
|
||||
}
|
||||
})
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)))))
|
||||
@@ -624,8 +638,8 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
let alertController = textAlertController(
|
||||
context: call.accountContext,
|
||||
forceTheme: defaultDarkPresentationTheme,
|
||||
title: nil,
|
||||
text: presentationData.strings.VoiceChat_StopRecordingTitle,
|
||||
title: presentationData.strings.LiveStream_EndConfirmationTitle,
|
||||
text: presentationData.strings.LiveStream_EndConfirmationText,
|
||||
actions: [
|
||||
TextAlertAction(
|
||||
type: .genericAction,
|
||||
@@ -633,8 +647,8 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
action: {}
|
||||
),
|
||||
TextAlertAction(
|
||||
type: .defaultAction,
|
||||
title: presentationData.strings.VoiceChat_StopRecordingStop,
|
||||
type: .destructiveAction,
|
||||
title: presentationData.strings.VoiceChat_EndConfirmationEnd,
|
||||
action: { [weak call] in
|
||||
guard let call = call else {
|
||||
return
|
||||
@@ -754,6 +768,9 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
return
|
||||
}
|
||||
controller.dismiss(closing: false, manual: true)
|
||||
if state.displayUI {
|
||||
state.toggleDisplayUI()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
guard let controller = controller() as? MediaStreamComponentController else {
|
||||
@@ -811,7 +828,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
sideInset: environment.safeInsets.left,
|
||||
leftItem: AnyComponent(Button(
|
||||
content: AnyComponent(RoundGradientButtonComponent(// BundleIconComponent(
|
||||
gradientColors: [UIColor(red: 0.18, green: 0.17, blue: 0.30, alpha: 1).cgColor, UIColor(red: 0.17, green: 0.16, blue: 0.30, alpha: 1).cgColor],
|
||||
gradientColors: [
|
||||
UIColor(red: 0.165, green: 0.173, blue: 0.357, alpha: 1).cgColor
|
||||
// UIColor(red: 0.18, green: 0.17, blue: 0.30, alpha: 1).cgColor,
|
||||
// UIColor(red: 0.17, green: 0.16, blue: 0.30, alpha: 1).cgColor
|
||||
],
|
||||
image: generateTintedImage(image: UIImage(bundleImageName: "Call/CallShareButton"), color: .white),
|
||||
// TODO: localize:
|
||||
title: "share")),
|
||||
@@ -824,7 +845,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
).minSize(CGSize(width: 65, height: 80))),
|
||||
rightItem: AnyComponent(Button(
|
||||
content: AnyComponent(RoundGradientButtonComponent(
|
||||
gradientColors: [UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor, UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor],
|
||||
gradientColors: [
|
||||
UIColor(red: 0.314, green: 0.161, blue: 0.197, alpha: 1).cgColor
|
||||
// UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor,
|
||||
// UIColor(red: 0.44, green: 0.18, blue: 0.22, alpha: 1).cgColor
|
||||
],
|
||||
image: generateImage(CGSize(width: 44.0 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in
|
||||
context.translateBy(x: size.width / 2, y: size.height / 2)
|
||||
context.scaleBy(x: 0.4, y: 0.4)
|
||||
@@ -853,7 +878,11 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
).minSize(CGSize(width: 44.0, height: 44.0))),
|
||||
centerItem: AnyComponent(Button(
|
||||
content: AnyComponent(RoundGradientButtonComponent(
|
||||
gradientColors: [UIColor(red: 0.23, green: 0.17, blue: 0.29, alpha: 1).cgColor, UIColor(red: 0.21, green: 0.16, blue: 0.29, alpha: 1).cgColor],
|
||||
gradientColors: [
|
||||
UIColor(red: 0.165, green: 0.173, blue: 0.357, alpha: 1).cgColor
|
||||
// UIColor(red: 0.23, green: 0.17, blue: 0.29, alpha: 1).cgColor,
|
||||
// UIColor(red: 0.21, green: 0.16, blue: 0.29, alpha: 1).cgColor
|
||||
],
|
||||
image: generateImage(CGSize(width: 44 * imageRenderScale, height: 44 * imageRenderScale), opaque: false, rotatedContext: { size, context in
|
||||
|
||||
let imageColor = UIColor.white
|
||||
@@ -930,7 +959,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
bottomButtonsRow: bottomComponent,
|
||||
topOffset: topOffset,
|
||||
sheetHeight: sheetHeight,
|
||||
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
|
||||
backgroundColor: (isFullscreen && !state.hasVideo) ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
|
||||
bottomPadding: bottomPadding,
|
||||
participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998)// [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()!
|
||||
isFullyExtended: isFullyDragged,
|
||||
@@ -944,22 +973,12 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let sheetOffset: CGFloat = context.availableSize.height - requiredSheetHeight + dragOffset
|
||||
let sheetPosition = sheetOffset + requiredSheetHeight / 2
|
||||
// let sheetOffset: CGFloat = context.availableSize.height - requiredSheetHeight + dragOffset
|
||||
// let sheetPosition = sheetOffset + requiredSheetHeight / 2
|
||||
// Sheet underneath the video when in modal sheet
|
||||
context.add(sheet
|
||||
.position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2))
|
||||
)
|
||||
let videoPos: CGFloat
|
||||
|
||||
if isFullscreen {
|
||||
videoPos = context.availableSize.height / 2 + dragOffset
|
||||
} else {
|
||||
videoPos = sheetPosition - requiredSheetHeight / 2 + videoHeight / 2 + 50 + 12
|
||||
}
|
||||
context.add(video
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos))
|
||||
)
|
||||
|
||||
//
|
||||
//
|
||||
@@ -1022,9 +1041,23 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let videoPos: CGFloat
|
||||
|
||||
if isFullscreen {
|
||||
videoPos = context.availableSize.height / 2 + dragOffset
|
||||
} else {
|
||||
videoPos = /*sheetPosition - requiredSheetHeight / 2*/topOffset + 28.0 + 28.0 + videoHeight / 2 // + 50 + 12
|
||||
}
|
||||
context.add(video
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos))
|
||||
)
|
||||
|
||||
context.add(topItem
|
||||
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 32)))
|
||||
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 28.0)))
|
||||
.opacity((!isFullscreen || state.displayUI) ? 1 : 0)
|
||||
.gesture(.pan { panState in
|
||||
onPanGesture(panState)
|
||||
})
|
||||
// .animation(key: "position")
|
||||
)
|
||||
|
||||
@@ -1043,7 +1076,7 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
//
|
||||
//
|
||||
} else {
|
||||
let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
|
||||
/*let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
sideInset: environment.safeInsets.left,
|
||||
leftItem: AnyComponent(Button(
|
||||
@@ -1104,9 +1137,18 @@ public final class MediaStreamComponent: CombinedComponent {
|
||||
|
||||
context.add(video
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset)
|
||||
))
|
||||
))*/
|
||||
}
|
||||
|
||||
// TODO: add variable isPictureInPictureActive
|
||||
// let isPictureInPictureActive = state.isPictureInPictureSupported && state.videoIsPlayable && state.hasVideo
|
||||
// if !state.isVisibleInHierarchy && isPictureInPictureActive && state.isFullscreen {
|
||||
// if !state.didAutoDismissForPiP {
|
||||
// state.didAutoDismissForPiP = true
|
||||
// (controller() as? MediaStreamComponentController)?.dismiss(closing: false, manual: true)
|
||||
// }
|
||||
// } else {
|
||||
// state.didAutoDismissForPiP = false
|
||||
// }
|
||||
return context.availableSize
|
||||
}
|
||||
}
|
||||
@@ -1155,7 +1197,7 @@ public final class MediaStreamComponentController: ViewControllerComponentContai
|
||||
|
||||
self.view.clipsToBounds = true
|
||||
|
||||
self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
self.view.layer.animatePosition(from: CGPoint(x: self.view.frame.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), to: self.view.center, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
|
||||
})
|
||||
|
||||
self.view.layer.allowsGroupOpacity = true
|
||||
@@ -1376,19 +1418,35 @@ final class StreamTitleComponent: Component {
|
||||
private let stalledAnimatedGradient = CAGradientLayer()
|
||||
private var wasLive = false
|
||||
|
||||
var desiredWidth: CGFloat { label.intrinsicContentSize.width + 6.0 + 6.0 }
|
||||
|
||||
override init(frame: CGRect = .zero) {
|
||||
super.init(frame: frame)
|
||||
|
||||
addSubview(label)
|
||||
label.text = "LIVE"
|
||||
label.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
label.textAlignment = .center
|
||||
label.textColor = .white
|
||||
let liveString = NSAttributedString(
|
||||
string: "LIVE",
|
||||
attributes: [
|
||||
.font: Font.with(size: 11.0, design: .round, weight: .bold),
|
||||
.paragraphStyle: {
|
||||
let style = NSMutableParagraphStyle()
|
||||
style.alignment = .center
|
||||
return style
|
||||
}(),
|
||||
.foregroundColor: UIColor.white,
|
||||
.kern: -0.6
|
||||
]
|
||||
)
|
||||
label.attributedText = liveString
|
||||
// label.text = "LIVE"
|
||||
// label.font = Font.with(size: 11.0, design: .round, weight: .bold)// .systemFont(ofSize: 12, weight: .semibold)
|
||||
// label.textAlignment = .center
|
||||
// label.textColor = .white
|
||||
layer.addSublayer(stalledAnimatedGradient)
|
||||
self.clipsToBounds = true
|
||||
if #available(iOS 13.0, *) {
|
||||
self.layer.cornerCurve = .continuous
|
||||
}
|
||||
// if #available(iOS 13.0, *) {
|
||||
// self.layer.cornerCurve = .continuous
|
||||
// }
|
||||
toggle(isLive: false)
|
||||
}
|
||||
|
||||
@@ -1549,7 +1607,9 @@ final class StreamTitleComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
let liveIndicatorWidth: CGFloat = 40
|
||||
let liveIndicatorWidth: CGFloat = self.liveIndicatorView.desiredWidth
|
||||
let liveIndicatorHeight: CGFloat = 20.0
|
||||
|
||||
let currentText = self.titleLabel.text
|
||||
if currentText != component.text {
|
||||
if currentText?.isEmpty == false {
|
||||
@@ -1605,7 +1665,7 @@ final class StreamTitleComponent: Component {
|
||||
self.updateTitleFadeLayer(textFrame: textFrame)
|
||||
}
|
||||
|
||||
liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: /*floorToScreenPixels((size.height - textSize.height) / 2.0 - 2) + 1.0*/textFrame.midY - 22 / 2), size: .init(width: 40, height: 22))
|
||||
liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: /*floorToScreenPixels((size.height - textSize.height) / 2.0 - 2) + 1.0*/textFrame.midY - liveIndicatorHeight / 2), size: .init(width: liveIndicatorWidth, height: liveIndicatorHeight))
|
||||
self.liveIndicatorView.toggle(isLive: component.isActive)
|
||||
|
||||
if let indicatorView = self.indicatorView, let image = indicatorView.image {
|
||||
@@ -1786,18 +1846,19 @@ private final class OriginInfoComponent: CombinedComponent {
|
||||
component: ParticipantsComponent(
|
||||
count: context.component.participantsCount,
|
||||
showsSubtitle: true,
|
||||
fontSize: 18.0
|
||||
fontSize: 18.0,
|
||||
gradientColors: [UIColor.white.cgColor]
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
var size = CGSize(width: viewerCounter.size.width, height: viewerCounter.size.height)
|
||||
let heightReduction: CGFloat = 16.0
|
||||
var size = CGSize(width: viewerCounter.size.width, height: viewerCounter.size.height - heightReduction)
|
||||
size.width = min(size.width, context.availableSize.width)
|
||||
size.height = min(size.height, context.availableSize.height)
|
||||
|
||||
context.add(viewerCounter
|
||||
.position(CGPoint(x: size.width / 2.0, y: (context.availableSize.height - viewerCounter.size.height) / 2.0))
|
||||
.position(CGPoint(x: size.width / 2.0, y: /*(context.availableSize.height - viewerCounter.size.height)*/context.availableSize.height / 2.0 + 16.0 - heightReduction / 2))
|
||||
)
|
||||
|
||||
return size
|
||||
@@ -2007,7 +2068,7 @@ private final class ButtonsRowComponent: CombinedComponent {
|
||||
|
||||
return { context in
|
||||
var availableWidth = context.availableSize.width
|
||||
let sideInset: CGFloat = 40 + context.component.sideInset
|
||||
let sideInset: CGFloat = 48.0 + context.component.sideInset
|
||||
|
||||
let contentHeight: CGFloat = 80 // 44
|
||||
let size = CGSize(width: context.availableSize.width, height: contentHeight + context.component.bottomInset)
|
||||
@@ -2126,7 +2187,7 @@ final class RoundGradientButtonComponent: Component {
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
titleLabel.invalidateIntrinsicContentSize()
|
||||
let heightForIcon = bounds.height - max(titleLabel.intrinsicContentSize.height, 12) - 6
|
||||
let heightForIcon = bounds.height - max(round(titleLabel.intrinsicContentSize.height), 12) - 8.0
|
||||
iconView.frame = .init(x: bounds.midX - heightForIcon / 2, y: 0, width: heightForIcon, height: heightForIcon)
|
||||
gradientLayer.masksToBounds = true
|
||||
gradientLayer.cornerRadius = min(iconView.frame.width, iconView.frame.height) / 2
|
||||
@@ -2141,6 +2202,12 @@ final class RoundGradientButtonComponent: Component {
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
view.iconView.image = image ?? icon.flatMap { UIImage(bundleImageName: $0) }
|
||||
let gradientColors: [CGColor]
|
||||
if self.gradientColors.count == 1 {
|
||||
gradientColors = [self.gradientColors[0], self.gradientColors[0]]
|
||||
} else {
|
||||
gradientColors = self.gradientColors
|
||||
}
|
||||
view.gradientLayer.colors = gradientColors
|
||||
view.titleLabel.text = title
|
||||
view.setNeedsLayout()
|
||||
|
||||
@@ -131,7 +131,7 @@ final class MediaStreamVideoComponent: Component {
|
||||
private var videoStalled = false {
|
||||
didSet {
|
||||
if videoStalled != oldValue {
|
||||
self.updateVideoStalled(isStalled: self.videoStalled)
|
||||
self.updateVideoStalled(isStalled: self.videoStalled, transition: nil)
|
||||
// state?.updated()
|
||||
}
|
||||
}
|
||||
@@ -181,14 +181,17 @@ final class MediaStreamVideoComponent: Component {
|
||||
return false
|
||||
}
|
||||
|
||||
var didPassExpandFromPiP = false
|
||||
|
||||
func expandFromPictureInPicture() {
|
||||
didPassExpandFromPiP = true
|
||||
if let pictureInPictureController = self.pictureInPictureController, pictureInPictureController.isPictureInPictureActive {
|
||||
self.requestedExpansion = true
|
||||
self.pictureInPictureController?.stopPictureInPicture()
|
||||
}
|
||||
}
|
||||
private var isAnimating = false
|
||||
private func updateVideoStalled(isStalled: Bool) {
|
||||
private func updateVideoStalled(isStalled: Bool, transition: Transition?) {
|
||||
if isStalled {
|
||||
guard let component = self.component else { return }
|
||||
|
||||
@@ -229,14 +232,30 @@ final class MediaStreamVideoComponent: Component {
|
||||
shimmerBorderLayer.cornerRadius = cornerRadius
|
||||
shimmerBorderLayer.masksToBounds = true
|
||||
shimmerBorderLayer.compositingFilter = "softLightBlendMode"
|
||||
shimmerBorderLayer.frame = loadingBlurView.bounds
|
||||
|
||||
|
||||
|
||||
let borderMask = CAShapeLayer()
|
||||
borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
|
||||
|
||||
shimmerBorderLayer.mask = borderMask
|
||||
|
||||
if let transition, shimmerBorderLayer.mask != nil {
|
||||
let initialPath = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
|
||||
borderMask.path = initialPath
|
||||
transition.setFrame(layer: shimmerBorderLayer, frame: loadingBlurView.bounds)
|
||||
|
||||
let borderMaskPath = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
|
||||
transition.setShapeLayerPath(layer: borderMask, path: borderMaskPath)
|
||||
} else {
|
||||
shimmerBorderLayer.frame = loadingBlurView.bounds
|
||||
let borderMaskPath = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil)
|
||||
borderMask.path = borderMaskPath
|
||||
}
|
||||
|
||||
borderMask.fillColor = UIColor.white.withAlphaComponent(0.4).cgColor
|
||||
borderMask.strokeColor = UIColor.white.withAlphaComponent(0.7).cgColor
|
||||
borderMask.lineWidth = 3
|
||||
borderMask.compositingFilter = "softLightBlendMode"
|
||||
shimmerBorderLayer.mask = borderMask
|
||||
|
||||
borderShimmer = StandaloneShimmerEffect()
|
||||
borderShimmer.layer = shimmerBorderLayer
|
||||
@@ -281,9 +300,9 @@ final class MediaStreamVideoComponent: Component {
|
||||
}
|
||||
|
||||
if !component.hasVideo || component.videoLoading || self.videoStalled {
|
||||
updateVideoStalled(isStalled: true)
|
||||
updateVideoStalled(isStalled: true, transition: transition)
|
||||
} else {
|
||||
updateVideoStalled(isStalled: false)
|
||||
updateVideoStalled(isStalled: false, transition: transition)
|
||||
}
|
||||
|
||||
if component.hasVideo, self.videoView == nil {
|
||||
@@ -543,7 +562,14 @@ final class MediaStreamVideoComponent: Component {
|
||||
}
|
||||
|
||||
let loadingBlurViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
videoFrameUpdateTransition.setFrame(view: loadingBlurView, frame: loadingBlurViewFrame)
|
||||
// UIView.animate(withDuration: 0.5) {
|
||||
// self.loadingBlurView.frame = loadingBlurViewFrame
|
||||
// }
|
||||
if loadingBlurView.frame == .zero {
|
||||
loadingBlurView.frame = loadingBlurViewFrame
|
||||
} else {
|
||||
transition.setFrame(view: loadingBlurView, frame: loadingBlurViewFrame)
|
||||
}
|
||||
// loadingBlurView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
|
||||
|
||||
videoFrameUpdateTransition.setCornerRadius(layer: loadingBlurView.layer, cornerRadius: videoCornerRadius)
|
||||
@@ -559,17 +585,28 @@ final class MediaStreamVideoComponent: Component {
|
||||
// $0.frame = placeholderView.bounds
|
||||
}
|
||||
|
||||
let initialShimmerBounds = shimmerBorderLayer.bounds
|
||||
videoFrameUpdateTransition.setFrame(layer: shimmerBorderLayer, frame: loadingBlurView.bounds)
|
||||
// shimmerBorderLayer.frame = loadingBlurView.bounds
|
||||
|
||||
let borderMask = CAShapeLayer()
|
||||
borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: videoCornerRadius, cornerHeight: videoCornerRadius, transform: nil)
|
||||
let initialPath = CGPath(roundedRect: .init(x: 0, y: 0, width: initialShimmerBounds.width, height: initialShimmerBounds.height), cornerWidth: videoCornerRadius, cornerHeight: videoCornerRadius, transform: nil)
|
||||
borderMask.path = initialPath
|
||||
// borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: videoCornerRadius, cornerHeight: videoCornerRadius, transform: nil)
|
||||
videoFrameUpdateTransition.setShapeLayerPath(layer: borderMask, path: CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: videoCornerRadius, cornerHeight: videoCornerRadius, transform: nil))
|
||||
|
||||
borderMask.fillColor = UIColor.white.withAlphaComponent(0.4).cgColor
|
||||
borderMask.strokeColor = UIColor.white.withAlphaComponent(0.7).cgColor
|
||||
borderMask.lineWidth = 3
|
||||
shimmerBorderLayer.mask = borderMask
|
||||
shimmerBorderLayer.cornerRadius = videoCornerRadius
|
||||
|
||||
// if component.isAdmin {
|
||||
// shimmerBorderLayer.isHidden = true
|
||||
// } else {
|
||||
// shimmerBorderLayer.isHidden = false
|
||||
// }
|
||||
//
|
||||
if !self.hadVideo {
|
||||
|
||||
if self.noSignalTimer == nil {
|
||||
@@ -623,7 +660,7 @@ final class MediaStreamVideoComponent: Component {
|
||||
guard let strongSelf = self, let pictureInPictureController = strongSelf.pictureInPictureController else {
|
||||
return
|
||||
}
|
||||
|
||||
print("[pip] started")
|
||||
pictureInPictureController.startPictureInPicture()
|
||||
|
||||
completion(Void())
|
||||
@@ -701,25 +738,27 @@ final class MediaStreamVideoComponent: Component {
|
||||
completionHandler(false)
|
||||
return
|
||||
}
|
||||
|
||||
didRequestBringBack = true
|
||||
component.bringBackControllerForPictureInPictureDeactivation {
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
|
||||
var didRequestBringBack = false
|
||||
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
self.didRequestBringBack = false
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
|
||||
if self.requestedExpansion {
|
||||
self.requestedExpansion = false
|
||||
} else {
|
||||
} else if !didRequestBringBack {
|
||||
self.component?.pictureInPictureClosed()
|
||||
}
|
||||
didRequestBringBack = false
|
||||
// TODO: extract precise animation timing or observe window changes
|
||||
// Handle minimized case separatelly (can we detect minimized?)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||
self.videoView?.alpha = 1
|
||||
}
|
||||
UIView.animate(withDuration: 0.3) { [self] in
|
||||
|
||||
@@ -168,7 +168,7 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
let background = background.update(
|
||||
component: SheetBackgroundComponent(
|
||||
color: context.component.backgroundColor,
|
||||
radius: context.component.isFullyExtended ? context.component.deviceCornerRadius : 16,
|
||||
radius: context.component.isFullyExtended ? context.component.deviceCornerRadius : 10.0,
|
||||
offset: backgroundExtraOffset
|
||||
),
|
||||
availableSize: CGSize(width: size.width, height: context.component.sheetHeight),
|
||||
@@ -184,7 +184,7 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
let viewerCounter = viewerCounter.update(
|
||||
component: ParticipantsComponent(count: context.component.participantsCount),
|
||||
component: ParticipantsComponent(count: context.component.participantsCount, fontSize: 44.0),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: 70),
|
||||
transition: context.transition
|
||||
)
|
||||
@@ -208,17 +208,18 @@ final class StreamSheetComponent: CombinedComponent {
|
||||
|
||||
if let topItem = topItem {
|
||||
context.add(topItem
|
||||
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 32)))
|
||||
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 28)))
|
||||
)
|
||||
(context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height))
|
||||
}
|
||||
let videoHeight = context.component.videoHeight
|
||||
let sheetHeight = context.component.sheetHeight
|
||||
let animatedParticipantsVisible = context.component.participantsCount != -1
|
||||
let animatedParticipantsVisible = !isFullscreen// context.component.participantsCount != -1
|
||||
if true {
|
||||
context.add(viewerCounter
|
||||
.position(CGPoint(x: context.availableSize.width / 2, y: topOffset + 50 + videoHeight + (sheetHeight - 69 - videoHeight - 50 - context.component.bottomPadding) / 2 - 12))
|
||||
.position(CGPoint(x: context.availableSize.width / 2, y: topOffset + 50 + videoHeight + (sheetHeight - 69 - videoHeight - 50 - context.component.bottomPadding) / 2 - 10))
|
||||
.opacity(animatedParticipantsVisible ? 1 : 0)
|
||||
// .animation(key: "position")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -259,18 +260,24 @@ final class SheetBackgroundComponent: Component {
|
||||
let extraBottom: CGFloat = 500
|
||||
|
||||
if backgroundView.backgroundColor != color && backgroundView.backgroundColor != nil {
|
||||
UIView.animate(withDuration: 0.4) { [self] in
|
||||
backgroundView.backgroundColor = color
|
||||
// TODO: determine if animation is needed (with logic, not color)
|
||||
backgroundView.frame = .init(origin: .init(x: 0, y: offset), size: .init(width: availableSize.width, height: availableSize.height + extraBottom))
|
||||
if transition.animation.isImmediate {
|
||||
UIView.animate(withDuration: 0.4) { [self] in
|
||||
backgroundView.backgroundColor = color
|
||||
// TODO: determine if animation is needed (with logic, not color)
|
||||
backgroundView.frame = .init(origin: .init(x: 0, y: offset), size: .init(width: availableSize.width, height: availableSize.height + extraBottom))
|
||||
}
|
||||
|
||||
let anim = CABasicAnimation(keyPath: "cornerRadius")
|
||||
anim.fromValue = backgroundView.layer.cornerRadius
|
||||
backgroundView.layer.cornerRadius = cornerRadius
|
||||
anim.toValue = cornerRadius
|
||||
anim.duration = 0.4
|
||||
backgroundView.layer.add(anim, forKey: "cornerRadius")
|
||||
} else {
|
||||
transition.setBackgroundColor(view: backgroundView, color: color)
|
||||
transition.setFrame(view: backgroundView, frame: CGRect(origin: .init(x: 0, y: offset), size: .init(width: availableSize.width, height: availableSize.height + extraBottom)))
|
||||
transition.setCornerRadius(layer: backgroundView.layer, cornerRadius: cornerRadius)
|
||||
}
|
||||
|
||||
let anim = CABasicAnimation(keyPath: "cornerRadius")
|
||||
anim.fromValue = backgroundView.layer.cornerRadius
|
||||
backgroundView.layer.cornerRadius = cornerRadius
|
||||
anim.toValue = cornerRadius
|
||||
anim.duration = 0.4
|
||||
backgroundView.layer.add(anim, forKey: "cornerRadius")
|
||||
} else {
|
||||
backgroundView.backgroundColor = color
|
||||
backgroundView.frame = .init(origin: .init(x: 0, y: offset), size: .init(width: availableSize.width, height: availableSize.height + extraBottom))
|
||||
@@ -325,7 +332,8 @@ final class ParticipantsComponent: Component {
|
||||
view.counter.update(
|
||||
countString: self.count > 0 ? presentationStringsFormattedNumber(Int32(count), ",") : "",
|
||||
subtitle: self.showsSubtitle ? (self.count > 0 ? "watching" : "no viewers") : "",
|
||||
fontSize: self.fontSize
|
||||
fontSize: self.fontSize,
|
||||
gradientColors: self.gradientColors
|
||||
)// environment.strings.LiveStream_NoViewers)
|
||||
return availableSize
|
||||
}
|
||||
@@ -333,11 +341,13 @@ final class ParticipantsComponent: Component {
|
||||
private let count: Int
|
||||
private let showsSubtitle: Bool
|
||||
private let fontSize: CGFloat
|
||||
private let gradientColors: [CGColor]
|
||||
|
||||
init(count: Int, showsSubtitle: Bool = true, fontSize: CGFloat = 48) {
|
||||
init(count: Int, showsSubtitle: Bool = true, fontSize: CGFloat = 48.0, gradientColors: [CGColor] = [pink.cgColor, purple.cgColor, purple.cgColor]) {
|
||||
self.count = count
|
||||
self.showsSubtitle = showsSubtitle
|
||||
self.fontSize = fontSize
|
||||
self.gradientColors = gradientColors
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
|
||||
Reference in New Issue
Block a user