BTHLABS-58: Share Extension in Apple Apps

This commit is contained in:
2025-10-04 08:02:13 +02:00
parent 0c12f52569
commit 99e9226338
122 changed files with 5488 additions and 411 deletions

View File

@@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors introduced in macOS 10.14" minToolsVersion="10.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ShareViewController">
<connections>
<outlet property="progressIndicator" destination="3hY-1S-Wo2" id="Dpk-yi-LVA"/>
<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="258"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="339-Mg-wWu">
<rect key="frame" x="163" y="174" width="64" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="icon-mac-384" id="NT0-XU-t9f"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KZW-gY-pvX">
<rect key="frame" x="18" y="138" width="352" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="urI-Z1-yMm">
<font key="font" metaFont="system" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<customView id="ERc-i0-Lfz" userLabel="Error View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hwr-tf-MxL">
<rect key="frame" x="140" y="-7" width="69" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="7Kd-FS-0AY">
<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="yRt-GR-jQ6"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5X8-4n-wWm">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="HotPocket couldn't complete this operation." id="fmg-RT-3FA">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="95C-iI-GaL">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="multiply.circle.fill" catalog="system" id="iXK-aQ-lIy"/>
<color key="contentTintColor" name="DangerColor"/>
</imageView>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.errorViewHidden" id="WUX-Pk-WLH"/>
</connections>
</customView>
<customView id="lQA-9N-11c" userLabel="Done View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="QBX-Dv-VAz">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="checkmark.circle.fill" catalog="system" id="iFW-1s-X0l"/>
<color key="contentTintColor" name="SuccessColor"/>
</imageView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DFX-X0-zX8">
<rect key="frame" x="140" y="-7" width="69" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="2jG-9M-YQb">
<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="close:" target="-2" id="3aP-Lu-EzX"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mfx-pW-oi2">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="Your link has been saved!" id="JhJ-K4-UFb">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.doneViewHidden" id="4mN-62-nDm"/>
</connections>
</customView>
<customView id="XMP-x8-OMJ" userLabel="Needs Setup View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="p8k-QM-ZwX">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="3kO-Gq-csg"/>
<color key="contentTintColor" name="WarningColor"/>
</imageView>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YLC-Bx-qKZ">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="Open the HotPocket application to set it up." id="eYb-eq-cbo">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ufJ-HY-8Ir">
<rect key="frame" x="136" y="-7" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="aH5-RV-e1O">
<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="ZRZ-Es-NM6"/>
</connections>
</button>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.needsSetupViewHidden" id="zQw-vW-9Y4"/>
</connections>
</customView>
<customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Szo-4N-z5M" userLabel="Saving View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="b64-XQ-Anx">
<rect key="frame" x="136" y="-7" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="7U4-so-kvt">
<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="Aia-vP-eiA"/>
</connections>
</button>
<progressIndicator fixedFrame="YES" maxValue="100" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="3hY-1S-Wo2">
<rect key="frame" x="158" y="52" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.savingViewHidden" id="HNo-aa-0OR"/>
</connections>
</customView>
<customView id="cFw-BG-nDd" userLabel="Unprocessable Entity View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="hQi-Zt-thw">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="66K-cT-2Vw"/>
<color key="contentTintColor" name="WarningColor"/>
</imageView>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LS4-qN-h75">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="This item couldn't be shared :(." id="b0i-Lf-21f">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cDA-ec-njT">
<rect key="frame" x="136" y="-7" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="lr2-gc-RFv">
<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="yWF-hM-ejy"/>
</connections>
</button>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.unprocessableEntityViewHidden" id="lqC-lO-ll8"/>
</connections>
</customView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1yJ-sU-Spr" userLabel="uname Label">
<rect key="frame" x="6" y="4" width="376" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" id="nQ0-Es-oIB">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="controlAccentColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<connections>
<binding destination="-2" name="value" keyPath="self.uname" id="rKp-CV-Mpj"/>
</connections>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="132" y="-45"/>
</customView>
</objects>
<resources>
<image name="checkmark.circle.fill" catalog="system" width="15" height="15"/>
<image name="exclamationmark.circle.fill" catalog="system" width="15" height="15"/>
<image name="icon-mac-384" width="384" height="384"/>
<image name="multiply.circle.fill" catalog="system" width="15" height="15"/>
<namedColor name="DangerColor">
<color red="0.93300002813339233" green="0.3919999897480011" blue="0.46299999952316284" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SuccessColor">
<color red="0.054901960784313725" green="0.65490196078431373" blue="0.40392156862745099" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="WarningColor">
<color red="0.94509803921568625" green="0.58823529411764708" blue="0.1803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -0,0 +1,29 @@
<?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>CFBundleIconFile</key>
<string>icon</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>ShareExtensionHelper</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>ShareViewController</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
//
// ShareViewController.h
// macOS (Share Extension)
//
// Created by Tomek Wójcik on 22/09/2025.
//
#import <Cocoa/Cocoa.h>
@class HPAPI;
@interface ShareViewController : NSViewController
@property HPAPI *api;
@property BOOL savingViewHidden;
@property BOOL needsSetupViewHidden;
@property BOOL doneViewHidden;
@property BOOL errorViewHidden;
@property BOOL unprocessableEntityViewHidden;
@property NSString *uname;
@property IBOutlet NSProgressIndicator *progressIndicator;
@end

View File

@@ -0,0 +1,128 @@
//
// ShareViewController.m
// macOS (Share Extension)
//
// Created by Tomek Wójcik on 22/09/2025.
//
#import "ShareViewController.h"
#import "HPAPI.h"
#import "HPShareExtensionHelper.h"
@implementation ShareViewController (ShareViewControllerPrivate)
#pragma mark - Private interface
-(void)saveURL:(NSURL *)url {
#ifdef DEBUG
NSLog(@"-[ShareViewController save:] url=`%@`", url);
#endif
BOOL callResult = [self.api save:url completionHandler:^(NSString * _Nullable callId, HPRPCCallResult * _Nullable result) {
dispatch_async(dispatch_get_main_queue(), ^{
self.savingViewHidden = YES;
if (result.error != nil) {
#ifdef DEBUG
NSLog(@"-[ShareViewController resolveLinkAndSave] saveError=`%@`", result.error);
#endif
self.errorViewHidden = NO;
} else {
self.doneViewHidden = NO;
}
});
}];
if (callResult == NO) {
self.savingViewHidden = YES;
self.errorViewHidden = NO;
}
}
-(void)resolveLinkAndSave {
HPShareExtensionHelper *helper = [[HPShareExtensionHelper alloc] initWithContext:self.extensionContext];
[helper processItems:^(NSURL *url) {
if (url == nil) {
self.savingViewHidden = YES;
self.unprocessableEntityViewHidden = NO;
} else {
[self saveURL:url];
}
}];
}
@end
@implementation ShareViewController
#pragma mark - View lifecycle
-(NSString *)nibName {
return @"ShareViewController";
}
-(void)viewDidLoad {
[super viewDidLoad];
self.savingViewHidden = NO;
self.needsSetupViewHidden = YES;
self.doneViewHidden = YES;
self.errorViewHidden = YES;
self.unprocessableEntityViewHidden = YES;
NSBundle *mainBundle = [NSBundle mainBundle];
self.uname = [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
self.api = [[HPAPI alloc] init];
if (self.api.rpcClient.hasCredentials == YES) {
self.savingViewHidden = NO;
self.needsSetupViewHidden = YES;
} else {
self.savingViewHidden = YES;
self.needsSetupViewHidden = NO;
}
}
-(void)viewWillAppear {
[super viewWillAppear];
[self.progressIndicator startAnimation:self];
}
-(void)viewDidAppear {
[super viewDidAppear];
[self.api checkAuth:^(BOOL authValid, NSError *error, NSString *callId) {
dispatch_async(dispatch_get_main_queue(), ^{
if (authValid == NO) {
#ifdef DEBUG
NSLog(@"-[ShareViewController viewDidAppear:] checkAuthError=`%@`", error);
#endif
self.savingViewHidden = YES;
self.needsSetupViewHidden = NO;
} else {
[self resolveLinkAndSave];
}
});
}];
}
-(void)viewDidDisappear {
[super viewDidDisappear];
[self.progressIndicator stopAnimation:self];
}
#pragma mark - Actions
-(IBAction)close:(id)sender {
NSExtensionItem *outputItem = [[NSExtensionItem alloc] init];
NSArray *outputItems = @[outputItem];
[self.extensionContext completeRequestReturningItems:outputItems completionHandler:nil];
}
-(IBAction)cancel:(id)sender {
NSError *cancelError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil];
[self.extensionContext cancelRequestWithError:cancelError];
}
@end

Binary file not shown.

View File

@@ -0,0 +1,21 @@
<?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.application-groups</key>
<array>
<string>group.pl.bthlabs.HotPocket</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket.ShareExtension</string>
</array>
</dict>
</plist>