fix: Game detection improvements
Some checks failed
Queue Release Build / prepare (push) Successful in 27s
Deploy Web Apps / deploy (push) Successful in 10m8s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-windows (push) Has been cancelled
Queue Release Build / build-linux (push) Has been cancelled
Some checks failed
Queue Release Build / prepare (push) Successful in 27s
Deploy Web Apps / deploy (push) Successful in 10m8s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-windows (push) Has been cancelled
Queue Release Build / build-linux (push) Has been cancelled
This commit is contained in:
@@ -215,6 +215,24 @@ export interface ContextMenuParams {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ActiveGameCandidate {
|
||||
processName: string;
|
||||
rawProcessName: string;
|
||||
executablePath?: string;
|
||||
windowTitle?: string;
|
||||
pid?: number;
|
||||
isFullscreen: boolean;
|
||||
bounds?: { width: number; height: number };
|
||||
confidence: number;
|
||||
source: 'foreground' | 'process-scan';
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export interface ActiveGameCandidateResult {
|
||||
candidate: ActiveGameCandidate | null;
|
||||
fallbackProcessNames: string[];
|
||||
}
|
||||
|
||||
export interface ElectronApi {
|
||||
linuxDisplayServer: string;
|
||||
minimizeWindow: () => void;
|
||||
@@ -223,6 +241,9 @@ export interface ElectronApi {
|
||||
openExternal: (url: string) => Promise<boolean>;
|
||||
getSources: () => Promise<{ id: string; name: string; thumbnail: string }[]>;
|
||||
getRunningProcessNames: () => Promise<string[]>;
|
||||
getActiveGameCandidate?: () => Promise<ActiveGameCandidateResult>;
|
||||
getIgnoredGameProcesses?: () => Promise<string[]>;
|
||||
setIgnoredGameProcesses?: (list: string[]) => Promise<string[]>;
|
||||
prepareLinuxScreenShareAudioRouting: () => Promise<LinuxScreenShareAudioRoutingInfo>;
|
||||
activateLinuxScreenShareAudioRouting: () => Promise<LinuxScreenShareAudioRoutingInfo>;
|
||||
deactivateLinuxScreenShareAudioRouting: () => Promise<boolean>;
|
||||
|
||||
@@ -15,7 +15,7 @@ infrastructure adapters and UI.
|
||||
| **direct-message** | One-to-one WebRTC messages, offline queueing, delivery state, and friends | `DirectMessageService`, `FriendService` |
|
||||
| **direct-call** | Direct and small-group private calls initiated from people cards and direct messages | `DirectCallService` |
|
||||
| **experimental-media** | Optional media playback experiments kept isolated from the default attachment path | `ExperimentalMediaSettingsService` |
|
||||
| **game-activity** | Local game detection, server metadata matching, P2P now-playing sync, and elapsed playtime formatting | `GameActivityService`, `formatGameActivityElapsed()` |
|
||||
| **game-activity** | Foreground-window-first game detection with confidence scoring (`MIN_GAME_CONFIDENCE`), server metadata matching, P2P now-playing sync, and elapsed playtime formatting | `GameActivityService`, `formatGameActivityElapsed()` |
|
||||
| **notifications** | Notification preferences, unread tracking, desktop alert orchestration | `NotificationsFacade` |
|
||||
| **plugins** | Client-only plugin manifests, load ordering, registry state, and signal-server support metadata | `PluginHostService`, `PluginRegistryService` |
|
||||
| **profile-avatar** | Profile picture upload, crop/zoom editing, processing, local persistence, and P2P avatar sync | `ProfileAvatarFacade` |
|
||||
|
||||
@@ -120,7 +120,7 @@ export class GameActivityService implements OnDestroy {
|
||||
|
||||
const api = this.electron.getApi();
|
||||
|
||||
if (!api?.getRunningProcessNames) {
|
||||
if (!api?.getRunningProcessNames && !api?.getActiveGameCandidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,14 +154,33 @@ export class GameActivityService implements OnDestroy {
|
||||
|
||||
const api = this.electron.getApi();
|
||||
|
||||
if (!api?.getRunningProcessNames) {
|
||||
if (!api?.getRunningProcessNames && !api?.getActiveGameCandidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scanInFlight = true;
|
||||
|
||||
try {
|
||||
const processNames = (await api.getRunningProcessNames()).slice(0, MAX_PROCESS_NAMES_PER_REQUEST);
|
||||
const candidateResult = api.getActiveGameCandidate
|
||||
? await api.getActiveGameCandidate().catch(() => null)
|
||||
: null;
|
||||
|
||||
let processNames: string[];
|
||||
let preferredProcessName: string | undefined;
|
||||
|
||||
if (candidateResult?.candidate) {
|
||||
// Main process already scored & filtered this; trust it.
|
||||
preferredProcessName = candidateResult.candidate.rawProcessName ?? candidateResult.candidate.processName;
|
||||
processNames = [preferredProcessName];
|
||||
} else if (candidateResult && candidateResult.fallbackProcessNames.length > 0) {
|
||||
processNames = candidateResult.fallbackProcessNames.slice(0, MAX_PROCESS_NAMES_PER_REQUEST);
|
||||
} else if (!candidateResult && api.getRunningProcessNames) {
|
||||
// Old preload without the new API: fall back to legacy whole-system scan.
|
||||
processNames = (await api.getRunningProcessNames()).slice(0, MAX_PROCESS_NAMES_PER_REQUEST);
|
||||
} else {
|
||||
processNames = [];
|
||||
}
|
||||
|
||||
const processHash = this.buildProcessHash(processNames);
|
||||
|
||||
if (processHash === this.lastProcessHash) {
|
||||
@@ -170,6 +189,12 @@ export class GameActivityService implements OnDestroy {
|
||||
|
||||
this.lastProcessHash = processHash;
|
||||
|
||||
if (processNames.length === 0) {
|
||||
this.ngZone.run(() => this.applyMatchedGame(null));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedGame = await this.matchRunningGame(processNames);
|
||||
|
||||
this.ngZone.run(() => this.applyMatchedGame(matchedGame));
|
||||
|
||||
@@ -139,4 +139,61 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (isElectron) {
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<h4 class="text-sm font-semibold text-foreground">Game detection</h4>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-border bg-secondary/20 p-4 space-y-3">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
MetoYou prefers the currently focused window when detecting your game. Add process names here to permanently hide
|
||||
apps that get mistakenly identified as games (e.g. "spotify", "obs64"). Entries are matched case-insensitively
|
||||
against the executable name without its extension.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="Process name (e.g. spotify)"
|
||||
[value]="ignoredProcessDraft()"
|
||||
(input)="onIgnoredProcessDraftChange($event)"
|
||||
(keydown.enter)="addIgnoredProcess()"
|
||||
aria-label="Process name to ignore"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
[disabled]="savingIgnoredGameProcesses() || !ignoredProcessDraft().trim()"
|
||||
(click)="addIgnoredProcess()"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (ignoredGameProcesses().length === 0) {
|
||||
<p class="text-xs text-muted-foreground italic">No ignored processes yet.</p>
|
||||
} @else {
|
||||
<ul class="flex flex-wrap gap-2">
|
||||
@for (entry of ignoredGameProcesses(); track entry) {
|
||||
<li class="inline-flex items-center gap-1 rounded-md bg-secondary/40 px-2 py-1 text-xs text-foreground">
|
||||
<span>{{ entry }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground hover:text-foreground"
|
||||
[disabled]="savingIgnoredGameProcesses()"
|
||||
(click)="removeIgnoredProcess(entry)"
|
||||
[attr.aria-label]="'Remove ' + entry + ' from ignore list'"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,16 @@ export class GeneralSettingsComponent {
|
||||
closeToTray = signal(true);
|
||||
savingAutoStart = signal(false);
|
||||
savingCloseToTray = signal(false);
|
||||
ignoredGameProcesses = signal<string[]>([]);
|
||||
ignoredProcessDraft = signal('');
|
||||
savingIgnoredGameProcesses = signal(false);
|
||||
|
||||
constructor() {
|
||||
this.loadGeneralSettings();
|
||||
|
||||
if (this.isElectron) {
|
||||
void this.loadDesktopSettings();
|
||||
void this.loadIgnoredGameProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,4 +135,61 @@ export class GeneralSettingsComponent {
|
||||
this.autoStart.set(snapshot.autoStart);
|
||||
this.closeToTray.set(snapshot.closeToTray);
|
||||
}
|
||||
|
||||
onIgnoredProcessDraftChange(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
|
||||
this.ignoredProcessDraft.set(input.value);
|
||||
}
|
||||
|
||||
async addIgnoredProcess(): Promise<void> {
|
||||
const draft = this.ignoredProcessDraft().trim();
|
||||
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = Array.from(new Set([...this.ignoredGameProcesses(), draft]));
|
||||
|
||||
await this.saveIgnoredGameProcesses(next);
|
||||
this.ignoredProcessDraft.set('');
|
||||
}
|
||||
|
||||
async removeIgnoredProcess(name: string): Promise<void> {
|
||||
const next = this.ignoredGameProcesses().filter((entry) => entry !== name);
|
||||
|
||||
await this.saveIgnoredGameProcesses(next);
|
||||
}
|
||||
|
||||
private async loadIgnoredGameProcesses(): Promise<void> {
|
||||
const api = this.electronBridge.getApi();
|
||||
|
||||
if (!api?.getIgnoredGameProcesses) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const list = await api.getIgnoredGameProcesses();
|
||||
|
||||
this.ignoredGameProcesses.set(list);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private async saveIgnoredGameProcesses(list: string[]): Promise<void> {
|
||||
const api = this.electronBridge.getApi();
|
||||
|
||||
if (!api?.setIgnoredGameProcesses) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.savingIgnoredGameProcesses.set(true);
|
||||
|
||||
try {
|
||||
const normalized = await api.setIgnoredGameProcesses(list);
|
||||
|
||||
this.ignoredGameProcesses.set(normalized);
|
||||
} finally {
|
||||
this.savingIgnoredGameProcesses.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user