[vlc-commits] [Git][videolan/vlc][master] macosx: show loading overlay in media source view while awaiting remote content

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Fri Jun 26 17:02:36 UTC 2026



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
ecb38c9e by Serhii Bykov at 2026-06-26T18:51:47+02:00
macosx: show loading overlay in media source view while awaiting remote content

- - - - -


3 changed files:

- modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewController.m
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.h
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m


Changes:

=====================================
modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewController.m
=====================================
@@ -29,8 +29,11 @@
 #import "extensions/NSFont+VLCAdditions.h"
 #import "extensions/NSString+Helpers.h"
 #import "extensions/NSTextField+VLCAdditions.h"
+#import "extensions/NSView+VLCAdditions.h"
 #import "extensions/NSWindow+VLCAdditions.h"
 
+#import "views/VLCLoadingOverlayView.h"
+
 #import "library/VLCLibraryCollectionView.h"
 #import "library/VLCLibraryCollectionViewFlowLayout.h"
 #import "library/VLCLibraryCollectionViewItem.h"
@@ -43,6 +46,9 @@
 #import "main/VLCMain.h"
 
 @interface VLCLibraryMediaSourceViewController ()
+{
+    VLCLoadingOverlayView *_loadingOverlayView;
+}
 
 #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000
 @property (readonly) NSGlassEffectView *pathControlGlassEffectView API_AVAILABLE(macos(26.0));
@@ -52,6 +58,17 @@
 
 @implementation VLCLibraryMediaSourceViewController
 
+// The shared mediaSourceView is the source of truth for the active overlay.
+- (nullable VLCLoadingOverlayView *)currentLoadingOverlayInMediaSourceView
+{
+    for (NSView * const subview in self.mediaSourceView.subviews) {
+        if ([subview isKindOfClass:VLCLoadingOverlayView.class]) {
+            return (VLCLoadingOverlayView *)subview;
+        }
+    }
+    return nil;
+}
+
 - (instancetype)initWithLibraryWindow:(VLCLibraryWindow *)libraryWindow
 {
     self = [super initWithLibraryWindow:libraryWindow];
@@ -64,13 +81,21 @@
         [self setupPlaceholderLabel];
 
         NSNotificationCenter * const defaultCenter = NSNotificationCenter.defaultCenter;
-        [defaultCenter addObserver:self 
-                          selector:@selector(updatePlaceholderLabel:) 
-                              name:VLCMediaSourceBaseDataSourceNodeChanged 
+        [defaultCenter addObserver:self
+                          selector:@selector(updatePlaceholderLabel:)
+                              name:VLCMediaSourceBaseDataSourceNodeChanged
+                            object:nil];
+        [defaultCenter addObserver:self
+                          selector:@selector(updatePlaceholderLabel:)
+                              name:VLCMediaSourceDataSourceNodeChanged
+                            object:nil];
+        [defaultCenter addObserver:self
+                          selector:@selector(mediaSourceLoadingStarted:)
+                              name:VLCMediaSourceDataSourceLoadingStarted
                             object:nil];
-        [defaultCenter addObserver:self 
-                          selector:@selector(updatePlaceholderLabel:) 
-                              name:VLCMediaSourceDataSourceNodeChanged 
+        [defaultCenter addObserver:self
+                          selector:@selector(mediaSourceLoadingEnded:)
+                              name:VLCMediaSourceDataSourceLoadingEnded
                             object:nil];
     }
     return self;
@@ -213,7 +238,51 @@
 
 - (void)updatePlaceholderLabel:(NSNotification *)notification
 {
-    self.browsePlaceholderLabel.hidden = self.baseDataSource.hasDisplayedItems;
+    _loadingOverlayView = [self currentLoadingOverlayInMediaSourceView];
+    const BOOL overlayPresent = _loadingOverlayView != nil;
+    self.browsePlaceholderLabel.hidden = self.baseDataSource.hasDisplayedItems || overlayPresent;
+}
+
+- (void)prepareLoadingOverlay
+{
+    _loadingOverlayView = [self currentLoadingOverlayInMediaSourceView];
+
+    if (_loadingOverlayView != nil)
+        return;
+    if (!_loadingOverlayView)
+        _loadingOverlayView = [[VLCLoadingOverlayView alloc] init];
+    _loadingOverlayView.translatesAutoresizingMaskIntoConstraints = NO;
+    _loadingOverlayView.wantsLayer = YES;
+    _loadingOverlayView.alphaValue = 0.0;
+    [self.mediaSourceView addSubview:_loadingOverlayView];
+    [_loadingOverlayView applyConstraintsToFillSuperview];
+    [_loadingOverlayView.indicator startAnimation:self];
+}
+
+- (void)mediaSourceLoadingStarted:(NSNotification *)notification
+{
+    _loadingOverlayView = [self currentLoadingOverlayInMediaSourceView];
+
+    [self prepareLoadingOverlay];
+    _loadingOverlayView.alphaValue = 1.0;
+    [self updatePlaceholderLabel:nil];
+}
+
+- (void)mediaSourceLoadingEnded:(NSNotification *)notification
+{
+    _loadingOverlayView = [self currentLoadingOverlayInMediaSourceView];
+
+    if (_loadingOverlayView == nil)
+        return;
+
+    _loadingOverlayView.alphaValue = 1.0;
+    [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
+        _loadingOverlayView.animator.alphaValue = 0.0;
+    } completionHandler:^{
+        [_loadingOverlayView removeFromSuperview];
+        [_loadingOverlayView.indicator stopAnimation:self];
+        [self updatePlaceholderLabel:nil];
+    }];
 }
 
 - (NSView *)pathControlContainerView
@@ -238,7 +307,14 @@
 - (void)presentMediaSourceView:(VLCLibrarySegmentType)viewSegment
 {
     [self.libraryWindow displayLibraryView:self.mediaSourceView];
-    _baseDataSource.mediaSourceMode = viewSegment == VLCLibraryBrowseSegmentType ? VLCMediaSourceModeLAN : VLCMediaSourceModeInternet;
+    const BOOL isStreams = viewSegment != VLCLibraryBrowseSegmentType;
+    _loadingOverlayView = [self currentLoadingOverlayInMediaSourceView];
+
+    if (!isStreams) {
+        [self mediaSourceLoadingEnded:nil];
+    }
+
+    _baseDataSource.mediaSourceMode = isStreams ? VLCMediaSourceModeInternet : VLCMediaSourceModeLAN;
     [_baseDataSource reloadViews];
 }
 


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.h
=====================================
@@ -32,6 +32,8 @@ NS_ASSUME_NONNULL_BEGIN
 @class VLCMediaSourceBaseDataSource;
 
 extern NSString * const VLCMediaSourceDataSourceNodeChanged;
+extern NSString * const VLCMediaSourceDataSourceLoadingStarted;
+extern NSString * const VLCMediaSourceDataSourceLoadingEnded;
 
 @interface VLCMediaSourceDataSource : NSObject <NSCollectionViewDataSource,
                                                 NSCollectionViewDelegate,


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
=====================================
@@ -50,6 +50,8 @@
 #import "views/VLCImageView.h"
 
 NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourceNodeChanged";
+NSString * const VLCMediaSourceDataSourceLoadingStarted = @"VLCMediaSourceDataSourceLoadingStarted";
+NSString * const VLCMediaSourceDataSourceLoadingEnded = @"VLCMediaSourceDataSourceLoadingEnded";
 
 @interface VLCMediaSourceDataSource()
 {
@@ -99,6 +101,12 @@ NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourc
         return;
     dispatch_async(dispatch_get_main_queue(), ^{
         [self reloadData];
+        if (self.hasDisplayedItems ||
+            [notification.name isEqualToString:VLCMediaSourcePreparsingEnded]) {
+            [NSNotificationCenter.defaultCenter
+                postNotificationName:VLCMediaSourceDataSourceLoadingEnded
+                              object:self];
+        }
     });
 }
 
@@ -138,39 +146,48 @@ NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourc
     if (self.parentBaseDataSource.mediaSourceMode == VLCMediaSourceModeLAN) {
         NSURL * const nodeUrl = [NSURL URLWithString:nodeToDisplay.inputItem.MRL];
 
-        if (!nodeUrl.isFileURL) {
-            [self reloadData];
-            return;
-        }
+        if (nodeUrl.isFileURL) {
+            if (self.observedPathDispatchSource) {
+                dispatch_source_cancel(self.observedPathDispatchSource);
+                self.observedPathDispatchSource = nil;
+            }
 
-        if (self.observedPathDispatchSource) {
-            dispatch_source_cancel(self.observedPathDispatchSource);
-            self.observedPathDispatchSource = nil;
+            const __weak typeof(self) weakSelf = self;
+            self.observedPathDispatchSource = [self observeLocalUrl:nodeUrl
+                                                    forVnodeEvents:DISPATCH_VNODE_WRITE |
+                                                                   DISPATCH_VNODE_DELETE |
+                                                                   DISPATCH_VNODE_RENAME
+                                                withEventHandler:^{
+                const uintptr_t eventFlags =
+                    dispatch_source_get_data(weakSelf.observedPathDispatchSource);
+                if (eventFlags & DISPATCH_VNODE_DELETE || eventFlags & DISPATCH_VNODE_RENAME) {
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        [weakSelf.parentBaseDataSource homeButtonAction:weakSelf];
+                    });
+                } else {
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        [weakSelf.displayedMediaSource generateChildNodesForDirectoryNode:nodeToDisplay
+                                                                                  withUrl:nodeUrl];
+                        [weakSelf reloadData];
+                    });
+                }
+            }];
         }
-
-        const __weak typeof(self) weakSelf = self;
-        self.observedPathDispatchSource = [self observeLocalUrl:nodeUrl
-                                                forVnodeEvents:DISPATCH_VNODE_WRITE |
-                                                               DISPATCH_VNODE_DELETE |
-                                                               DISPATCH_VNODE_RENAME
-                                            withEventHandler:^{
-            const uintptr_t eventFlags =
-                dispatch_source_get_data(weakSelf.observedPathDispatchSource);
-            if (eventFlags & DISPATCH_VNODE_DELETE || eventFlags & DISPATCH_VNODE_RENAME) {
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [weakSelf.parentBaseDataSource homeButtonAction:weakSelf];
-                });
-            } else {
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [weakSelf.displayedMediaSource generateChildNodesForDirectoryNode:nodeToDisplay
-                                                                              withUrl:nodeUrl];
-                    [weakSelf reloadData];
-                });
-            }
-        }];
     }
 
     [self reloadData];
+    if (!self.hasDisplayedItems) {
+        [NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceDataSourceLoadingStarted
+                                                          object:self];
+    } else {
+        [NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceDataSourceLoadingEnded
+                                                          object:self];
+    }
+}
+
+- (BOOL)hasDisplayedItems
+{
+    return _nodeToDisplay.numberOfChildren > 0;
 }
 
 - (void)setupViews



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/ecb38c9e0d2c8c1c408e26952b59292f67028796

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/ecb38c9e0d2c8c1c408e26952b59292f67028796
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list