Fix private calls

This commit is contained in:
2026-05-17 15:14:52 +02:00
parent 0f6cb3ee77
commit e769a6ee4a
71 changed files with 5821 additions and 349 deletions

View File

@@ -278,6 +278,19 @@ export class ChatMessagesComponent {
const electronApi = this.electronBridge.getApi();
if (electronApi) {
const diskPath = this.getAttachmentDiskPath(attachment);
if (diskPath && electronApi.saveExistingFileAs) {
try {
const result = await electronApi.saveExistingFileAs(diskPath, attachment.filename);
if (result.saved || result.cancelled)
return;
} catch {
/* fall back to blob/browser download */
}
}
const blob = await this.getAttachmentBlob(attachment);
if (blob) {
@@ -326,6 +339,9 @@ export class ChatMessagesComponent {
if (!attachment.objectUrl)
return null;
if (attachment.objectUrl.startsWith('file:'))
return null;
try {
const response = await fetch(attachment.objectUrl);
@@ -335,6 +351,10 @@ export class ChatMessagesComponent {
}
}
private getAttachmentDiskPath(attachment: Attachment): string | null {
return attachment.savedPath || attachment.filePath || null;
}
private blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();

View File

@@ -112,7 +112,8 @@
type="button"
class="font-semibold text-primary underline-offset-4 hover:underline"
(click)="openMissingPluginStore(missingEmbed)"
>store</button
>
store</button
>.
</article>
}
@@ -359,6 +360,30 @@
</button>
}
} @else {
@if (att.canOpenExternally) {
<button
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
(click)="openAttachmentExternally(att)"
>
<ng-icon
name="lucideExternalLink"
class="h-3.5 w-3.5"
/>
Open
</button>
}
@if (att.canUseExperimentalPlayer) {
<button
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
(click)="openExperimentalPlayer(att)"
>
<ng-icon
name="lucidePlay"
class="h-3.5 w-3.5"
/>
Play
</button>
}
<button
class="rounded bg-primary px-2 py-1 text-xs text-primary-foreground"
(click)="downloadAttachment(att)"
@@ -368,6 +393,30 @@
}
} @else {
<div class="text-xs text-muted-foreground">Shared from your device</div>
@if (att.canOpenExternally) {
<button
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
(click)="openAttachmentExternally(att)"
>
<ng-icon
name="lucideExternalLink"
class="h-3.5 w-3.5"
/>
Open
</button>
}
@if (att.canUseExperimentalPlayer) {
<button
class="inline-flex items-center gap-1.5 rounded bg-secondary px-2 py-1 text-xs text-foreground transition-colors hover:bg-secondary/80"
(click)="openExperimentalPlayer(att)"
>
<ng-icon
name="lucidePlay"
class="h-3.5 w-3.5"
/>
Play
</button>
}
}
</div>
</div>
@@ -379,6 +428,22 @@
</div>
}
</div>
@if (att.experimentalPlayerActive && att.objectUrl) {
@defer {
<app-experimental-vlc-player
[src]="att.objectUrl"
[filename]="att.filename"
[mime]="att.mime"
[sizeLabel]="formatBytes(att.size)"
(closed)="closeExperimentalPlayer()"
(downloadRequested)="downloadAttachment(att)"
/>
} @loading {
<div class="mt-2 max-w-xl rounded-md border border-border bg-secondary/20 p-3 text-xs text-muted-foreground">
Loading experimental player...
</div>
}
}
}
}
</div>

View File

@@ -20,7 +20,9 @@ import {
lucideDownload,
lucideEdit,
lucideExpand,
lucideExternalLink,
lucideImage,
lucidePlay,
lucideReply,
lucideSmile,
lucideTrash2,
@@ -29,8 +31,15 @@ import {
import {
Attachment,
AttachmentFacade,
MAX_BROWSER_INLINE_MEDIA_SIZE_BYTES,
MAX_AUTO_SAVE_SIZE_BYTES
} from '../../../../../attachment';
import { PlatformService } from '../../../../../../core/platform';
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
import {
ExperimentalMediaSettingsService
} from '../../../../../experimental-media';
import { ExperimentalVlcPlayerComponent } from '../../../../../experimental-media/feature/experimental-vlc-player/experimental-vlc-player.component';
import { KlipyService } from '../../../../application/services/klipy.service';
import { hasDedicatedChatEmbed } from '../../../../domain/rules/link-embed.rules';
import {
@@ -81,6 +90,9 @@ const RICH_MARKDOWN_PATTERNS = [
];
interface ChatMessageAttachmentViewModel extends Attachment {
canOpenExternally: boolean;
canUseExperimentalPlayer: boolean;
experimentalPlayerActive: boolean;
isAudio: boolean;
isUploader: boolean;
isVideo: boolean;
@@ -112,6 +124,7 @@ interface MissingPluginEmbedFallback {
ChatLinkEmbedComponent,
UserAvatarComponent,
PluginRenderHostComponent,
ExperimentalVlcPlayerComponent,
ThemeNodeDirective
],
viewProviders: [
@@ -120,7 +133,9 @@ interface MissingPluginEmbedFallback {
lucideDownload,
lucideEdit,
lucideExpand,
lucideExternalLink,
lucideImage,
lucidePlay,
lucideReply,
lucideSmile,
lucideTrash2,
@@ -140,9 +155,14 @@ export class ChatMessageItemComponent {
private readonly klipy = inject(KlipyService);
private readonly pluginRequirements = inject(PluginRequirementStateService);
private readonly pluginUi = inject(PluginUiRegistryService);
private readonly electronBridge = inject(ElectronBridgeService);
private readonly platform = inject(PlatformService);
private readonly experimentalMedia = inject(ExperimentalMediaSettingsService);
private readonly profileCard = inject(ProfileCardService);
private readonly router = inject(Router);
private readonly attachmentVersion = signal(this.attachmentsSvc.updated());
private readonly experimentalPlayerAttachmentId = signal<string | null>(null);
private readonly mediaSupportCache = new Map<string, boolean>();
readonly message = input.required<Message>();
readonly repliedMessage = input<Message | undefined>();
@@ -539,13 +559,51 @@ export class ChatMessageItemComponent {
this.downloadRequested.emit(attachment);
}
openExperimentalPlayer(attachment: Attachment): void {
if (!attachment.available || !attachment.objectUrl) {
return;
}
this.experimentalPlayerAttachmentId.set(attachment.id);
}
async openAttachmentExternally(attachment: Attachment): Promise<void> {
const diskPath = this.getAttachmentDiskPath(attachment);
const electronApi = this.electronBridge.getApi();
if (!diskPath || !electronApi?.openFilePath) {
return;
}
await electronApi.openFilePath(diskPath);
}
closeExperimentalPlayer(): void {
this.experimentalPlayerAttachmentId.set(null);
}
private buildAttachmentViewModel(attachment: Attachment): ChatMessageAttachmentViewModel {
const isVideo = this.isVideoAttachment(attachment);
const isAudio = this.isAudioAttachment(attachment);
const isRawVideo = this.isVideoAttachment(attachment);
const isRawAudio = this.isAudioAttachment(attachment);
const isRawPlayableMedia = isRawVideo || isRawAudio;
const isNativePlayableMedia = this.canPlayMediaType(attachment.mime);
const shouldUseDefaultFileInterface = isRawPlayableMedia &&
(!isNativePlayableMedia ||
(this.platform.isBrowser && attachment.size > MAX_BROWSER_INLINE_MEDIA_SIZE_BYTES));
const isVideo = isRawVideo && !shouldUseDefaultFileInterface;
const isAudio = isRawAudio && !shouldUseDefaultFileInterface;
const requiresMediaDownloadAcceptance = (isVideo || isAudio) && attachment.size > MAX_AUTO_SAVE_SIZE_BYTES;
const canUseExperimentalPlayer = this.experimentalMedia.vlcJsPlaybackEnabled() &&
shouldUseDefaultFileInterface &&
isRawPlayableMedia &&
attachment.available &&
!!attachment.objectUrl;
return {
...attachment,
canOpenExternally: this.platform.isElectron && attachment.available && !!this.getAttachmentDiskPath(attachment),
canUseExperimentalPlayer,
experimentalPlayerActive: canUseExperimentalPlayer && this.experimentalPlayerAttachmentId() === attachment.id,
isAudio,
isUploader: this.isUploader(attachment),
isVideo,
@@ -572,6 +630,30 @@ export class ChatMessageItemComponent {
private getLiveAttachment(attachmentId: string): Attachment | undefined {
return this.attachmentsSvc.getForMessage(this.message().id).find((attachment) => attachment.id === attachmentId);
}
private getAttachmentDiskPath(attachment: Attachment): string | null {
return attachment.savedPath || attachment.filePath || null;
}
private canPlayMediaType(mime: string): boolean {
if (!mime.startsWith('video/') && !mime.startsWith('audio/')) {
return false;
}
const cached = this.mediaSupportCache.get(mime);
if (cached !== undefined) {
return cached;
}
const element = document.createElement(mime.startsWith('video/') ? 'video' : 'audio');
const canPlay = element.canPlayType(mime) !== '';
this.mediaSupportCache.set(mime, canPlay);
return canPlay;
}
}
function parsePluginEmbedToken(content: string): PluginEmbedToken | null {