mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Initial in-app browser implementation
This commit is contained in:
368
submodules/BrowserUI/Sources/BrowserToolbarComponent.swift
Normal file
368
submodules/BrowserUI/Sources/BrowserToolbarComponent.swift
Normal file
@@ -0,0 +1,368 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import BlurredBackgroundComponent
|
||||
import BundleIconComponent
|
||||
import TelegramPresentationData
|
||||
|
||||
final class BrowserToolbarComponent: CombinedComponent {
|
||||
let backgroundColor: UIColor
|
||||
let separatorColor: UIColor
|
||||
let textColor: UIColor
|
||||
let bottomInset: CGFloat
|
||||
let sideInset: CGFloat
|
||||
let item: AnyComponentWithIdentity<Empty>?
|
||||
let collapseFraction: CGFloat
|
||||
|
||||
init(
|
||||
backgroundColor: UIColor,
|
||||
separatorColor: UIColor,
|
||||
textColor: UIColor,
|
||||
bottomInset: CGFloat,
|
||||
sideInset: CGFloat,
|
||||
item: AnyComponentWithIdentity<Empty>?,
|
||||
collapseFraction: CGFloat
|
||||
) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
self.textColor = textColor
|
||||
self.bottomInset = bottomInset
|
||||
self.sideInset = sideInset
|
||||
self.item = item
|
||||
self.collapseFraction = collapseFraction
|
||||
}
|
||||
|
||||
static func ==(lhs: BrowserToolbarComponent, rhs: BrowserToolbarComponent) -> Bool {
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.separatorColor != rhs.separatorColor {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.bottomInset != rhs.bottomInset {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.item != rhs.item {
|
||||
return false
|
||||
}
|
||||
if lhs.collapseFraction != rhs.collapseFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let background = Child(BlurredBackgroundComponent.self)
|
||||
let separator = Child(Rectangle.self)
|
||||
let centerItems = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self)
|
||||
|
||||
return { context in
|
||||
let contentHeight: CGFloat = 49.0
|
||||
let totalHeight = contentHeight + context.component.bottomInset
|
||||
let offset = context.component.collapseFraction * totalHeight
|
||||
let size = CGSize(width: context.availableSize.width, height: totalHeight)
|
||||
|
||||
let background = background.update(
|
||||
component: BlurredBackgroundComponent(color: context.component.backgroundColor),
|
||||
availableSize: CGSize(width: size.width, height: size.height),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let separator = separator.update(
|
||||
component: Rectangle(color: context.component.separatorColor, height: UIScreenPixel),
|
||||
availableSize: CGSize(width: size.width, height: size.height),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let item = context.component.item.flatMap { item in
|
||||
return centerItems[item.id].update(
|
||||
component: item.component,
|
||||
availableSize: CGSize(width: context.availableSize.width - context.component.sideInset * 2.0, height: contentHeight),
|
||||
transition: context.transition
|
||||
)
|
||||
}
|
||||
|
||||
context.add(background
|
||||
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0 + offset))
|
||||
)
|
||||
|
||||
context.add(separator
|
||||
.position(CGPoint(x: size.width / 2.0, y: 0.0 + offset))
|
||||
)
|
||||
|
||||
if let centerItem = item {
|
||||
context.add(centerItem
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight / 2.0 + offset))
|
||||
.appear(Transition.Appear({ _, view, transition in
|
||||
transition.animatePosition(view: view, from: CGPoint(x: 0.0, y: size.height), to: .zero, additive: true)
|
||||
}))
|
||||
.disappear(Transition.Disappear({ view, transition, completion in
|
||||
transition.animatePosition(view: view, from: .zero, to: CGPoint(x: 0.0, y: size.height), additive: true, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class NavigationToolbarContentComponent: CombinedComponent {
|
||||
let textColor: UIColor
|
||||
let canGoBack: Bool
|
||||
let canGoForward: Bool
|
||||
let performAction: ActionSlot<BrowserScreen.Action>
|
||||
|
||||
init(
|
||||
textColor: UIColor,
|
||||
canGoBack: Bool,
|
||||
canGoForward: Bool,
|
||||
performAction: ActionSlot<BrowserScreen.Action>
|
||||
) {
|
||||
self.textColor = textColor
|
||||
self.canGoBack = canGoBack
|
||||
self.canGoForward = canGoForward
|
||||
self.performAction = performAction
|
||||
}
|
||||
|
||||
static func ==(lhs: NavigationToolbarContentComponent, rhs: NavigationToolbarContentComponent) -> Bool {
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.canGoBack != rhs.canGoBack {
|
||||
return false
|
||||
}
|
||||
if lhs.canGoForward != rhs.canGoForward {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let back = Child(Button.self)
|
||||
let forward = Child(Button.self)
|
||||
let share = Child(Button.self)
|
||||
let openIn = Child(Button.self)
|
||||
|
||||
return { context in
|
||||
let availableSize = context.availableSize
|
||||
let performAction = context.component.performAction
|
||||
|
||||
let sideInset: CGFloat = 5.0
|
||||
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
|
||||
let spacing = (availableSize.width - buttonSize.width * 4.0 - sideInset * 2.0) / 3.0
|
||||
|
||||
let back = back.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Back",
|
||||
tintColor: context.component.textColor
|
||||
)
|
||||
),
|
||||
isEnabled: context.component.canGoBack,
|
||||
action: {
|
||||
performAction.invoke(.navigateBack)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(back
|
||||
.position(CGPoint(x: sideInset + back.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let forward = forward.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Instant View/Forward",
|
||||
tintColor: context.component.textColor
|
||||
)
|
||||
),
|
||||
isEnabled: context.component.canGoForward,
|
||||
action: {
|
||||
performAction.invoke(.navigateForward)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(forward
|
||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let share = share.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat List/NavigationShare",
|
||||
tintColor: context.component.textColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.share)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(share
|
||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let openIn = openIn.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat/Context Menu/Browser",
|
||||
tintColor: context.component.textColor
|
||||
)
|
||||
),
|
||||
action: {
|
||||
performAction.invoke(.openIn)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(openIn
|
||||
.position(CGPoint(x: sideInset + back.size.width + spacing + forward.size.width + spacing + share.size.width + spacing + openIn.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class SearchToolbarContentComponent: CombinedComponent {
|
||||
let strings: PresentationStrings
|
||||
let textColor: UIColor
|
||||
let index: Int
|
||||
let count: Int
|
||||
let isEmpty: Bool
|
||||
let performAction: ActionSlot<BrowserScreen.Action>
|
||||
|
||||
init(
|
||||
strings: PresentationStrings,
|
||||
textColor: UIColor,
|
||||
index: Int,
|
||||
count: Int,
|
||||
isEmpty: Bool,
|
||||
performAction: ActionSlot<BrowserScreen.Action>
|
||||
) {
|
||||
self.strings = strings
|
||||
self.textColor = textColor
|
||||
self.index = index
|
||||
self.count = count
|
||||
self.isEmpty = isEmpty
|
||||
self.performAction = performAction
|
||||
}
|
||||
|
||||
static func ==(lhs: SearchToolbarContentComponent, rhs: SearchToolbarContentComponent) -> Bool {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
if lhs.index != rhs.index {
|
||||
return false
|
||||
}
|
||||
if lhs.count != rhs.count {
|
||||
return false
|
||||
}
|
||||
if lhs.isEmpty != rhs.isEmpty {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let down = Child(Button.self)
|
||||
let up = Child(Button.self)
|
||||
let text = Child(Text.self)
|
||||
|
||||
return { context in
|
||||
let availableSize = context.availableSize
|
||||
let performAction = context.component.performAction
|
||||
|
||||
let sideInset: CGFloat = 3.0
|
||||
let buttonSize = CGSize(width: 50.0, height: availableSize.height)
|
||||
|
||||
let down = down.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat/Input/Search/DownButton",
|
||||
tintColor: context.component.textColor
|
||||
)
|
||||
),
|
||||
isEnabled: context.component.count > 0,
|
||||
action: {
|
||||
performAction.invoke(.scrollToNextSearchResult)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(down
|
||||
.position(CGPoint(x: availableSize.width - sideInset - down.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let up = up.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Chat/Input/Search/UpButton",
|
||||
tintColor: context.component.textColor
|
||||
)
|
||||
),
|
||||
isEnabled: context.component.count > 0,
|
||||
action: {
|
||||
performAction.invoke(.scrollToPreviousSearchResult)
|
||||
}
|
||||
).minSize(buttonSize),
|
||||
availableSize: buttonSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(up
|
||||
.position(CGPoint(x: availableSize.width - sideInset - down.size.width + 7.0 - up.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
let currentText: String
|
||||
if context.component.isEmpty {
|
||||
currentText = ""
|
||||
} else if context.component.count == 0 {
|
||||
currentText = context.component.strings.Conversation_SearchNoResults
|
||||
} else {
|
||||
currentText = context.component.strings.Items_NOfM("\(context.component.index + 1)", "\(context.component.count)").string
|
||||
}
|
||||
|
||||
let text = text.update(
|
||||
component: Text(
|
||||
text: currentText,
|
||||
font: Font.regular(15.0),
|
||||
color: context.component.textColor
|
||||
),
|
||||
availableSize: availableSize,
|
||||
transition: .easeInOut(duration: 0.2)
|
||||
)
|
||||
context.add(text
|
||||
.position(CGPoint(x: availableSize.width - sideInset - down.size.width - up.size.width - text.size.width / 2.0, y: availableSize.height / 2.0))
|
||||
)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user