Skip to content

Commit

Permalink
feat: Add iOS share extension (#696)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbmorley authored Jun 29, 2023
1 parent 8428206 commit ab37eb7
Show file tree
Hide file tree
Showing 27 changed files with 805 additions and 28 deletions.
9 changes: 9 additions & 0 deletions Bookmarks.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
"version": "1.0.0"
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup.git",
"state": {
"branch": null,
"revision": "8b6cf29eead8841a1fa7822481cb3af4ddaadba6",
"version": "2.6.1"
}
},
{
"package": "WrappingHStack",
"repositoryURL": "https://github.com/ksemianov/WrappingHStack.git",
Expand Down
2 changes: 2 additions & 0 deletions core/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let package = Package(
.package(path: "./../SelectableCollectionView"),
.package(url: "https://github.com/ksemianov/WrappingHStack.git", branch: "main"),
.package(url: "https://github.com/saramah/HashRainbow.git", branch: "main"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
],
targets: [
.target(
Expand All @@ -33,6 +34,7 @@ let package = Package(
.product(name: "TFHpple", package: "hpple"),
"WrappingHStack",
"HashRainbow",
"SwiftSoup",
.product(name: "SelectableCollectionView",
package: "SelectableCollectionView",
condition: .when(platforms: [.macOS])),
Expand Down
4 changes: 2 additions & 2 deletions core/Sources/BookmarksCore/Extensions/Publisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Combine

extension Publisher {

func asyncMap<T>(_ transform: @escaping (Output) async -> T) -> Publishers.FlatMap<Future<T, Never>, Self> {
public func asyncMap<T>(_ transform: @escaping (Output) async -> T) -> Publishers.FlatMap<Future<T, Never>, Self> {
flatMap { value in
Future { promise in
Task {
Expand All @@ -33,7 +33,7 @@ extension Publisher {
}
}

func asyncMap<T>(_ transform: @escaping (Output) async throws -> T) -> Publishers.FlatMap<Future<T, Error>, Self> {
public func asyncMap<T>(_ transform: @escaping (Output) async throws -> T) -> Publishers.FlatMap<Future<T, Error>, Self> {
flatMap { value in
Future { promise in
Task {
Expand Down
14 changes: 14 additions & 0 deletions core/Sources/BookmarksCore/Extensions/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import Foundation

import SwiftSoup

enum URLFormat {
case short
}
Expand Down Expand Up @@ -69,4 +71,16 @@ extension URL: Identifiable {
}
}

public func document() async throws -> SwiftSoup.Document {
let (data, _) = try await URLSession.shared.data(from: self)
guard let html = String(data: data, encoding: .utf8) else {
throw BookmarksError.invalidResponse
}
return try SwiftSoup.parse(html, self.absoluteString)
}

public func title() async throws -> String? {
return try await document().title()
}

}
2 changes: 1 addition & 1 deletion core/Sources/BookmarksCore/Model/ItemViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ItemViewModel: ObservableObject, Runnable {
if let post = result.posts.first {
DispatchQueue.main.async {
self.post = post
self.title = post.description ?? "" // TODO: Update the model to remove the optionality here.
self.title = post.description
self.tokens = post.tags
}
}
Expand Down
29 changes: 22 additions & 7 deletions core/Sources/BookmarksCore/Modifiers/Dismissable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,56 @@

import SwiftUI

enum DismissableAction {
public enum DismissableAction {
case close
case done
case cancel
}

struct Dismissable: ViewModifier {

@Environment(\.dismiss) var dismiss

let action: DismissableAction
private let action: DismissableAction
private let perform: (() -> Void)?

var placement: ToolbarItemPlacement {
private var placement: ToolbarItemPlacement {
switch action {
case .close:
return .cancellationAction
case .done:
return .confirmationAction
case .cancel:
return .cancellationAction
}
}

var text: String {
private var text: String {
switch action {
case .close:
return "Close"
case .done:
return "Done"
case.cancel:
return "Cancel"
}
}

init(action: DismissableAction, perform: (() -> Void)? = nil) {
self.action = action
self.perform = perform
}

func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: placement) {
Button {
dismiss()
if let perform {
perform()
} else {
dismiss()
}
} label: {
Text(text)
}
Expand All @@ -66,8 +81,8 @@ struct Dismissable: ViewModifier {

extension View {

func dismissable(_ action: DismissableAction) -> some View {
return modifier(Dismissable(action: action))
public func dismissable(_ action: DismissableAction, perform: (() -> Void)? = nil) -> some View {
return modifier(Dismissable(action: action, perform: perform))
}

}
7 changes: 4 additions & 3 deletions core/Sources/BookmarksCore/Pinboard/Pinboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ public class Pinboard {
}
}

func postsAdd(_ post: Post) async throws -> Result {
// TODO: This is a duplciate?
// TODO: Post _MUST_ have a description?
public func postsAdd(_ post: Post) async throws -> Result {
guard let url = post.href?.absoluteString else {
throw BookmarksError.malformedBookmark
}
Expand All @@ -151,7 +153,6 @@ public class Pinboard {
private func postsAdd(post: Post, replace: Bool, completion: @escaping (Swift.Result<Void, Error>) -> Void) {
let completion = DispatchQueue.global().asyncClosure(completion)
guard let url = post.href?.absoluteString,
let description = post.description,
let date = post.time else {
completion(.failure(BookmarksError.malformedBookmark))
return
Expand All @@ -162,7 +163,7 @@ public class Pinboard {

let parameters: [String: String] = [
"url": url,
"description": description,
"description": post.description,
"extended": post.extended,
"tags": post.tags.joined(separator: " "),
"dt": dt,
Expand Down
8 changes: 4 additions & 4 deletions core/Sources/BookmarksCore/Pinboard/Post.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ extension Pinboard {
// https://github.com/inseven/bookmarks/issues/216
public struct Post: Codable {

public var description: String?
public var description: String
public var extended: String
public var hash: String
public var href: URL?
Expand All @@ -48,7 +48,7 @@ extension Pinboard {
case toRead = "toread"
}

init(href: URL,
public init(href: URL,
description: String = "",
extended: String = "",
hash: String = "",
Expand Down Expand Up @@ -78,7 +78,7 @@ extension Pinboard {
} catch DecodingError.typeMismatch {
// We double check that we can parse the key as a boolean to ensure the structure is as we expect.
let _ = try container.decode(Bool.self, forKey: .description)
description = nil
description = ""
}

extended = try container.decode(String.self, forKey: .extended)
Expand All @@ -103,7 +103,7 @@ extension Bookmark {
return nil
}
self.init(identifier: post.hash,
title: post.description ?? "",
title: post.description,
url: url,
tags: Set(post.tags),
date: date,
Expand Down
2 changes: 1 addition & 1 deletion core/Sources/BookmarksCore/Pinboard/Result.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Foundation

extension Pinboard {

struct Result: Codable {
public struct Result: Codable {

private enum CodingKeys: String, CodingKey {
case resultCode = "result_code"
Expand Down
24 changes: 24 additions & 0 deletions ios/Bookmarks Share Extension/Base.lproj/MainInterface.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
78 changes: 78 additions & 0 deletions ios/Bookmarks Share Extension/Base.lproj/ShareViewController.xib
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13150" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13150"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ShareViewController" customModuleProvider="target">
<connections>
<outlet property="view" destination="1" id="2"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="1">
<rect key="frame" x="0.0" y="0.0" width="388" height="202"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1uM-r7-H1c">
<rect key="frame" x="302" y="3" width="82" height="32"/>
<buttonCell key="cell" type="push" title="Send" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="2l4-PO-we5">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent">D</string>
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
</buttonCell>
<connections>
<action selector="send:" target="-2" id="yic-EC-GGk"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NVE-vN-dkz">
<rect key="frame" x="224" y="3" width="82" height="32"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="60" id="cP1-hK-9ZX"/>
</constraints>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6Up-t3-mwm">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="Qav-AK-DGt"/>
</connections>
</button>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aNc-0i-CWK">
<rect key="frame" x="140" y="170" width="108" height="17"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="My Service Title" id="0xp-rC-2gr">
<font key="font" metaFont="systemBold"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4M6-D5-WIf">
<rect key="frame" x="110" y="170" width="22" height="22"/>
<constraints>
<constraint firstAttribute="width" constant="22" id="BOe-aZ-Njc"/>
<constraint firstAttribute="height" constant="22" id="zLg-1a-wlZ"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="q3u-Am-ZIA"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="1uM-r7-H1c" firstAttribute="leading" secondItem="NVE-vN-dkz" secondAttribute="trailing" constant="8" id="1UO-J1-LbJ"/>
<constraint firstItem="NVE-vN-dkz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" symbolic="YES" id="3N9-qo-UfS"/>
<constraint firstAttribute="bottom" secondItem="1uM-r7-H1c" secondAttribute="bottom" constant="10" id="4wH-De-nMF"/>
<constraint firstAttribute="bottom" secondItem="NVE-vN-dkz" secondAttribute="bottom" constant="10" id="USG-Gg-of3"/>
<constraint firstItem="1uM-r7-H1c" firstAttribute="leading" secondItem="NVE-vN-dkz" secondAttribute="trailing" constant="8" id="a8N-vS-Ew9"/>
<constraint firstItem="aNc-0i-CWK" firstAttribute="centerY" secondItem="4M6-D5-WIf" secondAttribute="centerY" constant="2.5" id="ilP-G0-GVG"/>
<constraint firstItem="NVE-vN-dkz" firstAttribute="width" secondItem="1uM-r7-H1c" secondAttribute="width" id="qPo-ky-Fcw"/>
<constraint firstAttribute="trailing" secondItem="1uM-r7-H1c" secondAttribute="trailing" constant="10" id="qfT-cw-QQ2"/>
<constraint firstAttribute="centerX" secondItem="aNc-0i-CWK" secondAttribute="centerX" id="uV3-Wn-RA3"/>
<constraint firstItem="aNc-0i-CWK" firstAttribute="leading" secondItem="4M6-D5-WIf" secondAttribute="trailing" constant="10" id="vFR-5i-Dvo"/>
<constraint firstItem="aNc-0i-CWK" firstAttribute="top" secondItem="1" secondAttribute="top" constant="15" id="vpR-tf-ebx"/>
</constraints>
</customView>
</objects>
</document>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.uk.co.inseven.bookmarks</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
37 changes: 37 additions & 0 deletions ios/Bookmarks Share Extension/Extensions/NSItemProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2020-2023 InSeven Limited
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation

extension NSItemProvider {

func item(typeIdentifier: String) async throws -> Any? {
try await withCheckedThrowingContinuation { continuation in
loadItem(forTypeIdentifier: typeIdentifier) { object, error in
if let error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: error)
}
}
}

}
Loading

0 comments on commit ab37eb7

Please sign in to comment.