mirror of
https://github.com/yattee/yattee.git
synced 2024-11-09 15:58:20 +00:00
Add pull to refresh for Subscriptions, Popular and Trending (fixes #31)
This commit is contained in:
parent
1db4a3197d
commit
363424fa74
@ -27,11 +27,17 @@ struct FavoritesView: View {
|
|||||||
FavoriteItemView(item: item, dragging: $dragging)
|
FavoriteItemView(item: item, dragging: $dragging)
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
#if os(iOS)
|
||||||
|
let first = favorites.first
|
||||||
|
#endif
|
||||||
ForEach(favorites) { item in
|
ForEach(favorites) { item in
|
||||||
FavoriteItemView(item: item, dragging: $dragging)
|
FavoriteItemView(item: item, dragging: $dragging)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.workaroundForVerticalScrollingBug()
|
.workaroundForVerticalScrollingBug()
|
||||||
#endif
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
.padding(.top, item == first && RefreshControl.navigationBarTitleDisplayMode == .inline ? 10 : 0)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -54,6 +60,9 @@ struct FavoritesView: View {
|
|||||||
.background(Color.secondaryBackground)
|
.background(Color.secondaryBackground)
|
||||||
.frame(minWidth: 360)
|
.frame(minWidth: 360)
|
||||||
#endif
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,6 +127,9 @@ struct PlaylistsView: View {
|
|||||||
.onChange(of: accounts.current) { _ in
|
.onChange(of: accounts.current) { _ in
|
||||||
model.load(force: true)
|
model.load(force: true)
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
@ -48,19 +48,7 @@ struct TrendingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
|
||||||
.fullScreenCover(isPresented: $presentingCountrySelection) {
|
|
||||||
TrendingCountry(selectedCountry: $country)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
.sheet(isPresented: $presentingCountrySelection) {
|
|
||||||
TrendingCountry(selectedCountry: $country)
|
|
||||||
#if os(macOS)
|
|
||||||
.frame(minWidth: 400, minHeight: 400)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
.navigationTitle("Trending")
|
|
||||||
#endif
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ToolbarItemGroup {
|
ToolbarItemGroup {
|
||||||
@ -77,8 +65,8 @@ struct TrendingView: View {
|
|||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
ToolbarItemGroup(placement: .bottomBar) {
|
ToolbarItemGroup(placement: .bottomBar) {
|
||||||
Group {
|
Group {
|
||||||
HStack {
|
|
||||||
if accounts.app.supportsTrendingCategories {
|
if accounts.app.supportsTrendingCategories {
|
||||||
|
HStack {
|
||||||
Text("Category")
|
Text("Category")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
@ -87,9 +75,9 @@ struct TrendingView: View {
|
|||||||
// force redraw of the view when it changes
|
// force redraw of the view when it changes
|
||||||
.id(UUID())
|
.id(UUID())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
if let favoriteItem = favoriteItem {
|
if let favoriteItem = favoriteItem {
|
||||||
FavoriteButton(item: favoriteItem)
|
FavoriteButton(item: favoriteItem)
|
||||||
@ -122,6 +110,28 @@ struct TrendingView: View {
|
|||||||
|
|
||||||
updateFavoriteItem()
|
updateFavoriteItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
.fullScreenCover(isPresented: $presentingCountrySelection) {
|
||||||
|
TrendingCountry(selectedCountry: $country)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
.sheet(isPresented: $presentingCountrySelection) {
|
||||||
|
TrendingCountry(selectedCountry: $country)
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(minWidth: 400, minHeight: 400)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.navigationTitle("Trending")
|
||||||
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
.refreshControl { refreshControl in
|
||||||
|
resource.load().onCompletion { _ in
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
@ -30,5 +30,13 @@ struct PopularView: View {
|
|||||||
FavoriteButton(item: FavoriteItem(section: .popular))
|
FavoriteButton(item: FavoriteItem(section: .popular))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.refreshControl { refreshControl in
|
||||||
|
resource?.load().onCompletion { _ in
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,13 @@ struct SubscriptionsView: View {
|
|||||||
.onChange(of: accounts.current) { _ in
|
.onChange(of: accounts.current) { _ in
|
||||||
loadResources(force: true)
|
loadResources(force: true)
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.refreshControl { refreshControl in
|
||||||
|
loadResources(force: true) {
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@ -31,26 +38,35 @@ struct SubscriptionsView: View {
|
|||||||
FavoriteButton(item: FavoriteItem(section: .subscriptions))
|
FavoriteButton(item: FavoriteItem(section: .subscriptions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if os(iOS)
|
||||||
|
.navigationBarTitleDisplayMode(RefreshControl.navigationBarTitleDisplayMode)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func loadResources(force: Bool = false) {
|
private func loadResources(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
|
||||||
feed?.addObserver(store)
|
feed?.addObserver(store)
|
||||||
|
|
||||||
if accounts.app == .invidious {
|
if accounts.app == .invidious {
|
||||||
// Invidious for some reason won't refresh feed until homepage is loaded
|
// Invidious for some reason won't refresh feed until homepage is loaded
|
||||||
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
|
if let request = force ? accounts.api.home?.load() : accounts.api.home?.loadIfNeeded() {
|
||||||
request.onSuccess { _ in
|
request.onSuccess { _ in
|
||||||
loadFeed(force: force)
|
loadFeed(force: force, onCompletion: onCompletion)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loadFeed(force: force)
|
loadFeed(force: force, onCompletion: onCompletion)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loadFeed(force: force)
|
loadFeed(force: force, onCompletion: onCompletion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func loadFeed(force: Bool = false) {
|
private func loadFeed(force: Bool = false, onCompletion: @escaping () -> Void = {}) {
|
||||||
_ = force ? feed?.load() : feed?.loadIfNeeded()
|
if let request = force ? feed?.load() : feed?.loadIfNeeded() {
|
||||||
|
request.onCompletion { _ in
|
||||||
|
onCompletion()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onCompletion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
Vendor/RefreshControl/Extensions/UIResponder+Extensions.swift
vendored
Normal file
15
Vendor/RefreshControl/Extensions/UIResponder+Extensions.swift
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// UIResponder+Extensions.swift
|
||||||
|
// SwiftUI_Pull_to_Refresh
|
||||||
|
//
|
||||||
|
// Created by Geri Borbás on 21/09/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIResponder {
|
||||||
|
var parentViewController: UIViewController? {
|
||||||
|
next as? UIViewController ?? next?.parentViewController
|
||||||
|
}
|
||||||
|
}
|
70
Vendor/RefreshControl/Extensions/UIView+Extensions.swift
vendored
Normal file
70
Vendor/RefreshControl/Extensions/UIView+Extensions.swift
vendored
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// UIView+Extensions.swift
|
||||||
|
// SwiftUI_Pull_to_Refresh
|
||||||
|
//
|
||||||
|
// Created by Geri Borbás on 19/09/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIView {
|
||||||
|
/// Returs frame in screen coordinates.
|
||||||
|
var globalFrame: CGRect {
|
||||||
|
if let window = window {
|
||||||
|
return convert(bounds, to: window.screen.coordinateSpace)
|
||||||
|
} else {
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns with all the instances of the given view type in the view hierarchy.
|
||||||
|
func viewsInHierarchy<ViewType: UIView>() -> [ViewType]? {
|
||||||
|
var views: [ViewType] = []
|
||||||
|
viewsInHierarchy(views: &views)
|
||||||
|
return views.isEmpty ? nil : views
|
||||||
|
}
|
||||||
|
|
||||||
|
private func viewsInHierarchy<ViewType: UIView>(views: inout [ViewType]) {
|
||||||
|
subviews.forEach { eachSubView in
|
||||||
|
if let matchingView = eachSubView as? ViewType {
|
||||||
|
views.append(matchingView)
|
||||||
|
}
|
||||||
|
eachSubView.viewsInHierarchy(views: &views)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search ancestral view hierarcy for the given view type.
|
||||||
|
func searchViewAnchestorsFor<ViewType: UIView>(
|
||||||
|
_ onViewFound: (ViewType) -> Void
|
||||||
|
) {
|
||||||
|
if let matchingView = superview as? ViewType {
|
||||||
|
onViewFound(matchingView)
|
||||||
|
} else {
|
||||||
|
superview?.searchViewAnchestorsFor(onViewFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search ancestral view hierarcy for the given view type.
|
||||||
|
func searchViewAnchestorsFor<ViewType: UIView>() -> ViewType? {
|
||||||
|
if let matchingView = superview as? ViewType {
|
||||||
|
return matchingView
|
||||||
|
} else {
|
||||||
|
return superview?.searchViewAnchestorsFor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printViewHierarchyInformation(_ level: Int = 0) {
|
||||||
|
printViewInformation(level)
|
||||||
|
subviews.forEach { $0.printViewHierarchyInformation(level + 1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func printViewInformation(_ level: Int) {
|
||||||
|
let leadingWhitespace = String(repeating: " ", count: level)
|
||||||
|
let className = "\(Self.self)"
|
||||||
|
let superclassName = "\(superclass!)"
|
||||||
|
let frame = "\(self.frame)"
|
||||||
|
let id = (accessibilityIdentifier == nil) ? "" : " `\(accessibilityIdentifier!)`"
|
||||||
|
print("\(leadingWhitespace)\(className): \(superclassName)\(id) \(frame)")
|
||||||
|
}
|
||||||
|
}
|
47
Vendor/RefreshControl/README
vendored
Normal file
47
Vendor/RefreshControl/README
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
https://github.com/Geri-Borbas/iOS.Blog.SwiftUI_Pull_to_Refresh
|
||||||
|
|
||||||
|
# SwiftUI Pull to Refresh
|
||||||
|
⇣ SwiftUI Pull to Refresh (for iOS 13 and iOS 14) condensed into a single modifier.
|
||||||
|
|
||||||
|
|
||||||
|
Complementary repository for article [**SwiftUI Pull to Refresh**] (in progress). See [`ContentView.swift`] for usage, and [`RefreshControlModifier.swift`] for the source. Designed to work with **multiple scroll views** on the same screen.
|
||||||
|
|
||||||
|
```Swift
|
||||||
|
struct ContentView: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
List {
|
||||||
|
ForEach(1...100, id: \.self) { eachRowIndex in
|
||||||
|
Text("Left \(eachRowIndex)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.refreshControl { refreshControl in
|
||||||
|
Network.refresh {
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List {
|
||||||
|
ForEach(1...100, id: \.self) { eachRowIndex in
|
||||||
|
Text("Right \(eachRowIndex)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.refreshControl { refreshControl in
|
||||||
|
Network.refresh {
|
||||||
|
refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
> Licensed under the [**MIT License**](https://en.wikipedia.org/wiki/MIT_License).
|
||||||
|
|
||||||
|
[`ContentView.swift`]: SwiftUI_Pull_to_Refresh/Views/ContentView.swift
|
||||||
|
[`RefreshControl.swift`]: SwiftUI_Pull_to_Refresh/Views/RefreshControl.swift
|
56
Vendor/RefreshControl/RefreshControl.swift
vendored
Normal file
56
Vendor/RefreshControl/RefreshControl.swift
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// RefreshControl.swift
|
||||||
|
// SwiftUI_Pull_to_Refresh
|
||||||
|
//
|
||||||
|
// Created by Geri Borbás on 18/09/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class RefreshControl: ObservableObject {
|
||||||
|
static var navigationBarTitleDisplayMode: NavigationBarItem.TitleDisplayMode {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
return .automatic
|
||||||
|
}
|
||||||
|
|
||||||
|
return .inline
|
||||||
|
}
|
||||||
|
|
||||||
|
let onValueChanged: (_ refreshControl: UIRefreshControl) -> Void
|
||||||
|
|
||||||
|
internal init(onValueChanged: @escaping ((UIRefreshControl) -> Void)) {
|
||||||
|
self.onValueChanged = onValueChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `UIRefreshControl` to the `UIScrollView` provided.
|
||||||
|
func add(to scrollView: UIScrollView) {
|
||||||
|
scrollView.refreshControl = UIRefreshControl().withTarget(
|
||||||
|
self,
|
||||||
|
action: #selector(onValueChangedAction),
|
||||||
|
for: .valueChanged
|
||||||
|
)
|
||||||
|
.testable(as: "RefreshControl")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func onValueChangedAction(sender: UIRefreshControl) {
|
||||||
|
onValueChanged(sender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIRefreshControl {
|
||||||
|
/// Convinience method to assign target action inline.
|
||||||
|
func withTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) -> UIRefreshControl {
|
||||||
|
addTarget(target, action: action, for: controlEvents)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convinience method to match refresh control for UI testing.
|
||||||
|
func testable(as id: String) -> UIRefreshControl {
|
||||||
|
isAccessibilityElement = true
|
||||||
|
accessibilityIdentifier = id
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
42
Vendor/RefreshControl/RefreshControlModifier.swift
vendored
Normal file
42
Vendor/RefreshControl/RefreshControlModifier.swift
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// RefreshControlModifier.swift
|
||||||
|
// SwiftUI_Pull_to_Refresh
|
||||||
|
//
|
||||||
|
// Created by Geri Borbás on 18/09/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct RefreshControlModifier: ViewModifier {
|
||||||
|
@State private var geometryReaderFrame: CGRect = .zero
|
||||||
|
let refreshControl: RefreshControl
|
||||||
|
|
||||||
|
internal init(onValueChanged: @escaping (UIRefreshControl) -> Void) {
|
||||||
|
refreshControl = RefreshControl(onValueChanged: onValueChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.background(
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ScrollViewMatcher(
|
||||||
|
onResolve: { scrollView in
|
||||||
|
refreshControl.add(to: scrollView)
|
||||||
|
},
|
||||||
|
geometryReaderFrame: $geometryReaderFrame
|
||||||
|
)
|
||||||
|
.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
|
||||||
|
.onPreferenceChange(FramePreferenceKey.self) { frame in
|
||||||
|
self.geometryReaderFrame = frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func refreshControl(onValueChanged: @escaping (_ refreshControl: UIRefreshControl) -> Void) -> some View {
|
||||||
|
modifier(RefreshControlModifier(onValueChanged: onValueChanged))
|
||||||
|
}
|
||||||
|
}
|
18
Vendor/RefreshControl/ScrollViewMatcher/FramePreferenceKey.swift
vendored
Normal file
18
Vendor/RefreshControl/ScrollViewMatcher/FramePreferenceKey.swift
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// FramePreferenceKey.swift
|
||||||
|
// SwiftUI_Pull_to_Refresh
|
||||||
|
//
|
||||||
|
// Created by Geri Borbás on 21/09/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FramePreferenceKey: PreferenceKey {
|
||||||
|
typealias Value = CGRect
|
||||||
|
static var defaultValue = CGRect.zero
|
||||||
|
|
||||||
|
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
|
||||||
|
value = nextValue()
|
||||||
|
}
|
||||||
|
}
|
106
Vendor/RefreshControl/ScrollViewMatcher/ScrollViewMatcher.swift
vendored
Normal file
106
Vendor/RefreshControl/ScrollViewMatcher/ScrollViewMatcher.swift
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// ScrollViewMatcher.swift
|
||||||
|
// SwiftUI_Pull_to_Refresh
|
||||||
|
//
|
||||||
|
// Created by Geri Borbás on 17/09/2021.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class ScrollViewMatcher: UIViewControllerRepresentable {
|
||||||
|
let onMatch: (UIScrollView) -> Void
|
||||||
|
@Binding var geometryReaderFrame: CGRect
|
||||||
|
|
||||||
|
init(onResolve: @escaping (UIScrollView) -> Void, geometryReaderFrame: Binding<CGRect>) {
|
||||||
|
onMatch = onResolve
|
||||||
|
_geometryReaderFrame = geometryReaderFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIViewController(context _: Context) -> ScrollViewMatcherViewController {
|
||||||
|
ScrollViewMatcherViewController(onResolve: onMatch, geometryReaderFrame: geometryReaderFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ viewController: ScrollViewMatcherViewController, context _: Context) {
|
||||||
|
viewController.geometryReaderFrame = geometryReaderFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ScrollViewMatcherViewController: UIViewController {
|
||||||
|
let onMatch: (UIScrollView) -> Void
|
||||||
|
private var scrollView: UIScrollView? {
|
||||||
|
didSet {
|
||||||
|
if oldValue != scrollView,
|
||||||
|
let scrollView = scrollView
|
||||||
|
{
|
||||||
|
onMatch(scrollView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var geometryReaderFrame: CGRect {
|
||||||
|
didSet {
|
||||||
|
match()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(onResolve: @escaping (UIScrollView) -> Void, geometryReaderFrame: CGRect, debug _: Bool = false) {
|
||||||
|
onMatch = onResolve
|
||||||
|
self.geometryReaderFrame = geometryReaderFrame
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("Use init(onMatch:) to instantiate ScrollViewMatcherViewController.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func match() {
|
||||||
|
// matchUsingHierarchy()
|
||||||
|
matchUsingGeometry()
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchUsingHierarchy() {
|
||||||
|
if parent != nil {
|
||||||
|
// Lookup view ancestry for any `UIScrollView`.
|
||||||
|
view.searchViewAnchestorsFor { (scrollView: UIScrollView) in
|
||||||
|
self.scrollView = scrollView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchUsingGeometry() {
|
||||||
|
if let parent = parent {
|
||||||
|
if let scrollViewsInHierarchy: [UIScrollView] = parent.view.viewsInHierarchy() {
|
||||||
|
// Return first match if only a single scroll view is found in the hierarchy.
|
||||||
|
if scrollViewsInHierarchy.count == 1,
|
||||||
|
let firstScrollViewInHierarchy = scrollViewsInHierarchy.first
|
||||||
|
{
|
||||||
|
scrollView = firstScrollViewInHierarchy
|
||||||
|
|
||||||
|
// Filter by frame origins if multiple matches found.
|
||||||
|
} else {
|
||||||
|
if let firstMatchingFrameOrigin = scrollViewsInHierarchy.filter({
|
||||||
|
$0.globalFrame.origin.close(to: geometryReaderFrame.origin)
|
||||||
|
}).first {
|
||||||
|
scrollView = firstMatchingFrameOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
|
super.didMove(toParent: parent)
|
||||||
|
match()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CGPoint {
|
||||||
|
/// Returns `true` if this point is close the other point (considering a ~1 pt tolerance).
|
||||||
|
func close(to point: CGPoint) -> Bool {
|
||||||
|
let inset = Double(1)
|
||||||
|
let rect = CGRect(x: x - inset, y: y - inset, width: inset * 2, height: inset * 2)
|
||||||
|
return rect.contains(point)
|
||||||
|
}
|
||||||
|
}
|
@ -515,6 +515,14 @@
|
|||||||
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
|
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
|
||||||
37DD9DA42785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
|
37DD9DA42785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
|
||||||
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
|
37DD9DA52785BBC900539416 /* NoCommentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DA22785BBC900539416 /* NoCommentsView.swift */; };
|
||||||
|
37DD9DB12785D58D00539416 /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DAF2785D58D00539416 /* RefreshControl.swift */; };
|
||||||
|
37DD9DB42785D58D00539416 /* RefreshControlModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB02785D58D00539416 /* RefreshControlModifier.swift */; };
|
||||||
|
37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB82785D60200539416 /* FramePreferenceKey.swift */; };
|
||||||
|
37DD9DBB2785D60300539416 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB82785D60200539416 /* FramePreferenceKey.swift */; };
|
||||||
|
37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB82785D60200539416 /* FramePreferenceKey.swift */; };
|
||||||
|
37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DB92785D60200539416 /* ScrollViewMatcher.swift */; };
|
||||||
|
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DC22785D63A00539416 /* UIResponder+Extensions.swift */; };
|
||||||
|
37DD9DCB2785E28C00539416 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD9DC12785D63A00539416 /* UIView+Extensions.swift */; };
|
||||||
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; };
|
37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; };
|
||||||
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
|
37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
|
||||||
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
|
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; };
|
||||||
@ -778,6 +786,12 @@
|
|||||||
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
||||||
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
|
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
|
||||||
|
37DD9DAF2785D58D00539416 /* RefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControl.swift; sourceTree = "<group>"; };
|
||||||
|
37DD9DB02785D58D00539416 /* RefreshControlModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControlModifier.swift; sourceTree = "<group>"; };
|
||||||
|
37DD9DB82785D60200539416 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
|
||||||
|
37DD9DB92785D60200539416 /* ScrollViewMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewMatcher.swift; sourceTree = "<group>"; };
|
||||||
|
37DD9DC12785D63A00539416 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
37DD9DC22785D63A00539416 /* UIResponder+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; };
|
37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = "<group>"; };
|
||||||
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
|
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
|
||||||
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
|
37E2EEAA270656EC00170416 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
|
||||||
@ -1204,6 +1218,7 @@
|
|||||||
3722AEBA274DA312005EA4D6 /* Backports */,
|
3722AEBA274DA312005EA4D6 /* Backports */,
|
||||||
37D4B1B72672CFE300C925CA /* Model */,
|
37D4B1B72672CFE300C925CA /* Model */,
|
||||||
37C7A9022679058300E721B4 /* Extensions */,
|
37C7A9022679058300E721B4 /* Extensions */,
|
||||||
|
37DD9DCC2785EE6F00539416 /* Vendor */,
|
||||||
3748186426A762300084E870 /* Fixtures */,
|
3748186426A762300084E870 /* Fixtures */,
|
||||||
37A3B15827255E7F000FB5EE /* Open in Yattee */,
|
37A3B15827255E7F000FB5EE /* Open in Yattee */,
|
||||||
377FC7D1267A080300A6BBAF /* Frameworks */,
|
377FC7D1267A080300A6BBAF /* Frameworks */,
|
||||||
@ -1346,6 +1361,43 @@
|
|||||||
path = Gestures;
|
path = Gestures;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
37DD9DAE2785D58D00539416 /* RefreshControl */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37DD9DC02785D63A00539416 /* Extensions */,
|
||||||
|
37DD9DB72785D60200539416 /* ScrollViewMatcher */,
|
||||||
|
37DD9DAF2785D58D00539416 /* RefreshControl.swift */,
|
||||||
|
37DD9DB02785D58D00539416 /* RefreshControlModifier.swift */,
|
||||||
|
);
|
||||||
|
path = RefreshControl;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
37DD9DB72785D60200539416 /* ScrollViewMatcher */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37DD9DB82785D60200539416 /* FramePreferenceKey.swift */,
|
||||||
|
37DD9DB92785D60200539416 /* ScrollViewMatcher.swift */,
|
||||||
|
);
|
||||||
|
path = ScrollViewMatcher;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
37DD9DC02785D63A00539416 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37DD9DC22785D63A00539416 /* UIResponder+Extensions.swift */,
|
||||||
|
37DD9DC12785D63A00539416 /* UIView+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
37DD9DCC2785EE6F00539416 /* Vendor */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
37DD9DAE2785D58D00539416 /* RefreshControl */,
|
||||||
|
);
|
||||||
|
path = Vendor;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
37FB283F2721B20800A57617 /* Search */ = {
|
37FB283F2721B20800A57617 /* Search */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -1846,6 +1898,7 @@
|
|||||||
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */,
|
||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||||
|
37DD9DCB2785E28C00539416 /* UIView+Extensions.swift in Sources */,
|
||||||
3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */,
|
3782B94F27553A6700990149 /* SearchSuggestions.swift in Sources */,
|
||||||
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
|
378E50FF26FE8EEE00F49626 /* AccountsMenuView.swift in Sources */,
|
||||||
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
37169AA62729E2CC0011DE61 /* AccountsBridge.swift in Sources */,
|
||||||
@ -1864,6 +1917,7 @@
|
|||||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||||
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
|
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
|
||||||
|
37DD9DB12785D58D00539416 /* RefreshControl.swift in Sources */,
|
||||||
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */,
|
37B4E805277D0AB4004BF56A /* Orientation.swift in Sources */,
|
||||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E662759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
@ -1881,6 +1935,7 @@
|
|||||||
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
|
||||||
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||||
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */,
|
||||||
|
37DD9DB42785D58D00539416 /* RefreshControlModifier.swift in Sources */,
|
||||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
378AE940274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||||
376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */,
|
376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */,
|
||||||
@ -1909,6 +1964,7 @@
|
|||||||
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794326DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */,
|
||||||
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
37AAF29026740715007FC770 /* Channel.swift in Sources */,
|
||||||
|
37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */,
|
||||||
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||||
@ -1939,9 +1995,11 @@
|
|||||||
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965A26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
37D526DE2720AC4400ED2F5E /* VideosAPI.swift in Sources */,
|
||||||
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
|
37484C2526FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||||
|
37DD9DBD2785D60300539416 /* ScrollViewMatcher.swift in Sources */,
|
||||||
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
37B767DB2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,
|
||||||
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
|
||||||
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
|
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
|
||||||
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||||
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||||
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
37141673267A8E10006CA35D /* Country.swift in Sources */,
|
||||||
@ -2012,6 +2070,7 @@
|
|||||||
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
|
||||||
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
|
||||||
3784CDE327772EE40055BBF2 /* Watch.swift in Sources */,
|
3784CDE327772EE40055BBF2 /* Watch.swift in Sources */,
|
||||||
|
37DD9DBB2785D60300539416 /* FramePreferenceKey.swift in Sources */,
|
||||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */,
|
||||||
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||||
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB285F272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
@ -2216,6 +2275,7 @@
|
|||||||
files = (
|
files = (
|
||||||
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
37EAD871267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||||
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
|
373C8FE6275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||||
|
37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */,
|
||||||
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
|
37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */,
|
||||||
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||||
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,
|
||||||
|
Loading…
Reference in New Issue
Block a user