mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
[WIP] iOS 14 widget
This commit is contained in:
parent
bba1d2f80b
commit
517fcabed9
@ -2185,6 +2185,8 @@ Unused sets are archived when you add more.";
|
||||
|
||||
"Widget.AuthRequired" = "Log in to Telegram";
|
||||
"Widget.NoUsers" = "Start messaging to see your friends here";
|
||||
"Widget.GalleryTitle" = "Telegram";
|
||||
"Widget.GalleryDescription" = "See your friends here";
|
||||
|
||||
"ShareMenu.CopyShareLinkGame" = "Copy link to game";
|
||||
|
||||
|
@ -94,6 +94,14 @@ private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId:
|
||||
|
||||
private let avatarSize = CGSize(width: 50.0, height: 50.0)
|
||||
|
||||
func avatarImage(accountPeerId: Int64, peer: WidgetDataPeer, size: CGSize) -> UIImage {
|
||||
if let path = peer.avatarPath, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) {
|
||||
return roundImage
|
||||
} else {
|
||||
return avatarViewLettersImage(size: size, peerId: peer.id, accountPeerId: accountPeerId, letters: peer.letters)!
|
||||
}
|
||||
}
|
||||
|
||||
private final class AvatarView: UIImageView {
|
||||
init(accountPeerId: Int64, peer: WidgetDataPeer, size: CGSize) {
|
||||
super.init(frame: CGRect())
|
||||
|
@ -51,6 +51,7 @@ struct Static_WidgetEntryView: View {
|
||||
|
||||
enum PeersWidgetData {
|
||||
case placeholder
|
||||
case data(WidgetData)
|
||||
}
|
||||
|
||||
extension PeersWidgetData {
|
||||
@ -59,21 +60,39 @@ extension PeersWidgetData {
|
||||
|
||||
struct WidgetView: View {
|
||||
let data: PeersWidgetData
|
||||
@Environment(\.widgetFamily) var widgetFamily
|
||||
|
||||
func peerViews(geometry: GeometryProxy) -> some View {
|
||||
print("geometry: \(geometry.safeAreaInsets) frame \(geometry.frame(in: .local))")
|
||||
|
||||
func peerViews(geometry: GeometryProxy) -> AnyView {
|
||||
let defaultItemSize: CGFloat = 60.0
|
||||
let defaultPaddingFraction: CGFloat = 0.36
|
||||
|
||||
let rowCount = Int(round(geometry.size.width / defaultItemSize))
|
||||
let itemSize = floor(geometry.size.width / CGFloat(rowCount))
|
||||
let rowCount = Int(round(geometry.size.width / (defaultItemSize * (1.0 + defaultPaddingFraction))))
|
||||
let itemSize = floor(geometry.size.width / (CGFloat(rowCount) + defaultPaddingFraction * CGFloat(rowCount - 1)))
|
||||
|
||||
let firstRowY = itemSize / 2.0
|
||||
let secondRowY = itemSize / 2.0 + geometry.size.height - itemSize
|
||||
|
||||
switch data {
|
||||
case .placeholder:
|
||||
return ZStack {
|
||||
ForEach(0 ..< rowCount, content: { i in
|
||||
Circle().frame(width: itemSize, height: itemSize).position(x: CGFloat(i) * itemSize, y: 0.0).foregroundColor(.gray)
|
||||
return AnyView(ZStack {
|
||||
ForEach(0 ..< rowCount * 2, content: { i in
|
||||
return Circle().frame(width: itemSize, height: itemSize).position(x: itemSize / 2.0 + floor(CGFloat(i % rowCount) * itemSize * (1.0 + defaultPaddingFraction)), y: i / rowCount == 0 ? firstRowY : secondRowY).foregroundColor(.gray)
|
||||
})
|
||||
})
|
||||
case let .data(data):
|
||||
switch data {
|
||||
case let .peers(peers):
|
||||
return AnyView(ZStack {
|
||||
ForEach(0 ..< min(peers.peers.count, rowCount * 2), content: { i in
|
||||
Link(destination: URL(string: "\(buildConfig.appSpecificUrlScheme)://localpeer?id=\(peers.peers[i].id)")!, label: {
|
||||
Image(uiImage: avatarImage(accountPeerId: peers.accountPeerId, peer: peers.peers[i], size: CGSize(width: itemSize, height: itemSize)))
|
||||
.frame(width: itemSize, height: itemSize)
|
||||
}).frame(width: itemSize, height: itemSize)
|
||||
.position(x: itemSize / 2.0 + floor(CGFloat(i % rowCount) * itemSize * (1.0 + defaultPaddingFraction)), y: i / rowCount == 0 ? firstRowY : secondRowY)
|
||||
})
|
||||
})
|
||||
default:
|
||||
return AnyView(ZStack {
|
||||
Circle()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -81,7 +100,7 @@ struct WidgetView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(.white)
|
||||
Color(.systemBackground)
|
||||
GeometryReader { geometry in
|
||||
peerViews(geometry: geometry)
|
||||
}
|
||||
@ -90,21 +109,38 @@ struct WidgetView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private let presentationData: WidgetPresentationData = {
|
||||
private let buildConfig: BuildConfig = {
|
||||
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
||||
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return WidgetPresentationData(applicationLockedString: "Unlock the app to use the widget", applicationStartRequiredString: "Open the app to use the widget")
|
||||
preconditionFailure()
|
||||
}
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
||||
//self.buildConfig = buildConfig
|
||||
return buildConfig
|
||||
}()
|
||||
|
||||
private extension WidgetPresentationData {
|
||||
static var `default` = WidgetPresentationData(
|
||||
applicationLockedString: "Unlock the app to use the widget",
|
||||
applicationStartRequiredString: "Open the app to use the widget",
|
||||
widgetGalleryTitle: "Telegram",
|
||||
widgetGalleryDescription: ""
|
||||
)
|
||||
}
|
||||
|
||||
private let presentationData: WidgetPresentationData = {
|
||||
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
||||
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return WidgetPresentationData.default
|
||||
}
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return WidgetPresentationData(applicationLockedString: "Unlock the app to use the widget", applicationStartRequiredString: "Open the app to use the widget")
|
||||
return WidgetPresentationData.default
|
||||
}
|
||||
|
||||
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
||||
@ -112,7 +148,32 @@ private let presentationData: WidgetPresentationData = {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: widgetPresentationDataPath(rootPath: rootPath))), let value = try? JSONDecoder().decode(WidgetPresentationData.self, from: data) {
|
||||
return value
|
||||
} else {
|
||||
return WidgetPresentationData(applicationLockedString: "Unlock the app to use the widget", applicationStartRequiredString: "Open the app to use the widget")
|
||||
return WidgetPresentationData.default
|
||||
}
|
||||
}()
|
||||
|
||||
let widgetData: WidgetData? = {
|
||||
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
||||
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return nil
|
||||
}
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
||||
|
||||
let dataPath = rootPath + "/widget-data"
|
||||
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)), let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) {
|
||||
return widgetData
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
@ -121,177 +182,22 @@ struct Static_Widget: Widget {
|
||||
private let kind: String = "Static_Widget"
|
||||
|
||||
public var body: some WidgetConfiguration {
|
||||
StaticConfiguration(
|
||||
let data: PeersWidgetData
|
||||
if let widgetData = widgetData {
|
||||
data = .data(widgetData)
|
||||
} else {
|
||||
data = .placeholder
|
||||
}
|
||||
|
||||
return StaticConfiguration(
|
||||
kind: kind,
|
||||
provider: Provider(),
|
||||
content: { entry in
|
||||
WidgetView(data: .previewData)
|
||||
WidgetView(data: data)
|
||||
}
|
||||
)
|
||||
.supportedFamilies([.systemMedium])
|
||||
.configurationDisplayName(presentationData.applicationLockedString)
|
||||
.description(presentationData.applicationStartRequiredString)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(TodayViewController)
|
||||
class TodayViewController: UIViewController, NCWidgetProviding {
|
||||
private var initializedInterface = false
|
||||
|
||||
private var buildConfig: BuildConfig?
|
||||
|
||||
private var primaryColor: UIColor = .black
|
||||
private var placeholderLabel: UILabel?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
||||
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return
|
||||
}
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
||||
self.buildConfig = buildConfig
|
||||
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
||||
|
||||
let presentationData: WidgetPresentationData
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: widgetPresentationDataPath(rootPath: rootPath))), let value = try? JSONDecoder().decode(WidgetPresentationData.self, from: data) {
|
||||
presentationData = value
|
||||
} else {
|
||||
presentationData = WidgetPresentationData(applicationLockedString: "Unlock the app to use the widget", applicationStartRequiredString: "Open the app to use the widget")
|
||||
}
|
||||
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) {
|
||||
self.setPlaceholderText(presentationData.applicationLockedString)
|
||||
return
|
||||
}
|
||||
|
||||
if self.initializedInterface {
|
||||
return
|
||||
}
|
||||
self.initializedInterface = true
|
||||
|
||||
let dataPath = rootPath + "/widget-data"
|
||||
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: dataPath)), let widgetData = try? JSONDecoder().decode(WidgetData.self, from: data) {
|
||||
self.setWidgetData(widgetData: widgetData, presentationData: presentationData)
|
||||
}
|
||||
}
|
||||
|
||||
private func setPlaceholderText(_ text: String) {
|
||||
let fontSize = UIFont.preferredFont(forTextStyle: .body).pointSize
|
||||
let placeholderLabel = UILabel()
|
||||
if #available(iOSApplicationExtension 13.0, iOS 13.0, *) {
|
||||
placeholderLabel.textColor = UIColor.label
|
||||
} else {
|
||||
placeholderLabel.textColor = self.primaryColor
|
||||
}
|
||||
placeholderLabel.font = UIFont.systemFont(ofSize: fontSize)
|
||||
placeholderLabel.text = text
|
||||
placeholderLabel.sizeToFit()
|
||||
self.placeholderLabel = placeholderLabel
|
||||
self.view.addSubview(placeholderLabel)
|
||||
}
|
||||
|
||||
func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
|
||||
completionHandler(.newData)
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
|
||||
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
|
||||
|
||||
}
|
||||
|
||||
private var widgetData: WidgetData?
|
||||
|
||||
private func setWidgetData(widgetData: WidgetData, presentationData: WidgetPresentationData) {
|
||||
self.widgetData = widgetData
|
||||
self.peerViews.forEach {
|
||||
$0.removeFromSuperview()
|
||||
}
|
||||
self.peerViews = []
|
||||
switch widgetData {
|
||||
case .notAuthorized, .disabled:
|
||||
break
|
||||
case let .peers(peers):
|
||||
for peer in peers.peers {
|
||||
let peerView = PeerView(primaryColor: self.primaryColor, accountPeerId: peers.accountPeerId, peer: peer, tapped: { [weak self] in
|
||||
if let strongSelf = self, let buildConfig = strongSelf.buildConfig {
|
||||
if let url = URL(string: "\(buildConfig.appSpecificUrlScheme)://localpeer?id=\(peer.id)") {
|
||||
strongSelf.extensionContext?.open(url, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
self.view.addSubview(peerView)
|
||||
self.peerViews.append(peerView)
|
||||
}
|
||||
}
|
||||
|
||||
if self.peerViews.isEmpty {
|
||||
self.setPlaceholderText(presentationData.applicationStartRequiredString)
|
||||
} else {
|
||||
self.placeholderLabel?.removeFromSuperview()
|
||||
self.placeholderLabel = nil
|
||||
}
|
||||
|
||||
if let size = self.validLayout {
|
||||
self.updateLayout(size: size)
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var peerViews: [PeerView] = []
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
self.updateLayout(size: self.view.bounds.size)
|
||||
}
|
||||
|
||||
private func updateLayout(size: CGSize) {
|
||||
self.validLayout = size
|
||||
|
||||
if let placeholderLabel = self.placeholderLabel {
|
||||
placeholderLabel.frame = CGRect(origin: CGPoint(x: floor((size.width - placeholderLabel.bounds.width) / 2.0), y: floor((size.height - placeholderLabel.bounds.height) / 2.0)), size: placeholderLabel.bounds.size)
|
||||
}
|
||||
|
||||
let peerSize = CGSize(width: 70.0, height: 100.0)
|
||||
|
||||
var peerFrames: [CGRect] = []
|
||||
|
||||
var offset: CGFloat = 0.0
|
||||
for _ in self.peerViews {
|
||||
let peerFrame = CGRect(origin: CGPoint(x: offset, y: 10.0), size: peerSize)
|
||||
offset += peerFrame.size.width
|
||||
if peerFrame.maxX > size.width {
|
||||
break
|
||||
}
|
||||
peerFrames.append(peerFrame)
|
||||
}
|
||||
|
||||
var totalSize: CGFloat = 0.0
|
||||
for i in 0 ..< peerFrames.count {
|
||||
totalSize += peerFrames[i].width
|
||||
}
|
||||
|
||||
let spacing: CGFloat = floor((size.width - totalSize) / CGFloat(peerFrames.count))
|
||||
offset = floor(spacing / 2.0)
|
||||
for i in 0 ..< peerFrames.count {
|
||||
let peerView = self.peerViews[i]
|
||||
peerView.frame = CGRect(origin: CGPoint(x: offset, y: 16.0), size: peerFrames[i].size)
|
||||
peerView.updateLayout(size: peerFrames[i].size)
|
||||
offset += peerFrames[i].width + spacing
|
||||
}
|
||||
.configurationDisplayName(presentationData.widgetGalleryTitle)
|
||||
.description(presentationData.widgetGalleryDescription)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -6,6 +6,7 @@ import SyncCore
|
||||
import WidgetItems
|
||||
import TelegramPresentationData
|
||||
import NotificationsPresentationData
|
||||
import WidgetKit
|
||||
|
||||
final class WidgetDataContext {
|
||||
private var currentAccount: Account?
|
||||
@ -62,7 +63,7 @@ final class WidgetDataContext {
|
||||
|
||||
self.widgetPresentationDataDisposable = (presentationData
|
||||
|> map { presentationData -> WidgetPresentationData in
|
||||
return WidgetPresentationData(applicationLockedString: presentationData.strings.Widget_ApplicationLocked, applicationStartRequiredString: presentationData.strings.Widget_ApplicationStartRequired)
|
||||
return WidgetPresentationData(applicationLockedString: presentationData.strings.Widget_ApplicationLocked, applicationStartRequiredString: presentationData.strings.Widget_ApplicationStartRequired, widgetGalleryTitle: presentationData.strings.Widget_GalleryTitle, widgetGalleryDescription: presentationData.strings.Widget_GalleryDescription)
|
||||
}
|
||||
|> distinctUntilChanged).start(next: { value in
|
||||
let path = widgetPresentationDataPath(rootPath: basePath)
|
||||
@ -71,6 +72,10 @@ final class WidgetDataContext {
|
||||
} else {
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
}
|
||||
|
||||
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
})
|
||||
|
||||
self.notificationPresentationDataDisposable = (presentationData
|
||||
|
Binary file not shown.
@ -449,12 +449,12 @@ public final class WalletStrings: Equatable {
|
||||
public var Wallet_Send_ConfirmationConfirm: String { return self._s[218]! }
|
||||
public var Wallet_Created_ExportErrorTitle: String { return self._s[219]! }
|
||||
public var Wallet_Info_TransactionPendingHeader: String { return self._s[220]! }
|
||||
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
|
||||
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
|
||||
let form = getPluralizationForm(self.lc, value)
|
||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
|
||||
}
|
||||
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
|
||||
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
|
||||
let form = getPluralizationForm(self.lc, value)
|
||||
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
|
||||
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)
|
||||
|
@ -33,10 +33,14 @@ public struct WidgetDataPeers: Codable, Equatable {
|
||||
public struct WidgetPresentationData: Codable, Equatable {
|
||||
public var applicationLockedString: String
|
||||
public var applicationStartRequiredString: String
|
||||
public var widgetGalleryTitle: String
|
||||
public var widgetGalleryDescription: String
|
||||
|
||||
public init(applicationLockedString: String, applicationStartRequiredString: String) {
|
||||
public init(applicationLockedString: String, applicationStartRequiredString: String, widgetGalleryTitle: String, widgetGalleryDescription: String) {
|
||||
self.applicationLockedString = applicationLockedString
|
||||
self.applicationStartRequiredString = applicationStartRequiredString
|
||||
self.widgetGalleryTitle = widgetGalleryTitle
|
||||
self.widgetGalleryDescription = widgetGalleryDescription
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user