Prechádzať zdrojové kódy

refactor VLCNetworkServerBrowser view
try to split model logic from view logic
add NetworkImageView which automatically does caching and supports request cancelation
move alert view logic to UIViewController category which supports tvOS and iOS

Tobias Conradi 9 rokov pred
rodič
commit
41c8276222

+ 1 - 1
Resources/VLCNetworkListCell.xib

@@ -16,7 +16,7 @@
                 <rect key="frame" x="0.0" y="0.0" width="320" height="67"/>
                 <autoresizingMask key="autoresizingMask"/>
                 <subviews>
-                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" translatesAutoresizingMaskIntoConstraints="NO" id="4">
+                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" translatesAutoresizingMaskIntoConstraints="NO" id="4" customClass="VLCNetworkImageView">
                         <rect key="frame" x="8" y="9" width="50" height="50"/>
                         <animations/>
                         <constraints>

+ 16 - 0
SharedSources/ServerBrowsing/UIViewController+VLCAlert.h

@@ -0,0 +1,16 @@
+/*****************************************************************************
+ * VLC for iOS
+ *****************************************************************************
+ * Copyright (c) 2015 VideoLAN. All rights reserved.
+ * $Id$
+ *
+ * Authors: Tobias Conradi <videolan # tobias-conradi.de>
+ *
+ * Refer to the COPYING file of the official project for license.
+ *****************************************************************************/
+
+#import <UIKit/UIKit.h>
+
+@interface UIViewController (UIViewController_VLCAlert)
+- (void)vlc_showAlertWithTitle:(NSString *)title message:(NSString *)message buttonTitle:(NSString *)buttonTitle;
+@end

+ 33 - 0
SharedSources/ServerBrowsing/UIViewController+VLCAlert.m

@@ -0,0 +1,33 @@
+/*****************************************************************************
+ * VLC for iOS
+ *****************************************************************************
+ * Copyright (c) 2015 VideoLAN. All rights reserved.
+ * $Id$
+ *
+ * Authors: Tobias Conradi <videolan # tobias-conradi.de>
+ *
+ * Refer to the COPYING file of the official project for license.
+ *****************************************************************************/
+
+#import "UIViewController+VLCAlert.h"
+
+@implementation UIViewController (UIViewController_VLCAlert)
+#if TARGET_OS_TV
+- (void)vlc_showAlertWithTitle:(NSString *)title message:(NSString *)message buttonTitle:(NSString *)buttonTitle
+{
+    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
+    [alertController addAction:[UIAlertAction actionWithTitle:buttonTitle style:UIAlertActionStyleDefault handler:nil]];
+    [self presentViewController:alertController animated:YES completion:nil];
+}
+#else
+- (void)vlc_showAlertWithTitle:(NSString *)title message:(NSString *)message buttonTitle:(NSString *)buttonTitle
+    {
+    VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:title
+                                                      message:message
+                                                     delegate:self
+                                            cancelButtonTitle:buttonTitle
+                                            otherButtonTitles:nil];
+    [alert show];
+}
+#endif
+@end

Sources/BasicUPnPDevice+VLC.h → SharedSources/ServerBrowsing/UPnP/BasicUPnPDevice+VLC.h


Sources/BasicUPnPDevice+VLC.m → SharedSources/ServerBrowsing/UPnP/BasicUPnPDevice+VLC.m


+ 22 - 0
SharedSources/UI Elements/VLCNetworkImageView.h

@@ -0,0 +1,22 @@
+/*****************************************************************************
+ * VLC for iOS
+ *****************************************************************************
+ * Copyright (c) 2015 VideoLAN. All rights reserved.
+ * $Id$
+ *
+ * Authors: Tobias Conradi <videolan # tobias-conradi.de>
+ *
+ * Refer to the COPYING file of the official project for license.
+ *****************************************************************************/
+
+
+#import <UIKit/UIKit.h>
+
+@interface VLCNetworkImageView : UIImageView
++ (NSCache *)sharedImageCache;
++ (void)setSharedImageCache:(NSCache *)sharedCache;
+@property (nonatomic) NSURLSessionDataTask *downloadTask;
+- (void)setImageWithURL:(NSURL *)url;
+- (void)cancelLoading;
+
+@end

+ 69 - 0
SharedSources/UI Elements/VLCNetworkImageView.m

@@ -0,0 +1,69 @@
+/*****************************************************************************
+ * VLC for iOS
+ *****************************************************************************
+ * Copyright (c) 2015 VideoLAN. All rights reserved.
+ * $Id$
+ *
+ * Authors: Tobias Conradi <videolan # tobias-conradi.de>
+ *
+ * Refer to the COPYING file of the official project for license.
+ *****************************************************************************/
+
+
+#import "VLCNetworkImageView.h"
+
+@implementation VLCNetworkImageView
+
+static NSCache *sharedImageCache = nil;
++ (void)setSharedImageCache:(NSCache *)sharedCache {
+    sharedImageCache = sharedCache;
+}
++ (NSCache *)sharedImageCache {
+    if (!sharedImageCache) {
+        sharedImageCache = [[NSCache alloc] init];
+        [sharedImageCache setCountLimit:50];
+    }
+    return sharedImageCache;
+}
+
+- (UIImage *)cacheImageForURL:(NSURL *)url {
+
+    UIImage *image = [[self.class sharedImageCache] objectForKey:url];
+    if ((image != nil) && [image isKindOfClass:[UIImage class]]) {
+        return image;
+    }
+    return nil;
+}
+
+- (void)cancelLoading {
+    [self.downloadTask cancel];
+    self.downloadTask = nil;
+}
+
+- (void)setImageWithURL:(NSURL *)url {
+
+    [self cancelLoading];
+    UIImage *cachedImage = [self cacheImageForURL:url];
+    if (cachedImage) {
+        self.image = cachedImage;
+    } else {
+        __weak typeof(self) weakSelf = self;
+        NSURLSession *sharedSession = [NSURLSession sharedSession];
+        self.downloadTask = [sharedSession dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
+            if (!data) {
+                return;
+            }
+            UIImage *image = [UIImage imageWithData:data];
+            if (!image) { return; }
+            [[[weakSelf class] sharedImageCache] setObject:image forKey:url];
+            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+                __strong typeof(weakSelf) strongSelf = weakSelf;
+                if ([strongSelf.downloadTask.originalRequest.URL isEqual:url]) {
+                    strongSelf.image = image;
+                    strongSelf.downloadTask = nil;
+                }
+            }];
+        }];
+    }
+}
+@end

+ 2 - 1
Sources/LocalNetworkConnectivity/VLCNetworkListCell.h

@@ -11,6 +11,7 @@
  *****************************************************************************/
 
 #import <UIKit/UIKit.h>
+#import "VLCNetworkImageView.h"
 
 @class VLCStatusLabel;
 
@@ -21,7 +22,7 @@
 @property (nonatomic, strong) IBOutlet UILabel *titleLabel;
 @property (nonatomic, strong) IBOutlet UILabel *folderTitleLabel;
 @property (nonatomic, strong) IBOutlet UILabel *subtitleLabel;
-@property (nonatomic, strong) IBOutlet UIImageView *thumbnailView;
+@property (nonatomic, strong) IBOutlet VLCNetworkImageView *thumbnailView;
 @property (nonatomic, strong) IBOutlet UIButton *downloadButton;
 @property (nonatomic, strong) IBOutlet VLCStatusLabel *statusLabel;
 

+ 5 - 10
Sources/LocalNetworkConnectivity/VLCNetworkListCell.m

@@ -80,16 +80,7 @@
 - (void)setIconURL:(NSURL *)iconURL
 {
     _iconURL = iconURL;
-    [self performSelectorInBackground:@selector(_updateIconFromURL) withObject:@""];
-}
-
-- (void)_updateIconFromURL
-{
-    NSData* imageData = [[NSData alloc]initWithContentsOfURL:self.iconURL];
-    if (!imageData)
-        return;
-    UIImage* image = [[UIImage alloc] initWithData:imageData];
-    [self performSelectorOnMainThread:@selector(setIcon:) withObject:image waitUntilDone:NO];
+    [self.thumbnailView setImageWithURL:iconURL];
 }
 
 - (void)setIsDownloadable:(BOOL)isDownloadable
@@ -103,6 +94,10 @@
         [self.delegate triggerDownloadForCell:self];
 }
 
+- (void)prepareForReuse {
+    [self.thumbnailView cancelLoading];
+}
+
 + (CGFloat)heightOfCell
 {
     if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)

+ 103 - 116
Sources/LocalNetworkConnectivity/VLCNetworkServerBrowserViewController.m

@@ -15,13 +15,13 @@
 #import "VLCNetworkServerBrowserViewController.h"
 #import "VLCNetworkListCell.h"
 #import "VLCActivityManager.h"
-#import "NSString+SupportedMedia.h"
-#import "UIDevice+VLC.h"
 #import "VLCStatusLabel.h"
 #import "VLCPlaybackController.h"
 #import "VLCDownloadViewController.h"
+#import "UIDevice+VLC.h"
+#import "NSString+SupportedMedia.h"
+#import "UIDevice+VLC.h"
 
-#import "WhiteRaccoon.h"
 #import "VLCNetworkServerBrowser-Protocol.h"
 
 @interface VLCNetworkServerBrowserViewController () <VLCNetworkServerBrowserDelegate,VLCNetworkListCellDelegate, UITableViewDataSource, UITableViewDelegate, UIActionSheetDelegate>
@@ -31,7 +31,6 @@
 @property (nonatomic) id<VLCNetworkServerBrowser> serverBrowser;
 @property (nonatomic) NSByteCountFormatter *byteCounterFormatter;
 @property (nonatomic) NSArray<id<VLCNetworkServerBrowserItem>> *searchArray;
-@property (nonatomic, readonly) NSCache *imageCache;
 @end
 
 @implementation VLCNetworkServerBrowserViewController
@@ -42,9 +41,6 @@
     if (self) {
         _serverBrowser = browser;
         browser.delegate = self;
-
-        _imageCache = [[NSCache alloc] init];
-        [_imageCache setCountLimit:50];
     }
     return self;
 }
@@ -71,13 +67,9 @@
 
 - (void)networkServerBrowser:(id<VLCNetworkServerBrowser>)networkBrowser requestDidFailWithError:(NSError *)error {
 
-    VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:NSLocalizedString(@"LOCAL_SERVER_CONNECTION_FAILED_TITLE", nil)
-                                                      message:NSLocalizedString(@"LOCAL_SERVER_CONNECTION_FAILED_MESSAGE", nil)
-                                                     delegate:self
-                                            cancelButtonTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
-                                            otherButtonTitles:nil];
-    [alert show];
-    
+    [self vlc_showAlertWithTitle:NSLocalizedString(@"LOCAL_SERVER_CONNECTION_FAILED_TITLE", nil)
+                         message:NSLocalizedString(@"LOCAL_SERVER_CONNECTION_FAILED_MESSAGE", nil)
+                     buttonTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)];
 }
 
 - (void)update
@@ -124,6 +116,96 @@
                                                      fileNameOfMedia:filename];
 }
 
+- (BOOL)triggerDownloadForItem:(id<VLCNetworkServerBrowserItem>)item
+{
+    if (item.fileSizeBytes.longLongValue  < [[UIDevice currentDevice] freeDiskspace].longLongValue) {
+        [self _downloadItem:item];
+        return YES;
+    } else {
+        NSString *title = NSLocalizedString(@"DISK_FULL", nil);
+        NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"DISK_FULL_FORMAT", nil), item.name, [[UIDevice currentDevice] model]];
+        NSString *button = NSLocalizedString(@"BUTTON_OK", nil);
+        [self vlc_showAlertWithTitle:title message:messsage buttonTitle:button];
+        return NO;
+    }
+}
+
+
+
+- (void)didSelectItem:(id<VLCNetworkServerBrowserItem>)item index:(NSUInteger)index singlePlayback:(BOOL)singlePlayback
+{
+    if (item.isContainer) {
+        VLCNetworkServerBrowserViewController *targetViewController = [[VLCNetworkServerBrowserViewController alloc] initWithServerBrowser:item.containerBrowser];
+        [self.navigationController pushViewController:targetViewController animated:YES];
+    } else {
+        if (singlePlayback) {
+            [self _streamFileForItem:item];
+        } else {
+            VLCMediaList *mediaList = self.serverBrowser.mediaList;
+            [self configureSubtitlesInMediaList:mediaList];
+            [self _streamMediaList:mediaList startingAtIndex:index];
+        }
+    }
+}
+
+
+- (void)configureSubtitlesInMediaList:(VLCMediaList *)mediaList {
+    NSArray *items = self.serverBrowser.items;
+    id<VLCNetworkServerBrowserItem> loopItem;
+    [mediaList lock];
+    NSUInteger count = mediaList.count;
+    for (NSUInteger i = 0; i < count; i++) {
+        loopItem = items[i];
+        NSString *URLofSubtitle = nil;
+        NSURL *remoteSubtitleURL = nil;
+        if ([loopItem respondsToSelector:@selector(subtitleURL)]) {
+            [loopItem subtitleURL];
+        }
+        if (remoteSubtitleURL == nil) {
+            NSArray *subtitlesList = [self _searchSubtitle:loopItem.URL.lastPathComponent];
+            remoteSubtitleURL = subtitlesList.firstObject;
+        }
+
+        if(remoteSubtitleURL != nil) {
+            URLofSubtitle = [self _getFileSubtitleFromServer:remoteSubtitleURL];
+            if (URLofSubtitle != nil)
+                [[mediaList mediaAtIndex:i] addOptions:@{ kVLCSettingSubtitlesFilePath : URLofSubtitle }];
+        }
+    }
+    [mediaList unlock];
+}
+
+
+- (NSString *)_getFileSubtitleFromServer:(NSURL *)subtitleURL
+{
+    NSString *FileSubtitlePath = nil;
+    NSData *receivedSub = [NSData dataWithContentsOfURL:subtitleURL]; // TODO: fix synchronous load
+
+    if (receivedSub.length < [[UIDevice currentDevice] freeDiskspace].longLongValue) {
+        NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
+        NSString *directoryPath = searchPaths[0];
+        FileSubtitlePath = [directoryPath stringByAppendingPathComponent:[subtitleURL lastPathComponent]];
+
+        NSFileManager *fileManager = [NSFileManager defaultManager];
+        if (![fileManager fileExistsAtPath:FileSubtitlePath]) {
+            //create local subtitle file
+            [fileManager createFileAtPath:FileSubtitlePath contents:nil attributes:nil];
+            if (![fileManager fileExistsAtPath:FileSubtitlePath]) {
+                APLog(@"file creation failed, no data was saved");
+                return nil;
+            }
+        }
+        [receivedSub writeToFile:FileSubtitlePath atomically:YES];
+    } else {
+
+        [self vlc_showAlertWithTitle:NSLocalizedString(@"DISK_FULL", nil)
+                                            message:[NSString stringWithFormat:NSLocalizedString(@"DISK_FULL_FORMAT", nil), [subtitleURL lastPathComponent], [[UIDevice currentDevice] model]]
+                                        buttonTitle:NSLocalizedString(@"BUTTON_OK", nil)];
+    }
+    
+    return FileSubtitlePath;
+}
+
 - (void)_streamMediaList:(VLCMediaList *)mediaList startingAtIndex:(NSInteger)startIndex
 {
     VLCPlaybackController *vpc = [VLCPlaybackController sharedInstance];
@@ -168,52 +250,6 @@
     return [NSArray arrayWithArray:urls];
 }
 
-- (NSString *)_getFileSubtitleFromServer:(NSURL *)subtitleURL
-{
-    NSString *FileSubtitlePath = nil;
-    NSData *receivedSub = [NSData dataWithContentsOfURL:subtitleURL]; // TODO: fix synchronous load
-
-    if (receivedSub.length < [[UIDevice currentDevice] freeDiskspace].longLongValue) {
-        NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
-        NSString *directoryPath = searchPaths[0];
-        FileSubtitlePath = [directoryPath stringByAppendingPathComponent:[subtitleURL lastPathComponent]];
-
-        NSFileManager *fileManager = [NSFileManager defaultManager];
-        if (![fileManager fileExistsAtPath:FileSubtitlePath]) {
-            //create local subtitle file
-            [fileManager createFileAtPath:FileSubtitlePath contents:nil attributes:nil];
-            if (![fileManager fileExistsAtPath:FileSubtitlePath]) {
-                APLog(@"file creation failed, no data was saved");
-                return nil;
-            }
-        }
-        [receivedSub writeToFile:FileSubtitlePath atomically:YES];
-    } else {
-        VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:NSLocalizedString(@"DISK_FULL", nil)
-                                                          message:[NSString stringWithFormat:NSLocalizedString(@"DISK_FULL_FORMAT", nil), [subtitleURL lastPathComponent], [[UIDevice currentDevice] model]]
-                                                         delegate:self
-                                                cancelButtonTitle:NSLocalizedString(@"BUTTON_OK", nil)
-                                                otherButtonTitles:nil];
-        [alert show];
-    }
-
-    return FileSubtitlePath;
-}
-
-- (UIImage *)getCachedImage:(NSURL *)url
-{
-    UIImage *image = [self.imageCache objectForKey:url];
-    if ((image != nil) && [image isKindOfClass:[UIImage class]]) {
-        return image;
-    } else {
-        NSData *imageData = [[NSData alloc] initWithContentsOfURL:url];
-        if (imageData) {
-            image = [[UIImage alloc] initWithData:imageData];
-            [self.imageCache setObject:image forKey:url];
-        }
-        return image;
-    }
-}
 
 #pragma mark - table view data source, for more see super
 
@@ -271,19 +307,10 @@
         if ([item respondsToSelector:@selector(thumbnailURL)]) {
             thumbnailURL = item.thumbnailURL;
         }
-        
+
         if (thumbnailURL) {
-            dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
-            dispatch_async(queue, ^{
-                UIImage *img = [self getCachedImage:thumbnailURL];
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    if (img) {
-                        [cell setIcon:img];
-                    }
-                });
-            });
+            [cell setIconURL:thumbnailURL];
         }
-
     }
 
     return cell;
@@ -304,52 +331,20 @@
 {
     id<VLCNetworkServerBrowserItem> item;
     NSInteger row = indexPath.row;
-    BOOL searchResult = NO;
+    BOOL singlePlayback = NO;
     if (tableView == self.searchDisplayController.searchResultsTableView) {
         item = _searchArray[row];
-        searchResult = YES;
+        singlePlayback = YES;
     } else {
         item = self.serverBrowser.items[row];
     }
 
-    if (item.isContainer) {
-        VLCNetworkServerBrowserViewController *targetViewController = [[VLCNetworkServerBrowserViewController alloc] initWithServerBrowser:item.containerBrowser];
-        [self.navigationController pushViewController:targetViewController animated:YES];
-    } else {
-        if (searchResult) {
-            [self _streamFileForItem:item];
-        } else {
-            VLCMediaList *mediaList = self.serverBrowser.mediaList;
-
-            if ([item respondsToSelector:@selector(subtitleURL)]) {
-                NSArray *items = self.serverBrowser.items;
-                id<VLCNetworkServerBrowserItem> loopItem;
-                [mediaList lock];
-                NSUInteger count = mediaList.count;
-                for (NSUInteger i = 0; i < count; i++) {
-                    loopItem = items[i];
-                    NSString *URLofSubtitle = nil;
-                    NSURL *remoteSubtitleURL = [loopItem subtitleURL];
-                    if (remoteSubtitleURL == nil) {
-                        NSArray *SubtitlesList = [self _searchSubtitle:loopItem.URL.lastPathComponent];
-                        remoteSubtitleURL = SubtitlesList.firstObject;
-                    }
-
-                    if(remoteSubtitleURL != nil) {
-                        URLofSubtitle = [self _getFileSubtitleFromServer:remoteSubtitleURL];
-                        if (URLofSubtitle != nil)
-                            [[mediaList mediaAtIndex:i] addOptions:@{ kVLCSettingSubtitlesFilePath : URLofSubtitle }];
-                    }
-                }
-                [mediaList unlock];
-            }
+    [self didSelectItem:item index:row singlePlayback:singlePlayback];
 
-            [self _streamMediaList:mediaList startingAtIndex:row];
-        }
-    }
     [tableView deselectRowAtIndexPath:indexPath animated:NO];
 }
 
+
 #pragma mark - VLCNetworkListCell delegation
 - (void)triggerDownloadForCell:(VLCNetworkListCell *)cell
 {
@@ -359,16 +354,8 @@
     else
         item = self.serverBrowser.items[[self.tableView indexPathForCell:cell].row];
 
-    if (item.fileSizeBytes.longLongValue  < [[UIDevice currentDevice] freeDiskspace].longLongValue) {
-        [self _downloadItem:item];
+    if ([self triggerDownloadForItem:item]) {
         [cell.statusLabel showStatusMessage:NSLocalizedString(@"DOWNLOADING", nil)];
-    } else {
-        VLCAlertView *alert = [[VLCAlertView alloc] initWithTitle:NSLocalizedString(@"DISK_FULL", nil)
-                                                          message:[NSString stringWithFormat:NSLocalizedString(@"DISK_FULL_FORMAT", nil), item.name, [[UIDevice currentDevice] model]]
-                                                         delegate:self
-                                                cancelButtonTitle:NSLocalizedString(@"BUTTON_OK", nil)
-                                                otherButtonTitles:nil];
-        [alert show];
     }
 }
 

+ 1 - 0
Sources/VLC for iOS-Prefix.pch

@@ -30,6 +30,7 @@
 #import "VLCAlertView.h"
 #import "VLCNavigationController.h"
 #import "VLCSidebarController.h"
+#import "UIViewController+VLCAlert.h"
 
 #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[UIDevice currentDevice] systemVersion].floatValue >= [v floatValue])
 

+ 24 - 6
VLC for iOS.xcodeproj/project.pbxproj

@@ -49,7 +49,6 @@
 		41B93C081A53853B00102E8B /* VLCCloudServiceCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 41B93C071A53853B00102E8B /* VLCCloudServiceCell.xib */; };
 		41CD695C1A29D72600E60BCE /* VLCBoxController.m in Sources */ = {isa = PBXBuildFile; fileRef = 41CD69591A29D72600E60BCE /* VLCBoxController.m */; };
 		41CD695D1A29D72600E60BCE /* VLCBoxTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 41CD695B1A29D72600E60BCE /* VLCBoxTableViewController.m */; };
-		493B1A1D195D06B1000A491A /* BasicUPnPDevice+VLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 493B1A1C195D06B1000A491A /* BasicUPnPDevice+VLC.m */; };
 		690D0AB5E34F29B46839C92E /* libPods-VLC-TV.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C12FD7AD79D78B13860606CE /* libPods-VLC-TV.a */; };
 		7AC8629D1765DC560011611A /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = 7AC8629B1765DC560011611A /* style.css */; };
 		7AC862A61765E9510011611A /* jquery-1.10.1.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 7AC8629E1765E90C0011611A /* jquery-1.10.1.min.js */; };
@@ -276,6 +275,12 @@
 		DD3EA6311AF50CFE007FF096 /* VLCWatchMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EA6301AF50CFE007FF096 /* VLCWatchMessage.m */; };
 		DD3EABE91BE13D5B003668DA /* VLCServerBrowsingTVCell.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EABE81BE13D5B003668DA /* VLCServerBrowsingTVCell.m */; };
 		DD3EABEF1BE14720003668DA /* VLCPlaybackControlsFocusView.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EABEE1BE14720003668DA /* VLCPlaybackControlsFocusView.m */; };
+		DD3EABF81BE14BD6003668DA /* BasicUPnPDevice+VLC.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EABF71BE14BD6003668DA /* BasicUPnPDevice+VLC.m */; };
+		DD3EABF91BE14BD6003668DA /* BasicUPnPDevice+VLC.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EABF71BE14BD6003668DA /* BasicUPnPDevice+VLC.m */; };
+		DD3EABFC1BE14C4B003668DA /* UIViewController+VLCAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EABFB1BE14C4B003668DA /* UIViewController+VLCAlert.m */; };
+		DD3EABFD1BE14C4B003668DA /* UIViewController+VLCAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EABFB1BE14C4B003668DA /* UIViewController+VLCAlert.m */; };
+		DD3EAC041BE153B4003668DA /* VLCNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EAC031BE153B4003668DA /* VLCNetworkImageView.m */; };
+		DD3EAC051BE153B4003668DA /* VLCNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EAC031BE153B4003668DA /* VLCNetworkImageView.m */; };
 		DD3EFEED1BDEBA3800B68579 /* VLCNetworkServerBrowserViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EFEEA1BDEBA3800B68579 /* VLCNetworkServerBrowserViewController.m */; };
 		DD3EFEEE1BDEBA3800B68579 /* VLCServerListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EFEEC1BDEBA3800B68579 /* VLCServerListViewController.m */; };
 		DD3EFF2D1BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserFTP.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3EFEF21BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserFTP.m */; };
@@ -546,8 +551,6 @@
 		41CD69591A29D72600E60BCE /* VLCBoxController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VLCBoxController.m; path = Sources/VLCBoxController.m; sourceTree = SOURCE_ROOT; };
 		41CD695A1A29D72600E60BCE /* VLCBoxTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VLCBoxTableViewController.h; path = Sources/VLCBoxTableViewController.h; sourceTree = SOURCE_ROOT; };
 		41CD695B1A29D72600E60BCE /* VLCBoxTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VLCBoxTableViewController.m; path = Sources/VLCBoxTableViewController.m; sourceTree = SOURCE_ROOT; };
-		493B1A1B195D06B1000A491A /* BasicUPnPDevice+VLC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "BasicUPnPDevice+VLC.h"; path = "Sources/BasicUPnPDevice+VLC.h"; sourceTree = SOURCE_ROOT; };
-		493B1A1C195D06B1000A491A /* BasicUPnPDevice+VLC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "BasicUPnPDevice+VLC.m"; path = "Sources/BasicUPnPDevice+VLC.m"; sourceTree = SOURCE_ROOT; };
 		4C4A6D2751DCA7AD1BE2F573 /* Pods-VLC-TV.distribution.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VLC-TV.distribution.xcconfig"; path = "Pods/Target Support Files/Pods-VLC-TV/Pods-VLC-TV.distribution.xcconfig"; sourceTree = "<group>"; };
 		521108CBC3CAA0810AF3CBA8 /* Pods-vlc-ios.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-vlc-ios.debug.xcconfig"; path = "Pods/Target Support Files/Pods-vlc-ios/Pods-vlc-ios.debug.xcconfig"; sourceTree = "<group>"; };
 		6104D285EA8F1AEDA6136FCF /* Pods-VLC-TV.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VLC-TV.release.xcconfig"; path = "Pods/Target Support Files/Pods-VLC-TV/Pods-VLC-TV.release.xcconfig"; sourceTree = "<group>"; };
@@ -915,6 +918,12 @@
 		DD3EABE81BE13D5B003668DA /* VLCServerBrowsingTVCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCServerBrowsingTVCell.m; sourceTree = "<group>"; };
 		DD3EABED1BE14720003668DA /* VLCPlaybackControlsFocusView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VLCPlaybackControlsFocusView.h; sourceTree = "<group>"; };
 		DD3EABEE1BE14720003668DA /* VLCPlaybackControlsFocusView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCPlaybackControlsFocusView.m; sourceTree = "<group>"; };
+		DD3EABF61BE14BD6003668DA /* BasicUPnPDevice+VLC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "BasicUPnPDevice+VLC.h"; sourceTree = "<group>"; };
+		DD3EABF71BE14BD6003668DA /* BasicUPnPDevice+VLC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "BasicUPnPDevice+VLC.m"; sourceTree = "<group>"; };
+		DD3EABFA1BE14C4B003668DA /* UIViewController+VLCAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIViewController+VLCAlert.h"; path = "ServerBrowsing/UIViewController+VLCAlert.h"; sourceTree = "<group>"; };
+		DD3EABFB1BE14C4B003668DA /* UIViewController+VLCAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+VLCAlert.m"; path = "ServerBrowsing/UIViewController+VLCAlert.m"; sourceTree = "<group>"; };
+		DD3EAC021BE153B4003668DA /* VLCNetworkImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VLCNetworkImageView.h; path = "UI Elements/VLCNetworkImageView.h"; sourceTree = "<group>"; };
+		DD3EAC031BE153B4003668DA /* VLCNetworkImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VLCNetworkImageView.m; path = "UI Elements/VLCNetworkImageView.m"; sourceTree = "<group>"; };
 		DD3EFEE91BDEBA3800B68579 /* VLCNetworkServerBrowserViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VLCNetworkServerBrowserViewController.h; path = Sources/LocalNetworkConnectivity/VLCNetworkServerBrowserViewController.h; sourceTree = SOURCE_ROOT; };
 		DD3EFEEA1BDEBA3800B68579 /* VLCNetworkServerBrowserViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VLCNetworkServerBrowserViewController.m; path = Sources/LocalNetworkConnectivity/VLCNetworkServerBrowserViewController.m; sourceTree = SOURCE_ROOT; };
 		DD3EFEEB1BDEBA3800B68579 /* VLCServerListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VLCServerListViewController.h; path = Sources/LocalNetworkConnectivity/VLCServerListViewController.h; sourceTree = SOURCE_ROOT; };
@@ -1813,6 +1822,8 @@
 			children = (
 				9BADAF43185FBD9D00108BD8 /* VLCFrostedGlasView.h */,
 				9BADAF44185FBD9D00108BD8 /* VLCFrostedGlasView.m */,
+				DD3EAC021BE153B4003668DA /* VLCNetworkImageView.h */,
+				DD3EAC031BE153B4003668DA /* VLCNetworkImageView.m */,
 			);
 			name = "UI Elements";
 			sourceTree = "<group>";
@@ -1947,8 +1958,6 @@
 		A7C3025A175A538700AD4388 /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
-				493B1A1B195D06B1000A491A /* BasicUPnPDevice+VLC.h */,
-				493B1A1C195D06B1000A491A /* BasicUPnPDevice+VLC.m */,
 				7D3784C4183A9972009EE944 /* NSString+SupportedMedia.h */,
 				7D3784C5183A9972009EE944 /* NSString+SupportedMedia.m */,
 				7D3784C6183A9972009EE944 /* UIDevice+VLC.h */,
@@ -2153,6 +2162,8 @@
 		DD3EFF211BDEBCE500B68579 /* UPnP */ = {
 			isa = PBXGroup;
 			children = (
+				DD3EABF61BE14BD6003668DA /* BasicUPnPDevice+VLC.h */,
+				DD3EABF71BE14BD6003668DA /* BasicUPnPDevice+VLC.m */,
 				DD3EFF221BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserUPnP.h */,
 				DD3EFF231BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserUPnP.m */,
 				DD3EFF241BDEBCE500B68579 /* VLCLocalNetworkServiceUPnP.h */,
@@ -2172,6 +2183,8 @@
 				7DEC8BE11BD686FA006E1093 /* Library */,
 				DD3EFEEF1BDEBCE500B68579 /* ServerBrowsing */,
 				DD3EA62F1AF50CFE007FF096 /* VLCWatchMessage.h */,
+				DD3EABFA1BE14C4B003668DA /* UIViewController+VLCAlert.h */,
+				DD3EABFB1BE14C4B003668DA /* UIViewController+VLCAlert.m */,
 				DD3EA6301AF50CFE007FF096 /* VLCWatchMessage.m */,
 			);
 			path = SharedSources;
@@ -2642,6 +2655,7 @@
 				7DEC8C1F1BD6A113006E1093 /* UIDevice+VLC.m in Sources */,
 				DDEAECFE1BDFFAEE00756C83 /* Reachability.m in Sources */,
 				DD3EFF561BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserDSM.m in Sources */,
+				DD3EABF91BE14BD6003668DA /* BasicUPnPDevice+VLC.m in Sources */,
 				7D0C34E71BD951080058CD19 /* NSString+SupportedMedia.m in Sources */,
 				7DC71D211BC83058001FACAA /* VLCAppSharesTVViewController.m in Sources */,
 				DDEAECBF1BDEBF6700756C83 /* VLCNetworkServerLoginInformation.m in Sources */,
@@ -2659,6 +2673,7 @@
 				DD3EFF521BDEBCE500B68579 /* VLCPlexWebAPI.m in Sources */,
 				7D60696C1BD93BE200AB765C /* VLCCloudStorageTableViewCell.m in Sources */,
 				DD3EABE91BE13D5B003668DA /* VLCServerBrowsingTVCell.m in Sources */,
+				DD3EABFD1BE14C4B003668DA /* UIViewController+VLCAlert.m in Sources */,
 				7D7EF3DD1BD5779F00CD4CEE /* VLCPlaybackController.m in Sources */,
 				DD3EFF361BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserNetService.m in Sources */,
 				7D1334831BE135700012E919 /* VLCCloudStorageTVTableViewController.m in Sources */,
@@ -2674,6 +2689,7 @@
 				7D5278E21BD7E06E00D0CA0E /* VLCDropboxController.m in Sources */,
 				7DEC8BD91BD670EB006E1093 /* VLCPlaybackNavigationController.m in Sources */,
 				7DEC8BDA1BD67112006E1093 /* VLCFrostedGlasView.m in Sources */,
+				DD3EAC051BE153B4003668DA /* VLCNetworkImageView.m in Sources */,
 				7D0C35341BD97C7B0058CD19 /* VLCOneDriveObject.m in Sources */,
 				7D0C35331BD97C100058CD19 /* VLCOneDriveController.m in Sources */,
 				7D60696B1BD93AC800AB765C /* VLCDropboxTableViewController.m in Sources */,
@@ -2715,6 +2731,7 @@
 				7DC19B0C1868D21800810BF7 /* VLCFirstStepsSixthPageViewController.m in Sources */,
 				DD3EFEED1BDEBA3800B68579 /* VLCNetworkServerBrowserViewController.m in Sources */,
 				2915540717490A1E00B86CAD /* HTTPServer.m in Sources */,
+				DD3EABFC1BE14C4B003668DA /* UIViewController+VLCAlert.m in Sources */,
 				2915540817490A1E00B86CAD /* MultipartFormDataParser.m in Sources */,
 				2915540917490A1E00B86CAD /* MultipartMessageHeader.m in Sources */,
 				2915540A17490A1E00B86CAD /* MultipartMessageHeaderField.m in Sources */,
@@ -2738,6 +2755,7 @@
 				9BADAF45185FBD9D00108BD8 /* VLCFrostedGlasView.m in Sources */,
 				DDC10BE41AEE8EA700890DC3 /* VLCTimeNavigationTitleView.m in Sources */,
 				2915543E17490B9C00B86CAD /* HTTPErrorResponse.m in Sources */,
+				DD3EAC041BE153B4003668DA /* VLCNetworkImageView.m in Sources */,
 				E0C04F951A25B4410080331A /* VLCDocumentPickerController.m in Sources */,
 				2915543F17490B9C00B86CAD /* HTTPFileResponse.m in Sources */,
 				DD2789E21B67A7BE00CED769 /* VLCWatchCommunication.m in Sources */,
@@ -2790,6 +2808,7 @@
 				7D1276621AADA0E600F0260C /* VLCMultiSelectionMenuView.m in Sources */,
 				7D3784C8183A9972009EE944 /* NSString+SupportedMedia.m in Sources */,
 				DD3EFF5B1BDEBCE500B68579 /* VLCNetworkServerBrowserUPnP.m in Sources */,
+				DD3EABF81BE14BD6003668DA /* BasicUPnPDevice+VLC.m in Sources */,
 				7D3784C9183A9972009EE944 /* UIDevice+VLC.m in Sources */,
 				7D3784CC183A99BA009EE944 /* PAPasscodeViewController.m in Sources */,
 				DDEAECCD1BDECCB800756C83 /* VLCNetworkListViewController.m in Sources */,
@@ -2812,7 +2831,6 @@
 				DD3EFF571BDEBCE500B68579 /* VLCLocalNetworkServiceBrowserUPnP.m in Sources */,
 				7D9CB9DC1A4C55EF00BB74B4 /* VLCPlaybackNavigationController.m in Sources */,
 				7D30F3D0183AB2AC00FFC021 /* VLCMediaFileDiscoverer.m in Sources */,
-				493B1A1D195D06B1000A491A /* BasicUPnPDevice+VLC.m in Sources */,
 				7D18F0A21B34522000651A30 /* VLCActivityManager.m in Sources */,
 				DD3EFEEE1BDEBA3800B68579 /* VLCServerListViewController.m in Sources */,
 				7DB847D71A5871570002DC30 /* VLCOneDriveObject.m in Sources */,