[vlc-commits] [Git][videolan/vlc][master] 11 commits: macosx: Add convenience method to get file type description

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Tue Oct 13 07:06:52 UTC 2026



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


Commits:
3c9e1cbf by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add convenience method to get file type description

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
240cf026 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add input type description string property to VLCInputItem

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
12fd9571 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add VLCMediaSource category description string property

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
f7d54151 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add test for new file type description nsstring method

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
77338ed5 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Define column and sort identifiers in base data source

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
99efea71 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Simplify media source base data source's viewForTableColumn

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
94b26700 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Simplify media source data source's viewForTableColumn

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
3c984cb1 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add media source table view sorting helper class

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
e50d4f74 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Implement sorting for list view in the media source base data source

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
07f3f31c by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Implement sorting for list view in the media source data source

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
716e856c by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add test suite for VLCMediaSourceTableViewSorting

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -


17 changed files:

- extras/package/macosx/VLC.xcodeproj/project.pbxproj
- modules/gui/macosx/Makefile.am
- modules/gui/macosx/extensions/NSString+Helpers.h
- modules/gui/macosx/extensions/NSString+Helpers.m
- modules/gui/macosx/library/VLCInputItem.h
- modules/gui/macosx/library/VLCInputItem.m
- modules/gui/macosx/library/media-source/VLCMediaSource.h
- modules/gui/macosx/library/media-source/VLCMediaSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.h
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.h
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
- + modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.h
- + modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m
- modules/gui/macosx/tests/Makefile.am
- modules/gui/macosx/tests/NSStringHelpersTest.m
- + modules/gui/macosx/tests/VLCMediaSourceTableViewSortingTest.m


Changes:

=====================================
extras/package/macosx/VLC.xcodeproj/project.pbxproj
=====================================
@@ -289,6 +289,7 @@
 		CBA75925CFF3CDBA090978F9 /* VLCConnectToServerDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = BD7C30D642B4C89C9F539A27 /* VLCConnectToServerDialog.m */; };
 		D5E843223C3B2003665D0392 /* VLCLibrarySearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BAD0D16D79A606065692135 /* VLCLibrarySearchViewController.m */; };
 		EAA945B2B98EEA0417A2BE39 /* NSMenuItem+VLCAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CC1F1AD9B6357DC114DCF464 /* NSMenuItem+VLCAdditions.m */; };
+		F905D8ED29CD4DF0B8A46B24 /* VLCMediaSourceTableViewSorting.m in Sources */ = {isa = PBXBuildFile; fileRef = 499CEBBD50D8423086BD3682 /* VLCMediaSourceTableViewSorting.m */; };
 		FDF2CACBF02A94F2ED2E0CE3 /* NSAnimationContext+VLCAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F42980861A7BC3BDB5546FB5 /* NSAnimationContext+VLCAdditions.m */; };
 /* End PBXBuildFile section */
 
@@ -336,6 +337,7 @@
 		3D46D70C2E1C023F00741E96 /* VLCLibraryRatingIndicator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibraryRatingIndicator.m; sourceTree = "<group>"; };
 		45C012BF53A7082827B907FA /* VLCConnectToServerDialog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCConnectToServerDialog.h; sourceTree = "<group>"; };
 		4618AD10A0A0997054DE8AD9 /* VLCLibrarySearchProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibrarySearchProvider.m; sourceTree = "<group>"; };
+		499CEBBD50D8423086BD3682 /* VLCMediaSourceTableViewSorting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCMediaSourceTableViewSorting.m; sourceTree = "<group>"; };
 		4B8B3AAE11FC272D811B71EC /* VLCLibraryGroupHeaderDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCLibraryGroupHeaderDelegate.h; sourceTree = "<group>"; };
 		4DEC6E8A21C184846D985A69 /* VLCLibraryNameCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibraryNameCache.m; sourceTree = "<group>"; };
 		50EC718DBC26E627D0539815 /* NSAnimationContext+VLCAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSAnimationContext+VLCAdditions.h"; sourceTree = "<group>"; };
@@ -577,6 +579,7 @@
 		5CCED71314C0D4A90057F8D1 /* VLCExtensionsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCExtensionsManager.m; sourceTree = "<group>"; };
 		5CCED71414C0D4A90057F8D1 /* VLCUIWidgets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VLCUIWidgets.h; sourceTree = "<group>"; };
 		5CCED71514C0D4A90057F8D1 /* VLCUIWidgets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCUIWidgets.m; sourceTree = "<group>"; };
+		5E9D41F61DDE4F2D8A16D6EE /* VLCMediaSourceTableViewSorting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCMediaSourceTableViewSorting.h; sourceTree = "<group>"; };
 		5EA1F8D351D1520B21E49E57 /* VLCUIUnits.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCUIUnits.h; sourceTree = "<group>"; };
 		633121CA1B51122700E636DA /* VLCResumeDialogController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VLCResumeDialogController.h; sourceTree = "<group>"; };
 		633121CB1B51122700E636DA /* VLCResumeDialogController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCResumeDialogController.m; sourceTree = "<group>"; };
@@ -2153,6 +2156,8 @@
 				7D2554C322C8E8B400F2D9C2 /* VLCMediaSourceDeviceCollectionViewItem.m */,
 				7DFBDCB9226CED6300B700A5 /* VLCMediaSourceProvider.h */,
 				7DFBDCBA226CED6300B700A5 /* VLCMediaSourceProvider.m */,
+				5E9D41F61DDE4F2D8A16D6EE /* VLCMediaSourceTableViewSorting.h */,
+				499CEBBD50D8423086BD3682 /* VLCMediaSourceTableViewSorting.m */,
 			);
 			path = "media-source";
 			sourceTree = "<group>";
@@ -2553,6 +2558,7 @@
 				1C3113AD1E508C6900D4DD76 /* VLCExtensionsManager.m in Sources */,
 				53A8F9D42A7F815900BC11BF /* VLCLibraryPlaylistDataSource.m in Sources */,
 				7DFBDCBB226CED6300B700A5 /* VLCMediaSourceProvider.m in Sources */,
+				F905D8ED29CD4DF0B8A46B24 /* VLCMediaSourceTableViewSorting.m in Sources */,
 				7D66D4362200BC340040D04A /* VLCClickerManager.m in Sources */,
 				535053EE2B610C3400D7EAF2 /* VLCLoadingOverlayView.m in Sources */,
 				53B447F9293BB47B00857588 /* VLCLibraryVideoDataSource.m in Sources */,


=====================================
modules/gui/macosx/Makefile.am
=====================================
@@ -346,6 +346,8 @@ libmacosx_plugin_la_SOURCES = \
 	gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationState.m \
 	gui/macosx/library/media-source/VLCMediaSourceProvider.h \
 	gui/macosx/library/media-source/VLCMediaSourceProvider.m \
+	gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.h \
+	gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m \
 	gui/macosx/library/playlist-library/VLCLibraryPlaylistDataSource.h \
 	gui/macosx/library/playlist-library/VLCLibraryPlaylistDataSource.m \
 	gui/macosx/library/playlist-library/VLCLibraryPlaylistViewController.h \


=====================================
modules/gui/macosx/extensions/NSString+Helpers.h
=====================================
@@ -183,6 +183,11 @@ bool fixIntfSettings(void);
  */
 - (nullable NSString *)stringWithIncrementedTrailingNumber;
 
+/**
+ Returns a description of the receiver's file extension, or "File" if it has none.
+ */
+- (NSString *)fileTypeDescription;
+
 /**
  * Returns an array of file extensions from a semicolon-separated string of extensions,
  * stripping any leading "*." prefix.


=====================================
modules/gui/macosx/extensions/NSString+Helpers.m
=====================================
@@ -34,6 +34,7 @@
 #import <sys/param.h>
 #import <sys/mount.h>
 
+#import <CoreServices/CoreServices.h>
 #import <IOKit/storage/IOMedia.h>
 #import <IOKit/storage/IOCDMedia.h>
 #import <IOKit/storage/IODVDMedia.h>
@@ -278,6 +279,27 @@ NSString * flagEmojiStringForCountryCode(NSString *countryCode)
     return result.copy;
 }
 
+- (NSString *)fileTypeDescription
+{
+    NSString * const extension = self.pathExtension.lowercaseString;
+    if (extension.length == 0) {
+        return NSTR("File");
+    }
+
+    const CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
+                                                               (__bridge CFStringRef)extension,
+                                                               NULL);
+    if (uti != NULL) {
+        const CFStringRef description = UTTypeCopyDescription(uti);
+        CFRelease(uti);
+        if (description != NULL) {
+            return CFBridgingRelease(description);
+        }
+    }
+
+    return [NSString stringWithFormat:@"%@ File", extension.capitalizedString];
+}
+
 + (NSArray<NSString *> *)extensionsArrayFromVLCStyleString:(const char *)extensionsString
 {
     if (extensionsString == NULL) {


=====================================
modules/gui/macosx/library/VLCInputItem.h
=====================================
@@ -72,6 +72,7 @@ extern NSString * const VLCInputItemCommonDataDifferingFlagString;
 @property (readonly) NSString *totalNumberOfDiscs;
 @property (readonly) vlc_tick_t duration;
 @property (readonly) enum input_item_type_e inputType;
+ at property (readonly) NSString *inputTypeDescription;
 @property (readonly) struct input_item_node_t *subTree;
 @property (readwrite) NSURL *artworkURL;
 @property (readonly) BOOL preparsed;


=====================================
modules/gui/macosx/library/VLCInputItem.m
=====================================
@@ -358,6 +358,31 @@ static const struct input_item_parser_cbs_t parserCallbacks =
     return _vlcInputItem->i_type;
 }
 
+- (NSString *)inputTypeDescription
+{
+    switch (self.inputType) {
+        case ITEM_TYPE_UNKNOWN:
+            return NSTR("Unknown");
+        case ITEM_TYPE_FILE:
+            return self.MRL.fileTypeDescription;
+        case ITEM_TYPE_DIRECTORY:
+            return NSTR("Directory");
+        case ITEM_TYPE_DISC:
+            return NSTR("Disc");
+        case ITEM_TYPE_CARD:
+            return NSTR("Card");
+        case ITEM_TYPE_STREAM:
+            return NSTR("Stream");
+        case ITEM_TYPE_PLAYLIST:
+            return NSTR("Playlist");
+        case ITEM_TYPE_NODE:
+            return NSTR("Node");
+        case ITEM_TYPE_NUMBER:
+            return NSTR("Undefined");
+    }
+    return NSTR("Unknown");
+}
+
 - (NSURL *)artworkURL
 {
     char *p_artworkURL = input_item_GetArtworkURL(_vlcInputItem);


=====================================
modules/gui/macosx/library/media-source/VLCMediaSource.h
=====================================
@@ -63,6 +63,7 @@ extern NSString * const VLCMediaSourcePreparseStatusKey;
 @property (nonatomic, readonly) NSString *mediaSourceDescription;
 @property (nonatomic, readonly) VLCInputNode *rootNode;
 @property (nonatomic, readonly) enum services_discovery_category_e category;
+ at property (nonatomic, readonly) NSString *categoryDescription;
 @property (readwrite) void (^willStartGeneratingChildNodesForNodeHandler)(input_item_node_t *);
 @property (readwrite) void (^didFinishGeneratingChildNodesForNodeHandler)(input_item_node_t *);
 


=====================================
modules/gui/macosx/library/media-source/VLCMediaSource.m
=====================================
@@ -616,6 +616,22 @@ static const char *const remoteBrowseDescription = "Remote Browse";
     return inputNode;
 }
 
+- (NSString *)categoryDescription
+{
+    switch (self.category) {
+        case SD_CAT_DEVICES:
+            return _NS("Devices");
+        case SD_CAT_LAN:
+            return _NS("LAN");
+        case SD_CAT_INTERNET:
+            return _NS("Internet");
+        case SD_CAT_MYCOMPUTER:
+            return _NS("My Computer");
+        default:
+            return @"";
+    }
+}
+
 #pragma mark - Local Media Items Methods
 - (void)volumeIsMounted:(NSNotification *)aNotification
 {


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.h
=====================================
@@ -38,6 +38,16 @@ typedef NS_ENUM(NSInteger, VLCMediaSourceMode) {
 
 extern NSString * const VLCMediaSourceBaseDataSourceNodeChanged;
 
+extern NSString * const VLCMediaSourceTableNameColumnIdentifier;
+extern NSString * const VLCMediaSourceTableCountColumnIdentifier;
+extern NSString * const VLCMediaSourceTableKindColumnIdentifier;
+extern NSString * const VLCMediaSourceTableTagsColumnIdentifier;
+
+extern NSString * const VLCMediaSourceTableNameSortDescriptorKey;
+extern NSString * const VLCMediaSourceTableCountSortDescriptorKey;
+extern NSString * const VLCMediaSourceTableKindSortDescriptorKey;
+extern NSString * const VLCMediaSourceTableTagsSortDescriptorKey;
+
 @interface VLCMediaSourceBaseDataSource : NSObject <NSCollectionViewDataSource,
                                                     NSCollectionViewDelegate,
                                                     NSCollectionViewDelegateFlowLayout,


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
=====================================
@@ -28,6 +28,7 @@
 #import "VLCMediaSourceDataSource.h"
 #import "VLCMediaSourceDeviceCollectionViewItem.h"
 #import "VLCMediaSourceProvider.h"
+#import "VLCMediaSourceTableViewSorting.h"
 
 #import "extensions/NSImage+VLCAdditions.h"
 #import "extensions/NSPasteboardItem+VLCAdditions.h"
@@ -54,8 +55,17 @@
 #import "views/VLCUIUnits.h"
 
 NSString * const VLCMediaSourceBaseDataSourceNodeChanged = @"VLCMediaSourceBaseDataSourceNodeChanged";
+
+NSString * const VLCMediaSourceTableNameColumnIdentifier = @"VLCMediaSourceTableNameColumn";
+NSString * const VLCMediaSourceTableCountColumnIdentifier = @"VLCMediaSourceTableCountColumn";
+NSString * const VLCMediaSourceTableKindColumnIdentifier = @"VLCMediaSourceTableKindColumn";
 NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTableTagsColumn";
 
+NSString * const VLCMediaSourceTableNameSortDescriptorKey = @"name";
+NSString * const VLCMediaSourceTableCountSortDescriptorKey = @"childCount";
+NSString * const VLCMediaSourceTableKindSortDescriptorKey = @"kind";
+NSString * const VLCMediaSourceTableTagsSortDescriptorKey = @"tags";
+
 @interface VLCLANDeviceRecord : NSObject
 @property (readonly) VLCMediaSource *mediaSource;
 @property (readonly) VLCInputNode *inputNode;
@@ -83,8 +93,13 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
 {
     NSArray<VLCMediaSource *> *_mediaSources;
     NSArray<NSString *> *_mediaSourceNotificationNames;
-    NSMapTable<VLCMediaSource *, NSArray<VLCLANDeviceRecord *> *> *_sourceRecords;
-    NSArray<VLCLANDeviceRecord *> *_lanDeviceSnapshot;
+    // LAN device records grouped by source, updated independently.
+    NSMapTable<VLCMediaSource *, NSArray<VLCLANDeviceRecord *> *> *_deviceRecordsBySource;
+    // Flattened discovery records in Browse table sort order.
+    NSArray<VLCLANDeviceRecord *> *_tableViewDevices;
+    // Internet providers in Streams table sort order; grid order remains in _mediaSources.
+    NSArray<VLCMediaSource *> *_tableViewSources;
+    BOOL _ignoringTableViewSelectionChanges;
 }
 @end
 
@@ -95,17 +110,18 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
     self = [super init];
     if (self) {
         _mediaSources = @[];
-        _lanDeviceSnapshot = @[];
+        _tableViewDevices = @[];
+        _tableViewSources = @[];
         _mediaSourceNotificationNames = @[
             VLCMediaSourceChildrenReset,
             VLCMediaSourceChildrenAdded,
             VLCMediaSourceChildrenRemoved,
             VLCMediaSourcePreparsingEnded
         ];
-        _sourceRecords = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory |
-                                                           NSPointerFunctionsObjectPointerPersonality
-                                              valueOptions:NSPointerFunctionsStrongMemory |
-                                                          NSPointerFunctionsObjectPersonality];
+        _deviceRecordsBySource = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory |
+                                                                    NSPointerFunctionsObjectPointerPersonality
+                                                       valueOptions:NSPointerFunctionsStrongMemory |
+                                                                    NSPointerFunctionsObjectPersonality];
         _mediaSourceMode = VLCMediaSourceModeLAN;
         [self loadMediaSources];
         [self returnHome];
@@ -164,10 +180,30 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
     NSNib * const tableCellViewNib = [[NSNib alloc] initWithNibNamed:NSStringFromClass(VLCLibraryTableCellView.class) bundle:nil];
     [self.tableView registerNib:tableCellViewNib forIdentifier:VLCLibraryTableCellViewIdentifier];
 
+    [self setupTableViewSorting];
     [self updateTableColumnVisibility];
     [self reloadViews];
 }
 
+- (void)setupTableViewSorting
+{
+    NSDictionary<NSString *, NSString *> * const keysByColumn = @{
+        VLCMediaSourceTableNameColumnIdentifier: VLCMediaSourceTableNameSortDescriptorKey,
+        VLCMediaSourceTableCountColumnIdentifier: VLCMediaSourceTableCountSortDescriptorKey,
+        VLCMediaSourceTableKindColumnIdentifier: VLCMediaSourceTableKindSortDescriptorKey,
+        VLCMediaSourceTableTagsColumnIdentifier: VLCMediaSourceTableTagsSortDescriptorKey
+    };
+    for (NSTableColumn * const column in self.tableView.tableColumns) {
+        NSString * const key = keysByColumn[column.identifier];
+        NSAssert(key != nil, @"Unexpected media source table column: %@", column.identifier);
+        const SEL selector = [key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey]
+            ? @selector(compare:) : @selector(localizedStandardCompare:);
+        column.sortDescriptorPrototype = [NSSortDescriptor sortDescriptorWithKey:key
+                                                                       ascending:YES
+                                                                        selector:selector];
+    }
+}
+
 - (void)updateTableColumnVisibility
 {
     NSTableColumn * const tagsColumn =
@@ -177,6 +213,7 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
 
 - (void)reloadViews
 {
+    [self updateTableViewItems];
     const VLCLibraryViewModeSegment viewModeSegment = self.viewMode;
     if (viewModeSegment == VLCLibraryGridViewModeSegment) {
         self.collectionViewScrollView.hidden = NO;
@@ -185,7 +222,11 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
     } else if (viewModeSegment == VLCLibraryListViewModeSegment) {
         self.collectionViewScrollView.hidden = YES;
         self.tableViewScrollView.hidden = NO;
-        [self.tableView reloadData];
+        if (self.childDataSource != nil) {
+            [self.childDataSource reloadData];
+        } else {
+            [self.tableView reloadData];
+        }
     } else {
         NSAssert(false, @"View mode must be grid or list mode");
     }
@@ -214,7 +255,7 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
     }
 
     [self setMediaSources:mediaSources];
-    _lanDeviceSnapshot = self.mediaSourceMode == VLCMediaSourceModeLAN ? [self buildMediaSourceSnapshot] : @[];
+    [self rebuildLANDeviceRecords];
     [self reloadData];
 }
 
@@ -268,7 +309,7 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
     }
 
     if (_mediaSourceMode == VLCMediaSourceModeLAN) {
-        return _lanDeviceSnapshot.count > 0;
+        return _tableViewDevices.count > 0;
     }
 
     return _mediaSources.count > 0;
@@ -440,22 +481,22 @@ referenceSizeForHeaderInSection:(NSInteger)section
 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
 {
     if (_mediaSourceMode == VLCMediaSourceModeLAN) {
-        return _lanDeviceSnapshot.count;
+        return _tableViewDevices.count;
     }
 
-    return _mediaSources.count;
+    return _tableViewSources.count;
 }
 
 - (NSView *)tableView:(NSTableView *)tableView
    viewForTableColumn:(NSTableColumn *)tableColumn
                   row:(NSInteger)row
 {
-    if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableNameColumn"]) {
+    if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableNameColumnIdentifier]) {
         VLCLibraryTableCellView * const cellView =
             [tableView makeViewWithIdentifier:VLCLibraryTableCellViewIdentifier owner:self];
 
         if (_mediaSourceMode == VLCMediaSourceModeLAN) {
-            VLCInputItem * const currentNodeInput = _lanDeviceSnapshot[row].inputNode.inputItem;
+            VLCInputItem * const currentNodeInput = _tableViewDevices[row].inputNode.inputItem;
             NSURL * const artworkURL = currentNodeInput.artworkURL;
             NSImage * const placeholder = NSImage.VLCDefaultAppIconImage;
             if (currentNodeInput.radioCountryCodeForFlagArtwork) {
@@ -474,15 +515,15 @@ referenceSizeForHeaderInSection:(NSInteger)section
         }
 
         NSString * const name = _mediaSourceMode == VLCMediaSourceModeLAN
-            ? _lanDeviceSnapshot[row].inputNode.inputItem.name
-            : _mediaSources[row].mediaSourceDescription;
+            ? _tableViewDevices[row].inputNode.inputItem.name
+            : _tableViewSources[row].mediaSourceDescription;
 
         cellView.primaryTitleTextField.hidden = YES;
         cellView.secondaryTitleTextField.hidden = YES;
         cellView.singlePrimaryTitleTextField.hidden = NO;
         cellView.singlePrimaryTitleTextField.stringValue = name;
         return cellView;
-    } else if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableKindColumn"]) {
+    } else if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableKindColumnIdentifier]) {
         static NSString * const basicCellViewIdentifier = @"BasicTableCellViewIdentifier";
         NSTableCellView *cellView =
             [tableView makeViewWithIdentifier:basicCellViewIdentifier owner:self];
@@ -493,26 +534,10 @@ referenceSizeForHeaderInSection:(NSInteger)section
         NSAssert(cellView, @"Cell view should not be nil");
 
         if (_mediaSourceMode == VLCMediaSourceModeLAN) {
-            VLCInputItem * const currentNodeInput = _lanDeviceSnapshot[row].inputNode.inputItem;
-            if (currentNodeInput.inputType == ITEM_TYPE_DIRECTORY) {
-                cellView.textField.stringValue = _NS("Directory");
-            }
+            cellView.textField.stringValue = _tableViewDevices[row].inputNode.inputItem.inputTypeDescription;
         } else {
-            VLCMediaSource * const mediaSource = _mediaSources[row];
-            switch(mediaSource.category) {
-                case SD_CAT_DEVICES:
-                    cellView.textField.stringValue = _NS("Devices");
-                    break;
-                case SD_CAT_LAN:
-                    cellView.textField.stringValue = _NS("LAN");
-                    break;
-                case SD_CAT_INTERNET:
-                    cellView.textField.stringValue = _NS("Internet");
-                    break;
-                case SD_CAT_MYCOMPUTER:
-                    cellView.textField.stringValue = _NS("My Computer");
-                    break;
-            }
+            VLCMediaSource * const mediaSource = _tableViewSources[row];
+            cellView.textField.stringValue = mediaSource.categoryDescription;
         }
         return cellView;
     }
@@ -521,6 +546,9 @@ referenceSizeForHeaderInSection:(NSInteger)section
 
 - (void)tableViewSelectionDidChange:(NSNotification *)notification
 {
+    if (_ignoringTableViewSelectionChanges) {
+        return;
+    }
     const NSInteger selectedRow = self.tableView.selectedRow;
     if (selectedRow < 0) {
         return;
@@ -529,11 +557,11 @@ referenceSizeForHeaderInSection:(NSInteger)section
     VLCMediaSource *mediaSource = nil;
     VLCInputNode *childNode = nil;
     if (_mediaSourceMode == VLCMediaSourceModeLAN) {
-        VLCLANDeviceRecord * const record = _lanDeviceSnapshot[selectedRow];
+        VLCLANDeviceRecord * const record = _tableViewDevices[selectedRow];
         mediaSource = record.mediaSource;
         childNode = record.inputNode;
     } else {
-        mediaSource = _mediaSources[selectedRow];
+        mediaSource = _tableViewSources[selectedRow];
         childNode = mediaSource.rootNode;
     }
     NSAssert(mediaSource != nil, @"Media source should not be nil");
@@ -546,8 +574,8 @@ referenceSizeForHeaderInSection:(NSInteger)section
 - (id<NSPasteboardWriting>)tableView:(NSTableView *)tableView pasteboardWriterForRow:(NSInteger)row
 {
     VLCInputItem * const inputItem = _mediaSourceMode == VLCMediaSourceModeLAN
-        ? _lanDeviceSnapshot[row].inputNode.inputItem
-        : _mediaSources[row].rootNode.inputItem;
+        ? _tableViewDevices[row].inputNode.inputItem
+        : _tableViewSources[row].rootNode.inputItem;
     return [self pasteboardWriterForInputItem:inputItem];
 }
 
@@ -583,44 +611,28 @@ referenceSizeForHeaderInSection:(NSInteger)section
     return [NSPasteboardItem pasteboardItemWithInputItem:inputItem];
 }
 
-#pragma mark - LAN device snapshot
+#pragma mark - LAN device records
 
-- (NSArray<VLCLANDeviceRecord *> *)buildMediaSourceSnapshot
+- (void)rebuildLANDeviceRecords
 {
-    [_sourceRecords removeAllObjects];
-    NSMutableArray<VLCLANDeviceRecord *> * const flat = [NSMutableArray array];
+    [_deviceRecordsBySource removeAllObjects];
+    if (self.mediaSourceMode != VLCMediaSourceModeLAN) {
+        return;
+    }
     for (VLCMediaSource * const mediaSource in _mediaSources) {
-        NSMutableArray<VLCLANDeviceRecord *> * const sourceRecs = [NSMutableArray array];
-        for (VLCInputNode * const child in mediaSource.rootNode.children) {
-            VLCLANDeviceRecord * const record = [[VLCLANDeviceRecord alloc] initWithMediaSource:mediaSource
-                                                                                          inputNode:child];
-            [sourceRecs addObject:record];
-        }
-        NSArray<VLCLANDeviceRecord *> * const frozen = [sourceRecs copy];
-        [_sourceRecords setObject:frozen forKey:mediaSource];
-        [flat addObjectsFromArray:frozen];
+        [self updateLANDeviceRecordsForMediaSource:mediaSource];
     }
-    return [flat copy];
 }
 
-- (NSArray<VLCLANDeviceRecord *> *)snapshotByUpdatingSource:(VLCMediaSource *)mediaSource
+- (void)updateLANDeviceRecordsForMediaSource:(VLCMediaSource *)mediaSource
 {
-    NSMutableArray<VLCLANDeviceRecord *> * const sourceRecs = [NSMutableArray array];
-    for (VLCInputNode * const child in mediaSource.rootNode.children) {
+    NSMutableArray<VLCLANDeviceRecord *> * const records = NSMutableArray.array;
+    for (VLCInputNode * const node in mediaSource.rootNode.children) {
         VLCLANDeviceRecord * const record = [[VLCLANDeviceRecord alloc] initWithMediaSource:mediaSource
-                                                                                      inputNode:child];
-        [sourceRecs addObject:record];
-    }
-    [_sourceRecords setObject:[sourceRecs copy] forKey:mediaSource];
-
-    NSMutableArray<VLCLANDeviceRecord *> * const flat = [NSMutableArray array];
-    for (VLCMediaSource * const source in _mediaSources) {
-        NSArray<VLCLANDeviceRecord *> * const recs = [_sourceRecords objectForKey:source];
-        if (recs) {
-            [flat addObjectsFromArray:recs];
-        }
+                                                                                  inputNode:node];
+        [records addObject:record];
     }
-    return [flat copy];
+    [_deviceRecordsBySource setObject:records.copy forKey:mediaSource];
 }
 
 #pragma mark - glue code
@@ -667,6 +679,10 @@ referenceSizeForHeaderInSection:(NSInteger)section
         return;
     }
     
+    // The shared table's old row indexes belong to the previous location.
+    _ignoringTableViewSelectionChanges = YES;
+    [self.tableView deselectAll:self];
+    _ignoringTableViewSelectionChanges = NO;
     _childDataSource = childDataSource;
 
     if (_mediaSourceMode == VLCMediaSourceModeLAN) {
@@ -799,9 +815,9 @@ referenceSizeForHeaderInSection:(NSInteger)section
     if (self.mediaSourceMode == VLCMediaSourceModeLAN) {
         VLCMediaSource * const source = aNotification.object;
         if ([aNotification.name isEqualToString:VLCMediaSourceChildrenReset]) {
-            _lanDeviceSnapshot = [self buildMediaSourceSnapshot];
+            [self rebuildLANDeviceRecords];
         } else {
-            _lanDeviceSnapshot = [self snapshotByUpdatingSource:source];
+            [self updateLANDeviceRecordsForMediaSource:source];
         }
     }
     
@@ -810,12 +826,56 @@ referenceSizeForHeaderInSection:(NSInteger)section
     });
 }
 
+- (void)updateTableViewItems
+{
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:self.tableView.sortDescriptors ?: @[]];
+    NSMutableArray<VLCLANDeviceRecord *> * const devices = NSMutableArray.array;
+    for (VLCMediaSource * const source in _mediaSources) {
+        NSArray<VLCLANDeviceRecord *> * const records = [_deviceRecordsBySource objectForKey:source];
+        if (records != nil) {
+            [devices addObjectsFromArray:records];
+        }
+    }
+    _tableViewDevices = [sorting sortedItems:devices
+                                valueForItem:^id(VLCLANDeviceRecord *record, NSString *key) {
+        if ([key isEqualToString:VLCMediaSourceTableNameSortDescriptorKey]) {
+            return record.inputNode.inputItem.name ?: @"";
+        }
+        if ([key isEqualToString:VLCMediaSourceTableKindSortDescriptorKey]) {
+            return record.inputNode.inputItem.inputTypeDescription;
+        }
+        return [key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey] ? @(-1) : @"";
+    }];
+    _tableViewSources = [sorting sortedItems:_mediaSources
+                                valueForItem:^id(VLCMediaSource *source, NSString *key) {
+        if ([key isEqualToString:VLCMediaSourceTableNameSortDescriptorKey]) {
+            return source.mediaSourceDescription ?: @"";
+        }
+        if ([key isEqualToString:VLCMediaSourceTableKindSortDescriptorKey]) {
+            return source.categoryDescription;
+        }
+        return [key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey] ? @(-1) : @"";
+    }];
+}
+
+- (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray<NSSortDescriptor *> *)oldDescriptors
+{
+    [self reloadData];
+}
+
 - (void)reloadData
 {
+    [self updateTableViewItems];
     if (self.viewMode == VLCLibraryGridViewModeSegment) {
         [self.collectionView reloadData];
+    } else if (self.childDataSource != nil) {
+        [self.childDataSource reloadData];
     } else {
+        _ignoringTableViewSelectionChanges = YES;
+        [self.tableView deselectAll:self];
         [self.tableView reloadData];
+        _ignoringTableViewSelectionChanges = NO;
     }
 
     [NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceBaseDataSourceNodeChanged


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.h
=====================================
@@ -52,6 +52,7 @@ extern NSString * const VLCMediaSourceDataSourceLoadingEnded;
 
 - (instancetype)initWithParentBaseDataSource:(VLCMediaSourceBaseDataSource *)parentBaseDataSource;
 - (void)setupViews;
+- (void)reloadData;
 - (VLCInputItem *)mediaSourceInputItemAtRow:(NSInteger)tableViewRow;
 - (NSArray<VLCInputItem *> *)mediaSourceInputItemsAtIndexPaths:(NSSet<NSIndexPath *> *const)indexPaths;
 


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
=====================================
@@ -25,6 +25,7 @@
 #import "VLCLibraryMediaSourceViewNavigationStack.h"
 #import "VLCMediaSource.h"
 #import "VLCMediaSourceBaseDataSource.h"
+#import "VLCMediaSourceTableViewSorting.h"
 
 #import "extensions/NSPasteboardItem+VLCAdditions.h"
 #import "extensions/NSString+Helpers.h"
@@ -55,6 +56,9 @@ NSString * const VLCMediaSourceDataSourceLoadingEnded = @"VLCMediaSourceDataSour
 @interface VLCMediaSourceDataSource()
 {
     VLCInputItem *_childRootInput;
+    // Table row order; grid items continue to use nodeToDisplay.children.
+    NSArray<VLCInputNode *> *_tableViewNodes;
+    BOOL _ignoringTableViewSelectionChanges;
 }
 
 @property (readwrite) dispatch_source_t observedPathDispatchSource;
@@ -157,6 +161,13 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
 
     if (!preparseStarted) {
         [self.nodeToDisplay clearChildrenCache];
+    }
+
+    if (self.parentBaseDataSource.childDataSource != self) {
+        return;
+    }
+
+    if (!preparseStarted) {
         [self reloadData];
     }
 
@@ -167,9 +178,27 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
                                                       object:self];
 }
 
+- (nullable NSNumber *)knownChildCountForInputNode:(VLCInputNode *)inputNode
+{
+    if (inputNode.inputItem.inputType != ITEM_TYPE_DIRECTORY) {
+        return nil;
+    }
+
+    NSValue * const identifier = inputItemIdentifier(inputNode.inputItem);
+    if ([self.unavailableInputItemIdentifiers containsObject:identifier]) {
+        return nil;
+    }
+
+    const int numberOfChildren = inputNode.numberOfChildren;
+    if (numberOfChildren > 0 || [self.preparedInputItemIdentifiers containsObject:identifier]) {
+        return @(numberOfChildren);
+    }
+    return self.childCountsByInputItemIdentifier[identifier];
+}
+
 - (void)reloadCountForInputItemIdentifier:(NSValue *)identifier
 {
-    NSArray<VLCInputNode *> * const children = self.nodeToDisplay.children;
+    NSArray<VLCInputNode *> * const children = _tableViewNodes;
     const NSUInteger row = [children indexOfObjectPassingTest:^BOOL(VLCInputNode * const childNode,
                                                                     NSUInteger __unused idx,
                                                                     BOOL * const __unused stop) {
@@ -180,8 +209,20 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
     }
 
     [children[row] clearChildrenCache];
+    if (self.tableView.dataSource != self) {
+        return;
+    }
 
-    const NSInteger countColumn = [self.tableView columnWithIdentifier:@"VLCMediaSourceTableCountColumn"];
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:self.tableView.sortDescriptors];
+    // A changed count can move rows when Count is the primary sort or a tie breaker.
+    // Reload to apply that ordering and restore selection; otherwise refresh only the count cell.
+    if ([sorting sortsByKey:VLCMediaSourceTableCountSortDescriptorKey]) {
+        [self reloadData];
+        return;
+    }
+
+    const NSInteger countColumn = [self.tableView columnWithIdentifier:VLCMediaSourceTableCountColumnIdentifier];
     if (self.tableView.hidden || countColumn == -1) {
         return;
     }
@@ -204,6 +245,10 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
         return;
     dispatch_async(dispatch_get_main_queue(), ^{
         [self clearDisplayedNodeChildrenCaches];
+        // Navigation history retains inactive data sources that also receive these notifications.
+        if (self.parentBaseDataSource.childDataSource != self) {
+            return;
+        }
         [self reloadData];
     });
 }
@@ -220,7 +265,7 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
     self.nodeObservation = [self.displayedMediaSource observeInputNode:nodeToDisplay
                                                               onChange:^(VLCMediaSourceNodeChange change) {
         const typeof(self) strongSelf = weakSelf;
-        if (strongSelf == nil) {
+        if (strongSelf == nil || strongSelf.parentBaseDataSource.childDataSource != strongSelf) {
             return;
         }
         switch (change) {
@@ -233,7 +278,12 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
         }
     }];
 
-    [self reloadData];
+    // Initial node configuration precedes attaching the shared views.
+    if (self.tableView == nil) {
+        [self updateTableViewNodes];
+    } else {
+        [self reloadData];
+    }
     NSValue * const identifier = inputItemIdentifier(nodeToDisplay.inputItem);
     NSString * const loadingNotificationName =
         [self.preparingInputItemIdentifiers containsObject:identifier]
@@ -250,6 +300,7 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
 
 - (void)setupViews
 {
+    [self updateTableViewNodes];
     [self.tableView setDoubleAction:@selector(tableViewAction:)];
     [self.tableView setTarget:self];
     [self.tableView registerForDraggedTypes:@[NSFilenamesPboardType]];
@@ -348,22 +399,49 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
 
 #pragma mark - table view data source and delegation
 
+- (void)updateTableViewNodes
+{
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:self.tableView.sortDescriptors ?: @[]];
+    _tableViewNodes = [sorting sortedItems:self.nodeToDisplay.children ?: @[]
+                              valueForItem:^id(VLCInputNode *node, NSString *key) {
+        VLCInputItem * const inputItem = node.inputItem;
+        if ([key isEqualToString:VLCMediaSourceTableNameSortDescriptorKey]) {
+            return inputItem.name ?: @"";
+        }
+        if ([key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey]) {
+            return [self knownChildCountForInputNode:node] ?: @(-1);
+        }
+        if ([key isEqualToString:VLCMediaSourceTableKindSortDescriptorKey]) {
+            return inputItem.inputTypeDescription;
+        }
+        NSAssert([key isEqualToString:VLCMediaSourceTableTagsSortDescriptorKey],
+                 @"Unexpected media source sort key: %@", key);
+        return [inputItem.finderTags componentsJoinedByString:@", "] ?: @"";
+    }];
+}
+
 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
 {
     if (_nodeToDisplay) {
-        return _nodeToDisplay.numberOfChildren;
+        return _tableViewNodes.count;
     }
 
     return 0;
 }
 
+- (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray<NSSortDescriptor *> *)oldDescriptors
+{
+    [self reloadData];
+}
+
 - (NSView *)tableView:(NSTableView *)tableView
    viewForTableColumn:(NSTableColumn *)tableColumn
                   row:(NSInteger)row
 {
     VLCInputNode * const inputNode = [self mediaSourceInputNodeAtRow:row];
 
-    if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableNameColumn"]) {
+    if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableNameColumnIdentifier]) {
         VLCLibraryTableCellView * const cellView =
             [tableView makeViewWithIdentifier:VLCLibraryTableCellViewIdentifier owner:self];
         cellView.representedInputItem = inputNode.inputItem;
@@ -373,7 +451,7 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
     }
 
      // Only present count view for folders
-    if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableCountColumn"] &&
+    if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableCountColumnIdentifier] &&
         inputNode.inputItem.inputType != ITEM_TYPE_DIRECTORY) {
         return nil;
     }
@@ -387,18 +465,14 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
     }
     NSAssert(cellView, @"Cell view should not be nil");
 
-    if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableCountColumn"]) {
+    if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableCountColumnIdentifier]) {
         NSValue * const identifier = inputItemIdentifier(inputNode.inputItem);
-        const int numberOfChildren = inputNode.numberOfChildren;
-        NSNumber * const cachedChildCount = self.childCountsByInputItemIdentifier[identifier];
+        NSNumber * const knownChildCount = [self knownChildCountForInputNode:inputNode];
         if ([self.unavailableInputItemIdentifiers containsObject:identifier]) {
             cellView.textField.stringValue = NSTR("Unavailable");
-        } else if (numberOfChildren > 0 || [self.preparedInputItemIdentifiers containsObject:identifier]) {
+        } else if (knownChildCount != nil) {
             cellView.textField.stringValue =
-                [NSString stringWithFormat:@"%i items", numberOfChildren];
-        } else if (cachedChildCount != nil) {
-            cellView.textField.stringValue =
-                [NSString stringWithFormat:@"%li items", cachedChildCount.integerValue];
+                [NSString stringWithFormat:@"%li items", knownChildCount.integerValue];
         } else {
             cellView.textField.stringValue = NSTR("Loading…");
             if (![self.preparingInputItemIdentifiers containsObject:identifier]) {
@@ -426,57 +500,9 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
                 });
             }
         }
-    } else if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableKindColumn"]) {
-        NSString *typeName = NSTR("Unknown");
-        switch (inputNode.inputItem.inputType) {
-            case ITEM_TYPE_UNKNOWN:
-                typeName = NSTR("Unknown");
-                break;
-            case ITEM_TYPE_FILE:
-            {
-                NSString * const filePath = inputNode.inputItem.MRL;
-                NSString * const extension = filePath.pathExtension.lowercaseString;
-                if (extension.length > 0) {
-                    typeName = [NSString stringWithFormat:@"%@ File", extension.capitalizedString];
-
-                    const CFStringRef extCF = (__bridge CFStringRef)extension;
-                    const CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extCF, NULL);
-                    if (uti) {
-                        CFStringRef descriptionCF = UTTypeCopyDescription(uti);
-                        if (descriptionCF) {
-                            typeName = CFBridgingRelease(descriptionCF);
-                        }
-                        CFRelease(uti);
-                    }
-                } else {
-                    typeName = NSTR("File");
-                }
-                break;
-            }
-            case ITEM_TYPE_DIRECTORY:
-                typeName = NSTR("Directory");
-                break;
-            case ITEM_TYPE_DISC:
-                typeName = NSTR("Disc");
-                break;
-            case ITEM_TYPE_CARD:
-                typeName = NSTR("Card");
-                break;
-            case ITEM_TYPE_STREAM:
-                typeName = NSTR("Stream");
-                break;
-            case ITEM_TYPE_PLAYLIST:
-                typeName = NSTR("Playlist");
-                break;
-            case ITEM_TYPE_NODE:
-                typeName = NSTR("Node");
-                break;
-            case ITEM_TYPE_NUMBER:
-                typeName = NSTR("Undefined");
-                break;
-        }
-        cellView.textField.stringValue = typeName;
-    } else if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableTagsColumn"]) {
+    } else if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableKindColumnIdentifier]) {
+        cellView.textField.stringValue = inputNode.inputItem.inputTypeDescription;
+    } else if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableTagsColumnIdentifier]) {
         static NSString * const basicCellViewIdentifier = @"BasicTableCellViewIdentifier";
         NSTableCellView *cellView = [tableView makeViewWithIdentifier:basicCellViewIdentifier
                                                                 owner:self];
@@ -498,6 +524,9 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
 
 - (void)tableViewSelectionDidChange:(NSNotification *)notification
 {
+    if (_ignoringTableViewSelectionChanges) {
+        return;
+    }
     NSInteger selectedIndex = self.tableView.selectedRow;
     if (selectedIndex < 0) {
         return;
@@ -557,14 +586,10 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
         return nil;
     }
 
-    VLCInputNode *rootNode = _nodeToDisplay;
-    NSArray *nodeChildren = rootNode.children;
-
-    if (nodeChildren == nil || nodeChildren.count == 0) {
+    if (tableViewRow < 0 || (NSUInteger)tableViewRow >= _tableViewNodes.count) {
         return nil;
     }
-
-    return nodeChildren[tableViewRow];
+    return _tableViewNodes[tableViewRow];
 }
 
 - (VLCInputItem*)mediaSourceInputItemAtRow:(NSInteger)tableViewRow
@@ -609,13 +634,37 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
 
 - (void)reloadData
 {
+    NSParameterAssert(self.tableView.dataSource == self);
+
     if (!_collectionView.hidden) {
         [_collectionView reloadData];
     }
 
-    if(!_tableView.hidden) {
-        [_tableView reloadData];
-    }
+    // Row indexes change when sorting; preserve selection by input-item identity.
+    NSMutableSet<NSValue *> * const selectedInputItemIdentifiers = NSMutableSet.set;
+    [self.tableView.selectedRowIndexes enumerateIndexesUsingBlock:^(NSUInteger row, BOOL * const __unused stop) {
+        if (row >= self->_tableViewNodes.count) {
+            return;
+        }
+
+        NSValue * const identifier = inputItemIdentifier(self->_tableViewNodes[row].inputItem);
+        if (identifier == nil) {
+            return;
+        }
+        [selectedInputItemIdentifiers addObject:identifier];
+    }];
+    [self updateTableViewNodes];
+    _ignoringTableViewSelectionChanges = YES;
+    [self.tableView reloadData];
+    NSMutableIndexSet * const selectedRows = NSMutableIndexSet.indexSet;
+    [_tableViewNodes enumerateObjectsUsingBlock:^(VLCInputNode * const node, NSUInteger row, BOOL * const __unused stop) {
+        if ([selectedInputItemIdentifiers containsObject:inputItemIdentifier(node.inputItem)]) {
+            [selectedRows addIndex:row];
+        }
+    }];
+    // Restoring selection must not trigger folder navigation.
+    [self.tableView selectRowIndexes:selectedRows byExtendingSelection:NO];
+    _ignoringTableViewSelectionChanges = NO;
 
     [NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceDataSourceNodeChanged
                                                       object:self];


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.h
=====================================
@@ -0,0 +1,42 @@
+/****************************************************************************
+ * VLCMediaSourceTableViewSorting.h: Browse table sorting
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+ at interface VLCMediaSourceTableViewSorting : NSObject
+
+// Descriptors are applied in array order: the first is the primary sort,
+// and each subsequent descriptor breaks ties left by the preceding ones.
+- (instancetype)initWithSortDescriptors:(NSArray<NSSortDescriptor *> *)sortDescriptors;
+
+// Returns YES if any configured descriptor uses this key, including tie breakers,
+// and NO otherwise. This query leaves descriptor order and priority unchanged.
+- (BOOL)sortsByKey:(NSString *)sortDescriptorKey;
+
+- (NSArray *)sortedItems:(NSArray *)items
+            valueForItem:(id (^)(id item, NSString *key))valueForItem;
+
+ at end
+
+NS_ASSUME_NONNULL_END


=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m
=====================================
@@ -0,0 +1,89 @@
+/****************************************************************************
+ * VLCMediaSourceTableViewSorting.m: Browse table sorting
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#import "VLCMediaSourceTableViewSorting.h"
+
+ at interface VLCMediaSourceTableViewSorting ()
+ at property (readonly, copy) NSArray<NSSortDescriptor *> *sortDescriptors;
+ at end
+
+ at implementation VLCMediaSourceTableViewSorting
+
+- (instancetype)initWithSortDescriptors:(NSArray<NSSortDescriptor *> *)sortDescriptors
+{
+    self = [super init];
+    if (self) {
+        _sortDescriptors = [sortDescriptors copy];
+    }
+    return self;
+}
+
+- (BOOL)sortsByKey:(NSString *)sortDescriptorKey
+{
+    for (NSSortDescriptor * const descriptor in self.sortDescriptors) {
+        if ([descriptor.key isEqualToString:sortDescriptorKey]) {
+            return YES;
+        }
+    }
+    return NO;
+}
+
+- (NSArray *)sortedItems:(NSArray *)items
+            valueForItem:(id (^)(id item, NSString *key))valueForItem
+{
+    if (self.sortDescriptors.count == 0) {
+        return items;
+    }
+    // Cache by object identity for this call only; subsequent sorts read fresh values.
+    NSMapTable<id, NSDictionary<NSString *, id> *> * const valuesByItem =
+        [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality
+                              valueOptions:NSPointerFunctionsStrongMemory];
+
+    // Read each distinct object's active keys once to avoid metadata reads during comparisons.
+    for (id item in items) {
+        if ([valuesByItem objectForKey:item] != nil) {
+            continue;
+        }
+        NSMutableDictionary<NSString *, id> * const values = NSMutableDictionary.dictionary;
+        for (NSSortDescriptor * const descriptor in self.sortDescriptors) {
+            if (values[descriptor.key] == nil) {
+                values[descriptor.key] = valueForItem(item, descriptor.key);
+            }
+        }
+        [valuesByItem setObject:values forKey:item];
+    }
+
+    // Sort the original objects, preserving their input order when all descriptors compare equal.
+    return [items sortedArrayWithOptions:NSSortStable usingComparator:^NSComparisonResult(id left, id right) {
+        // Compare cached values in descriptor order, using later descriptors to break ties.
+        for (NSSortDescriptor * const descriptor in self.sortDescriptors) {
+            const NSComparisonResult result = [descriptor compareObject:[valuesByItem objectForKey:left]
+                                                               toObject:[valuesByItem objectForKey:right]];
+            if (result != NSOrderedSame) {
+                return result;
+            }
+        }
+        return NSOrderedSame;
+    }];
+}
+
+ at end


=====================================
modules/gui/macosx/tests/Makefile.am
=====================================
@@ -16,6 +16,7 @@ VLCMacOSXTests_SOURCES = \
 	gui/macosx/tests/VLCInputItemTest.m \
 	gui/macosx/tests/VLCInputItemTestSupport.m \
 	gui/macosx/tests/VLCInputNodeTest.m \
+	gui/macosx/tests/VLCMediaSourceTableViewSortingTest.m \
 	gui/macosx/tests/VLCLibraryDataTypesIntegrationTest.m \
 	gui/macosx/tests/VLCLibraryDataTypesIntegrationTestSupport.m \
 	gui/macosx/tests/VLCLogMessageTest.m \
@@ -27,6 +28,7 @@ VLCMacOSXTests_SOURCES = \
 	gui/macosx/library/VLCLibraryDataTypes.m \
 	gui/macosx/library/VLCInputItem.m \
 	gui/macosx/library/VLCInputNode.m \
+	gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m \
 	gui/macosx/windows/logging/VLCLogMessage.m \
 	gui/macosx/views/VLCTimeFormatter.m
 
@@ -58,6 +60,7 @@ VLCMacOSXTests_LDFLAGS = \
 	-Wl,-framework,XCTest \
 	-Wl,-framework,Foundation \
 	-Wl,-framework,Cocoa \
+	-Wl,-framework,CoreServices \
 	-Wl,-framework,IOKit
 
 VLCMacOSXTests_LDADD = \


=====================================
modules/gui/macosx/tests/NSStringHelpersTest.m
=====================================
@@ -12,6 +12,7 @@
  *****************************************************************************/
 
 #import <XCTest/XCTest.h>
+#import <CoreServices/CoreServices.h>
 #import <vlc_tick.h>
 
 #import <string.h>
@@ -100,6 +101,28 @@
                                                negative:YES], @"01:00");
 }
 
+- (void)testFileTypeDescriptionWithoutExtension
+{
+    XCTAssertEqualObjects([@"" fileTypeDescription], NSTR("File"));
+    XCTAssertEqualObjects([@"/media/README" fileTypeDescription], NSTR("File"));
+    XCTAssertEqualObjects([@"file:///media/README" fileTypeDescription], NSTR("File"));
+    XCTAssertEqualObjects([@"/media/.hidden" fileTypeDescription], NSTR("File"));
+}
+
+- (void)testFileTypeDescriptionForUnknownExtension
+{
+    XCTAssertEqualObjects([@"/media/file.VLCUNKNOWNFILETYPE18436572" fileTypeDescription],
+                          @"Vlcunknownfiletype18436572 File");
+}
+
+- (void)testFileTypeDescriptionUsesSystemDescription
+{
+    NSString * const description = CFBridgingRelease(UTTypeCopyDescription(kUTTypePlainText));
+    XCTAssertNotNil(description);
+    XCTAssertEqualObjects([@"/media/file.txt" fileTypeDescription], description);
+    XCTAssertEqualObjects([@"file:///media/file.TXT" fileTypeDescription], description);
+}
+
 - (void)testExtensionsArrayFromVLCStyleString
 {
     XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"*.mp4;*.mkv"],


=====================================
modules/gui/macosx/tests/VLCMediaSourceTableViewSortingTest.m
=====================================
@@ -0,0 +1,227 @@
+/****************************************************************************
+ * VLCMediaSourceTableViewSortingTest.m: Browse table sorting tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#import <XCTest/XCTest.h>
+
+#import "library/media-source/VLCMediaSourceTableViewSorting.h"
+
+static NSString * const VLCSortingNameKey = @"name";
+static NSString * const VLCSortingCountKey = @"count";
+static NSString * const VLCSortingKindKey = @"kind";
+static NSString * const VLCSortingTagsKey = @"tags";
+
+ at interface VLCSortingItem : NSObject
+
+ at property (copy) NSDictionary<NSString *, id> *values;
+ at property (readonly) NSMutableDictionary<NSString *, NSNumber *> *valueReadCounts;
+
+- (instancetype)initWithValues:(NSDictionary<NSString *, id> *)values;
+- (id)sortValueForKey:(NSString *)key;
+
+ at end
+
+ at implementation VLCSortingItem
+
+- (instancetype)initWithValues:(NSDictionary<NSString *, id> *)values
+{
+    self = [super init];
+    if (self) {
+        _values = [values copy];
+        _valueReadCounts = NSMutableDictionary.dictionary;
+    }
+    return self;
+}
+
+- (id)sortValueForKey:(NSString *)key
+{
+    _valueReadCounts[key] = @(_valueReadCounts[key].unsignedIntegerValue + 1);
+    return self.values[key];
+}
+
+ at end
+
+ at interface VLCMediaSourceTableViewSortingTest : XCTestCase
+ at end
+
+ at implementation VLCMediaSourceTableViewSortingTest
+
+- (void)testNameSortingUsesNaturalOrderAndReverses
+{
+    NSArray<NSDictionary *> * const items = @[
+        @{VLCSortingNameKey: @"Video 10"},
+        @{VLCSortingNameKey: @"video 2"},
+        @{VLCSortingNameKey: @"Video 1"}
+    ];
+    NSSortDescriptor * const descriptor = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey
+                                                                        ascending:YES
+                                                                         selector:@selector(localizedStandardCompare:)];
+    VLCMediaSourceTableViewSorting * const ascending =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor]];
+    VLCMediaSourceTableViewSorting * const descending =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor.reversedSortDescriptor]];
+    id (^valueForItem)(id, NSString *) = ^id(NSDictionary *item, NSString *key) {
+        return item[key];
+    };
+    XCTAssertEqualObjects([ascending sortedItems:items valueForItem:valueForItem],
+                          (@[items[2], items[1], items[0]]));
+    XCTAssertEqualObjects([descending sortedItems:items valueForItem:valueForItem],
+                          (@[items[0], items[1], items[2]]));
+}
+
+- (void)testCountsSortNumericallyIncludingUnknownCounts
+{
+    NSArray<NSDictionary *> * const items = @[
+        @{VLCSortingCountKey: @10},
+        @{VLCSortingCountKey: @2},
+        @{VLCSortingCountKey: @(-1)},
+        @{VLCSortingCountKey: @0}
+    ];
+    NSSortDescriptor * const descriptor = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey
+                                                                        ascending:YES
+                                                                         selector:@selector(compare:)];
+    VLCMediaSourceTableViewSorting * const ascending =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor]];
+    VLCMediaSourceTableViewSorting * const descending =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor.reversedSortDescriptor]];
+    id (^valueForItem)(id, NSString *) = ^id(NSDictionary *item, NSString *key) {
+        return item[key];
+    };
+    XCTAssertEqualObjects([ascending sortedItems:items valueForItem:valueForItem],
+                          (@[items[2], items[3], items[1], items[0]]));
+    XCTAssertEqualObjects([descending sortedItems:items valueForItem:valueForItem],
+                          (@[items[0], items[1], items[3], items[2]]));
+}
+
+- (void)testSortQueriesIncludeSecondaryDescriptors
+{
+    NSSortDescriptor * const name = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey ascending:YES];
+    NSSortDescriptor * const count = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey ascending:YES];
+    VLCMediaSourceTableViewSorting * const namesAndCounts =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[name, count]];
+    XCTAssertTrue([namesAndCounts sortsByKey:VLCSortingNameKey]);
+    XCTAssertTrue([namesAndCounts sortsByKey:VLCSortingCountKey]);
+    XCTAssertFalse([namesAndCounts sortsByKey:VLCSortingKindKey]);
+    XCTAssertFalse([namesAndCounts sortsByKey:VLCSortingTagsKey]);
+
+    NSSortDescriptor * const kind = [NSSortDescriptor sortDescriptorWithKey:VLCSortingKindKey ascending:YES];
+    NSSortDescriptor * const tags = [NSSortDescriptor sortDescriptorWithKey:VLCSortingTagsKey ascending:NO];
+    VLCMediaSourceTableViewSorting * const kindsAndTags =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[kind, tags]];
+    XCTAssertTrue([kindsAndTags sortsByKey:VLCSortingKindKey]);
+    XCTAssertTrue([kindsAndTags sortsByKey:VLCSortingTagsKey]);
+    XCTAssertFalse([kindsAndTags sortsByKey:VLCSortingNameKey]);
+    XCTAssertFalse([kindsAndTags sortsByKey:VLCSortingCountKey]);
+}
+
+- (void)testSecondaryDescriptorsAndStableTiesPreserveObjectIdentity
+{
+    NSArray<VLCSortingItem *> * const items = @[
+        [[VLCSortingItem alloc] initWithValues:@{VLCSortingKindKey: @"File", VLCSortingNameKey: @"B"}],
+        [[VLCSortingItem alloc] initWithValues:@{VLCSortingKindKey: @"File", VLCSortingNameKey: @"A"}],
+        [[VLCSortingItem alloc] initWithValues:@{VLCSortingKindKey: @"File", VLCSortingNameKey: @"A"}]
+    ];
+    NSSortDescriptor * const kind = [NSSortDescriptor sortDescriptorWithKey:VLCSortingKindKey ascending:YES];
+    NSSortDescriptor * const name = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey ascending:YES];
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[kind, name]];
+    NSArray * const sorted = [sorting sortedItems:items
+                                     valueForItem:^id(VLCSortingItem *item, NSString *key) {
+        return [item sortValueForKey:key];
+    }];
+    XCTAssertTrue(sorted[0] == items[1]);
+    XCTAssertTrue(sorted[1] == items[2]);
+    XCTAssertTrue(sorted[2] == items[0]);
+}
+
+- (void)testUnsortedItemsDoNotReadValues
+{
+    NSArray<NSDictionary *> * const items = @[
+        @{VLCSortingNameKey: @"B"},
+        @{VLCSortingNameKey: @"A"}
+    ];
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[]];
+    XCTAssertFalse([sorting sortsByKey:VLCSortingNameKey]);
+    XCTAssertFalse([sorting sortsByKey:VLCSortingCountKey]);
+    XCTAssertFalse([sorting sortsByKey:VLCSortingKindKey]);
+    XCTAssertFalse([sorting sortsByKey:VLCSortingTagsKey]);
+    NSArray * const sorted = [sorting sortedItems:items
+                                     valueForItem:^id(id __unused item, NSString * __unused key) {
+        XCTFail(@"Discovery order should not read sort values");
+        return nil;
+    }];
+    XCTAssertTrue(sorted == items);
+    XCTAssertEqualObjects([sorting sortedItems:@[] valueForItem:^id(id __unused item, NSString * __unused key) {
+        XCTFail(@"An empty list should not read sort values");
+        return nil;
+    }], @[]);
+}
+
+- (void)testOnlyActiveKeysAreReadOnceForRepeatedObjectsAndDescriptors
+{
+    VLCSortingItem * const first = [[VLCSortingItem alloc] initWithValues:@{
+        VLCSortingNameKey: @"Folder", VLCSortingCountKey: @2,
+        VLCSortingKindKey: @"Directory", VLCSortingTagsKey: @"Blue"
+    }];
+    VLCSortingItem * const second = [[VLCSortingItem alloc] initWithValues:@{
+        VLCSortingNameKey: @"Folder", VLCSortingCountKey: @1,
+        VLCSortingKindKey: @"Directory", VLCSortingTagsKey: @"Red"
+    }];
+    NSArray<VLCSortingItem *> * const items = @[first, second, first];
+    NSSortDescriptor * const name = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey ascending:YES];
+    NSSortDescriptor * const count = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey ascending:YES];
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[name, count, name]];
+    NSArray * const sorted = [sorting sortedItems:items
+                                     valueForItem:^id(VLCSortingItem *item, NSString *key) {
+        return [item sortValueForKey:key];
+    }];
+    XCTAssertTrue(sorted[0] == second);
+    XCTAssertTrue(sorted[1] == first);
+    XCTAssertTrue(sorted[2] == first);
+    for (VLCSortingItem * const item in @[first, second]) {
+        XCTAssertEqualObjects(item.valueReadCounts[VLCSortingNameKey], @1);
+        XCTAssertEqualObjects(item.valueReadCounts[VLCSortingCountKey], @1);
+        XCTAssertNil(item.valueReadCounts[VLCSortingKindKey]);
+        XCTAssertNil(item.valueReadCounts[VLCSortingTagsKey]);
+    }
+}
+
+- (void)testSortingAgainReadsUpdatedValues
+{
+    VLCSortingItem * const first = [[VLCSortingItem alloc] initWithValues:@{VLCSortingCountKey: @(-1)}];
+    VLCSortingItem * const second = [[VLCSortingItem alloc] initWithValues:@{VLCSortingCountKey: @2}];
+    NSArray<VLCSortingItem *> * const items = @[first, second];
+    NSSortDescriptor * const descriptor = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey ascending:YES];
+    VLCMediaSourceTableViewSorting * const sorting =
+        [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor]];
+    id (^valueForItem)(id, NSString *) = ^id(VLCSortingItem *item, NSString *key) {
+        return [item sortValueForKey:key];
+    };
+    XCTAssertTrue([sorting sortedItems:items valueForItem:valueForItem][0] == first);
+    first.values = @{VLCSortingCountKey: @10};
+    XCTAssertTrue([sorting sortedItems:items valueForItem:valueForItem][0] == second);
+    XCTAssertEqualObjects(first.valueReadCounts[VLCSortingCountKey], @2);
+    XCTAssertEqualObjects(second.valueReadCounts[VLCSortingCountKey], @2);
+}
+
+ at end



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/3c3b719fc6be39c796e9312b4157aa701d7f7bbc...716e856c9f56000a6cf73c49dbc63a0c67ebb620

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/3c3b719fc6be39c796e9312b4157aa701d7f7bbc...716e856c9f56000a6cf73c49dbc63a0c67ebb620
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