Skip to content

Commit 362da89

Browse files
authored
Merge pull request #175 from televator-apps/172-customisation
Customisation
2 parents cd74280 + aa06c1f commit 362da89

File tree

12 files changed

+571
-358
lines changed

12 files changed

+571
-358
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ Changelog
22
-------------
33

44
### Unreleased
5+
* Add user customisation (based on the work of @nieldm [#163](https://github.com/televator-apps/vimari/pull/163)).
6+
* Update Vimari interface to allow users access to their configuration.
7+
* Remove `closeTabReverse` action.
58

69
### 2.0.3 (2019-09-26)
710

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//
2+
// ConfigurationModel.swift
3+
// Vimari Extension
4+
//
5+
// Created by Daniel Mendez on 12/15/19.
6+
// Copyright © 2019 net.televator. All rights reserved.
7+
//
8+
9+
protocol ConfigurationModelProtocol {
10+
func editConfigFile() throws
11+
func resetConfigFile() throws
12+
func getDefaultSettings() throws -> [String: Any]
13+
func getUserSettings() throws -> [String : Any]
14+
}
15+
16+
import Foundation
17+
import SafariServices
18+
19+
class ConfigurationModel: ConfigurationModelProtocol {
20+
21+
private enum Constant {
22+
static let settingsFileName = "defaultSettings"
23+
static let userSettingsFileName = "userSettings"
24+
static let defaultEditor = "TextEdit"
25+
}
26+
27+
let userSettingsUrl: URL = FileManager.documentDirectoryURL
28+
.appendingPathComponent(Constant.userSettingsFileName)
29+
.appendingPathExtension("json")
30+
31+
func editConfigFile() throws {
32+
let settingsFilePath = try findOrCreateUserSettings()
33+
NSWorkspace.shared.openFile(
34+
settingsFilePath,
35+
withApplication: Constant.defaultEditor
36+
)
37+
}
38+
39+
func resetConfigFile() throws {
40+
let settingsFilePath = try overwriteUserSettings()
41+
NSWorkspace.shared.openFile(
42+
settingsFilePath,
43+
withApplication: Constant.defaultEditor
44+
)
45+
}
46+
47+
func getDefaultSettings() throws -> [String : Any] {
48+
return try loadSettings(fromFile: Constant.settingsFileName)
49+
}
50+
51+
func getUserSettings() throws -> [String : Any] {
52+
let userFilePath = try findOrCreateUserSettings()
53+
let urlSettingsFile = URL(fileURLWithPath: userFilePath)
54+
let settingsData = try Data(contentsOf: urlSettingsFile)
55+
return try settingsData.toJSONObject()
56+
}
57+
58+
private func loadSettings(fromFile file: String) throws -> [String : Any] {
59+
let settingsData = try Bundle.main.getJSONData(from: file)
60+
return try settingsData.toJSONObject()
61+
}
62+
63+
private func findOrCreateUserSettings() throws -> String {
64+
let url = userSettingsUrl
65+
let urlString = url.path
66+
if FileManager.default.fileExists(atPath: urlString) {
67+
return urlString
68+
}
69+
let data = try Bundle.main.getJSONData(from: Constant.settingsFileName)
70+
try data.write(to: url)
71+
return urlString
72+
}
73+
74+
private func overwriteUserSettings() throws -> String {
75+
let url = userSettingsUrl
76+
let urlString = userSettingsUrl.path
77+
let data = try Bundle.main.getJSONData(from: Constant.settingsFileName)
78+
try data.write(to: url)
79+
return urlString
80+
}
81+
}
82+
83+
enum DataError: Error {
84+
case unableToParse
85+
case notFound
86+
}
87+
88+
private extension Data {
89+
func toJSONObject() throws -> [String: Any] {
90+
let serialized = try JSONSerialization.jsonObject(with: self, options: [])
91+
guard let result = serialized as? [String: Any] else {
92+
throw DataError.unableToParse
93+
}
94+
return result
95+
}
96+
}
97+
98+
private extension Bundle {
99+
func getJSONPath(for file: String) throws -> String {
100+
guard let result = self.path(forResource: file, ofType: ".json") else {
101+
throw DataError.notFound
102+
}
103+
return result
104+
}
105+
106+
func getJSONData(from file: String) throws -> Data {
107+
let settingsPath = try self.getJSONPath(for: file)
108+
let urlSettingsFile = URL(fileURLWithPath: settingsPath)
109+
return try Data(contentsOf: urlSettingsFile)
110+
}
111+
}
112+
113+
private extension FileManager {
114+
static var documentDirectoryURL: URL {
115+
let documentDirectoryURL = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
116+
return documentDirectoryURL
117+
}
118+
}

Vimari Extension/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<array>
3333
<dict>
3434
<key>Script</key>
35-
<string>settings.js</string>
35+
<string>SafariExtensionCommunicator.js</string>
3636
</dict>
3737
<dict>
3838
<key>Script</key>

Vimari Extension/SafariExtensionHandler.swift

Lines changed: 147 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,51 @@ enum ActionType: String {
66
case tabForward
77
case tabBackward
88
case closeTab
9+
case updateSettings
10+
}
11+
12+
enum InputAction: String {
13+
case openSettings
14+
case resetSettings
915
}
1016

1117
enum TabDirection: String {
1218
case forward
1319
case backward
1420
}
1521

16-
func mod(_ a: Int, _ n: Int) -> Int {
17-
// https://stackoverflow.com/questions/41180292/negative-number-modulo-in-swift
18-
precondition(n > 0, "modulus must be positive")
19-
let r = a % n
20-
return r >= 0 ? r : r + n
21-
}
22-
2322
class SafariExtensionHandler: SFSafariExtensionHandler {
24-
override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) {
25-
guard let action = ActionType(rawValue: messageName) else {
26-
NSLog("Received message with unsupported type: \(messageName)")
27-
return
23+
24+
private enum Constant {
25+
static let mainAppName = "Vimari"
26+
static let newTabPageURL = "https://duckduckgo.com" //Try it :D
27+
}
28+
29+
let configuration: ConfigurationModelProtocol = ConfigurationModel()
30+
31+
//MARK: Overrides
32+
33+
// This method handles messages from the Vimari App (located /Vimari in the repository)
34+
override func messageReceivedFromContainingApp(withName messageName: String, userInfo: [String : Any]? = nil) {
35+
do {
36+
switch InputAction(rawValue: messageName) {
37+
case .openSettings:
38+
try configuration.editConfigFile()
39+
case .resetSettings:
40+
try configuration.resetConfigFile()
41+
case .none:
42+
NSLog("Input not supported " + messageName)
43+
}
44+
} catch {
45+
NSLog(error.localizedDescription)
2846
}
2947

48+
}
49+
50+
// This method handles messages from the extension (in the browser page)
51+
override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) {
3052
NSLog("Received message: \(messageName)")
31-
switch action {
53+
switch ActionType(rawValue: messageName) {
3254
case .openLinkInTab:
3355
let url = URL(string: userInfo?["url"] as! String)
3456
openInNewTab(url: url!)
@@ -40,66 +62,156 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
4062
changeTab(withDirection: .backward, from: page)
4163
case .closeTab:
4264
closeTab(from: page)
65+
case .updateSettings:
66+
updateSettings(page: page)
67+
case .none:
68+
NSLog("Received message with unsupported type: \(messageName)")
4369
}
4470
}
4571

46-
func openInNewTab(url: URL) {
72+
override func toolbarItemClicked(in _: SFSafariWindow) {
73+
// This method will be called when your toolbar item is clicked.
74+
NSLog("The extension's toolbar item was clicked")
75+
NSWorkspace.shared.launchApplication(Constant.mainAppName)
76+
}
77+
78+
override func validateToolbarItem(in _: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {
79+
// This is called when Safari's state changed in some way that would require the extension's toolbar item to be validated again.
80+
validationHandler(true, "")
81+
}
82+
83+
override func popoverViewController() -> SFSafariExtensionViewController {
84+
return SafariExtensionViewController.shared
85+
}
86+
87+
// MARK: Tabs Methods
88+
89+
private func openInNewTab(url: URL) {
4790
SFSafariApplication.getActiveWindow { activeWindow in
4891
activeWindow?.openTab(with: url, makeActiveIfPossible: false, completionHandler: { _ in
4992
// Perform some action here after the page loads
5093
})
5194
}
5295
}
5396

54-
func openNewTab() {
55-
// Ideally this URL would be something that represents an empty tab better than localhost
56-
let url = URL(string: "http://localhost")!
97+
private func openNewTab() {
98+
var newPageUrl: String? = getSetting("openTabUrl") as? String
99+
if newPageUrl == nil || newPageUrl!.isEmpty {
100+
newPageUrl = Constant.newTabPageURL
101+
}
102+
let url = URL(string: newPageUrl!)!
57103
SFSafariApplication.getActiveWindow { activeWindow in
58104
activeWindow?.openTab(with: url, makeActiveIfPossible: true, completionHandler: { _ in
59105
// Perform some action here after the page loads
60106
})
61107
}
62108
}
63109

64-
func changeTab(withDirection direction: TabDirection, from page: SFSafariPage, completionHandler: (() -> Void)? = nil ) {
65-
page.getContainingTab(completionHandler: { currentTab in
66-
currentTab.getContainingWindow(completionHandler: { window in
67-
window?.getAllTabs(completionHandler: { tabs in
110+
private func changeTab(withDirection direction: TabDirection, from page: SFSafariPage, completionHandler: (() -> Void)? = nil ) {
111+
page.getContainingTab() { currentTab in
112+
// Using .currentWindow instead of .containingWindow, this prevents the window being nil in the case of a pinned tab.
113+
self.currentWindow(from: page) { window in
114+
window?.getAllTabs() { tabs in
115+
tabs.forEach { tab in NSLog(tab.description) }
68116
if let currentIndex = tabs.firstIndex(of: currentTab) {
69117
let indexStep = direction == TabDirection.forward ? 1 : -1
70118

71119
// Wrap around the ends with a modulus operator.
72120
// % calculates the remainder, not the modulus, so we need a
73121
// custom function.
74122
let newIndex = mod(currentIndex + indexStep, tabs.count)
75-
123+
76124
tabs[newIndex].activate(completionHandler: completionHandler ?? {})
77-
125+
78126
}
79-
})
80-
})
81-
})
127+
}
128+
}
129+
}
130+
}
131+
132+
/**
133+
Returns the containing window of a SFSafariPage, if not available default to the current active window.
134+
*/
135+
private func currentWindow(from page: SFSafariPage, completionHandler: @escaping ((SFSafariWindow?) -> Void)) {
136+
page.getContainingTab() { $0.getContainingWindow() { window in
137+
if window != nil {
138+
return completionHandler(window)
139+
} else {
140+
SFSafariApplication.getActiveWindow() { window in
141+
return completionHandler(window)
142+
}
143+
}
144+
}}
82145
}
83146

84-
func closeTab(from page: SFSafariPage) {
147+
private func closeTab(from page: SFSafariPage) {
85148
page.getContainingTab {
86149
tab in
87150
tab.close()
88151
}
89152
}
153+
154+
// MARK: Settings
90155

91-
override func toolbarItemClicked(in _: SFSafariWindow) {
92-
// This method will be called when your toolbar item is clicked.
93-
NSLog("The extension's toolbar item was clicked")
94-
NSWorkspace.shared.launchApplication("Vimari")
156+
private func getSetting(_ settingKey: String) -> Any? {
157+
do {
158+
let settings = try configuration.getUserSettings()
159+
return settings[settingKey]
160+
} catch {
161+
NSLog("Was not able to retrieve the user settings\n\(error.localizedDescription)")
162+
return nil
163+
}
164+
}
165+
166+
private func updateSettings(page: SFSafariPage) {
167+
do {
168+
let settings: [String: Any]
169+
if let userSettings = try? configuration.getUserSettings() {
170+
settings = userSettings
171+
} else {
172+
settings = try configuration.getDefaultSettings()
173+
}
174+
page.dispatch(settings: settings)
175+
} catch {
176+
NSLog(error.localizedDescription)
177+
}
178+
}
179+
180+
private func fallbackSettings(page: SFSafariPage) {
181+
do {
182+
let settings = try configuration.getUserSettings()
183+
page.dispatch(settings: settings)
184+
} catch {
185+
NSLog(error.localizedDescription)
186+
}
95187
}
188+
}
96189

97-
override func validateToolbarItem(in _: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {
98-
// This is called when Safari's state changed in some way that would require the extension's toolbar item to be validated again.
99-
validationHandler(true, "")
190+
// MARK: Helpers
191+
192+
private func mod(_ a: Int, _ n: Int) -> Int {
193+
// https://stackoverflow.com/questions/41180292/negative-number-modulo-in-swift
194+
precondition(n > 0, "modulus must be positive")
195+
let r = a % n
196+
return r >= 0 ? r : r + n
197+
}
198+
199+
private extension SFSafariPage {
200+
func dispatch(settings: [String: Any]) {
201+
self.dispatchMessageToScript(
202+
withName: "updateSettingsEvent",
203+
userInfo: settings
204+
)
100205
}
206+
}
101207

102-
override func popoverViewController() -> SFSafariExtensionViewController {
103-
return SafariExtensionViewController.shared
208+
private extension SFSafariApplication {
209+
static func getActivePage(completionHandler: @escaping (SFSafariPage?) -> Void) {
210+
SFSafariApplication.getActiveWindow {
211+
$0?.getActiveTab {
212+
$0?.getActivePage(completionHandler: completionHandler)
213+
}
214+
}
104215
}
105216
}
217+

0 commit comments

Comments
 (0)