Skip to content

Commit

Permalink
Cellular info notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
XITRIX committed Dec 23, 2024
1 parent b64d4b5 commit 0d38104
Show file tree
Hide file tree
Showing 13 changed files with 633 additions and 9 deletions.
28 changes: 28 additions & 0 deletions iTorrent.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
7CB831192D19484B005F3F42 /* UIApplication+KeySceneWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CB831182D194840005F3F42 /* UIApplication+KeySceneWindow.swift */; };
7CB8311B2D196257005F3F42 /* InitialSetupFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CB8311A2D19624E005F3F42 /* InitialSetupFlow.swift */; };
7CB8311D2D1962A0005F3F42 /* InitialSetupFlowProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CB8311C2D19629D005F3F42 /* InitialSetupFlowProtocol.swift */; };
7CB831202D19750F005F3F42 /* CellularNotAllowedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CB8311F2D1974F7005F3F42 /* CellularNotAllowedOverlay.swift */; };
7CB831232D197540005F3F42 /* MessageOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CB831222D197540005F3F42 /* MessageOverlayView.swift */; };
7CB831252D19754A005F3F42 /* MessageOverlayView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CB831242D19754A005F3F42 /* MessageOverlayView.xib */; };
7CBDBAAD2C31EF0C008C986B /* UserDefaults+AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CBDBAAC2C31EF0C008C986B /* UserDefaults+AppGroup.swift */; };
7CBDBAAE2C31EF52008C986B /* UserDefaults+AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CBDBAAC2C31EF0C008C986B /* UserDefaults+AppGroup.swift */; };
7CC411622BD319AE00CA8B13 /* AppDelegate+RemoteConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC411612BD319AE00CA8B13 /* AppDelegate+RemoteConfig.swift */; };
Expand Down Expand Up @@ -319,6 +322,9 @@
7CB831182D194840005F3F42 /* UIApplication+KeySceneWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+KeySceneWindow.swift"; sourceTree = "<group>"; };
7CB8311A2D19624E005F3F42 /* InitialSetupFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialSetupFlow.swift; sourceTree = "<group>"; };
7CB8311C2D19629D005F3F42 /* InitialSetupFlowProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialSetupFlowProtocol.swift; sourceTree = "<group>"; };
7CB8311F2D1974F7005F3F42 /* CellularNotAllowedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellularNotAllowedOverlay.swift; sourceTree = "<group>"; };
7CB831222D197540005F3F42 /* MessageOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageOverlayView.swift; sourceTree = "<group>"; };
7CB831242D19754A005F3F42 /* MessageOverlayView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MessageOverlayView.xib; sourceTree = "<group>"; };
7CBDBAAC2C31EF0C008C986B /* UserDefaults+AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AppGroup.swift"; sourceTree = "<group>"; };
7CC411612BD319AE00CA8B13 /* AppDelegate+RemoteConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+RemoteConfig.swift"; sourceTree = "<group>"; };
7CC411632BD326C300CA8B13 /* AppDelegate+Firebase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Firebase.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -700,6 +706,24 @@
path = FileSharing;
sourceTree = "<group>";
};
7CB8311E2D1974ED005F3F42 /* Overlays */ = {
isa = PBXGroup;
children = (
7CB831212D197528005F3F42 /* MessageOverlayView */,
7CB8311F2D1974F7005F3F42 /* CellularNotAllowedOverlay.swift */,
);
path = Overlays;
sourceTree = "<group>";
};
7CB831212D197528005F3F42 /* MessageOverlayView */ = {
isa = PBXGroup;
children = (
7CB831222D197540005F3F42 /* MessageOverlayView.swift */,
7CB831242D19754A005F3F42 /* MessageOverlayView.xib */,
);
path = MessageOverlayView;
sourceTree = "<group>";
};
7CC411562BD2DCC000CA8B13 /* Assets */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1046,6 +1070,7 @@
D19E001C2AEFF9F9000A17A2 /* Components */ = {
isa = PBXGroup;
children = (
7CB8311E2D1974ED005F3F42 /* Overlays */,
7C5FBE0D2BBB206B0069E5A0 /* Views */,
D19E001D2AEFFA02000A17A2 /* Cells */,
);
Expand Down Expand Up @@ -1543,6 +1568,7 @@
D135C59A2AEFB96100440680 /* TorrentDetailsViewController.xib in Resources */,
D11138492AF9464E008907F7 /* TorrentAddViewController.xib in Resources */,
D1EFCD0A2AF56A4C00D33A7A /* TorrentFilesViewController.xib in Resources */,
7CB831252D19754A005F3F42 /* MessageOverlayView.xib in Resources */,
D1352D332BBD720C00104E7B /* PRStorageCell.xib in Resources */,
D173D9E82BC033CF00E4F9EB /* 3.wav in Resources */,
7C5FBE2E2BBF3FF80069E5A0 /* PRSwitchView.xib in Resources */,
Expand Down Expand Up @@ -1636,6 +1662,7 @@
D1B1BECC2AFE25BD0030C2A4 /* TorrentTrackersViewModel.swift in Sources */,
D19E00222AEFFA2A000A17A2 /* DetailCellViewModel.swift in Sources */,
D173D9E62BC0331300E4F9EB /* AudioBackgroundService.swift in Sources */,
7CB831232D197540005F3F42 /* MessageOverlayView.swift in Sources */,
7C609A5B2C1F5D9E00586635 /* SceneDelegate+AVPlayer.swift in Sources */,
D1D1279B2BC7CA7600C04533 /* SwiftUILayoutGuides.swift in Sources */,
D1352D1E2BBC2F7F00104E7B /* LocalizationAttribute.swift in Sources */,
Expand Down Expand Up @@ -1707,6 +1734,7 @@
D11138572AF976CE008907F7 /* TorrentAddDirectoryItemViewModel.swift in Sources */,
D1FFC9652C38136600233C2F /* URL+Extensions.swift in Sources */,
7C1C08AB2C31F31900569B45 /* PauseTorrentIntent.swift in Sources */,
7CB831202D19750F005F3F42 /* CellularNotAllowedOverlay.swift in Sources */,
D1CAB8872AF3B52E00EB6AFF /* ToggleCellViewModel.swift in Sources */,
D11BE5492AFBA03D00780C1B /* PRButtonView.swift in Sources */,
7CFEBE8B2BC439CF0013233F /* RssChannelItemCellViewModel.swift in Sources */,
Expand Down
178 changes: 178 additions & 0 deletions iTorrent/Components/Overlays/CellularNotAllowedOverlay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//
// CellularNotAllowedOverlay.swift
// iTorrent
//
// Created by Даниил Виноградов on 23.12.2024.
//

import Combine
import MvvmFoundation
import UIKit

class PassthrowView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self { return nil }
return view
}
}

class OverlayWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self { return nil }
return view
}

override func makeKey() {}
}

class OverlayViewController: UIViewController {
override func loadView() {
view = PassthrowView()
}
}

final class CellularNotAllowedOverlay: @unchecked Sendable {
private let overlayView = MessageOverlayView()
private let overlayViewController = OverlayViewController()
private lazy var overlayWindow: UIWindow = initOverlayWindow()
private var currentOverlayTask: Task<Void, Error>?
private var bottomConstraint: NSLayoutConstraint!
private let disposeBag = DisposeBag()

init() {
setup()
}

@Injected private var networkMonitoringService: NetworkMonitoringService
@Injected private var preferencesStorage: PreferencesStorage
}

private extension CellularNotAllowedOverlay {
func setup() {
overlayView.image = .init(systemName: "antenna.radiowaves.left.and.right.slash")
overlayView.title = %"overlay.cellular.title"
overlayView.message = %"overlay.cellular.message"

overlayView.translatesAutoresizingMaskIntoConstraints = false
overlayViewController.view.addSubview(overlayView)

bottomConstraint = overlayViewController.view.bottomAnchor.constraint(equalTo: overlayView.bottomAnchor)

NSLayoutConstraint.activate([
overlayViewController.view.layoutMarginsGuide.centerXAnchor.constraint(equalTo: overlayView.centerXAnchor),
overlayViewController.view.layoutMarginsGuide.leadingAnchor.constraint(greaterThanOrEqualTo: overlayView.leadingAnchor),
overlayView.trailingAnchor.constraint(greaterThanOrEqualTo: overlayViewController.view.layoutMarginsGuide.trailingAnchor),
bottomConstraint
])

overlayView.clickEvent = { [unowned self] in
showCellularPreferences()
hideOverlay()
}

disposeBag.bind {
Publishers.combineLatest(networkMonitoringService.$isCellularAvailable,
networkMonitoringService.$availableInterfaces,
networkMonitoringService.$cellularState,
preferencesStorage.$isCellularEnabled,
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification))
{ isCellularAvailable, interfaces, cellularDeviceAllowed, cellularAppAllowed, _ in

// Show if restricted by app or device
guard cellularDeviceAllowed == .restricted || !cellularAppAllowed
else { return false }

// Show if primary device is Cellular
guard isCellularAvailable && interfaces.isEmpty else { return false }

return true
}.receive(on: UIScheduler.shared)
.debounce(for: .seconds(1), scheduler: UIScheduler.shared)
.throttle(for: .seconds(4), scheduler: UIScheduler.shared, latest: false)
.sink { [unowned self] shouldShowOverlay in
guard shouldShowOverlay else { return }

currentOverlayTask?.cancel()
currentOverlayTask = Task { @MainActor in
showOverlay()
try await Task.sleep(for: .seconds(4))
hideOverlay()
}
}
}
}

func showOverlay() {
overlayWindow.isHidden = false

overlayViewController.view.setNeedsLayout()
overlayViewController.view.layoutIfNeeded()

if let viewController = UIApplication.shared.keySceneWindow.rootViewController?.topPresented,
let svc = viewController as? UISplitViewController,
let topVC = svc.detailNavigationController?.topViewController
{
bottomConstraint.constant = max(30, topVC.view.layoutMargins.bottom - overlayView.frame.height / 2)
} else {
bottomConstraint.constant = 30
}


overlayView.frame.origin.y = overlayViewController.view.frame.height
overlayViewController.view.setNeedsLayout()

overlayWindow.tintColor = preferencesStorage.tintColor

if #available(iOS 17.0, *) {
UIView.animate(springDuration: 0.3, bounce: 0.4, initialSpringVelocity: 0.3, delay: 0, options: []) {
overlayViewController.view.layoutIfNeeded()
}
} else {
UIView.animate(withDuration: 0.3) { [self] in
overlayViewController.view.layoutIfNeeded()
}
}
}

func hideOverlay() {
UIView.animate(withDuration: 0.3) { [self] in
overlayView.frame.origin.y = overlayViewController.view.frame.height
} completion: { [self] _ in
overlayWindow.isHidden = true
}
}

@MainActor
func showCellularPreferences() {
guard let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first,
let window = scene.keyWindow,
let viewController = window.rootViewController?.topPresented
else { return }

let networkVC = ConnectionPreferencesViewModel.resolveVC()
networkVC.navigationItem.trailingItemGroups = [.fixedGroup(items: [
.init(systemItem: .close, primaryAction: .init { [unowned networkVC] _ in
networkVC.dismiss(animated: true)
})
])]

let nvc = UINavigationController.resolve()
nvc.setViewControllers([networkVC], animated: false)
viewController.present(nvc, animated: true)
}

func initOverlayWindow() -> UIWindow {
guard let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first
else { fatalError("Impossible case") }

let overlayWindow = OverlayWindow(windowScene: windowScene)
overlayWindow.rootViewController = overlayViewController
overlayWindow.windowLevel = .alert

return overlayWindow
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// MessageOverlayView.swift
// iTorrent
//
// Created by Даниил Виноградов on 23.12.2024.
//

import UIKit

class MessageOverlayView: BaseView {
@IBOutlet private var imageView: UIImageView!
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var messageLabel: UILabel!

@IBOutlet private var holder: UIControl!
@IBOutlet private var blurView: UIVisualEffectView!

var clickEvent: (@MainActor() async -> Void)?

override func setup() {
blurView.layer.cornerRadius = 10
// blurView.layer.cornerCurve = .continuous

holder.layer.cornerRadius = 10
// holder.layer.cornerCurve = .continuous

holder.layoutMargins.left = 16
holder.layoutMargins.right = 16

holder.layer.borderWidth = 1 / traitCollection.displayScale
holder.layer.borderColor = UIColor.separator.cgColor

holder.addAction(.init { [unowned self] _ in
Task { await clickEvent?() }
}, for: .touchUpInside)
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
holder.layer.borderColor = UIColor.separator.cgColor
}
}

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if view == self { return nil }
return view
}
}

extension MessageOverlayView {
var image: UIImage? {
get { imageView.image }
set {
imageView.image = newValue
imageView.isHidden = newValue == nil
}
}

var title: String? {
get { titleLabel.text }
set {
titleLabel.text = newValue
titleLabel.isHidden = newValue.isNilOrEmpty
}
}

var message: String? {
get { messageLabel.text }
set {
messageLabel.text = newValue
messageLabel.isHidden = newValue.isNilOrEmpty
}
}
}
Loading

0 comments on commit 0d38104

Please sign in to comment.