Repository: xbmc/Official-Kodi-Remote-iOS
Pull request #1254: Feature: Share URL of item
Author: login: wutschel
Labels: name: feature
Created: 2025-03-15T09:36:25Z | Merged: 2025-05-07T18:22:26Z

## Description
<!--- Detailed info for reviewers and developers -->
Closes https://github.com/xbmc/Official-Kodi-Remote-iOS/issues/702.

This PR adds the option to share an item's URL with other applications or copying it to clipboard. This URL can be used as download link to play/download the selected item through another application on your iOS device, e.g. by pasting the URL to VLC. The item must be a downloadable file, the Remote App will not offer the option in other cases. The Share menu will show the item's label and a thumbnail (in case no item thumbnail is available the Remote App's logo is shown).

Screenshots: https://ibb.co/zhLMyWHx

Important: The iOS Remote App will not implement local playback itself as the iOS `AVPlayer` has *very* limited support for audio and video formats.

## Summary for release notes
<!--- This will be shown in Testflight to end users / testers -->
<!--- Please fill in, as usually PR title isn't clear to ordinary users -->
<!--- You can keep it empty if it's identical to the PR title though -->
<!--- If your changes don't modify app files, please add prefix [not app] -->
Feature: Share URL of item


Diff:
diff --git a/Kodi Remote.xcodeproj/project.pbxproj b/Kodi Remote.xcodeproj/project.pbxproj
index 240e6e74b..d49492ee2 100644
--- a/Kodi Remote.xcodeproj/project.pbxproj	
+++ b/Kodi Remote.xcodeproj/project.pbxproj	
@@ -100,6 +100,8 @@
 		C76594B62C5E885B00B93A2A /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0FE6087E164B3094009CA3A9 /* Roboto-Regular.ttf */; };
 		C76594B72C5E8A5B00B93A2A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C70F41D02BA4D98E00847C75 /* PrivacyInfo.xcprivacy */; };
 		C78C30F528F877870055CD95 /* VersionCheck.m in Sources */ = {isa = PBXBuildFile; fileRef = C78C30F428F877870055CD95 /* VersionCheck.m */; };
+		C78C30FA28F8AADA0055CD95 /* SharingActivityItemSource.m in Sources */ = {isa = PBXBuildFile; fileRef = C78C30F928F89A3E0055CD95 /* SharingActivityItemSource.m */; };
+		C795123F2DC95A7E00A8CEE5 /* LinkPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C795123E2DC95A7E00A8CEE5 /* LinkPresentation.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -315,6 +317,9 @@
 		C78C30F628F8779A0055CD95 /* VersionCheck.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VersionCheck.h; sourceTree = "<group>"; };
 		C79F6B322CBBF981009A785C /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
 		C79F6B332CBBF981009A785C /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
+		C78C30F828F89A1E0055CD95 /* SharingActivityItemSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SharingActivityItemSource.h; sourceTree = "<group>"; };
+		C78C30F928F89A3E0055CD95 /* SharingActivityItemSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SharingActivityItemSource.m; sourceTree = "<group>"; };
+		C795123E2DC95A7E00A8CEE5 /* LinkPresentation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LinkPresentation.framework; path = System/Library/Frameworks/LinkPresentation.framework; sourceTree = SDKROOT; };
 		C7A030F128B60DB200B27764 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ca-ES"; path = "ca-ES.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
 		C7A030F228B60DB200B27764 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ca-ES"; path = "ca-ES.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		C7A030F328B60EB900B27764 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -331,6 +336,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				C795123F2DC95A7E00A8CEE5 /* LinkPresentation.framework in Frameworks */,
 				0F5548FB151D1187007E633F /* UIKit.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -513,6 +519,7 @@
 		0F5548F9151D1187007E633F /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				C795123E2DC95A7E00A8CEE5 /* LinkPresentation.framework */,
 				C7432A6D2B12195600100E83 /* WebKit.framework */,
 				C703692827148CEF0049F9BF /* StoreKit.framework */,
 				C718444525FFD06300F5A060 /* SafariServices.framework */,
@@ -598,6 +605,7 @@
 				0F7F0DC71703490A0098FF75 /* BDKCollectionIndexView */,
 				0F05FB66170D79B000BBA833 /* NSString+MD5 */,
 				0FDC8917170DCAEA00419F58 /* SVPullToRefresh */,
+				C78C30F728F899DF0055CD95 /* SharingActivityItemSource */,
 				0F7983891C26ABBD002863D0 /* Network */,
 			);
 			name = classes;
@@ -702,6 +710,15 @@
 			path = SVPullToRefresh;
 			sourceTree = "<group>";
 		};
+		C78C30F728F899DF0055CD95 /* SharingActivityItemSource */ = {
+			isa = PBXGroup;
+			children = (
+				C78C30F828F89A1E0055CD95 /* SharingActivityItemSource.h */,
+				C78C30F928F89A3E0055CD95 /* SharingActivityItemSource.m */,
+			);
+			path = SharingActivityItemSource;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -857,6 +874,7 @@
 				0FB133AA165EC1E5003756C1 /* tcpJSONRPC.m in Sources */,
 				C76451DB29C51221000AE949 /* UIImageView+WebCache.m in Sources */,
 				0FEA79FC165FBBED00BB281F /* XBMCVirtualKeyboard.m in Sources */,
+				C78C30FA28F8AADA0055CD95 /* SharingActivityItemSource.m in Sources */,
 				0F90904516E53C590021EFA9 /* Utilities.m in Sources */,
 				0FEAE76816EA632800387DED /* ActorCell.m in Sources */,
 				0F00207816EE247A003822B5 /* OBSlider.m in Sources */,
diff --git a/XBMC Remote/DetailViewController.m b/XBMC Remote/DetailViewController.m
index 2af04a513..89ab3a342 100644
--- a/XBMC Remote/DetailViewController.m	
+++ b/XBMC Remote/DetailViewController.m	
@@ -29,6 +29,7 @@
 #import "SettingsValuesViewController.h"
 #import "customButton.h"
 #import "VersionCheck.h"
+#import "SharingActivityItemSource.h"
 
 @interface DetailViewController ()
 - (void)configureView;
@@ -3324,6 +3325,9 @@ - (void)handleLongPress:(UILongPressGestureRecognizer*)activeRecognizer {
                 if (![VersionCheck hasPlayUsingSupport]) {
                     [sheetActions removeObject:LOCALIZED_STR(@"Play using...")];
                 }
+                if ([item[@"file"] length] > 0 && ![item[@"filetype"] isEqualToString:@"directory"]) {
+                    [sheetActions addObject:LOCALIZED_STR(@"Share")];
+                }
                 BOOL isRecording = [self isTimerActiveForItem:item];
                 UIView *showFromView = self.view;
                 CGPoint sheetOrigin = [activeRecognizer locationInView:showFromView];
@@ -3533,6 +3537,9 @@ - (void)actionSheetHandler:(NSString*)actiontitle origin:(CGPoint)origin fromvie
             }
         }];
     }
+    else if ([actiontitle isEqualToString:LOCALIZED_STR(@"Share")]) {
+        [self shareItem:item indexPath:selectedIndexPath];
+    }
     else if ([actiontitle isEqualToString:LOCALIZED_STR(@"Record")] ||
              [actiontitle isEqualToString:LOCALIZED_STR(@"Stop Recording")]) {
         [self recordChannel:item indexPath:selectedIndexPath];
@@ -4224,6 +4231,47 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            
+            [[SDImageCache sharedImageCache] queryDiskCacheForKey:item[@"thumbnail"] done:^(UIImage *image, SDImageCacheType cacheType) {
+                // Image is loaded, now create an show the share action sheet
+                NSArray *activityItems = @[[[SharingActivityItemSource alloc] initWithUrlString:stringURL label:item[@"label"] image:image]];
+                NSArray *excludeActivities = @[
+                    UIActivityTypePostToFacebook,
+                    UIActivityTypePostToTwitter,
+                    UIActivityTypePostToVimeo,
+                    UIActivityTypePostToWeibo,
+                    UIActivityTypePostToTencentWeibo,
+                    UIActivityTypePrint,
+                    UIActivityTypeAddToReadingList,
+                    UIActivityTypePostToFlickr,
+                ];
+                UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];
+                activityController.excludedActivityTypes = excludeActivities;
+                
+                // Origin of popover is the selected item
+                UIView *itemCell = [self getCell:indexPath];
+                
+                // Position the source of the popover
+                UIPopoverPresentationController *popPresenter = [activityController popoverPresentationController];
+                if (popPresenter != nil) {
+                    popPresenter.sourceView = itemCell;
+                    popPresenter.sourceRect = itemCell.bounds;
+                }
+                [self presentViewController:activityController animated:YES completion:nil];
+            }];
+        }
+    }];
+}
+
 - (void)startPlayback:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath shuffle:(BOOL)shuffled {
     [self startPlayback:item indexPath:indexPath using:nil shuffle:shuffled];
 }
diff --git a/XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h b/XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h
new file mode 100644
index 000000000..a1845989f
--- /dev/null
+++ b/XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h	
@@ -0,0 +1,13 @@
+//
+//  SharingActivityItemSource.h
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+@interface SharingActivityItemSource : NSObject <UIActivityItemSource>
+
+- (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image;
+
+@end
diff --git a/XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m b/XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m
new file mode 100644
index 000000000..715f88c18
--- /dev/null
+++ b/XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m	
@@ -0,0 +1,50 @@
+//
+//  SharingActivityItemSource.m
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+#import "SharingActivityItemSource.h"
+
+@import UIKit;
+@import LinkPresentation;
+
+@interface SharingActivityItemSource ()
+
+@property (nonatomic, copy) NSURL *url;
+@property (nonatomic, copy) NSString *label;
+@property (nonatomic, copy) UIImage *thumbnail;
+
+@end
+
+@implementation SharingActivityItemSource
+
+- (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image {
+    if (self = [super init]) {
+        self.url = [NSURL URLWithString:urlString];
+        self.label = label;
+        self.thumbnail = image ?: [UIImage imageNamed:@"app_logo"];
+    }
+    return self;
+}
+
+- (id)activityViewControllerPlaceholderItem:(UIActivityViewController*)activityViewController {
+    return self.url;
+}
+
+- (id)activityViewController:(UIActivityViewController*)activityViewController itemForActivityType:(NSString*)activityType {
+    return self.url;
+}
+
+- (LPLinkMetadata*)activityViewControllerLinkMetadata:(UIActivityViewController*)activityViewController API_AVAILABLE(ios(13.0)) {
+    __auto_type meta = [LPLinkMetadata new];
+    meta.originalURL = self.url;
+    meta.URL = meta.originalURL;
+    meta.title = self.label;
+    meta.imageProvider = [[NSItemProvider alloc] initWithObject:self.thumbnail];
+    return meta;
+}
+
+@end
diff --git a/XBMC Remote/en.lproj/Localizable.strings b/XBMC Remote/en.lproj/Localizable.strings
index 4bbc53b7e..d8ef0cb70 100644
--- a/XBMC Remote/en.lproj/Localizable.strings	
+++ b/XBMC Remote/en.lproj/Localizable.strings	
@@ -91,6 +91,7 @@
 "Recording Details" = "Recording Details";
 "Details" = "Details";
 "Play Trailer" = "Play Trailer";
+"Share" = "Share";
 
 "Playlist" = "Playlist";
 "Details not found" = "Details not found";


Reviews:
user: login: kambala-decapitator
body: `LinkPresentation` framework must be weakly linked to the project (set it as Optional in the Frameworks phase similarly to UIKit)
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T17:19:31Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T04:25:07Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T04:26:48Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T04:29:29Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T04:32:31Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T04:34:05Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T04:47:47Z
user: login: kambala-decapitator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:30:20Z
user: login: kambala-decapitator
body: now all good, just last thing to adjust
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:33:26Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:34:26Z
user: login: kambala-decapitator
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:38:29Z
user: login: wutschel
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:39:34Z
user: login: kambala-decapitator
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T17:48:03Z

Review comments:
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
+            NSArray *excludeActivities = @[
+                UIActivityTypePostToFacebook,
+                UIActivityTypePostToTwitter,
+                UIActivityTypePostToVimeo,
+                UIActivityTypePostToWeibo,
+                UIActivityTypePostToTencentWeibo,
+                UIActivityTypePrint,
+                UIActivityTypeAddToReadingList,
+                UIActivityTypePostToFlickr,
+            ];
+            UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
+            activityController.excludedActivityTypes = excludeActivities;
+            activityController.completionWithItemsHandler = nil;
+            
+            // Origin of popover is center of selected item
+            UIView *cell = [self getCell:indexPath];
+            CGPoint origin = cell.center;
+            
+            // Position the source of the popover
+            UIPopoverPresentationController *popPresenter = [activityController popoverPresentationController];
+            if (popPresenter != nil) {
+                popPresenter.sourceView = activeLayoutView;
+                popPresenter.sourceRect = CGRectMake(origin.x, origin.y, 1, 1);
path: XBMC Remote/DetailViewController.m
user: login: kambala-decapitator
body: sourceView is activeLayoutView, but sourceRect references cell center - is it correct? Maybe easier to set the cell as sourceView and use its local coordinates?
created_at: 2025-05-05T17:14:43Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
+            NSArray *excludeActivities = @[
+                UIActivityTypePostToFacebook,
+                UIActivityTypePostToTwitter,
+                UIActivityTypePostToVimeo,
+                UIActivityTypePostToWeibo,
+                UIActivityTypePostToTencentWeibo,
+                UIActivityTypePrint,
+                UIActivityTypeAddToReadingList,
+                UIActivityTypePostToFlickr,
+            ];
+            UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
+            activityController.excludedActivityTypes = excludeActivities;
+            activityController.completionWithItemsHandler = nil;
+            
+            // Origin of popover is center of selected item
+            UIView *cell = [self getCell:indexPath];
+            CGPoint origin = cell.center;
path: XBMC Remote/DetailViewController.m
user: login: kambala-decapitator
body: bad naming
created_at: 2025-05-05T17:14:55Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
+            NSArray *excludeActivities = @[
+                UIActivityTypePostToFacebook,
+                UIActivityTypePostToTwitter,
+                UIActivityTypePostToVimeo,
+                UIActivityTypePostToWeibo,
+                UIActivityTypePostToTencentWeibo,
+                UIActivityTypePrint,
+                UIActivityTypeAddToReadingList,
+                UIActivityTypePostToFlickr,
+            ];
+            UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
+            activityController.excludedActivityTypes = excludeActivities;
+            activityController.completionWithItemsHandler = nil;
path: XBMC Remote/DetailViewController.m
user: login: kambala-decapitator
body: this line could be dropped
created_at: 2025-05-05T17:15:11Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
path: XBMC Remote/DetailViewController.m
user: login: kambala-decapitator
body: not really necessary to have a variable for this
created_at: 2025-05-05T17:15:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+//
+//  SharingActivityItemSource.m
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+#import "SharingActivityItemSource.h"
+
+@import UIKit;
+@import LinkPresentation;
+
+@implementation SharingActivityItemSource
+
+- (instancetype)initWithUrlString:(NSString*)urlString {
+    if (self = [super init]) {
+        self.url = [NSURL URLWithString:urlString];
+    }
+    return self;
+}
+
+- (id)activityViewControllerPlaceholderItem:(UIActivityViewController*)activityViewController {
+    return self.url;
+}
+
+- (id)activityViewController:(UIActivityViewController*)activityViewController itemForActivityType:(NSString*)activityType {
+    return self.url;
+}
+
+- (LPLinkMetadata*)activityViewControllerLinkMetadata:(UIActivityViewController*)activityViewController API_AVAILABLE(ios(13.0)) {
+    __auto_type meta = [LPLinkMetadata new];
+    meta.originalURL = self.url;
+    meta.URL = meta.originalURL;
+    meta.title = nil;
+    meta.imageProvider = [[NSItemProvider alloc] initWithObject:[UIImage imageNamed:@"AppIcon.png"]];
+    return meta;
+}
+
+- (id)copyWithZone:(NSZone*)zone {
+    id copy = [[[self class] allocWithZone:zone] init];
+    return copy;
+}
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m
user: login: kambala-decapitator
body: why is it needed?
created_at: 2025-05-05T17:16:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+//
+//  SharingActivityItemSource.h
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+@interface SharingActivityItemSource : NSObject <UIActivityItemSource>
+
+- (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image;
+
+@property (nonatomic, copy) NSURL *url;
+@property (nonatomic, copy) NSString *label;
+@property (nonatomic, copy) UIImage *thumbnail;
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h
user: login: kambala-decapitator
body: they should be private
created_at: 2025-05-05T17:18:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+//
+//  SharingActivityItemSource.h
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+@interface SharingActivityItemSource : NSObject <UIActivityItemSource>
+
+- (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image;
+
+@property (nonatomic, copy) NSURL *url;
+@property (nonatomic, copy) NSString *label;
+@property (nonatomic, copy) UIImage *thumbnail;
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h
user: login: wutschel
body: True. Could also use ivars, ok with this?
created_at: 2025-05-07T04:25:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,45 @@
+//
+//  SharingActivityItemSource.m
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+#import "SharingActivityItemSource.h"
+
+@import UIKit;
+@import LinkPresentation;
+
+@implementation SharingActivityItemSource
+
+- (instancetype)initWithUrlString:(NSString*)urlString {
+    if (self = [super init]) {
+        self.url = [NSURL URLWithString:urlString];
+    }
+    return self;
+}
+
+- (id)activityViewControllerPlaceholderItem:(UIActivityViewController*)activityViewController {
+    return self.url;
+}
+
+- (id)activityViewController:(UIActivityViewController*)activityViewController itemForActivityType:(NSString*)activityType {
+    return self.url;
+}
+
+- (LPLinkMetadata*)activityViewControllerLinkMetadata:(UIActivityViewController*)activityViewController API_AVAILABLE(ios(13.0)) {
+    __auto_type meta = [LPLinkMetadata new];
+    meta.originalURL = self.url;
+    meta.URL = meta.originalURL;
+    meta.title = nil;
+    meta.imageProvider = [[NSItemProvider alloc] initWithObject:[UIImage imageNamed:@"AppIcon.png"]];
+    return meta;
+}
+
+- (id)copyWithZone:(NSZone*)zone {
+    id copy = [[[self class] allocWithZone:zone] init];
+    return copy;
+}
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m
user: login: wutschel
body: Just a remnant of the reference code I started with.
created_at: 2025-05-07T04:26:48Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
path: XBMC Remote/DetailViewController.m
user: login: wutschel
body: Remnant of the reference code, will remove it and have fun to squash this.
created_at: 2025-05-07T04:29:29Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
+            NSArray *excludeActivities = @[
+                UIActivityTypePostToFacebook,
+                UIActivityTypePostToTwitter,
+                UIActivityTypePostToVimeo,
+                UIActivityTypePostToWeibo,
+                UIActivityTypePostToTencentWeibo,
+                UIActivityTypePrint,
+                UIActivityTypeAddToReadingList,
+                UIActivityTypePostToFlickr,
+            ];
+            UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
+            activityController.excludedActivityTypes = excludeActivities;
+            activityController.completionWithItemsHandler = nil;
path: XBMC Remote/DetailViewController.m
user: login: wutschel
body: Fine for me, removed it.
created_at: 2025-05-07T04:32:31Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
+            NSArray *excludeActivities = @[
+                UIActivityTypePostToFacebook,
+                UIActivityTypePostToTwitter,
+                UIActivityTypePostToVimeo,
+                UIActivityTypePostToWeibo,
+                UIActivityTypePostToTencentWeibo,
+                UIActivityTypePrint,
+                UIActivityTypeAddToReadingList,
+                UIActivityTypePostToFlickr,
+            ];
+            UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
+            activityController.excludedActivityTypes = excludeActivities;
+            activityController.completionWithItemsHandler = nil;
+            
+            // Origin of popover is center of selected item
+            UIView *cell = [self getCell:indexPath];
+            CGPoint origin = cell.center;
path: XBMC Remote/DetailViewController.m
user: login: wutschel
body: Using `itemCell` now.
created_at: 2025-05-07T04:34:05Z
author_association: COLLABORATOR
diff_hunk: @@ -4224,6 +4230,48 @@ - (void)playerOpen:(NSDictionary*)params indicator:(UIActivityIndicatorView*)cel
     }];
 }
 
+- (void)shareItem:(NSDictionary*)item indexPath:(NSIndexPath*)indexPath {
+    [[Utilities getJsonRPC]
+     callMethod:@"Files.PrepareDownload"
+     withParameters:[NSDictionary dictionaryWithObjectsAndKeys:item[@"file"], @"path", nil]
+     onCompletion:^(NSString *methodName, NSInteger callId, id methodResult, DSJSONRPCError *methodError, NSError *error) {
+        if (!error && !methodError) {
+            GlobalData *obj = [GlobalData getInstance];
+            NSString *serverURL = [NSString stringWithFormat:@"%@:%@/", obj.serverIP, obj.serverPort];
+            NSString *stringURL = [NSString stringWithFormat:@"http://%@%@", serverURL, methodResult[@"details"][@"path"]];
+            NSURL *videoURL = [NSURL URLWithString:stringURL];
+            
+            NSArray *activityItems = @[videoURL];
+            NSArray *applicationActivities = nil;
+            NSArray *excludeActivities = @[
+                UIActivityTypePostToFacebook,
+                UIActivityTypePostToTwitter,
+                UIActivityTypePostToVimeo,
+                UIActivityTypePostToWeibo,
+                UIActivityTypePostToTencentWeibo,
+                UIActivityTypePrint,
+                UIActivityTypeAddToReadingList,
+                UIActivityTypePostToFlickr,
+            ];
+            UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:applicationActivities];
+            activityController.excludedActivityTypes = excludeActivities;
+            activityController.completionWithItemsHandler = nil;
+            
+            // Origin of popover is center of selected item
+            UIView *cell = [self getCell:indexPath];
+            CGPoint origin = cell.center;
+            
+            // Position the source of the popover
+            UIPopoverPresentationController *popPresenter = [activityController popoverPresentationController];
+            if (popPresenter != nil) {
+                popPresenter.sourceView = activeLayoutView;
+                popPresenter.sourceRect = CGRectMake(origin.x, origin.y, 1, 1);
path: XBMC Remote/DetailViewController.m
user: login: wutschel
body: Good point, changed this.
created_at: 2025-05-07T04:47:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+//
+//  SharingActivityItemSource.h
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+@interface SharingActivityItemSource : NSObject <UIActivityItemSource>
+
+- (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image;
+
+@property (nonatomic, copy) NSURL *url;
+@property (nonatomic, copy) NSString *label;
+@property (nonatomic, copy) UIImage *thumbnail;
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h
user: login: kambala-decapitator
body: yes, also fine
created_at: 2025-05-07T17:30:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+//
+//  SharingActivityItemSource.h
+//  Kodi Remote
+//
+//  Created by Buschmann on 09.03.25.
+//  Copyright © 2025 Team Kodi. All rights reserved.
+//
+
+@interface SharingActivityItemSource : NSObject <UIActivityItemSource>
+
+- (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image;
+
+@property (nonatomic, copy) NSURL *url;
+@property (nonatomic, copy) NSString *label;
+@property (nonatomic, copy) UIImage *thumbnail;
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.h
user: login: wutschel
body: Done
created_at: 2025-05-07T17:34:25Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +15,27 @@ @implementation SharingActivityItemSource
 
 - (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image {
     if (self = [super init]) {
-        self.url = [NSURL URLWithString:urlString];
-        self.label = label;
-        self.thumbnail = image ?: [UIImage imageNamed:@"app_logo"];
+        url = [NSURL URLWithString:urlString];
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m
user: login: kambala-decapitator
body: now this is dangerous: this NSURL initialiser returns an "autoreleased" object, meaning there's no strong reference to it, and it will die at some point once autorelease pool decides to collect its toll. That's why using properties is preferred.

actually the same applies to the other ivars as well
created_at: 2025-05-07T17:38:29Z
author_association: COLLABORATOR
diff_hunk: @@ -15,27 +15,27 @@ @implementation SharingActivityItemSource
 
 - (instancetype)initWithUrlString:(NSString*)urlString label:(NSString*)label image:(UIImage*)image {
     if (self = [super init]) {
-        self.url = [NSURL URLWithString:urlString];
-        self.label = label;
-        self.thumbnail = image ?: [UIImage imageNamed:@"app_logo"];
+        url = [NSURL URLWithString:urlString];
path: XBMC Remote/SharingActivityItemSource/SharingActivityItemSource.m
user: login: wutschel
body: Ok, will move private properties.
created_at: 2025-05-07T17:39:34Z
author_association: COLLABORATOR

PR comments:
user: login: wutschel
created_at: 2025-05-07T04:47:55Z
body: > `LinkPresentation` framework must be weakly linked to the project (set it as Optional in the Frameworks phase similarly to UIKit)

Done.
author_association: COLLABORATOR
user: login: wutschel
created_at: 2025-05-07T18:08:55Z
body: You're ok, if I squash most of the commits into a single one, with proper comments of course? Otherwise applying the changes stepwise is a bit tedious...
author_association: COLLABORATOR
user: login: kambala-decapitator
created_at: 2025-05-07T18:12:19Z
body: > You're ok, if I squash most of the commits into a single one, with proper comments of course? Otherwise applying the changes stepwise is a bit tedious...

sure
author_association: COLLABORATOR
user: login: wutschel
created_at: 2025-05-07T18:18:47Z
body: Squashed and rebased to master.
author_association: COLLABORATOR