mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Unlike MyAppsViewController, AppViewController will attempt to update to the latest available version, rather than the latest supported version. If the latest version is not supported, it will fall back to asking user to install last supported version.
613 lines
23 KiB
Swift
613 lines
23 KiB
Swift
//
|
|
// AppViewController.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 7/22/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
import AltStoreCore
|
|
import Roxas
|
|
|
|
import Nuke
|
|
|
|
final class AppViewController: UIViewController
|
|
{
|
|
var app: StoreApp!
|
|
|
|
private var contentViewController: AppContentViewController!
|
|
private var contentViewControllerShadowView: UIView!
|
|
|
|
private var blurAnimator: UIViewPropertyAnimator?
|
|
private var navigationBarAnimator: UIViewPropertyAnimator?
|
|
|
|
private var contentSizeObservation: NSKeyValueObservation?
|
|
|
|
@IBOutlet private var scrollView: UIScrollView!
|
|
@IBOutlet private var contentView: UIView!
|
|
|
|
@IBOutlet private var bannerView: AppBannerView!
|
|
|
|
@IBOutlet private var backButton: UIButton!
|
|
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
|
|
|
|
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
|
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
|
|
|
@IBOutlet private var navigationBarTitleView: UIView!
|
|
@IBOutlet private var navigationBarDownloadButton: PillButton!
|
|
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
|
|
@IBOutlet private var navigationBarAppNameLabel: UILabel!
|
|
|
|
private var _shouldResetLayout = false
|
|
private var _backgroundBlurEffect: UIBlurEffect?
|
|
private var _backgroundBlurTintColor: UIColor?
|
|
|
|
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
|
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
return _preferredStatusBarStyle
|
|
}
|
|
|
|
override func viewDidLoad()
|
|
{
|
|
super.viewDidLoad()
|
|
|
|
self.navigationBarTitleView.sizeToFit()
|
|
self.navigationItem.titleView = self.navigationBarTitleView
|
|
|
|
self.contentViewControllerShadowView = UIView()
|
|
self.contentViewControllerShadowView.backgroundColor = .white
|
|
self.contentViewControllerShadowView.layer.cornerRadius = 38
|
|
self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
|
|
self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
|
|
self.contentViewControllerShadowView.layer.shadowRadius = 10
|
|
self.contentViewControllerShadowView.layer.shadowOpacity = 0.3
|
|
self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0)
|
|
|
|
self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer)
|
|
|
|
self.contentViewController.view.layer.cornerRadius = 38
|
|
self.contentViewController.view.layer.masksToBounds = true
|
|
|
|
self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer)
|
|
self.contentViewController.tableView.showsVerticalScrollIndicator = false
|
|
|
|
// Bring to front so the scroll indicators are visible.
|
|
self.view.bringSubviewToFront(self.scrollView)
|
|
self.scrollView.isUserInteractionEnabled = false
|
|
|
|
self.bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
|
self.bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
|
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
|
self.bannerView.iconImageView.image = nil
|
|
self.bannerView.iconImageView.tintColor = self.app.tintColor
|
|
self.bannerView.button.tintColor = self.app.tintColor
|
|
self.bannerView.tintColor = self.app.tintColor
|
|
|
|
self.bannerView.configure(for: self.app)
|
|
self.bannerView.accessibilityTraits.remove(.button)
|
|
|
|
self.bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
|
|
|
self.backButtonContainerView.tintColor = self.app.tintColor
|
|
|
|
self.navigationController?.navigationBar.tintColor = self.app.tintColor
|
|
self.navigationBarDownloadButton.tintColor = self.app.tintColor
|
|
self.navigationBarAppNameLabel.text = self.app.name
|
|
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
|
|
|
|
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
|
|
self?.view.setNeedsLayout()
|
|
self?.view.layoutIfNeeded()
|
|
}
|
|
|
|
self.update()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
|
|
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
|
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
|
|
|
// Load Images
|
|
for imageView in [self.bannerView.iconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
|
{
|
|
imageView.isIndicatingActivity = true
|
|
|
|
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
|
|
if response?.image != nil
|
|
{
|
|
imageView?.isIndicatingActivity = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool)
|
|
{
|
|
super.viewWillAppear(animated)
|
|
|
|
self.prepareBlur()
|
|
|
|
// Update blur immediately.
|
|
self.view.setNeedsLayout()
|
|
self.view.layoutIfNeeded()
|
|
|
|
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
|
self.hideNavigationBar()
|
|
}, completion: nil)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool)
|
|
{
|
|
super.viewDidAppear(animated)
|
|
|
|
self._shouldResetLayout = true
|
|
self.view.setNeedsLayout()
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool)
|
|
{
|
|
super.viewWillDisappear(animated)
|
|
|
|
// Guard against "dismissing" when presenting via 3D Touch pop.
|
|
guard self.navigationController != nil else { return }
|
|
|
|
// Store reference since self.navigationController will be nil after disappearing.
|
|
let navigationController = self.navigationController
|
|
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
|
|
|
|
self.transitionCoordinator?.animate(alongsideTransition: { (context) in
|
|
self.showNavigationBar(for: navigationController)
|
|
}, completion: { (context) in
|
|
if !context.isCancelled
|
|
{
|
|
self.showNavigationBar(for: navigationController)
|
|
}
|
|
})
|
|
}
|
|
|
|
override func viewDidDisappear(_ animated: Bool)
|
|
{
|
|
super.viewDidDisappear(animated)
|
|
|
|
if self.navigationController == nil
|
|
{
|
|
self.resetNavigationBarAnimation()
|
|
}
|
|
}
|
|
|
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
|
{
|
|
guard segue.identifier == "embedAppContentViewController" else { return }
|
|
|
|
self.contentViewController = segue.destination as? AppContentViewController
|
|
self.contentViewController.app = self.app
|
|
|
|
if #available(iOS 15, *)
|
|
{
|
|
// Fix navigation bar + tab bar appearance on iOS 15.
|
|
self.setContentScrollView(self.scrollView)
|
|
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
|
|
}
|
|
}
|
|
|
|
override func viewDidLayoutSubviews()
|
|
{
|
|
super.viewDidLayoutSubviews()
|
|
|
|
if self._shouldResetLayout
|
|
{
|
|
// Various events can cause UI to mess up, so reset affected components now.
|
|
|
|
if self.navigationController?.topViewController == self
|
|
{
|
|
self.hideNavigationBar()
|
|
}
|
|
|
|
self.prepareBlur()
|
|
|
|
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
|
self.resetNavigationBarAnimation()
|
|
|
|
self._shouldResetLayout = false
|
|
}
|
|
|
|
let statusBarHeight = self.view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
|
|
let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
|
|
|
let inset = 12 as CGFloat
|
|
let padding = 20 as CGFloat
|
|
|
|
let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
|
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
|
|
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
|
|
|
|
var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.bannerView.bounds.height)
|
|
var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height)
|
|
var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width)
|
|
|
|
let minimumHeaderY = backButtonFrame.maxY + 8
|
|
|
|
let minimumContentY = minimumHeaderY + headerFrame.height + padding
|
|
let maximumContentY = self.view.bounds.width * 0.667
|
|
|
|
// A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur.
|
|
let minimumBlurFraction = 0.3 as CGFloat
|
|
|
|
contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y
|
|
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
|
|
|
|
// Stretch the app icon image to fill additional vertical space if necessary.
|
|
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
|
|
backgroundIconFrame.size.height = height
|
|
|
|
let blurThreshold = 0 as CGFloat
|
|
if self.scrollView.contentOffset.y < blurThreshold
|
|
{
|
|
// Determine how much to lessen blur by.
|
|
|
|
let range = 75 as CGFloat
|
|
let difference = -self.scrollView.contentOffset.y
|
|
|
|
let fraction = min(difference, range) / range
|
|
|
|
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
|
|
self.blurAnimator?.fractionComplete = fractionComplete
|
|
}
|
|
else
|
|
{
|
|
// Set blur to default.
|
|
|
|
self.blurAnimator?.fractionComplete = minimumBlurFraction
|
|
}
|
|
|
|
// Animate navigation bar.
|
|
let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y
|
|
if self.scrollView.contentOffset.y > showNavigationBarThreshold
|
|
{
|
|
if self.navigationBarAnimator == nil
|
|
{
|
|
self.prepareNavigationBarAnimation()
|
|
}
|
|
|
|
let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold
|
|
let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top)
|
|
|
|
let fractionComplete = min(difference, range) / range
|
|
self.navigationBarAnimator?.fractionComplete = fractionComplete
|
|
}
|
|
else
|
|
{
|
|
self.resetNavigationBarAnimation()
|
|
}
|
|
|
|
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY)
|
|
if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold
|
|
{
|
|
let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold
|
|
backButtonFrame.origin.y -= difference
|
|
}
|
|
|
|
let pinContentToTopThreshold = maximumContentY
|
|
if self.scrollView.contentOffset.y > pinContentToTopThreshold
|
|
{
|
|
contentFrame.origin.y = 0
|
|
backgroundIconFrame.origin.y = 0
|
|
|
|
let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold
|
|
self.contentViewController.tableView.contentOffset.y = difference
|
|
}
|
|
else
|
|
{
|
|
// Keep content table view's content offset at the top.
|
|
self.contentViewController.tableView.contentOffset.y = 0
|
|
}
|
|
|
|
// Keep background app icon centered in gap between top of content and top of screen.
|
|
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
|
|
|
|
// Set frames.
|
|
self.contentViewController.view.superview?.frame = contentFrame
|
|
self.bannerView.frame = headerFrame
|
|
self.backgroundAppIconImageView.frame = backgroundIconFrame
|
|
self.backgroundBlurView.frame = backgroundIconFrame
|
|
self.backButtonContainerView.frame = backButtonFrame
|
|
|
|
self.contentViewControllerShadowView.frame = self.contentViewController.view.frame
|
|
|
|
self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY
|
|
|
|
self.scrollView.verticalScrollIndicatorInsets.top = statusBarHeight
|
|
|
|
// Adjust content offset + size.
|
|
let contentOffset = self.scrollView.contentOffset
|
|
|
|
var contentSize = self.contentViewController.tableView.contentSize
|
|
contentSize.height += maximumContentY
|
|
|
|
self.scrollView.contentSize = contentSize
|
|
self.scrollView.contentOffset = contentOffset
|
|
|
|
self.bannerView.backgroundEffectView.backgroundColor = .clear
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
|
{
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
self._shouldResetLayout = true
|
|
}
|
|
|
|
deinit
|
|
{
|
|
self.blurAnimator?.stopAnimation(true)
|
|
self.navigationBarAnimator?.stopAnimation(true)
|
|
}
|
|
}
|
|
|
|
extension AppViewController
|
|
{
|
|
final class func makeAppViewController(app: StoreApp) -> AppViewController
|
|
{
|
|
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
|
|
|
let appViewController = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
|
|
appViewController.app = app
|
|
return appViewController
|
|
}
|
|
}
|
|
|
|
private extension AppViewController
|
|
{
|
|
func update()
|
|
{
|
|
for button in [self.bannerView.button!, self.navigationBarDownloadButton!]
|
|
{
|
|
button.tintColor = self.app.tintColor
|
|
button.isIndicatingActivity = false
|
|
|
|
if let installedApp = self.app.installedApp
|
|
{
|
|
if let latestVersion = self.app.latestAvailableVersion, installedApp.version != latestVersion.version
|
|
{
|
|
button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal)
|
|
}
|
|
else
|
|
{
|
|
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
|
}
|
|
}
|
|
else
|
|
{
|
|
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
|
}
|
|
|
|
let progress = AppManager.shared.installationProgress(for: self.app)
|
|
button.progress = progress
|
|
}
|
|
|
|
if let versionDate = self.app.latestAvailableVersion?.date, versionDate > Date()
|
|
{
|
|
self.bannerView.button.countdownDate = versionDate
|
|
self.navigationBarDownloadButton.countdownDate = versionDate
|
|
}
|
|
else
|
|
{
|
|
self.bannerView.button.countdownDate = nil
|
|
self.navigationBarDownloadButton.countdownDate = nil
|
|
}
|
|
|
|
let barButtonItem = self.navigationItem.rightBarButtonItem
|
|
self.navigationItem.rightBarButtonItem = nil
|
|
self.navigationItem.rightBarButtonItem = barButtonItem
|
|
}
|
|
|
|
func showNavigationBar(for navigationController: UINavigationController? = nil)
|
|
{
|
|
let navigationController = navigationController ?? self.navigationController
|
|
navigationController?.navigationBar.alpha = 1.0
|
|
navigationController?.navigationBar.tintColor = .altPrimary
|
|
navigationController?.navigationBar.setNeedsLayout()
|
|
|
|
if self.traitCollection.userInterfaceStyle == .dark
|
|
{
|
|
self._preferredStatusBarStyle = .lightContent
|
|
}
|
|
else
|
|
{
|
|
self._preferredStatusBarStyle = .default
|
|
}
|
|
|
|
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
|
}
|
|
|
|
func hideNavigationBar(for navigationController: UINavigationController? = nil)
|
|
{
|
|
let navigationController = navigationController ?? self.navigationController
|
|
navigationController?.navigationBar.alpha = 0.0
|
|
|
|
self._preferredStatusBarStyle = .lightContent
|
|
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
|
}
|
|
|
|
func prepareBlur()
|
|
{
|
|
if let animator = self.blurAnimator
|
|
{
|
|
animator.stopAnimation(true)
|
|
}
|
|
|
|
self.backgroundBlurView.effect = self._backgroundBlurEffect
|
|
self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor
|
|
|
|
self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
|
self?.backgroundBlurView.effect = nil
|
|
self?.backgroundBlurView.contentView.backgroundColor = .clear
|
|
}
|
|
|
|
self.blurAnimator?.startAnimation()
|
|
self.blurAnimator?.pauseAnimation()
|
|
}
|
|
|
|
func prepareNavigationBarAnimation()
|
|
{
|
|
self.resetNavigationBarAnimation()
|
|
|
|
self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
|
self?.showNavigationBar()
|
|
self?.navigationController?.navigationBar.tintColor = self?.app.tintColor
|
|
self?.navigationController?.navigationBar.barTintColor = nil
|
|
self?.contentViewController.view.layer.cornerRadius = 0
|
|
}
|
|
|
|
self.navigationBarAnimator?.startAnimation()
|
|
self.navigationBarAnimator?.pauseAnimation()
|
|
|
|
self.update()
|
|
}
|
|
|
|
func resetNavigationBarAnimation()
|
|
{
|
|
self.navigationBarAnimator?.stopAnimation(true)
|
|
self.navigationBarAnimator = nil
|
|
|
|
self.hideNavigationBar()
|
|
|
|
self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius
|
|
}
|
|
}
|
|
|
|
extension AppViewController
|
|
{
|
|
@IBAction func popViewController(_ sender: UIButton)
|
|
{
|
|
self.navigationController?.popViewController(animated: true)
|
|
}
|
|
|
|
@IBAction func performAppAction(_ sender: PillButton)
|
|
{
|
|
if let installedApp = self.app.installedApp
|
|
{
|
|
if let latestVersion = self.app.latestAvailableVersion, installedApp.version != latestVersion.version
|
|
{
|
|
self.updateApp(installedApp)
|
|
}
|
|
else
|
|
{
|
|
self.open(installedApp)
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.downloadApp()
|
|
}
|
|
}
|
|
|
|
func downloadApp()
|
|
{
|
|
guard self.app.installedApp == nil else { return }
|
|
|
|
let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in
|
|
do
|
|
{
|
|
_ = try result.get()
|
|
}
|
|
catch OperationError.cancelled
|
|
{
|
|
// Ignore
|
|
}
|
|
catch
|
|
{
|
|
DispatchQueue.main.async {
|
|
let toastView = ToastView(error: error, opensLog: true)
|
|
toastView.show(in: self)
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.bannerView.button.progress = nil
|
|
self.navigationBarDownloadButton.progress = nil
|
|
self.update()
|
|
}
|
|
}
|
|
|
|
self.bannerView.button.progress = group.progress
|
|
self.navigationBarDownloadButton.progress = group.progress
|
|
}
|
|
|
|
func open(_ installedApp: InstalledApp)
|
|
{
|
|
UIApplication.shared.open(installedApp.openAppURL)
|
|
}
|
|
|
|
func updateApp(_ installedApp: InstalledApp)
|
|
{
|
|
let previousProgress = AppManager.shared.installationProgress(for: installedApp)
|
|
guard previousProgress == nil else {
|
|
//TODO: Handle cancellation
|
|
//previousProgress?.cancel()
|
|
return
|
|
}
|
|
|
|
_ = AppManager.shared.update(installedApp, to: .latestAvailableVersionWithFallback, presentingViewController: self) { (result) in
|
|
DispatchQueue.main.async {
|
|
switch result
|
|
{
|
|
case .success: print("Updated app from AppViewController:", installedApp.bundleIdentifier)
|
|
case .failure(OperationError.cancelled): break
|
|
case .failure(let error):
|
|
let toastView = ToastView(error: error)
|
|
toastView.opensErrorLog = true
|
|
toastView.show(in: self)
|
|
}
|
|
|
|
self.update()
|
|
}
|
|
}
|
|
|
|
self.update()
|
|
}
|
|
}
|
|
|
|
private extension AppViewController
|
|
{
|
|
@objc func didChangeApp(_ notification: Notification)
|
|
{
|
|
// Async so that AppManager.installationProgress(for:) is nil when we update.
|
|
DispatchQueue.main.async {
|
|
self.update()
|
|
}
|
|
}
|
|
|
|
@objc func willEnterForeground(_ notification: Notification)
|
|
{
|
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
|
|
|
self._shouldResetLayout = true
|
|
self.view.setNeedsLayout()
|
|
}
|
|
|
|
@objc func didBecomeActive(_ notification: Notification)
|
|
{
|
|
guard let navigationController = self.navigationController, navigationController.topViewController == self else { return }
|
|
|
|
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
|
|
self._shouldResetLayout = true
|
|
self.view.setNeedsLayout()
|
|
}
|
|
}
|
|
|
|
extension AppViewController: UIScrollViewDelegate
|
|
{
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView)
|
|
{
|
|
self.view.setNeedsLayout()
|
|
self.view.layoutIfNeeded()
|
|
}
|
|
}
|