Organic Maps sources as of 02.04.2025 (fad26bbf22ac3da75e01e62aa01e5c8e11861005)

To expand with full Organic Maps and Maps.ME commits history run:
  git remote add om-historic [om-historic.git repo url]
  git fetch --tags om-historic
  git replace squashed-history historic-commits
This commit is contained in:
Konstantin Pastbin
2025-04-13 16:37:30 +07:00
commit e3e4a1985a
12931 changed files with 13195100 additions and 0 deletions

View File

@@ -0,0 +1,453 @@
import OSLog
final class AboutController: MWMViewController {
fileprivate struct AboutInfoTableViewCellModel {
let title: String
let image: UIImage?
let didTapHandler: (() -> Void)?
}
fileprivate struct SocialMediaCollectionViewCellModel {
let image: UIImage
let didTapHandler: (() -> Void)?
}
private enum Constants {
static let infoTableViewCellHeight: CGFloat = 40
static let socialMediaCollectionViewCellMaxWidth: CGFloat = 50
static let socialMediaCollectionViewSpacing: CGFloat = 25
static let socialMediaCollectionNumberOfItemsInRowCompact: CGFloat = 5
static let socialMediaCollectionNumberOfItemsInRowRegular: CGFloat = 10
}
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private let logoImageView = UIImageView()
private let headerTitleLabel = UILabel()
private let additionalInfoStackView = UIStackView()
private let donationView = DonationView()
private let osmView = OSMView()
private let infoTableView = UITableView(frame: .zero, style: .plain)
private var infoTableViewHeightAnchor: NSLayoutConstraint?
private let socialMediaHeaderLabel = UILabel()
private let socialMediaCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
private lazy var socialMediaCollectionViewHeighConstraint = socialMediaCollectionView.heightAnchor.constraint(equalToConstant: .zero)
private let termsOfUseAndPrivacyPolicyView = ButtonsStackView()
private var infoTableViewData = [AboutInfoTableViewCellModel]()
private var socialMediaCollectionViewData = [SocialMediaCollectionViewCellModel]()
private var onDidAppearCompletionHandler: (() -> Void)?
init(onDidAppearCompletionHandler: (() -> Void)? = nil) {
self.onDidAppearCompletionHandler = onDidAppearCompletionHandler
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
arrangeViews()
layoutViews()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateCollection()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let completionHandler = onDidAppearCompletionHandler {
completionHandler()
onDidAppearCompletionHandler = nil
}
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateCollection()
}
}
// MARK: - Private
private extension AboutController {
func setupViews() {
func setupTitle() {
let titleView = UILabel()
titleView.text = Self.formattedAppVersion()
titleView.textColor = .white
titleView.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
titleView.isUserInteractionEnabled = true
titleView.numberOfLines = 1
titleView.allowsDefaultTighteningForTruncation = true
titleView.adjustsFontSizeToFitWidth = true
titleView.minimumScaleFactor = 0.5
let titleDidTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(appVersionButtonTapped))
titleView.addGestureRecognizer(titleDidTapGestureRecognizer)
navigationItem.titleView = titleView
}
func setupScrollAndStack() {
scrollView.delaysContentTouches = false
scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
stackView.axis = .vertical
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 15
}
func setupLogo() {
logoImageView.contentMode = .scaleAspectFit
logoImageView.image = UIImage(named: "logo")
}
func setupHeaderTitle() {
headerTitleLabel.setFontStyle(.semibold18, color: .blackPrimary)
headerTitleLabel.text = L("about_headline")
headerTitleLabel.textAlignment = .center
headerTitleLabel.numberOfLines = 1
headerTitleLabel.allowsDefaultTighteningForTruncation = true
headerTitleLabel.adjustsFontSizeToFitWidth = true
headerTitleLabel.minimumScaleFactor = 0.5
}
func setupAdditionalInfo() {
additionalInfoStackView.axis = .vertical
additionalInfoStackView.spacing = 15
[AboutInfo.noTracking, .noWifi, .community].forEach({ additionalInfoStackView.addArrangedSubview(InfoView(image: nil, title: $0.title)) })
}
func setupDonation() {
donationView.donateButtonDidTapHandler = { [weak self] in
guard let self else { return }
self.openUrl(self.isDonateEnabled() ? Settings.donateUrl() : L("translated_om_site_url") + "support-us/")
}
}
func setupOSM() {
osmView.setMapDate(Self.formattedMapsDataVersion())
osmView.didTapHandler = { [weak self] in
self?.openUrl("https://www.openstreetmap.org/")
}
}
func setupInfoTable() {
infoTableView.setStyle(.clearBackground)
infoTableView.delegate = self
infoTableView.dataSource = self
infoTableView.separatorStyle = .none
infoTableView.isScrollEnabled = false
infoTableView.showsVerticalScrollIndicator = false
infoTableView.contentInset = .zero
infoTableView.register(cell: InfoTableViewCell.self)
}
func setupSocialMediaCollection() {
socialMediaHeaderLabel.setFontStyle(.regular16, color: .blackPrimary)
socialMediaHeaderLabel.text = L("follow_us")
socialMediaHeaderLabel.numberOfLines = 1
socialMediaHeaderLabel.allowsDefaultTighteningForTruncation = true
socialMediaHeaderLabel.adjustsFontSizeToFitWidth = true
socialMediaHeaderLabel.minimumScaleFactor = 0.5
socialMediaCollectionView.backgroundColor = .clear
socialMediaCollectionView.isScrollEnabled = false
socialMediaCollectionView.dataSource = self
socialMediaCollectionView.delegate = self
socialMediaCollectionView.register(cell: SocialMediaCollectionViewCell.self)
}
func setupTermsAndPrivacy() {
termsOfUseAndPrivacyPolicyView.addButton(title: L("privacy_policy"), didTapHandler: { [weak self] in
self?.openUrl(L("translated_om_site_url") + "privacy/")
})
termsOfUseAndPrivacyPolicyView.addButton(title: L("terms_of_use"), didTapHandler: { [weak self] in
self?.openUrl(L("translated_om_site_url") + "terms/")
})
termsOfUseAndPrivacyPolicyView.addButton(title: L("copyright"), didTapHandler: { [weak self] in
self?.showCopyright()
})
}
view.setStyle(.pressBackground)
setupTitle()
setupScrollAndStack()
setupLogo()
setupHeaderTitle()
setupAdditionalInfo()
setupDonation()
setupOSM()
setupInfoTable()
setupSocialMediaCollection()
setupTermsAndPrivacy()
infoTableViewData = buildInfoTableViewData()
socialMediaCollectionViewData = buildSocialMediaCollectionViewData()
}
func arrangeViews() {
view.addSubview(scrollView)
scrollView.addSubview(stackView)
stackView.addArrangedSubview(logoImageView)
stackView.addArrangedSubview(headerTitleLabel)
stackView.addArrangedSubviewWithSeparator(additionalInfoStackView)
if isDonateEnabled() {
stackView.addArrangedSubviewWithSeparator(donationView)
}
stackView.addArrangedSubviewWithSeparator(osmView)
stackView.addArrangedSubviewWithSeparator(infoTableView)
stackView.addArrangedSubviewWithSeparator(socialMediaHeaderLabel)
stackView.addArrangedSubview(socialMediaCollectionView)
stackView.addArrangedSubviewWithSeparator(termsOfUseAndPrivacyPolicyView)
}
func layoutViews() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
logoImageView.translatesAutoresizingMaskIntoConstraints = false
additionalInfoStackView.translatesAutoresizingMaskIntoConstraints = false
donationView.translatesAutoresizingMaskIntoConstraints = false
infoTableView.translatesAutoresizingMaskIntoConstraints = false
socialMediaCollectionView.translatesAutoresizingMaskIntoConstraints = false
termsOfUseAndPrivacyPolicyView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
logoImageView.heightAnchor.constraint(equalToConstant: 64),
logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor),
additionalInfoStackView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
osmView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
infoTableView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
infoTableView.heightAnchor.constraint(equalToConstant: Constants.infoTableViewCellHeight * CGFloat(infoTableViewData.count)),
socialMediaHeaderLabel.leadingAnchor.constraint(equalTo: socialMediaCollectionView.leadingAnchor),
socialMediaCollectionView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
socialMediaCollectionView.contentLayoutGuide.widthAnchor.constraint(equalTo: stackView.widthAnchor),
socialMediaCollectionViewHeighConstraint,
termsOfUseAndPrivacyPolicyView.widthAnchor.constraint(equalTo: stackView.widthAnchor),
])
donationView.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = isDonateEnabled()
view.layoutIfNeeded()
updateCollection()
}
func updateCollection() {
socialMediaCollectionView.collectionViewLayout.invalidateLayout()
// On devices with the iOS 12 the actual collectionView layout update not always occurs during the current layout update cycle.
// So constraints update should be performed on the next layout update cycle.
DispatchQueue.main.async {
self.socialMediaCollectionViewHeighConstraint.constant = self.socialMediaCollectionView.collectionViewLayout.collectionViewContentSize.height
}
}
func isDonateEnabled() -> Bool {
return Settings.donateUrl() != nil
}
func buildInfoTableViewData() -> [AboutInfoTableViewCellModel] {
let infoContent: [AboutInfo] = [.faq, .reportMapDataProblem, .reportABug, .news, .volunteer, .rateTheApp]
let data = infoContent.map { [weak self] aboutInfo in
return AboutInfoTableViewCellModel(title: aboutInfo.title, image: aboutInfo.image, didTapHandler: {
switch aboutInfo {
case .faq:
self?.navigationController?.pushViewController(FaqController(), animated: true)
case .reportABug:
MailComposer.sendBugReportWith(title:"Organic Maps Bug Report")
case .reportMapDataProblem, .volunteer, .news:
self?.openUrl(aboutInfo.link)
case .rateTheApp:
UIApplication.shared.rateApp()
default:
break
}
})
}
return data
}
func buildSocialMediaCollectionViewData() -> [SocialMediaCollectionViewCellModel] {
let socialMediaContent: [SocialMedia] = [.telegram, .github, .instagram, .twitter, .linkedin, .organicMapsEmail, .reddit, .matrix, .facebook, .fosstodon]
let data = socialMediaContent.map { [weak self] socialMedia in
return SocialMediaCollectionViewCellModel(image: socialMedia.image, didTapHandler: {
switch socialMedia {
case .telegram: fallthrough
case .github: fallthrough
case .reddit: fallthrough
case .matrix: fallthrough
case .fosstodon: fallthrough
case .facebook: fallthrough
case .twitter: fallthrough
case .instagram: fallthrough
case .linkedin:
self?.openUrl(socialMedia.link, externally: true)
case .organicMapsEmail:
MailComposer.sendEmail(toRecipients: [socialMedia.link])
}
})
}
return data
}
// Returns a human-readable maps data version.
static func formattedMapsDataVersion() -> String {
// First, convert version code like 220131 to a date.
let df = DateFormatter()
df.locale = Locale(identifier:"en_US_POSIX")
df.dateFormat = "yyMMdd"
let mapsVersionInt = FrameworkHelper.dataVersion()
let mapsDate = df.date(from: String(mapsVersionInt))!
// Second, print the date in the local user's format.
df.locale = Locale.current
df.dateStyle = .long
df.timeStyle = .none
return df.string(from:mapsDate)
}
static func formattedAppVersion() -> String {
let appInfo = AppInfo.shared();
// Use strong left-to-right unicode direction characters for the app version.
return String(format: L("version"), "\u{2066}\(appInfo.bundleVersion)-\(appInfo.buildNumber)\u{2069}")
}
func showCopyright() {
let path = Bundle.main.path(forResource: "copyright", ofType: "html")!
let html = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
let webViewController = WebViewController.init(html: html, baseUrl: nil, title: L("copyright"))!
webViewController.openInSafari = true
self.navigationController?.pushViewController(webViewController, animated: true)
}
func copyToClipboard(_ content: String) {
UIPasteboard.general.string = content
let message = String(format: L("copied_to_clipboard"), content)
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
Toast.toast(withText: message).show(withAlignment: .bottom, pinToSafeArea: false)
}
}
// MARK: - Actions
private extension AboutController {
@objc func appVersionButtonTapped() {
copyToClipboard(Self.formattedAppVersion())
}
@objc func osmMapsDataButtonTapped() {
copyToClipboard(Self.formattedMapsDataVersion())
}
}
// MARK: - UITableViewDelegate
extension AboutController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
infoTableViewData[indexPath.row].didTapHandler?()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return Constants.infoTableViewCellHeight
}
}
// MARK: - UITableViewDataSource
extension AboutController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return infoTableViewData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(cell: InfoTableViewCell.self, indexPath: indexPath)
let aboutInfo = infoTableViewData[indexPath.row]
cell.set(image: aboutInfo.image, title: aboutInfo.title)
return cell
}
}
// MARK: - UICollectionViewDataSource
extension AboutController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return socialMediaCollectionViewData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(cell: SocialMediaCollectionViewCell.self, indexPath: indexPath)
cell.setImage(socialMediaCollectionViewData[indexPath.row].image)
return cell
}
}
// MARK: - UICollectionViewDelegate
extension AboutController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = socialMediaCollectionViewData[indexPath.row]
model.didTapHandler?()
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension AboutController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let spacing = Constants.socialMediaCollectionViewSpacing
let numberOfItemsInRowCompact = Constants.socialMediaCollectionNumberOfItemsInRowCompact
let numberOfItemsInRowRegular = Constants.socialMediaCollectionNumberOfItemsInRowRegular
var totalSpacing = (Constants.socialMediaCollectionNumberOfItemsInRowCompact - 1) * spacing
var width = (collectionView.bounds.width - totalSpacing) / numberOfItemsInRowCompact
if traitCollection.verticalSizeClass == .compact || traitCollection.horizontalSizeClass == .regular {
totalSpacing = (numberOfItemsInRowRegular - 1) * spacing
width = (collectionView.bounds.width - totalSpacing) / numberOfItemsInRowRegular
}
let maxWidth = Constants.socialMediaCollectionViewCellMaxWidth
width = min(width, maxWidth)
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return Constants.socialMediaCollectionViewSpacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return Constants.socialMediaCollectionViewSpacing
}
}
// MARK: - UIStackView + AddArrangedSubviewWithSeparator
private extension UIStackView {
func addArrangedSubviewWithSeparator(_ view: UIView) {
if !arrangedSubviews.isEmpty {
let separator = UIView()
separator.setStyleAndApply(.divider)
separator.isUserInteractionEnabled = false
separator.translatesAutoresizingMaskIntoConstraints = false
addArrangedSubview(separator)
NSLayoutConstraint.activate([
separator.heightAnchor.constraint(equalToConstant: 1.0),
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}
addArrangedSubview(view)
}
}

View File

@@ -0,0 +1,71 @@
enum AboutInfo {
case faq
case reportABug
case reportMapDataProblem
case volunteer
case news
case rateTheApp
case noTracking
case noWifi
case community
var title: String {
switch self {
case .faq:
return L("faq")
case .reportABug:
return L("report_a_bug")
case .reportMapDataProblem:
return L("report_incorrect_map_bug")
case .volunteer:
return L("volunteer")
case .news:
return L("news")
case .rateTheApp:
return L("rate_the_app")
case .noTracking:
return L("about_proposition_1")
case .noWifi:
return L("about_proposition_2")
case .community:
return L("about_proposition_3")
}
}
var image: UIImage? {
switch self {
case .faq:
return UIImage(named: "ic_about_faq")!
case .reportABug:
return UIImage(named: "ic_about_report_bug")!
case .reportMapDataProblem:
return UIImage(named: "ic_about_report_osm")!
case .volunteer:
return UIImage(named: "ic_about_volunteer")!
case .news:
return UIImage(named: "ic_about_news")!
case .rateTheApp:
return UIImage(named: "ic_about_rate_app")!
case .noTracking, .noWifi, .community:
// Dots are used for these cases
return nil
}
}
var link: String? {
switch self {
case .faq, .rateTheApp, .noTracking, .noWifi, .community:
// These cases don't provide redirection to the web
return nil
case .reportABug:
return "ios@organicmaps.app"
case .reportMapDataProblem:
return "https://www.openstreetmap.org/fixthemap"
case .volunteer:
return L("translated_om_site_url") + "support-us/"
case .news:
return L("translated_om_site_url") + "news/"
}
}
}

View File

@@ -0,0 +1,62 @@
enum SocialMedia {
case telegram
case twitter
case instagram
case facebook
case reddit
case matrix
case fosstodon
case linkedin
case organicMapsEmail
case github
var link: String {
switch self {
case .telegram:
return L("telegram_url")
case .github:
return "https://github.com/organicmaps/organicmaps/"
case .linkedin:
return "https://www.linkedin.com/company/organic-maps/"
case .organicMapsEmail:
return "ios@organicmaps.app"
case .matrix:
return "https://matrix.to/#/#organicmaps:matrix.org"
case .fosstodon:
return "https://fosstodon.org/@organicmaps"
case .facebook:
return "https://facebook.com/OrganicMaps"
case .twitter:
return "https://twitter.com/OrganicMapsApp"
case .instagram:
return L("instagram_url")
case .reddit:
return "https://www.reddit.com/r/organicmaps/"
}
}
var image: UIImage {
switch self {
case .telegram:
return UIImage(named: "ic_social_media_telegram")!
case .github:
return UIImage(named: "ic_social_media_github")!
case .linkedin:
return UIImage(named: "ic_social_media_linkedin")!
case .organicMapsEmail:
return UIImage(named: "ic_social_media_mail")!
case .matrix:
return UIImage(named: "ic_social_media_matrix")!
case .fosstodon:
return UIImage(named: "ic_social_media_fosstodon")!
case .facebook:
return UIImage(named: "ic_social_media_facebook")!
case .twitter:
return UIImage(named: "ic_social_media_x")!
case .instagram:
return UIImage(named: "ic_social_media_instagram")!
case .reddit:
return UIImage(named: "ic_social_media_reddit")!
}
}
}

View File

@@ -0,0 +1,56 @@
final class ButtonsStackView: UIView {
private let stackView = UIStackView()
private var didTapHandlers = [UIButton: (() -> Void)?]()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
arrangeViews()
layoutViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
arrangeViews()
layoutViews()
}
private func setupViews() {
stackView.distribution = .fillEqually
stackView.axis = .vertical
stackView.spacing = 20
}
private func arrangeViews() {
addSubview(stackView)
}
private func layoutViews() {
stackView.translatesAutoresizingMaskIntoConstraints = false
let offset = CGFloat(20)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: offset),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -offset),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
@objc private func buttonTapped(_ sender: UIButton) {
guard let didTapHandler = didTapHandlers[sender] else { return }
didTapHandler?()
}
// MARK: - Public
func addButton(title: String, font: UIFont = .regular14(), didTapHandler: @escaping () -> Void) {
let button = UIButton()
button.setStyleAndApply(.flatPrimaryTransButton)
button.setTitle(title, for: .normal)
button.titleLabel?.font = font
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
stackView.addArrangedSubview(button)
didTapHandlers[button] = didTapHandler
}
}

View File

@@ -0,0 +1,67 @@
final class DonationView: UIView {
private let donateTextLabel = UILabel()
private let donateButton = UIButton()
var donateButtonDidTapHandler: (() -> Void)?
init() {
super.init(frame: .zero)
setupViews()
arrangeViews()
layoutViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
arrangeViews()
layoutViews()
}
private func setupViews() {
donateTextLabel.setFontStyle(.regular14, color: .blackPrimary)
donateTextLabel.text = L("donate_description")
donateTextLabel.textAlignment = .center
donateTextLabel.lineBreakMode = .byWordWrapping
donateTextLabel.numberOfLines = 0
donateButton.setStyle(.flatNormalButton)
donateButton.setTitle(L("donate").localizedUppercase, for: .normal)
donateButton.addTarget(self, action: #selector(donateButtonDidTap), for: .touchUpInside)
}
private func arrangeViews() {
addSubview(donateTextLabel)
addSubview(donateButton)
}
private func layoutViews() {
donateTextLabel.translatesAutoresizingMaskIntoConstraints = false
donateButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
donateTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
donateTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
donateTextLabel.topAnchor.constraint(equalTo: topAnchor),
donateButton.topAnchor.constraint(equalTo: donateTextLabel.bottomAnchor, constant: 10),
donateButton.widthAnchor.constraint(equalTo: widthAnchor, constant: -40).withPriority(.defaultHigh),
donateButton.widthAnchor.constraint(lessThanOrEqualToConstant: 400).withPriority(.defaultHigh),
donateButton.centerXAnchor.constraint(equalTo: centerXAnchor),
donateButton.heightAnchor.constraint(equalToConstant: 40),
donateButton.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
@objc private func donateButtonDidTap() {
donateButtonDidTapHandler?()
}
}
private extension NSLayoutConstraint {
func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
self.priority = priority
return self
}
}

View File

@@ -0,0 +1,33 @@
final class InfoTableViewCell: UITableViewCell {
private let infoView = InfoView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
setupView()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
backgroundView = UIView() // Set background color to clear
setStyle(.clearBackground)
contentView.addSubview(infoView)
infoView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
infoView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
infoView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
infoView.topAnchor.constraint(equalTo: contentView.topAnchor),
infoView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
// MARK: - Public
func set(image: UIImage?, title: String) {
infoView.set(image: image, title: title)
}
}

View File

@@ -0,0 +1,81 @@
final class InfoView: UIView {
private let stackView = UIStackView()
private let imageView = UIImageView()
private let titleLabel = UILabel()
private lazy var imageViewWidthConstrain = imageView.widthAnchor.constraint(equalToConstant: 0)
init() {
super.init(frame: .zero)
self.setupView()
self.arrangeViews()
self.layoutViews()
}
convenience init(image: UIImage?, title: String) {
self.init()
self.set(image: image, title: title)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *), traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
imageView.applyTheme()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 16
titleLabel.setFontStyle(.regular16, color: .blackPrimary)
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.numberOfLines = .zero
imageView.setStyle(.black)
imageView.contentMode = .scaleAspectFit
}
private func arrangeViews() {
addSubview(stackView)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(titleLabel)
}
private func layoutViews() {
stackView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
imageView.setContentHuggingPriority(.defaultHigh, for: .vertical)
imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.heightAnchor.constraint(equalToConstant: 24),
imageViewWidthConstrain
])
updateImageWidth()
}
private func updateImageWidth() {
imageViewWidthConstrain.constant = imageView.image == nil ? 0 : 24
imageView.isHidden = imageView.image == nil
}
// MARK: - Public
func set(image: UIImage?, title: String) {
imageView.image = image
titleLabel.text = title
updateImageWidth()
}
}

View File

@@ -0,0 +1,87 @@
final class OSMView: UIView {
private let OSMImageView = UIImageView()
private let OSMTextLabel = UILabel()
private var mapDate: String?
var didTapHandler: (() -> Void)?
init() {
super.init(frame: .zero)
setupViews()
arrangeViews()
layoutViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
arrangeViews()
layoutViews()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard let mapDate, traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
OSMTextLabel.attributedText = attributedString(for: mapDate)
}
// MARK: - Public
func setMapDate(_ mapDate: String) {
self.mapDate = mapDate
OSMTextLabel.attributedText = attributedString(for: mapDate)
}
// MARK: - Private
private func setupViews() {
OSMImageView.image = UIImage(named: "osm_logo")
OSMTextLabel.setFontStyle(.regular14, color: .blackPrimary)
OSMTextLabel.lineBreakMode = .byWordWrapping
OSMTextLabel.numberOfLines = 0
OSMTextLabel.isUserInteractionEnabled = true
let osmDidTapGesture = UITapGestureRecognizer(target: self, action: #selector(osmDidTap))
OSMTextLabel.addGestureRecognizer(osmDidTapGesture)
}
private func arrangeViews() {
addSubview(OSMImageView)
addSubview(OSMTextLabel)
}
private func layoutViews() {
OSMImageView.translatesAutoresizingMaskIntoConstraints = false
OSMTextLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
OSMImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
OSMImageView.heightAnchor.constraint(equalToConstant: 40),
OSMImageView.widthAnchor.constraint(equalTo: OSMImageView.heightAnchor),
OSMImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
OSMImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
OSMTextLabel.leadingAnchor.constraint(equalTo: OSMImageView.trailingAnchor, constant: 8),
OSMTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
OSMTextLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
OSMTextLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
OSMTextLabel.centerYAnchor.constraint(equalTo: OSMImageView.centerYAnchor)
])
}
@objc private func osmDidTap() {
didTapHandler?()
}
private func attributedString(for date: String) -> NSAttributedString {
let osmLink = "OpenStreetMap.org"
let attributedString = NSMutableAttributedString(string: String(format: L("osm_presentation"), date.trimmingCharacters(in: .punctuationCharacters)),
attributes: [.font: UIFont.regular14(),
.foregroundColor: StyleManager.shared.theme!.colors.blackPrimaryText]
)
let linkRange = attributedString.mutableString.range(of: osmLink)
attributedString.addAttribute(.link, value: "https://www.openstreetmap.org/", range: linkRange)
return attributedString
}
}

View File

@@ -0,0 +1,45 @@
final class SocialMediaCollectionViewCell: UICollectionViewCell {
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return }
updateImageColor()
}
private func setupView() {
setStyle(.clearBackground)
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
private func updateImageColor() {
imageView.tintColor = StyleManager.shared.theme?.colors.blackPrimaryText
}
// MARK: - Public
func setImage(_ image: UIImage) {
imageView.image = image
updateImageColor()
}
}

View File

@@ -0,0 +1,30 @@
final class SocialMediaCollectionViewHeader: UICollectionReusableView {
static let reuseIdentifier = String(describing: SocialMediaCollectionViewHeader.self)
private let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupView() {
addSubview(titleLabel)
titleLabel.setFontStyleAndApply(.regular16, color: .blackPrimary)
titleLabel.numberOfLines = 1
titleLabel.allowsDefaultTighteningForTruncation = true
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.minimumScaleFactor = 0.5
}
// MARK: - Public
func setTitle(_ title: String) {
titleLabel.text = title
}
}

View File

@@ -0,0 +1,24 @@
final class FaqController: MWMViewController {
override func loadView() {
super.loadView()
// TODO: FAQ?
self.title = L("help")
let path = Bundle.main.path(forResource: "faq", ofType: "html")!
let html = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
let webViewController = WebViewController.init(html: html, baseUrl: nil, title: nil)!
webViewController.openInSafari = true
addChild(webViewController)
let aboutView = webViewController.view!
view.addSubview(aboutView)
aboutView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
aboutView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
aboutView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
aboutView.topAnchor.constraint(equalTo: view.topAnchor),
aboutView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}