Skip to content

Commit

Permalink
Merge branch 'release/0.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
mangerlahn committed Aug 16, 2018
2 parents 90b96f9 + 5ef9442 commit 1f10dc5
Show file tree
Hide file tree
Showing 29 changed files with 1,914 additions and 405 deletions.
120 changes: 98 additions & 22 deletions Latest.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

477 changes: 421 additions & 56 deletions Latest/Base.lproj/Main.storyboard

Large diffs are not rendered by default.

99 changes: 87 additions & 12 deletions Latest/Model/App Bundles/AppBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import Cocoa
/**
Delegate Protocol defining functions for changes of an app update
*/
protocol AppBundleDelegate : class {
protocol AppBundleDelegate {

/**
The version information of the AppUpdate changed. This can euther be the version or currentVersion parameter

- parameter app: The app update with updated information
*/
func appDidUpdateVersionInformation(_ app: AppBundle)
mutating func appDidUpdateVersionInformation(_ app: AppBundle)
}

/**
Expand All @@ -30,41 +30,116 @@ class AppBundle : NSObject {
var version: Version!

/// The display name of the app
var appName = ""
var name = ""

/// The url of the app on the users computer
var appURL: URL?
var url: URL

/// The delegate to be notified when app information changes
weak var delegate : AppBundleDelegate?
var delegate : AppBundleDelegate?

/// The newest information available for this app
var newestVersion: UpdateInfo?

/**
Convenience initializer for creating an app object
- parameter appName: The name of the app
- parameter name: The name of the app
- parameter versionNumber: The current version number of the app
- parameter buildNumber: The current build number of the app
*/
init(appName: String, versionNumber: String?, buildNumber: String?) {
init(appName: String, versionNumber: String?, buildNumber: String?, url: URL) {
self.version = Version(versionNumber ?? "", buildNumber ?? "")
self.appName = appName
self.name = appName
self.url = url
}

/// Compares two apps on equality
static func ==(lhs: AppBundle, rhs: AppBundle) -> Bool {
return lhs.appName == rhs.appName && lhs.appURL == rhs.appURL
var updateAvailable: Bool {
if let version = self.newestVersion, version.version > self.version {
return true
}

return false
}


// MARK: - Actions

/// Opens the app and a given index
func open() {
var appStoreURL : URL?

if let appStoreApp = self as? MacAppStoreAppBundle {
appStoreURL = appStoreApp.appStoreURL
}

let url = appStoreURL ?? self.url
NSWorkspace.shared.open(url)
}

/// Reveals the app at a given index in Finder
func showInFinder() {
NSWorkspace.shared.activateFileViewerSelecting([self.url])
}


// MARK: - Debug

func printDebugDescription() {
print("-----------------------")
print("Debug description for app \(appName)")
print("Debug description for app \(name)")
print("Version number: \(version?.versionNumber ?? "not given")")
print("Build number: \(version?.buildNumber ?? "not given")")
}

}

// Version String Handling
extension AppBundle {

/// A container holding the current and new version information
struct DisplayableVersionInformation {

/// The localized version of the app present on the computer
var current: String {
return String(format: NSLocalizedString("Your version: %@", comment: "Current Version String"), "\(self.rawCurrent)")
}

/// The new available version of the app
var new: String {
return String(format: NSLocalizedString("New version: %@", comment: "New Version String"), "\(self.rawNew)")
}

fileprivate var rawCurrent: String
fileprivate var rawNew: String

}

var localizedVersionInformation: DisplayableVersionInformation? {
guard let info = self.newestVersion else { return nil }

var versionInformation: DisplayableVersionInformation?

if let v = self.version.versionNumber, let nv = info.version.versionNumber {
versionInformation = DisplayableVersionInformation(rawCurrent: v, rawNew: nv)

// If the shortVersion string is identical, but the bundle version is different
// Show the Bundle version in brackets like: "1.3 (21)"
if self.updateAvailable, v == nv, let v = self.version?.buildNumber, let nv = info.version.buildNumber {
versionInformation?.rawCurrent += " (\(v))"
versionInformation?.rawNew += " (\(nv))"
}
} else if let v = self.version.buildNumber, let nv = info.version.buildNumber {
versionInformation = DisplayableVersionInformation(rawCurrent: v, rawNew: nv)
}

return versionInformation
}

}

extension AppBundle {
/// Compares two apps on equality
static func ==(lhs: AppBundle, rhs: AppBundle) -> Bool {
return lhs.name == rhs.name && lhs.url == rhs.url
}
}
16 changes: 15 additions & 1 deletion Latest/Model/App Bundles/MacAppStoreAppBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ class MacAppStoreAppBundle: AppBundle {
/// The url of the app in the Mac App Store
var appStoreURL : URL?

/// The date formatter used for parsing
private var dateFormatter: DateFormatter!

override init(appName: String, versionNumber: String?, buildNumber: String?, url: URL) {
super.init(appName: appName, versionNumber: versionNumber, buildNumber: buildNumber, url: url)

self.dateFormatter = DateFormatter()
self.dateFormatter.locale = Locale(identifier: "en_US")

// Example of the date format: Mon, 28 Nov 2016 14:00:00 +0100
// This is problematic, because some developers use other date formats
self.dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
}

/**
Parses the data to extract information like release notes and version number

Expand All @@ -37,7 +51,7 @@ class MacAppStoreAppBundle: AppBundle {

// Get update date
if let dateString = data["currentVersionReleaseDate"] as? String,
let date = DateFormatter().date(from: dateString) {
let date = self.dateFormatter.date(from: dateString) {
info.date = date
}

Expand Down
8 changes: 4 additions & 4 deletions Latest/Model/App Bundles/SparkleAppBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class SparkleAppBundle: AppBundle, XMLParserDelegate {
/// The date formatter used for parsing
private var dateFormatter: DateFormatter!

override init(appName: String, versionNumber: String?, buildNumber: String?) {
super.init(appName: appName, versionNumber: versionNumber, buildNumber: buildNumber)
override init(appName: String, versionNumber: String?, buildNumber: String?, url: URL) {
super.init(appName: appName, versionNumber: versionNumber, buildNumber: buildNumber, url: url)

self.dateFormatter = DateFormatter()
self.dateFormatter.locale = Locale(identifier: "en_US")
Expand Down Expand Up @@ -78,12 +78,12 @@ class SparkleAppBundle: AppBundle, XMLParserDelegate {
func parser(_ parser: XMLParser, foundCharacters string: String) {
switch currentlyParsing {
case .pubDate:
if let date = self.dateFormatter.date(from: string) {
if let date = self.dateFormatter.date(from: string.trimmingCharacters(in: .whitespacesAndNewlines)) {
self.newestVersion?.date = date
}
case .releaseNotesLink:
if self.newestVersion?.releaseNotes == nil {
self.newestVersion?.releaseNotes = URL(string: string)
self.newestVersion?.releaseNotes = URL(string: string.trimmingCharacters(in: .whitespacesAndNewlines))
}
case .releaseNotesData:
if self.newestVersion?.releaseNotes == nil {
Expand Down
137 changes: 137 additions & 0 deletions Latest/Model/AppCollection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//
// AppCollection.swift
// Latest
//
// Created by Max Langer on 15.08.18.
// Copyright © 2018 Max Langer. All rights reserved.
//

import Foundation

/// The collection handling the apps
/// This structure supports the following states:
/// - All apps with updates available
/// - All installed apps, separated from the ones with updates through sections
struct AppCollection {

/// Holds the apps
fileprivate var data = [AppBundle]()

/// Flag indicating if all apps are presented
var showInstalledUpdates = false

/// The indexes of the sections as well as installed apps
var indexesOfInstalledApps: IndexSet {
// The first section header
var indexSet = IndexSet(integer: 0)

// All installed apps including the second header
indexSet.insert(integersIn: (self.countOfAvailableUpdates + 1)..<(self.data.count + 2))

return indexSet
}

/// The number of apps available
var count: Int {
if !self.showInstalledUpdates {
return self.data.filter({ $0.updateAvailable }).count
}

return self.data.count + 2
}

/// The cached value of the count of apps with updates available
private(set) var countOfAvailableUpdates: Int = 0

/// Adds a new app to the collection
mutating func append(_ element: Element) {
self.data.append(element)
self.data.sort { (bundle1, bundle2) -> Bool in
if bundle1.updateAvailable != bundle2.updateAvailable {
return bundle1.updateAvailable
}

return bundle1.name < bundle2.name
}

self.updateCountOfAvailableUpdates()
}

/// Returns the relative index of the element. This index may not reflect the internal position of the app due to section offsets
func index(of element: Element) -> Index? {
guard element.updateAvailable || self.showInstalledUpdates, let index = self.data.firstIndex(of: element) else { return nil }

return self.align(index)
}

/// Removes the app from the collection
@discardableResult
mutating func remove(_ appBundle: AppBundle) -> Int? {
guard let index = self.data.firstIndex(where: { $0 == appBundle }) else { return nil }

self.data.remove(at: index)
self.updateCountOfAvailableUpdates()

return self.align(index)
}

/// Returns whether there is a section at the given index
func isSectionHeader(at index: Int) -> Bool {
guard self.showInstalledUpdates else { return false }

return [0, self.countOfAvailableUpdates + 1].contains(index)
}

/// This method counts all available updates. It assumes that the array is sorted with all updates at the beginning
private mutating func updateCountOfAvailableUpdates() {
self.countOfAvailableUpdates = self.data.firstIndex(where: { !$0.updateAvailable }) ?? self.data.count
}

/// Aligns the index based on the section headers
private func align(_ index: Int) -> Int {
var index = index

if self.showInstalledUpdates {
index += self.countOfAvailableUpdates < index ? 2 : 1
}

if self.isSectionHeader(at: index) {
index += 1
}

return index
}

}

extension AppCollection: Collection {

typealias DataType = [AppBundle]

typealias Index = DataType.Index
typealias Element = DataType.Element
typealias Iterator = DataType.Iterator

var startIndex: Index { return self.data.startIndex }
var endIndex: Index { return self.data.endIndex }

subscript(position: Index) -> Element {
var position = position

if self.showInstalledUpdates {
position -= self.countOfAvailableUpdates < position ? 2 : 1 // Remove first row
}

return self.data[Swift.max(position, 0)]
}

func makeIterator() -> Iterator {
return self.data.makeIterator()
}

// Method that returns the next index when iterating
func index(after i: Index) -> Index {
return self.data.index(after: i)
}

}
34 changes: 34 additions & 0 deletions Latest/Model/IconCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// IconCache.swift
// Latest
//
// Created by Max Langer on 12.08.18.
// Copyright © 2018 Max Langer. All rights reserved.
//

import AppKit

class IconCache {

static var shared = IconCache()

private var cache: NSCache<NSURL, NSImage>

init() {
self.cache = NSCache()
}

func icon(for app: AppBundle, with completion: @escaping (NSImage) -> Void) {
if let icon = self.cache.object(forKey: app.url as NSURL) {
completion(icon)
}

DispatchQueue.main.async {
let icon = NSWorkspace.shared.icon(forFile: app.url.path)
self.cache.setObject(icon, forKey: app.url as NSURL)

completion(icon)
}
}

}
Loading

0 comments on commit 1f10dc5

Please sign in to comment.