Skip to content

Commit 43e4756

Browse files
Merge pull request #198 from writefreely/add-extension
Add action extension for Safari
2 parents d926a9f + 92e3190 commit 43e4756

File tree

20 files changed

+748
-19
lines changed

20 files changed

+748
-19
lines changed

ActionExtension-iOS/Action.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
var Action = function() {};
2+
3+
Action.prototype = {
4+
5+
run: function(parameters) {
6+
parameters.completionFunction({
7+
"URL": document.URL,
8+
"title": document.title,
9+
"selection": document.getSelection().toString()
10+
});
11+
},
12+
13+
finalize: function(parameters) {
14+
var customJavaScript = parameters["customJavaScript"];
15+
eval(customJavaScript);
16+
}
17+
18+
};
19+
20+
var ExtensionPreprocessingJS = new Action
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.application-groups</key>
6+
<array>
7+
<string>group.com.abunchtell.writefreely</string>
8+
</array>
9+
</dict>
10+
</plist>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
3+
class ActionViewController: UIViewController {
4+
5+
let moc = LocalStorageManager.standard.container.viewContext
6+
7+
override var prefersStatusBarHidden: Bool { true }
8+
9+
override func viewDidLoad() {
10+
super.viewDidLoad()
11+
12+
let contentView = ContentView()
13+
.environment(\.extensionContext, extensionContext)
14+
.environment(\.managedObjectContext, moc)
15+
16+
view = UIHostingView(rootView: contentView)
17+
view.isOpaque = true
18+
view.backgroundColor = .systemBackground
19+
}
20+
21+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import SwiftUI
2+
import MobileCoreServices
3+
import UniformTypeIdentifiers
4+
import WriteFreely
5+
6+
enum WFActionExtensionError: Error {
7+
case userCancelledRequest
8+
case couldNotParseInputItems
9+
}
10+
11+
struct ContentView: View {
12+
@Environment(\.extensionContext) private var extensionContext: NSExtensionContext!
13+
@Environment(\.managedObjectContext) private var managedObjectContext
14+
15+
@AppStorage(WFDefaults.defaultFontIntegerKey, store: UserDefaults.shared) var fontIndex: Int = 0
16+
17+
@FetchRequest(
18+
entity: WFACollection.entity(),
19+
sortDescriptors: [NSSortDescriptor(keyPath: \WFACollection.title, ascending: true)]
20+
) var collections: FetchedResults<WFACollection>
21+
22+
@State private var draftTitle: String = ""
23+
@State private var draftText: String = ""
24+
@State private var isShowingAlert: Bool = false
25+
@State private var selectedBlog: WFACollection?
26+
27+
private var draftsCollectionName: String {
28+
guard UserDefaults.shared.string(forKey: WFDefaults.serverStringKey) == "https://write.as" else {
29+
return "Drafts"
30+
}
31+
return "Anonymous"
32+
}
33+
34+
private var controls: some View {
35+
HStack {
36+
Group {
37+
Button(
38+
action: { extensionContext.cancelRequest(withError: WFActionExtensionError.userCancelledRequest) },
39+
label: { Image(systemName: "xmark.circle").imageScale(.large) }
40+
)
41+
.accessibilityLabel(Text("Cancel"))
42+
Spacer()
43+
Button(
44+
action: {
45+
savePostToCollection(collection: selectedBlog, title: draftTitle, body: draftText)
46+
extensionContext.completeRequest(returningItems: nil, completionHandler: nil)
47+
},
48+
label: { Image(systemName: "square.and.arrow.down").imageScale(.large) }
49+
)
50+
.accessibilityLabel(Text("Create new draft"))
51+
}
52+
.padding()
53+
}
54+
}
55+
56+
var body: some View {
57+
VStack {
58+
controls
59+
Form {
60+
Section(header: Text("Title")) {
61+
switch fontIndex {
62+
case 1:
63+
TextField("Draft Title", text: $draftTitle).font(.custom("OpenSans-Regular", size: 26))
64+
case 2:
65+
TextField("Draft Title", text: $draftTitle).font(.custom("Hack-Regular", size: 26))
66+
default:
67+
TextField("Draft Title", text: $draftTitle).font(.custom("Lora", size: 26))
68+
}
69+
}
70+
Section(header: Text("Content")) {
71+
switch fontIndex {
72+
case 1:
73+
TextEditor(text: $draftText).font(.custom("OpenSans-Regular", size: 17))
74+
case 2:
75+
TextEditor(text: $draftText).font(.custom("Hack-Regular", size: 17))
76+
default:
77+
TextEditor(text: $draftText).font(.custom("Lora", size: 17))
78+
}
79+
}
80+
Section(header: Text("Save To")) {
81+
Button(action: {
82+
self.selectedBlog = nil
83+
}, label: {
84+
HStack {
85+
Text(draftsCollectionName)
86+
.foregroundColor(selectedBlog == nil ? .primary : .secondary)
87+
Spacer()
88+
if selectedBlog == nil {
89+
Image(systemName: "checkmark")
90+
}
91+
}
92+
})
93+
ForEach(collections, id: \.self) { collection in
94+
Button(action: {
95+
self.selectedBlog = collection
96+
}, label: {
97+
HStack {
98+
Text(collection.title)
99+
.foregroundColor(selectedBlog == collection ? .primary : .secondary)
100+
Spacer()
101+
if selectedBlog == collection {
102+
Image(systemName: "checkmark")
103+
}
104+
}
105+
})
106+
}
107+
}
108+
}
109+
.padding(.bottom, 24)
110+
}
111+
.alert(isPresented: $isShowingAlert, content: {
112+
Alert(
113+
title: Text("Something Went Wrong"),
114+
message: Text("WriteFreely can't create a draft with the data received."),
115+
dismissButton: .default(Text("OK"), action: {
116+
extensionContext.cancelRequest(withError: WFActionExtensionError.couldNotParseInputItems)
117+
}))
118+
})
119+
.onAppear {
120+
do {
121+
try getPageDataFromExtensionContext()
122+
} catch {
123+
self.isShowingAlert = true
124+
}
125+
}
126+
}
127+
128+
private func savePostToCollection(collection: WFACollection?, title: String, body: String) {
129+
let post = WFAPost(context: managedObjectContext)
130+
post.createdDate = Date()
131+
post.title = title
132+
post.body = body
133+
post.status = PostStatus.local.rawValue
134+
post.collectionAlias = collection?.alias
135+
switch fontIndex {
136+
case 1:
137+
post.appearance = "sans"
138+
case 2:
139+
post.appearance = "wrap"
140+
default:
141+
post.appearance = "serif"
142+
}
143+
if let languageCode = Locale.current.languageCode {
144+
post.language = languageCode
145+
post.rtl = Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
146+
}
147+
LocalStorageManager.standard.saveContext()
148+
}
149+
150+
private func getPageDataFromExtensionContext() throws {
151+
if let inputItem = extensionContext.inputItems.first as? NSExtensionItem {
152+
if let itemProvider = inputItem.attachments?.first {
153+
154+
let typeIdentifier: String
155+
156+
if #available(iOS 15, *) {
157+
typeIdentifier = UTType.propertyList.identifier
158+
} else {
159+
typeIdentifier = kUTTypePropertyList as String
160+
}
161+
162+
itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { (dict, error) in
163+
if let error = error {
164+
print("⚠️", error)
165+
self.isShowingAlert = true
166+
}
167+
168+
guard let itemDict = dict as? NSDictionary else {
169+
return
170+
}
171+
guard let jsValues = itemDict[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary else {
172+
return
173+
}
174+
175+
let pageTitle = jsValues["title"] as? String ?? ""
176+
let pageURL = jsValues["URL"] as? String ?? ""
177+
let pageSelectedText = jsValues["selection"] as? String ?? ""
178+
179+
if pageSelectedText.isEmpty {
180+
// If there's no selected text, create a Markdown link to the webpage.
181+
self.draftText = "[\(pageTitle)](\(pageURL))"
182+
} else {
183+
// If there is selected text, create a Markdown blockquote with the selection
184+
// and add a Markdown link to the webpage.
185+
self.draftText = """
186+
> \(pageSelectedText)
187+
188+
Via: [\(pageTitle)](\(pageURL))
189+
"""
190+
}
191+
}
192+
} else {
193+
throw WFActionExtensionError.couldNotParseInputItems
194+
}
195+
} else {
196+
throw WFActionExtensionError.couldNotParseInputItems
197+
}
198+
}
199+
}

ActionExtension-iOS/Info.plist

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSExtension</key>
6+
<dict>
7+
<key>NSExtensionAttributes</key>
8+
<dict>
9+
<key>NSExtensionActivationRule</key>
10+
<dict>
11+
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
12+
<integer>1</integer>
13+
</dict>
14+
<key>NSExtensionJavaScriptPreprocessingFile</key>
15+
<string>Action</string>
16+
<key>NSExtensionServiceAllowsFinderPreviewItem</key>
17+
<true/>
18+
<key>NSExtensionServiceAllowsTouchBarItem</key>
19+
<true/>
20+
<key>NSExtensionServiceFinderPreviewIconName</key>
21+
<string>NSActionTemplate</string>
22+
<key>NSExtensionServiceTouchBarBezelColorName</key>
23+
<string>TouchBarBezel</string>
24+
<key>NSExtensionServiceTouchBarIconName</key>
25+
<string>NSActionTemplate</string>
26+
</dict>
27+
<key>NSExtensionPointIdentifier</key>
28+
<string>com.apple.ui-services</string>
29+
<key>NSExtensionPrincipalClass</key>
30+
<string>$(PRODUCT_MODULE_NAME).ActionViewController</string>
31+
</dict>
32+
<key>UIAppFonts</key>
33+
<array>
34+
<string>LoraGX.ttf</string>
35+
<string>OpenSans-Regular.ttf</string>
36+
<string>Hack-Regular.ttf</string>
37+
</array>
38+
</dict>
39+
</plist>
4.67 KB
Loading
7.34 KB
Loading
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "iphone",
5+
"scale" : "2x",
6+
"size" : "20x20"
7+
},
8+
{
9+
"idiom" : "iphone",
10+
"scale" : "3x",
11+
"size" : "20x20"
12+
},
13+
{
14+
"idiom" : "iphone",
15+
"scale" : "2x",
16+
"size" : "29x29"
17+
},
18+
{
19+
"idiom" : "iphone",
20+
"scale" : "3x",
21+
"size" : "29x29"
22+
},
23+
{
24+
"idiom" : "iphone",
25+
"scale" : "2x",
26+
"size" : "40x40"
27+
},
28+
{
29+
"idiom" : "iphone",
30+
"scale" : "3x",
31+
"size" : "40x40"
32+
},
33+
{
34+
"filename" : "AppIconExtension@2x.png",
35+
"idiom" : "iphone",
36+
"scale" : "2x",
37+
"size" : "60x60"
38+
},
39+
{
40+
"filename" : "AppIconExtension@3x.png",
41+
"idiom" : "iphone",
42+
"scale" : "3x",
43+
"size" : "60x60"
44+
},
45+
{
46+
"idiom" : "ipad",
47+
"scale" : "1x",
48+
"size" : "20x20"
49+
},
50+
{
51+
"idiom" : "ipad",
52+
"scale" : "2x",
53+
"size" : "20x20"
54+
},
55+
{
56+
"idiom" : "ipad",
57+
"scale" : "1x",
58+
"size" : "29x29"
59+
},
60+
{
61+
"idiom" : "ipad",
62+
"scale" : "2x",
63+
"size" : "29x29"
64+
},
65+
{
66+
"idiom" : "ipad",
67+
"scale" : "1x",
68+
"size" : "40x40"
69+
},
70+
{
71+
"idiom" : "ipad",
72+
"scale" : "2x",
73+
"size" : "40x40"
74+
},
75+
{
76+
"idiom" : "ipad",
77+
"scale" : "1x",
78+
"size" : "76x76"
79+
},
80+
{
81+
"idiom" : "ipad",
82+
"scale" : "2x",
83+
"size" : "76x76"
84+
},
85+
{
86+
"idiom" : "ipad",
87+
"scale" : "2x",
88+
"size" : "83.5x83.5"
89+
},
90+
{
91+
"idiom" : "ios-marketing",
92+
"scale" : "1x",
93+
"size" : "1024x1024"
94+
}
95+
],
96+
"info" : {
97+
"author" : "xcode",
98+
"version" : 1
99+
}
100+
}

0 commit comments

Comments
 (0)