- The Mac client’s Media tab now has pagination and a preview/play area.
What changed in [main.m](/Users/tyemeclifford/Documents/GH/blog/macclient/AppKitClient/main.m): Media list now paginates with Previous/Next and 10/25/50/100 per page. Selecting an image loads an inline preview. Selecting audio/video loads an AVPlayerView with native playback controls. Added an Open button and double-click support to open media in the default Mac app. Media file sizes now display as readable values like 1.4 MB. I also updated [build-app.sh](/Users/tyemeclifford/Documents/GH/blog/macclient/build-app.sh) to link AVKit and AVFoundation, and to honor the app’s CFBundleExecutable from Info.plist so the rebuilt bundle launches with the expected TCMS executable.
This commit is contained in:
@@ -5,13 +5,13 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>TCMSMacClient</string>
|
||||
<string>TCMS</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.tyclifford.tcms.macclient</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TCMS Mac Client</string>
|
||||
<string>TCMS</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
+267
-11
@@ -1,4 +1,5 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <AVKit/AVKit.h>
|
||||
|
||||
static NSString *TCMSString(id value) {
|
||||
if ([value isKindOfClass:[NSString class]]) {
|
||||
@@ -37,6 +38,28 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
static NSString *TCMSFormatBytes(id value) {
|
||||
long long bytes = 0;
|
||||
if ([value respondsToSelector:@selector(longLongValue)]) {
|
||||
bytes = [value longLongValue];
|
||||
}
|
||||
if (bytes <= 0) {
|
||||
return @"";
|
||||
}
|
||||
|
||||
NSArray *units = @[@"B", @"KB", @"MB", @"GB", @"TB"];
|
||||
double amount = (double)bytes;
|
||||
NSUInteger unitIndex = 0;
|
||||
while (amount >= 1024.0 && unitIndex + 1 < units.count) {
|
||||
amount /= 1024.0;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return unitIndex == 0
|
||||
? [NSString stringWithFormat:@"%lld %@", bytes, units[unitIndex]]
|
||||
: [NSString stringWithFormat:@"%.1f %@", amount, units[unitIndex]];
|
||||
}
|
||||
|
||||
@interface TCMSMarkdownTextView : NSTextView
|
||||
@end
|
||||
|
||||
@@ -53,6 +76,7 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
@property (nonatomic, copy) NSString *baseURLString;
|
||||
@property (nonatomic, copy) NSString *username;
|
||||
@property (nonatomic, copy) NSString *password;
|
||||
- (NSURL *)absoluteURLForPath:(NSString *)path;
|
||||
- (void)requestAction:(NSString *)action query:(NSDictionary *)query method:(NSString *)method body:(NSDictionary *)body completion:(void (^)(NSDictionary *, NSError *))completion;
|
||||
- (void)uploadFile:(NSURL *)fileURL completion:(void (^)(NSDictionary *, NSError *))completion;
|
||||
@end
|
||||
@@ -70,6 +94,34 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
return [base URLByAppendingPathComponent:@"api.php"];
|
||||
}
|
||||
|
||||
- (NSURL *)absoluteURLForPath:(NSString *)path {
|
||||
NSString *rawPath = TCMSString(path);
|
||||
if (rawPath.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *directURL = [NSURL URLWithString:rawPath];
|
||||
if (directURL.scheme.length > 0) {
|
||||
return directURL;
|
||||
}
|
||||
|
||||
NSURL *base = [NSURL URLWithString:self.baseURLString ?: @""];
|
||||
if (!base) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if ([rawPath hasPrefix:@"/"]) {
|
||||
NSURLComponents *components = [NSURLComponents componentsWithURL:base resolvingAgainstBaseURL:NO];
|
||||
components.path = rawPath;
|
||||
components.query = nil;
|
||||
components.fragment = nil;
|
||||
return components.URL;
|
||||
}
|
||||
|
||||
NSURL *siteBase = [[base.lastPathComponent lowercaseString] isEqualToString:@"api.php"] ? [base URLByDeletingLastPathComponent] : base;
|
||||
return [siteBase URLByAppendingPathComponent:rawPath];
|
||||
}
|
||||
|
||||
- (NSURL *)URLForAction:(NSString *)action query:(NSDictionary *)query {
|
||||
NSURL *endpoint = [self endpointURL];
|
||||
if (!endpoint) {
|
||||
@@ -212,6 +264,13 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
@property (nonatomic, strong) NSButton *autoCorrectButton;
|
||||
@property (nonatomic, strong) NSTextView *markdownView;
|
||||
@property (nonatomic, strong) NSScrollView *editorScroll;
|
||||
@property (nonatomic, strong) NSView *mediaPreviewContainer;
|
||||
@property (nonatomic, strong) NSTextField *mediaPreviewLabel;
|
||||
@property (nonatomic, strong) NSTextField *mediaDetailLabel;
|
||||
@property (nonatomic, strong) NSImageView *mediaImageView;
|
||||
@property (nonatomic, strong) AVPlayerView *mediaPlayerView;
|
||||
@property (nonatomic, strong) NSButton *openMediaButton;
|
||||
@property (nonatomic, copy) NSString *mediaPreviewURLString;
|
||||
@property (nonatomic, strong) NSPopUpButton *commentStatusPopup;
|
||||
@property (nonatomic, strong) NSButton *previousPageButton;
|
||||
@property (nonatomic, strong) NSButton *nextPageButton;
|
||||
@@ -302,7 +361,7 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
self.window.title = @"TCMS Mac Client";
|
||||
self.window.title = @"TCMS";
|
||||
self.window.minSize = NSMakeSize(1040, 680);
|
||||
self.window.delegate = self;
|
||||
self.window.releasedWhenClosed = NO;
|
||||
@@ -457,6 +516,14 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
}
|
||||
self.editorScroll = nil;
|
||||
self.markdownView = nil;
|
||||
[self.mediaPlayerView.player pause];
|
||||
self.mediaPreviewContainer = nil;
|
||||
self.mediaPreviewLabel = nil;
|
||||
self.mediaDetailLabel = nil;
|
||||
self.mediaImageView = nil;
|
||||
self.mediaPlayerView = nil;
|
||||
self.openMediaButton = nil;
|
||||
self.mediaPreviewURLString = nil;
|
||||
}
|
||||
|
||||
- (void)showContentSection:(NSString *)section {
|
||||
@@ -580,11 +647,82 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
- (void)showMediaSection {
|
||||
self.section = @"media";
|
||||
[self clearWorkspace];
|
||||
self.contentListPage = 1;
|
||||
[self.workspace addSubview:[self label:@"Media" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]]];
|
||||
[self addButton:@"Refresh" frame:NSMakeRect(210, 690, 90, 30) action:@selector(refreshCurrentSection)];
|
||||
[self addButton:@"Upload" frame:NSMakeRect(310, 690, 90, 30) action:@selector(uploadMedia)];
|
||||
NSScrollView *mediaScroll = [self buildTableWithFrame:NSMakeRect(20, 20, self.workspace.bounds.size.width - 40, 650) columns:@[@"name", @"mime", @"url"]];
|
||||
mediaScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.openMediaButton = [self addButton:@"Open" frame:NSMakeRect(410, 690, 80, 30) action:@selector(openSelectedMedia:)];
|
||||
self.openMediaButton.enabled = NO;
|
||||
|
||||
NSScrollView *mediaScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 420, 614) columns:@[@"name", @"mime", @"size"]];
|
||||
mediaScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
|
||||
self.tableView.target = self;
|
||||
self.tableView.doubleAction = @selector(openSelectedMedia:);
|
||||
|
||||
self.previousPageButton = [NSButton buttonWithTitle:@"Previous" target:self action:@selector(previousContentPage)];
|
||||
self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28);
|
||||
self.previousPageButton.bezelStyle = NSBezelStyleRounded;
|
||||
self.previousPageButton.autoresizingMask = NSViewMaxYMargin;
|
||||
[self.workspace addSubview:self.previousPageButton];
|
||||
|
||||
self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]];
|
||||
self.pageInfoLabel.alignment = NSTextAlignmentCenter;
|
||||
self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin;
|
||||
[self.workspace addSubview:self.pageInfoLabel];
|
||||
|
||||
self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)];
|
||||
self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28);
|
||||
self.nextPageButton.bezelStyle = NSBezelStyleRounded;
|
||||
self.nextPageButton.autoresizingMask = NSViewMaxYMargin;
|
||||
[self.workspace addSubview:self.nextPageButton];
|
||||
|
||||
self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)];
|
||||
self.perPagePopup.autoresizingMask = NSViewMaxYMargin;
|
||||
self.perPagePopup.toolTip = @"Items per page";
|
||||
self.perPagePopup.target = self;
|
||||
self.perPagePopup.action = @selector(contentPerPageChanged:);
|
||||
[self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]];
|
||||
NSString *perPageTitle = [NSString stringWithFormat:@"%ld", (long)self.contentListPerPage];
|
||||
if ([self.perPagePopup itemWithTitle:perPageTitle]) {
|
||||
[self.perPagePopup selectItemWithTitle:perPageTitle];
|
||||
} else {
|
||||
self.contentListPerPage = 25;
|
||||
[self.perPagePopup selectItemWithTitle:@"25"];
|
||||
}
|
||||
[self.workspace addSubview:self.perPagePopup];
|
||||
|
||||
CGFloat previewLeft = 460;
|
||||
CGFloat previewWidth = self.workspace.bounds.size.width - previewLeft - 20;
|
||||
self.mediaPreviewContainer = [[NSView alloc] initWithFrame:NSMakeRect(previewLeft, 20, previewWidth, 650)];
|
||||
self.mediaPreviewContainer.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.mediaPreviewContainer.wantsLayer = YES;
|
||||
self.mediaPreviewContainer.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.11 alpha:1].CGColor;
|
||||
[self.workspace addSubview:self.mediaPreviewContainer];
|
||||
|
||||
self.mediaPreviewLabel = [self label:@"No media selected" frame:NSMakeRect(16, 612, previewWidth - 32, 24) font:[NSFont boldSystemFontOfSize:15]];
|
||||
self.mediaPreviewLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||
self.mediaPreviewLabel.textColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1];
|
||||
[self.mediaPreviewContainer addSubview:self.mediaPreviewLabel];
|
||||
|
||||
self.mediaDetailLabel = [self label:@"" frame:NSMakeRect(16, 584, previewWidth - 32, 20) font:[NSFont systemFontOfSize:12]];
|
||||
self.mediaDetailLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||
self.mediaDetailLabel.textColor = [NSColor colorWithCalibratedWhite:0.72 alpha:1];
|
||||
[self.mediaPreviewContainer addSubview:self.mediaDetailLabel];
|
||||
|
||||
NSRect previewFrame = NSMakeRect(16, 16, previewWidth - 32, 552);
|
||||
self.mediaImageView = [[NSImageView alloc] initWithFrame:previewFrame];
|
||||
self.mediaImageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.mediaImageView.imageScaling = NSImageScaleProportionallyUpOrDown;
|
||||
self.mediaImageView.hidden = YES;
|
||||
[self.mediaPreviewContainer addSubview:self.mediaImageView];
|
||||
|
||||
self.mediaPlayerView = [[AVPlayerView alloc] initWithFrame:previewFrame];
|
||||
self.mediaPlayerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.mediaPlayerView.controlsStyle = AVPlayerViewControlsStyleDefault;
|
||||
self.mediaPlayerView.hidden = YES;
|
||||
[self.mediaPreviewContainer addSubview:self.mediaPlayerView];
|
||||
|
||||
[self resetMediaPreview];
|
||||
[self refreshMedia];
|
||||
[self refreshWindowLayout];
|
||||
}
|
||||
@@ -694,12 +832,15 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
self.previousPageButton.enabled = total > 0 && self.contentListPage > 1;
|
||||
self.nextPageButton.enabled = total > 0 && self.contentListPage < totalPages;
|
||||
self.perPagePopup.enabled = total > 0;
|
||||
if ([self.section isEqualToString:@"media"]) {
|
||||
[self resetMediaPreview];
|
||||
}
|
||||
[self refreshWindowLayout];
|
||||
}
|
||||
|
||||
- (NSString *)contentListStatusMessageForType:(NSString *)type {
|
||||
NSInteger total = self.allTableItems.count;
|
||||
NSString *noun = [type isEqualToString:@"page"] ? @"pages" : @"posts";
|
||||
NSString *noun = [type isEqualToString:@"media"] ? @"media items" : ([type isEqualToString:@"page"] ? @"pages" : @"posts");
|
||||
if (total == 0) {
|
||||
return [NSString stringWithFormat:@"No %@ found.", noun];
|
||||
}
|
||||
@@ -709,13 +850,20 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
return [NSString stringWithFormat:@"Showing %ld-%ld of %ld %@.", (long)start, (long)end, (long)total, noun];
|
||||
}
|
||||
|
||||
- (NSString *)currentListStatusMessage {
|
||||
if ([self.section isEqualToString:@"media"]) {
|
||||
return [self contentListStatusMessageForType:@"media"];
|
||||
}
|
||||
return [self contentListStatusMessageForType:[self.section isEqualToString:@"pages"] ? @"page" : @"post"];
|
||||
}
|
||||
|
||||
- (void)previousContentPage {
|
||||
if (self.contentListPage <= 1) {
|
||||
return;
|
||||
}
|
||||
self.contentListPage -= 1;
|
||||
[self applyContentListPagination];
|
||||
[self setStatus:[self contentListStatusMessageForType:[self.section isEqualToString:@"pages"] ? @"page" : @"post"]];
|
||||
[self setStatus:[self currentListStatusMessage]];
|
||||
}
|
||||
|
||||
- (void)nextContentPage {
|
||||
@@ -725,7 +873,7 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
}
|
||||
self.contentListPage += 1;
|
||||
[self applyContentListPagination];
|
||||
[self setStatus:[self contentListStatusMessageForType:[self.section isEqualToString:@"pages"] ? @"page" : @"post"]];
|
||||
[self setStatus:[self currentListStatusMessage]];
|
||||
}
|
||||
|
||||
- (void)contentPerPageChanged:(id)sender {
|
||||
@@ -734,7 +882,7 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
self.contentListPage = 1;
|
||||
[NSUserDefaults.standardUserDefaults setInteger:selected forKey:@"contentListPerPage"];
|
||||
[self applyContentListPagination];
|
||||
[self setStatus:[self contentListStatusMessageForType:[self.section isEqualToString:@"pages"] ? @"page" : @"post"]];
|
||||
[self setStatus:[self currentListStatusMessage]];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
|
||||
@@ -753,7 +901,7 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
field.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
}
|
||||
NSDictionary *item = self.tableItems[row];
|
||||
field.stringValue = TCMSString(item[tableColumn.identifier]);
|
||||
field.stringValue = [tableColumn.identifier isEqualToString:@"size"] ? TCMSFormatBytes(item[@"size"]) : TCMSString(item[tableColumn.identifier]);
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -765,6 +913,8 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
NSDictionary *item = self.tableItems[row];
|
||||
if ([self.section isEqualToString:@"posts"] || [self.section isEqualToString:@"pages"]) {
|
||||
[self loadContentSlug:TCMSString(item[@"slug"])];
|
||||
} else if ([self.section isEqualToString:@"media"]) {
|
||||
[self previewMediaItem:item];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,9 +1230,10 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
[self setStatus:error.localizedDescription];
|
||||
return;
|
||||
}
|
||||
self.tableItems = [NSMutableArray arrayWithArray:json[@"items"] ?: @[]];
|
||||
[self.tableView reloadData];
|
||||
[self setStatus:@"Media refreshed."];
|
||||
NSArray *items = [json[@"items"] isKindOfClass:[NSArray class]] ? json[@"items"] : @[];
|
||||
self.allTableItems = [NSMutableArray arrayWithArray:items];
|
||||
[self applyContentListPagination];
|
||||
[self setStatus:[self currentListStatusMessage]];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -1104,6 +1255,111 @@ static NSString *TCMSMarkdownCompareString(NSString *value) {
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSURL *)absoluteURLForMediaItem:(NSDictionary *)item {
|
||||
NSString *url = TCMSString(item[@"url"]);
|
||||
if (url.length == 0) {
|
||||
url = TCMSString(item[@"path"]);
|
||||
}
|
||||
return [self.client absoluteURLForPath:url];
|
||||
}
|
||||
|
||||
- (NSString *)mediaDetailForItem:(NSDictionary *)item {
|
||||
NSMutableArray *parts = [NSMutableArray array];
|
||||
NSString *mime = TCMSString(item[@"mime"]);
|
||||
NSString *size = TCMSFormatBytes(item[@"size"]);
|
||||
NSString *path = TCMSString(item[@"path"]);
|
||||
if (mime.length > 0) {
|
||||
[parts addObject:mime];
|
||||
}
|
||||
if (size.length > 0) {
|
||||
[parts addObject:size];
|
||||
}
|
||||
if (path.length > 0) {
|
||||
[parts addObject:path];
|
||||
}
|
||||
return [parts componentsJoinedByString:@" "];
|
||||
}
|
||||
|
||||
- (void)resetMediaPreview {
|
||||
[self.mediaPlayerView.player pause];
|
||||
self.mediaPlayerView.player = nil;
|
||||
self.mediaPlayerView.hidden = YES;
|
||||
self.mediaImageView.image = nil;
|
||||
self.mediaImageView.hidden = YES;
|
||||
self.mediaPreviewURLString = nil;
|
||||
self.openMediaButton.enabled = NO;
|
||||
self.mediaPreviewLabel.stringValue = @"No media selected";
|
||||
self.mediaDetailLabel.stringValue = @"";
|
||||
}
|
||||
|
||||
- (void)previewMediaItem:(NSDictionary *)item {
|
||||
NSURL *url = [self absoluteURLForMediaItem:item];
|
||||
if (!url) {
|
||||
[self resetMediaPreview];
|
||||
[self setStatus:@"That media item does not have a usable URL."];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *mime = [TCMSString(item[@"mime"]) lowercaseString];
|
||||
NSString *urlString = url.absoluteString ?: @"";
|
||||
self.mediaPreviewURLString = urlString;
|
||||
self.openMediaButton.enabled = YES;
|
||||
self.mediaPreviewLabel.stringValue = TCMSString(item[@"name"]).length ? TCMSString(item[@"name"]) : url.lastPathComponent;
|
||||
self.mediaDetailLabel.stringValue = [self mediaDetailForItem:item];
|
||||
self.mediaImageView.image = nil;
|
||||
self.mediaImageView.hidden = YES;
|
||||
[self.mediaPlayerView.player pause];
|
||||
self.mediaPlayerView.player = nil;
|
||||
self.mediaPlayerView.hidden = YES;
|
||||
|
||||
if ([mime hasPrefix:@"image/"]) {
|
||||
self.mediaImageView.hidden = NO;
|
||||
[self setStatus:@"Loading image preview."];
|
||||
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
NSImage *image = data.length > 0 ? [[NSImage alloc] initWithData:data] : nil;
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (![self.mediaPreviewURLString isEqualToString:urlString]) {
|
||||
return;
|
||||
}
|
||||
if (image) {
|
||||
self.mediaImageView.image = image;
|
||||
[self setStatus:@"Image preview ready."];
|
||||
} else {
|
||||
self.mediaImageView.hidden = YES;
|
||||
[self setStatus:error.localizedDescription ?: @"Unable to load image preview."];
|
||||
}
|
||||
});
|
||||
}] resume];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([mime hasPrefix:@"video/"] || [mime hasPrefix:@"audio/"]) {
|
||||
AVPlayer *player = [AVPlayer playerWithURL:url];
|
||||
self.mediaPlayerView.player = player;
|
||||
self.mediaPlayerView.hidden = NO;
|
||||
[self setStatus:[mime hasPrefix:@"audio/"] ? @"Audio ready to play." : @"Video ready to play."];
|
||||
return;
|
||||
}
|
||||
|
||||
[self setStatus:@"Preview unavailable for this file type. Use Open to view it."];
|
||||
}
|
||||
|
||||
- (void)openSelectedMedia:(id)sender {
|
||||
NSDictionary *item = [self selectedTableItem];
|
||||
if (!item) {
|
||||
[self setStatus:@"Select a media item first."];
|
||||
return;
|
||||
}
|
||||
|
||||
NSURL *url = [self absoluteURLForMediaItem:item];
|
||||
if (!url) {
|
||||
[self setStatus:@"That media item does not have a usable URL."];
|
||||
return;
|
||||
}
|
||||
|
||||
[NSWorkspace.sharedWorkspace openURL:url];
|
||||
}
|
||||
|
||||
- (void)refreshComments {
|
||||
NSString *status = self.commentStatusPopup.titleOfSelectedItem ?: @"pending";
|
||||
NSDictionary *query = [status isEqualToString:@"all"] ? @{} : @{@"status": status};
|
||||
|
||||
@@ -30,6 +30,7 @@ The client automatically calls `api.php` under that URL. Enter the username/pass
|
||||
- Toggle spell check and auto-correct in the Markdown editor.
|
||||
- Use standard Mac shortcuts, including Command-S for Update, Command-Comma for Settings, and Command-Q for Quit.
|
||||
- Upload media to `bl-content/uploads`.
|
||||
- Page through media, preview images, play audio/video, and open files in the default Mac app.
|
||||
- List and moderate comments.
|
||||
- Local autosave to Application Support.
|
||||
- Optional remote autosave as draft.
|
||||
|
||||
@@ -4,10 +4,11 @@ set -eu
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
APP="$ROOT/TCMSMacClient.app"
|
||||
SRC="$ROOT/AppKitClient/main.m"
|
||||
EXECUTABLE="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$ROOT/AppKitClient/Info.plist")"
|
||||
|
||||
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
||||
cp "$ROOT/AppKitClient/Info.plist" "$APP/Contents/Info.plist"
|
||||
/usr/bin/clang -fobjc-arc -mmacosx-version-min=12.0 -framework Cocoa "$SRC" -o "$APP/Contents/MacOS/TCMSMacClient"
|
||||
/usr/bin/clang -fobjc-arc -mmacosx-version-min=12.0 -framework Cocoa -framework AVKit -framework AVFoundation "$SRC" -o "$APP/Contents/MacOS/$EXECUTABLE"
|
||||
printf 'APPL????' > "$APP/Contents/PkgInfo"
|
||||
|
||||
echo "Built $APP"
|
||||
|
||||
Reference in New Issue
Block a user