You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
457 lines
16 KiB
457 lines
16 KiB
/*****************************************************************************
|
|
* VLCMediaSourceDataSource.m: MacOS X interface module
|
|
*****************************************************************************
|
|
* Copyright (C) 2019 VLC authors and VideoLAN
|
|
*
|
|
* Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
|
|
*
|
|
* 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 "VLCMediaSourceDataSource.h"
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#import "VLCLibraryMediaSourceViewNavigationStack.h"
|
|
#import "VLCMediaSourceBaseDataSource.h"
|
|
#import "VLCMediaSourceCollectionViewItem.h"
|
|
#import "VLCMediaSource.h"
|
|
|
|
#import "extensions/NSString+Helpers.h"
|
|
#import "extensions/NSTableCellView+VLCAdditions.h"
|
|
|
|
#import "library/VLCInputItem.h"
|
|
#import "library/VLCInputNodePathControl.h"
|
|
#import "library/VLCInputNodePathControlItem.h"
|
|
#import "library/VLCLibraryImageCache.h"
|
|
#import "library/VLCLibraryTableCellView.h"
|
|
#import "library/VLCLibraryUIUnits.h"
|
|
#import "library/VLCLibraryWindow.h"
|
|
|
|
#import "main/VLCMain.h"
|
|
|
|
#import "playqueue/VLCPlayQueueController.h"
|
|
|
|
#import "views/VLCImageView.h"
|
|
|
|
NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourceNodeChanged";
|
|
|
|
@interface VLCMediaSourceDataSource()
|
|
{
|
|
VLCInputItem *_childRootInput;
|
|
}
|
|
|
|
@property (readwrite) dispatch_source_t observedPathDispatchSource;
|
|
|
|
@end
|
|
|
|
@implementation VLCMediaSourceDataSource
|
|
|
|
- (instancetype)initWithParentBaseDataSource:(VLCMediaSourceBaseDataSource *)parentBaseDataSource
|
|
{
|
|
self = [super init];
|
|
if (self)
|
|
self.parentBaseDataSource = parentBaseDataSource;
|
|
return self;
|
|
}
|
|
|
|
- (dispatch_source_t)observeLocalUrl:(NSURL *)url
|
|
forVnodeEvents:(dispatch_source_vnode_flags_t)eventsFlags
|
|
withEventHandler:(dispatch_block_t)eventHandlerBlock
|
|
{
|
|
const uintptr_t descriptor = open(url.path.UTF8String, O_EVTONLY);
|
|
if (descriptor == (uintptr_t)-1) {
|
|
return nil;
|
|
}
|
|
struct stat fileStat;
|
|
const int statResult = fstat(descriptor, &fileStat);
|
|
if (statResult == -1) {
|
|
NSLog(@"Failed to stat file %@: %s", url.path, strerror(errno));
|
|
return nil;
|
|
}
|
|
|
|
const dispatch_queue_t globalQueue =
|
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
|
const dispatch_source_t fileDispatchSource =
|
|
dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, descriptor, eventsFlags, globalQueue);
|
|
dispatch_source_set_event_handler(fileDispatchSource, eventHandlerBlock);
|
|
dispatch_source_set_cancel_handler(fileDispatchSource, ^{
|
|
close(descriptor);
|
|
});
|
|
dispatch_resume(fileDispatchSource);
|
|
return fileDispatchSource;
|
|
}
|
|
|
|
- (void)setNodeToDisplay:(nonnull VLCInputNode*)nodeToDisplay
|
|
{
|
|
NSAssert(nodeToDisplay, @"Nil node to display, will not set");
|
|
_nodeToDisplay = nodeToDisplay;
|
|
|
|
input_item_node_t * const inputNode = nodeToDisplay.vlcInputItemNode;
|
|
|
|
NSParameterAssert(self.parentBaseDataSource);
|
|
if (self.parentBaseDataSource.mediaSourceMode == VLCMediaSourceModeLAN) {
|
|
NSURL * const nodeUrl = [NSURL URLWithString:nodeToDisplay.inputItem.MRL];
|
|
NSError * const error =
|
|
[self.displayedMediaSource generateChildNodesForDirectoryNode:inputNode
|
|
withUrl:nodeUrl];
|
|
if (error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSAlert * const alert = [NSAlert alertWithError:error];
|
|
alert.alertStyle = NSAlertStyleCritical;
|
|
[alert runModal];
|
|
});
|
|
return;
|
|
}
|
|
|
|
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:inputNode
|
|
withUrl:nodeUrl];
|
|
[weakSelf reloadData];
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
[self reloadData];
|
|
}
|
|
|
|
- (void)setupViews
|
|
{
|
|
[self.tableView setDoubleAction:@selector(tableViewAction:)];
|
|
[self.tableView setTarget:self];
|
|
}
|
|
|
|
- (nullable VLCInputNode *)inputNodeForIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
VLCInputNode * const rootNode = self.nodeToDisplay;
|
|
NSArray * const nodeChildren = rootNode.children;
|
|
return nodeChildren ? nodeChildren[indexPath.item] : nil;
|
|
}
|
|
|
|
- (NSArray<VLCInputItem *> *)mediaSourceInputItemsAtIndexPaths:(NSSet<NSIndexPath *> *const)indexPaths
|
|
{
|
|
NSMutableArray<VLCInputItem *> * const inputItems =
|
|
[NSMutableArray arrayWithCapacity:indexPaths.count];
|
|
|
|
for (NSIndexPath * const indexPath in indexPaths) {
|
|
VLCInputNode * const inputNode = [self inputNodeForIndexPath:indexPath];
|
|
if (!inputNode) {
|
|
continue;
|
|
}
|
|
VLCInputItem * const inputItem = inputNode.inputItem;
|
|
[inputItems addObject:inputItem];
|
|
}
|
|
|
|
return inputItems.copy;
|
|
}
|
|
|
|
#pragma mark - collection view data source and delegation
|
|
|
|
- (NSInteger)numberOfSectionsInCollectionView:(NSCollectionView *)collectionView
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
- (NSInteger)collectionView:(NSCollectionView *)collectionView
|
|
numberOfItemsInSection:(NSInteger)section
|
|
{
|
|
if (_nodeToDisplay) {
|
|
return _nodeToDisplay.numberOfChildren;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
- (NSCollectionViewItem *)collectionView:(NSCollectionView *)collectionView
|
|
itemForRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
VLCMediaSourceCollectionViewItem *viewItem = [collectionView makeItemWithIdentifier:VLCMediaSourceCellIdentifier forIndexPath:indexPath];
|
|
|
|
VLCInputNode *rootNode = _nodeToDisplay;
|
|
NSArray *nodeChildren = rootNode.children;
|
|
if (nodeChildren == nil) {
|
|
NSLog(@"No children for node %@, cannot provide correctly setup viewItem", rootNode);
|
|
return viewItem;
|
|
}
|
|
VLCInputNode *childNode = nodeChildren[indexPath.item];
|
|
VLCInputItem *childRootInput = childNode.inputItem;
|
|
|
|
viewItem.representedInputItem = childRootInput;
|
|
|
|
return viewItem;
|
|
}
|
|
|
|
- (void)collectionView:(NSCollectionView *)collectionView didSelectItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths
|
|
{
|
|
if (indexPaths.count != 1) {
|
|
return;
|
|
}
|
|
|
|
NSIndexPath * const indexPath = indexPaths.anyObject;
|
|
if (!indexPath) {
|
|
return;
|
|
}
|
|
VLCInputNode * const childNode = [self inputNodeForIndexPath:indexPath];
|
|
if (childNode) {
|
|
[self performActionForNode:childNode allowPlayback:YES];
|
|
}
|
|
}
|
|
|
|
- (NSSize)collectionView:(NSCollectionView *)collectionView
|
|
layout:(NSCollectionViewLayout *)collectionViewLayout
|
|
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
VLCLibraryCollectionViewFlowLayout *collectionViewFlowLayout = (VLCLibraryCollectionViewFlowLayout*)collectionViewLayout;
|
|
NSAssert(collectionViewLayout, @"This should be a flow layout and thus a valid pointer");
|
|
return [VLCLibraryUIUnits adjustedCollectionViewItemSizeForCollectionView:collectionView
|
|
withLayout:collectionViewFlowLayout
|
|
withItemsAspectRatio:VLCLibraryCollectionViewItemAspectRatioDefaultItem];
|
|
}
|
|
|
|
#pragma mark - table view data source and delegation
|
|
|
|
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
|
|
{
|
|
if (_nodeToDisplay) {
|
|
return _nodeToDisplay.numberOfChildren;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
- (NSView *)tableView:(NSTableView *)tableView
|
|
viewForTableColumn:(NSTableColumn *)tableColumn
|
|
row:(NSInteger)row
|
|
{
|
|
VLCInputNode * const inputNode = [self mediaSourceInputNodeAtRow:row];
|
|
|
|
if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableNameColumn"]) {
|
|
VLCLibraryTableCellView * const cellView =
|
|
[tableView makeViewWithIdentifier:VLCLibraryTableCellViewIdentifier owner:self];
|
|
[VLCLibraryImageCache thumbnailForInputItem:inputNode.inputItem
|
|
withCompletion:^(NSImage * _Nullable image) {
|
|
cellView.representedImageView.image = image;
|
|
}];
|
|
cellView.primaryTitleTextField.hidden = YES;
|
|
cellView.secondaryTitleTextField.hidden = YES;
|
|
cellView.singlePrimaryTitleTextField.hidden = NO;
|
|
cellView.singlePrimaryTitleTextField.stringValue = inputNode.inputItem.name;
|
|
return cellView;
|
|
}
|
|
|
|
// Only present count view for folders
|
|
if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableCountColumn"] &&
|
|
inputNode.inputItem.inputType != ITEM_TYPE_DIRECTORY) {
|
|
return nil;
|
|
}
|
|
|
|
static NSString * const basicCellViewIdentifier = @"BasicTableCellViewIdentifier";
|
|
NSTableCellView *cellView =
|
|
[tableView makeViewWithIdentifier:basicCellViewIdentifier owner:self];
|
|
if (cellView == nil) {
|
|
cellView =
|
|
[NSTableCellView tableCellViewWithIdentifier:basicCellViewIdentifier showingString:@""];
|
|
}
|
|
NSAssert(cellView, @"Cell view should not be nil");
|
|
|
|
if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableCountColumn"]) {
|
|
if (inputNode.numberOfChildren == 0) {
|
|
cellView.textField.stringValue = NSTR("Loading…");
|
|
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
NSURL * const inputNodeUrl = [NSURL URLWithString:inputNode.inputItem.MRL];
|
|
input_item_node_t * const p_inputNode = inputNode.vlcInputItemNode;
|
|
NSError * const error =
|
|
[self.displayedMediaSource generateChildNodesForDirectoryNode:p_inputNode
|
|
withUrl:inputNodeUrl];
|
|
if (error)
|
|
return;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
cellView.textField.stringValue =
|
|
[NSString stringWithFormat:@"%i items", inputNode.numberOfChildren];
|
|
});
|
|
});
|
|
} else {
|
|
cellView.textField.stringValue =
|
|
[NSString stringWithFormat:@"%i items", inputNode.numberOfChildren];
|
|
}
|
|
} 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;
|
|
}
|
|
return cellView;
|
|
}
|
|
|
|
- (void)tableViewSelectionDidChange:(NSNotification *)notification
|
|
{
|
|
NSInteger selectedIndex = self.tableView.selectedRow;
|
|
if (selectedIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
VLCInputNode *childNode = [self mediaSourceInputNodeAtRow:selectedIndex];
|
|
if (childNode) {
|
|
[self performActionForNode:childNode allowPlayback:NO];
|
|
}
|
|
}
|
|
|
|
- (void)tableViewAction:(id)sender
|
|
{
|
|
NSInteger selectedIndex = self.tableView.selectedRow;
|
|
if (selectedIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
VLCInputNode *childNode = [self mediaSourceInputNodeAtRow:selectedIndex];
|
|
if (childNode) {
|
|
[self performActionForNode:childNode allowPlayback:YES];
|
|
}
|
|
}
|
|
|
|
- (nullable VLCInputNode *)mediaSourceInputNodeAtRow:(NSInteger)tableViewRow
|
|
{
|
|
if (_nodeToDisplay == nil) {
|
|
return nil;
|
|
}
|
|
|
|
VLCInputNode *rootNode = _nodeToDisplay;
|
|
NSArray *nodeChildren = rootNode.children;
|
|
|
|
if (nodeChildren == nil || nodeChildren.count == 0) {
|
|
return nil;
|
|
}
|
|
|
|
return nodeChildren[tableViewRow];
|
|
}
|
|
|
|
- (VLCInputItem*)mediaSourceInputItemAtRow:(NSInteger)tableViewRow
|
|
{
|
|
VLCInputNode *childNode = [self mediaSourceInputNodeAtRow:tableViewRow];
|
|
|
|
if (childNode == nil) {
|
|
return nil;
|
|
}
|
|
|
|
return childNode.inputItem;
|
|
}
|
|
|
|
#pragma mark - generic actions
|
|
|
|
- (void)performActionForNode:(VLCInputNode *)node allowPlayback:(BOOL)allowPlayback
|
|
{
|
|
if(node == nil || node.inputItem == nil) {
|
|
return;
|
|
}
|
|
|
|
VLCInputItem *childRootInput = node.inputItem;
|
|
|
|
if (childRootInput.inputType == ITEM_TYPE_DIRECTORY || childRootInput.inputType == ITEM_TYPE_NODE) {
|
|
VLCInputNodePathControlItem *nodePathItem = [[VLCInputNodePathControlItem alloc] initWithInputNode:node];
|
|
[self.pathControl appendInputNodePathControlItem:nodePathItem];
|
|
|
|
NSError * const error = [self.displayedMediaSource preparseInputNodeWithinTree:node];
|
|
if (error) {
|
|
NSAlert * const alert = [NSAlert alertWithError:error];
|
|
alert.alertStyle = NSAlertStyleCritical;
|
|
[alert runModal];
|
|
return;
|
|
}
|
|
self.nodeToDisplay = node;
|
|
|
|
[self.navigationStack appendCurrentLibraryState];
|
|
} else if (childRootInput.inputType == ITEM_TYPE_FILE && allowPlayback) {
|
|
[VLCMain.sharedInstance.playQueueController addInputItem:childRootInput.vlcInputItem atPosition:-1 startPlayback:YES];
|
|
}
|
|
}
|
|
|
|
- (void)reloadData
|
|
{
|
|
if (!_collectionView.hidden) {
|
|
[_collectionView reloadData];
|
|
}
|
|
|
|
if(!_tableView.hidden) {
|
|
[_tableView reloadData];
|
|
}
|
|
|
|
[NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceDataSourceNodeChanged
|
|
object:self];
|
|
}
|
|
|
|
@end
|
|
|