Eingehender Ruf: Firefox-Tab aktivieren und Fenster nativ nach vorn holen
Protokollversion 9: neue Plugin-Aktion activate und der Eintrag IncomingCallTabTitle in websocket.json. Beginnt eine Leitung zu klingeln, holt SwyxTray den ersten Firefox-Tab mit passendem Titel ueber das Plugin nach vorn, ohne ihn neu zu laden. Das Plugin allein scheitert dabei am Foreground-Lock von Windows: es kann den Tab aktivieren, das Fenster bleibt aber hinter der gerade fokussierten Anwendung. FirefoxWindow uebernimmt deshalb den nativen Teil - EnumWindows auf MozillaWindowClass, minimierte Fenster wiederherstellen, dann SetForegroundWindow mit AttachThreadInput, notfalls ALT-Tastendruck. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,10 +129,20 @@ Beim ersten Start entsteht `%LOCALAPPDATA%\SwyxTray\websocket.json`:
|
||||
"Token": "E59BF4…",
|
||||
"AllowedOrigins": [],
|
||||
"MaxConnections": 8,
|
||||
"AllowRemoteAccess": false
|
||||
"AllowRemoteAccess": false,
|
||||
"IncomingCallTabTitle": "SwyxWeb"
|
||||
}
|
||||
```
|
||||
|
||||
`IncomingCallTabTitle` (seit Protokollversion 9, fehlt der Eintrag, gilt
|
||||
`"SwyxWeb"`): Beginnt eine Leitung zu klingeln, holt SwyxTray den ersten
|
||||
Firefox-Tab, dessen Titel diesen Text enthält (Groß-/Kleinschreibung ohne
|
||||
Belang, trifft also z. B. „SwyxWeb · STADTBOTE"), über das Plugin nach vorn —
|
||||
ohne ihn neu zu laden, damit der Web-Client seine WebSocket-Verbindung behält.
|
||||
Ist kein Plugin verbunden oder kein passender Tab offen, passiert nichts; es
|
||||
wird bewusst kein neuer Tab geöffnet. Ein leerer Eintrag (`""`) schaltet das
|
||||
Verhalten ab.
|
||||
|
||||
Solange die Prüfungen abgeschaltet sind (siehe unten), sind `Token` und
|
||||
`AllowedOrigins` ohne Wirkung — für einen ersten Test ist an dieser Datei nichts
|
||||
zu tun. Der Rest dieses Abschnitts gilt für den eingeschalteten Zustand.
|
||||
@@ -170,7 +180,7 @@ ab; in einer bestehenden Datei ohne diesen Eintrag gilt automatisch 17655).
|
||||
Auf diesem Port läuft **ausschließlich die Tab-Verwaltung** — keine
|
||||
Snapshots, keine Anruf-Ereignisse, keine Telefonie-Kommandos (sie werden mit
|
||||
`ok: false` abgewiesen). Nach dem `hello` ist die Richtung umgekehrt zum
|
||||
Hauptport: **SwyxTray fragt, das Plugin antwortet.** Es gibt drei Aufträge,
|
||||
Hauptport: **SwyxTray fragt, das Plugin antwortet.** Es gibt vier Aufträge,
|
||||
jeder wird mit einem `tabresult` samt unveränderter `id` beantwortet:
|
||||
|
||||
**Offene Tabs auslesen:**
|
||||
@@ -196,18 +206,44 @@ jeder wird mit einem `tabresult` samt unveränderter `id` beantwortet:
|
||||
← vom Plugin: {"cmd":"tabresult","id":3,"ok":true}
|
||||
```
|
||||
|
||||
**Tab aktivieren** (seit Protokollversion 9 — nach vorn holen, ohne ihn neu
|
||||
zu laden; das Plugin setzt das mit `browser.tabs.update(tabId,
|
||||
{active: true})` samt `browser.windows.update(windowId, {focused: true})`
|
||||
um):
|
||||
|
||||
```json
|
||||
→ an das Plugin: {"type":"tab","id":4,"action":"activate","tabId":42}
|
||||
← vom Plugin: {"cmd":"tabresult","id":4,"ok":true}
|
||||
```
|
||||
|
||||
Antwortet das Plugin binnen fünf Sekunden nicht (oder ist keines verbunden),
|
||||
gilt der Auftrag als gescheitert; die darauf aufbauenden Kommandos des
|
||||
Hauptports (`focus`, `tabs`, `opentab`, `closetab`) enden dann mit
|
||||
`ok: false`. Vom Plugin aus sind nur `tabresult` und `ping` zulässig. Ein
|
||||
„Tab aktivieren“ kennt der Kanal bewusst nicht — beim `focus`-Kommando holt
|
||||
SwyxTray einen vorhandenen Tab als Komposition nach vorn: `list`, passenden
|
||||
Tab per `close` schließen und die URL per `open` neu öffnen.
|
||||
Hauptports (`focus`, `tabs`, `opentab`, `closetab`, `activatetab`) enden dann
|
||||
mit `ok: false`. Vom Plugin aus sind nur `tabresult` und `ping` zulässig.
|
||||
Ein älteres Plugin, das `activate` noch nicht kennt, antwortet darauf mit
|
||||
`ok: false` — `focus` fällt dann auf den alten Weg zurück (Tab per `close`
|
||||
schließen und die URL per `open` neu öffnen), das Aktivieren beim
|
||||
eingehenden Ruf (`IncomingCallTabTitle`, siehe oben) unterbleibt und das
|
||||
Protokoll vermerkt es.
|
||||
|
||||
Das Plugin selbst (SwyxFFPlugin) wird als **eigenes Projekt** gepflegt und ist
|
||||
nicht Teil dieses Repositories. Sein `background.js` setzt genau diese drei
|
||||
nicht Teil dieses Repositories. Sein `background.js` setzt genau diese
|
||||
Aufträge um und verbindet sich selbsttätig neu (1 s Abstand, je Fehlversuch
|
||||
verdoppelt bis 30 s), falls SwyxTray noch nicht oder nicht mehr läuft.
|
||||
Für `activate` (seit Protokollversion 9) braucht das Plugin einen weiteren
|
||||
Zweig in seinem `handleMessage`:
|
||||
|
||||
```js
|
||||
} else if (m.action === "activate") {
|
||||
if (m.tabId == null) {
|
||||
reply({ ok: false });
|
||||
} else {
|
||||
const tab = await browser.tabs.update(m.tabId, { active: true });
|
||||
await browser.windows.update(tab.windowId, { focused: true });
|
||||
reply({ ok: true });
|
||||
}
|
||||
}
|
||||
```
|
||||
Installation zum Erproben: `about:debugging#/runtime/this-firefox` →
|
||||
*Temporäres Add-on laden…* → die `manifest.json` des Plugin-Projekts wählen.
|
||||
|
||||
@@ -229,6 +265,7 @@ wobei die UUID je Installation zufällig vergeben wird — sie steht in
|
||||
| `tabs` | — | liest die offenen Firefox-Tabs über das Plugin aus |
|
||||
| `opentab` | `url` | öffnet über das Plugin einen neuen Firefox-Tab |
|
||||
| `closetab` | `tabId` | schließt über das Plugin den Tab mit dieser Id |
|
||||
| `activatetab` | `tabId` | holt über das Plugin den Tab mit dieser Id nach vorn, ohne ihn neu zu laden |
|
||||
| `addresses` | — | liefert die zwischengespeicherten Adressdaten (Telefonbücher), Antwort im Feld `addresses` |
|
||||
| `contacts` | `query` | durchsucht die Adressdaten des Swyx-Clients (Telefonbücher und Kontakt-Plugins), Antwort im Feld `contacts` |
|
||||
| `resolve` | `number` | löst eine Rufnummer über die Adressdaten in einen Namen auf, Antwort im Feld `name` (leer = unbekannt) |
|
||||
@@ -256,21 +293,24 @@ Zustandsmeldungen.
|
||||
`focus` (seit Protokollversion 3, ab Version 5 ausschließlich über das
|
||||
**Firefox-Plugin**, siehe unten): war ein passender Tab schon offen — sein
|
||||
Titel enthält den `title` oder seine Adresse ist die `url` —, wird er
|
||||
geschlossen und die `url` neu geöffnet (`"focused": true`), sonst nur neu
|
||||
geöffnet (`"focused": false`). Ist kein Plugin verbunden oder antwortet es
|
||||
nicht, endet das Kommando mit `ok: false` — einen anderen Weg, eine Seite
|
||||
anzuzeigen, gibt es nicht. Die `url` ist Pflicht, `title` dient nur dem
|
||||
Tab-Abgleich; erlaubt sind nur absolute `http(s)`-Adressen.
|
||||
aktiviert (`"focused": true`; seit Version 9 ohne Neuladen, bei einem
|
||||
älteren Plugin ohne `activate` ersatzweise wie früher geschlossen und die
|
||||
`url` neu geöffnet), sonst nur neu geöffnet (`"focused": false`). Ist kein
|
||||
Plugin verbunden oder antwortet es nicht, endet das Kommando mit
|
||||
`ok: false` — einen anderen Weg, eine Seite anzuzeigen, gibt es nicht. Die
|
||||
`url` ist Pflicht, `title` dient nur dem Tab-Abgleich; erlaubt sind nur
|
||||
absolute `http(s)`-Adressen.
|
||||
|
||||
```json
|
||||
→ {"id":9,"cmd":"focus","title":"Kundenakte Muster GmbH","url":"https://crm.example.local/kunden/4711"}
|
||||
← {"type":"result","id":9,"ok":true,"focused":false}
|
||||
```
|
||||
|
||||
`tabs`, `opentab`, `closetab` (seit Protokollversion 6): die drei Aufträge des
|
||||
Tab-Verwaltungskanals stehen damit auch Webseiten offen — SwyxTray reicht sie
|
||||
unverändert an das Firefox-Plugin durch. Ist kein Plugin verbunden oder
|
||||
antwortet es nicht binnen fünf Sekunden, endet das Kommando mit `ok: false`.
|
||||
`tabs`, `opentab`, `closetab` (seit Protokollversion 6), `activatetab` (seit
|
||||
Version 9): die Aufträge des Tab-Verwaltungskanals stehen damit auch
|
||||
Webseiten offen — SwyxTray reicht sie unverändert an das Firefox-Plugin
|
||||
durch. Ist kein Plugin verbunden oder antwortet es nicht binnen fünf
|
||||
Sekunden, endet das Kommando mit `ok: false`.
|
||||
Für `opentab` gilt dieselbe Regel wie bei `focus`: nur absolute
|
||||
`http(s)`-Adressen. Die `tabId` für `closetab` stammt aus einer vorherigen
|
||||
`tabs`-Antwort; nach Öffnen oder Schließen verschickt die App **keine** neue
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SwyxTray;
|
||||
|
||||
/// <summary>
|
||||
/// Holt das Firefox-Fenster auf Betriebssystem-Ebene in den Vordergrund.
|
||||
///
|
||||
/// Das Firefox-Plugin aktiviert den Tab und setzt focused:true — gegen den
|
||||
/// Foreground-Lock von Windows kommt es aus dem Browser heraus aber nicht an:
|
||||
/// liegt der Eingabefokus gerade in einer anderen Anwendung, blinkt nur das
|
||||
/// Taskleisten-Symbol. Eine native Anwendung darf mehr: mit AttachThreadInput
|
||||
/// an den Thread des aktuellen Vordergrundfensters gilt SetForegroundWindow
|
||||
/// als Aufruf "aus dem Vordergrund" und zieht das Fenster tatsaechlich hoch.
|
||||
///
|
||||
/// Bewusst DllImport statt LibraryImport — wie in <see cref="NativeMethods"/>
|
||||
/// begruendet.
|
||||
/// </summary>
|
||||
internal static class FirefoxWindow
|
||||
{
|
||||
/// <summary>Klassenname aller Firefox-Hauptfenster.</summary>
|
||||
private const string MozillaWindowClass = "MozillaWindowClass";
|
||||
|
||||
private const int SwRestore = 9;
|
||||
private const byte VkMenu = 0x12;
|
||||
private const uint KeyeventfKeyup = 0x2;
|
||||
|
||||
/// <summary>
|
||||
/// Firefox braucht nach dem Aktivieren des Tabs einen Moment, bis der
|
||||
/// Fenstertitel den Tab-Titel traegt — darum mehrere kurze Versuche.
|
||||
/// </summary>
|
||||
private const int FindAttempts = 3;
|
||||
private static readonly TimeSpan FindRetryDelay = TimeSpan.FromMilliseconds(150);
|
||||
|
||||
/// <summary>
|
||||
/// Sucht das Firefox-Fenster, dessen Titel <paramref name="tabTitle"/>
|
||||
/// enthaelt (der aktive Tab bestimmt den Fenstertitel), und holt es in den
|
||||
/// Vordergrund. Ohne Titel-Treffer nimmt der letzte Versuch das erste
|
||||
/// sichtbare Firefox-Fenster. <c>false</c>, wenn kein Fenster gefunden
|
||||
/// wurde oder Windows den Wechsel endgueltig verweigert hat.
|
||||
/// </summary>
|
||||
public static bool TryBringToFront(string tabTitle)
|
||||
{
|
||||
var hwnd = IntPtr.Zero;
|
||||
|
||||
for (var attempt = 0; attempt < FindAttempts && hwnd == IntPtr.Zero; attempt++)
|
||||
{
|
||||
if (attempt > 0)
|
||||
{
|
||||
Thread.Sleep(FindRetryDelay);
|
||||
}
|
||||
|
||||
hwnd = FindWindowByTitle(tabTitle);
|
||||
}
|
||||
|
||||
if (hwnd == IntPtr.Zero)
|
||||
{
|
||||
hwnd = FindWindowByTitle(null);
|
||||
}
|
||||
|
||||
return hwnd != IntPtr.Zero && ForceForeground(hwnd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Das erste sichtbare Firefox-Hauptfenster, dessen Titel den Suchtext
|
||||
/// enthaelt; ohne Suchtext das erste sichtbare Firefox-Hauptfenster.
|
||||
/// </summary>
|
||||
private static IntPtr FindWindowByTitle(string? containsTitle)
|
||||
{
|
||||
var found = IntPtr.Zero;
|
||||
var className = new StringBuilder(64);
|
||||
var windowTitle = new StringBuilder(512);
|
||||
|
||||
EnumWindows((hwnd, _) =>
|
||||
{
|
||||
if (!IsWindowVisible(hwnd))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
className.Clear();
|
||||
if (GetClassName(hwnd, className, className.Capacity) == 0
|
||||
|| !string.Equals(className.ToString(), MozillaWindowClass, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (containsTitle is not null)
|
||||
{
|
||||
windowTitle.Clear();
|
||||
GetWindowText(hwnd, windowTitle, windowTitle.Capacity);
|
||||
if (!windowTitle.ToString().Contains(containsTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
found = hwnd;
|
||||
return false; // Treffer — Aufzaehlung beenden.
|
||||
}, IntPtr.Zero);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private static bool ForceForeground(IntPtr hwnd)
|
||||
{
|
||||
if (IsIconic(hwnd))
|
||||
{
|
||||
ShowWindow(hwnd, SwRestore);
|
||||
}
|
||||
|
||||
if (GetForegroundWindow() == hwnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// An den Eingabe-Thread des aktuellen Vordergrundfensters anheften:
|
||||
// solange die Threads verbunden sind, behandelt Windows unseren
|
||||
// SetForegroundWindow-Aufruf wie einen aus dem Vordergrundprozess.
|
||||
var foregroundThread = GetWindowThreadProcessId(GetForegroundWindow(), out _);
|
||||
var ownThread = GetCurrentThreadId();
|
||||
var attached = foregroundThread != 0
|
||||
&& foregroundThread != ownThread
|
||||
&& AttachThreadInput(ownThread, foregroundThread, true);
|
||||
|
||||
try
|
||||
{
|
||||
SetForegroundWindow(hwnd);
|
||||
BringWindowToTop(hwnd);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (attached)
|
||||
{
|
||||
AttachThreadInput(ownThread, foregroundThread, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (GetForegroundWindow() == hwnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Zweite Stufe: ein folgenloser ALT-Tastendruck setzt das interne
|
||||
// "letzte Eingabe"-Kriterium — danach laesst Windows den Wechsel zu.
|
||||
keybd_event(VkMenu, 0, 0, UIntPtr.Zero);
|
||||
keybd_event(VkMenu, 0, KeyeventfKeyup, UIntPtr.Zero);
|
||||
SetForegroundWindow(hwnd);
|
||||
|
||||
return GetForegroundWindow() == hwnd;
|
||||
}
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsWindowVisible(IntPtr hwnd);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool IsIconic(IntPtr hwnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetForegroundWindow(IntPtr hwnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool BringWindowToTop(IntPtr hwnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
|
||||
}
|
||||
@@ -270,6 +270,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
.Select(l => l.Index)
|
||||
.ToHashSet();
|
||||
|
||||
var anyNewRinging = false;
|
||||
|
||||
foreach (var line in current.Lines.Where(l => l.State.IsRinging()))
|
||||
{
|
||||
if (previouslyRinging.Contains(line.Index))
|
||||
@@ -277,6 +279,7 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
continue;
|
||||
}
|
||||
|
||||
anyNewRinging = true;
|
||||
Log.Info($"Eingehender Ruf auf Leitung {line.Index + 1}: {line.PeerDisplayLong}");
|
||||
_notifyIcon.ShowBalloonTip(
|
||||
10_000,
|
||||
@@ -284,6 +287,13 @@ internal sealed class TrayApplicationContext : ApplicationContext
|
||||
$"{line.PeerDisplayLong}\nLeitung {line.Index + 1}",
|
||||
ToolTipIcon.Info);
|
||||
}
|
||||
|
||||
if (anyNewRinging)
|
||||
{
|
||||
// Einmal je Ereignis, nicht je Leitung — mehr als nach vorn holen
|
||||
// laesst sich der Tab ohnehin nicht.
|
||||
_server?.ActivateIncomingCallTab();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowStatusBalloon()
|
||||
|
||||
@@ -95,6 +95,10 @@ internal sealed class CommandExecutor
|
||||
await CloseTabAsync(command, cancellationToken).ConfigureAwait(false);
|
||||
return new ResultMessage(command.Id, true);
|
||||
|
||||
case "activatetab":
|
||||
await ActivateTabAsync(command, cancellationToken).ConfigureAwait(false);
|
||||
return new ResultMessage(command.Id, true);
|
||||
|
||||
case "reconnect":
|
||||
await RunAsync<object?>(() => { _client.Reconnect(); return null; }, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@@ -133,10 +137,6 @@ internal sealed class CommandExecutor
|
||||
var url = ValidateUrl(command.Url);
|
||||
var title = command.Title?.Trim();
|
||||
|
||||
// Ein "Aktivieren" kennt der Tab-Kanal nicht — nach vorn kommt ein
|
||||
// vorhandener Tab als Komposition der drei Operationen: Liste holen,
|
||||
// den passenden Tab schliessen und die URL neu oeffnen (ein neuer Tab
|
||||
// ist in Firefox von selbst vorn).
|
||||
if (await _tabs.TryListTabsAsync(cancellationToken).ConfigureAwait(false) is not { } openTabs)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -150,6 +150,14 @@ internal sealed class CommandExecutor
|
||||
|
||||
if (match?.Id is { } tabId)
|
||||
{
|
||||
if (await _tabs.TryActivateTabAsync(tabId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return true; // Tab nach vorn geholt, ohne ihn neu zu laden.
|
||||
}
|
||||
|
||||
// Aeltere Plugins kennen 'activate' nicht — dann wie frueher als
|
||||
// Komposition: den Tab schliessen und die URL neu oeffnen (ein
|
||||
// neuer Tab ist in Firefox von selbst vorn).
|
||||
await _tabs.TryCloseTabAsync(tabId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -187,6 +195,21 @@ internal sealed class CommandExecutor
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ActivateTabAsync(ClientCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
if (command.TabId is not { } tabId)
|
||||
{
|
||||
throw new ArgumentException("Feld 'tabId' fehlt.");
|
||||
}
|
||||
|
||||
if (!await _tabs.TryActivateTabAsync(tabId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Kein Firefox-Plugin verbunden oder es hat den Tab nicht aktiviert " +
|
||||
"(kennt es die Aktion 'activate' schon?).");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Solange die Zugangspruefungen abgeschaltet sind, darf jeder im Netz
|
||||
/// Tabs oeffnen lassen — deshalb nur absolute http(s)-Adressen.
|
||||
|
||||
@@ -315,6 +315,67 @@ internal sealed class LocalWebSocketServer : IDisposable
|
||||
/// <summary>Verteilt nur den Zustand, ohne Anruf-Ereignisse abzuleiten.</summary>
|
||||
public void Broadcast(SwyxSnapshot snapshot) => Broadcast(snapshot, snapshot);
|
||||
|
||||
/// <summary>
|
||||
/// Holt bei einem eingehenden Ruf den konfigurierten Firefox-Tab nach
|
||||
/// vorn (<see cref="WebSocketConfig.IncomingCallTabTitle"/>): Tabs ueber
|
||||
/// das Plugin auslesen, den ersten mit passendem Titel aktivieren. Laeuft
|
||||
/// im Hintergrund, damit der UI-Thread nicht auf das Plugin wartet;
|
||||
/// scheitert still bis auf das Protokoll — ein fehlender Tab oder ein
|
||||
/// fehlendes Plugin ist kein Fehler des Anrufs.
|
||||
/// </summary>
|
||||
public void ActivateIncomingCallTab()
|
||||
{
|
||||
if (_config.IncomingCallTabTitle?.Trim() is not { Length: > 0 } title)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => ActivateTabByTitleAsync(title));
|
||||
}
|
||||
|
||||
private async Task ActivateTabByTitleAsync(string title)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await _tabChannel.TryListTabsAsync(_shutdown.Token).ConfigureAwait(false) is not { } tabs)
|
||||
{
|
||||
Log.Debug($"Eingehender Ruf: kein Firefox-Plugin verbunden — Tab \"{title}\" bleibt, wo er ist.");
|
||||
return;
|
||||
}
|
||||
|
||||
var match = tabs.FirstOrDefault(t =>
|
||||
t.Title?.Contains(title, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
if (match?.Id is not { } tabId)
|
||||
{
|
||||
Log.Debug($"Eingehender Ruf: kein offener Firefox-Tab mit \"{title}\" im Titel.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await _tabChannel.TryActivateTabAsync(tabId, _shutdown.Token).ConfigureAwait(false))
|
||||
{
|
||||
// Das Plugin kann nur den Tab aktivieren — gegen den
|
||||
// Foreground-Lock von Windows hilft erst der native Griff.
|
||||
var inFront = FirefoxWindow.TryBringToFront(match.Title ?? title);
|
||||
Log.Info($"Eingehender Ruf: Firefox-Tab \"{match.Title}\" nach vorn geholt" +
|
||||
(inFront ? " (Fenster im Vordergrund)." :
|
||||
" — das Fenster liess sich nicht in den Vordergrund holen."));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info($"Eingehender Ruf: das Firefox-Plugin hat das Aktivieren von Tab {tabId} " +
|
||||
"abgelehnt — kennt es die Aktion 'activate' noch nicht?");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Beenden — nichts zu melden.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Aktivieren des Firefox-Tabs beim eingehenden Ruf fehlgeschlagen.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verteilt die neu eingelesenen Adressdaten an alle Verbundenen — nur an
|
||||
/// den Hauptport, wie die Zustandsmeldungen. Neue Verbindungen bekommen
|
||||
|
||||
@@ -53,6 +53,15 @@ internal sealed class PluginTabChannel
|
||||
=> (await RequestAsync(id => new TabRequestMessage(id, "close", TabId: tabId), cancellationToken)
|
||||
.ConfigureAwait(false))?.Ok == true;
|
||||
|
||||
/// <summary>
|
||||
/// Holt den Tab nach vorn, ohne ihn neu zu laden. <c>false</c> auch bei
|
||||
/// einem aelteren Plugin, das die Aktion <c>activate</c> noch nicht kennt
|
||||
/// — es antwortet dann mit <c>ok: false</c>.
|
||||
/// </summary>
|
||||
public async Task<bool> TryActivateTabAsync(int tabId, CancellationToken cancellationToken)
|
||||
=> (await RequestAsync(id => new TabRequestMessage(id, "activate", TabId: tabId), cancellationToken)
|
||||
.ConfigureAwait(false))?.Ok == true;
|
||||
|
||||
private async Task<ClientCommand?> RequestAsync(
|
||||
Func<int, TabRequestMessage> request, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -50,8 +50,9 @@ internal sealed class ClientCommand
|
||||
public bool? Ok { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bei <c>closetab</c> (Hauptport): der zu schliessende Tab. Bei
|
||||
/// <c>tabresult</c> auf <c>open</c> (Plugin-Port): Id des neuen Tabs.
|
||||
/// Bei <c>closetab</c> und <c>activatetab</c> (Hauptport): der gemeinte
|
||||
/// Tab. Bei <c>tabresult</c> auf <c>open</c> (Plugin-Port): Id des neuen
|
||||
/// Tabs.
|
||||
/// </summary>
|
||||
public int? TabId { get; set; }
|
||||
|
||||
@@ -115,12 +116,13 @@ internal sealed record ResultMessage(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auftrag an das Firefox-Plugin, nur auf dem Plugin-Port. Drei Aktionen:
|
||||
/// Auftrag an das Firefox-Plugin, nur auf dem Plugin-Port. Vier Aktionen:
|
||||
/// <c>list</c> (offene Tabs melden), <c>open</c> (neuen Tab mit <c>Url</c>
|
||||
/// oeffnen), <c>close</c> (Tab <c>TabId</c> schliessen). Das Plugin antwortet
|
||||
/// jeweils mit <c>{"cmd":"tabresult","id":…,"ok":…}</c> — bei <c>list</c>
|
||||
/// zusaetzlich mit <c>tabs</c>, bei <c>open</c> mit <c>tabId</c>. Die Id
|
||||
/// stammt hier vom Server, nicht von einer Webseite.
|
||||
/// oeffnen), <c>close</c> (Tab <c>TabId</c> schliessen), <c>activate</c>
|
||||
/// (Tab <c>TabId</c> nach vorn holen, ohne ihn neu zu laden). Das Plugin
|
||||
/// antwortet jeweils mit <c>{"cmd":"tabresult","id":…,"ok":…}</c> — bei
|
||||
/// <c>list</c> zusaetzlich mit <c>tabs</c>, bei <c>open</c> mit <c>tabId</c>.
|
||||
/// Die Id stammt hier vom Server, nicht von einer Webseite.
|
||||
/// </summary>
|
||||
internal sealed record TabRequestMessage(int Id, string Action, string? Url = null, int? TabId = null)
|
||||
{
|
||||
|
||||
@@ -36,8 +36,15 @@ internal sealed class WebSocketConfig
|
||||
/// jedem Neueinlesen (Start, Neuverbindung, alle 60 Minuten). Dazu das
|
||||
/// Kommando <c>addresses</c>, mit dem eine Seite den Cache jederzeit
|
||||
/// selbst abrufen kann.
|
||||
/// Version 9: Tab-Aktion <c>activate</c> auf dem Plugin-Port (Tab nach
|
||||
/// vorn holen, ohne ihn neu zu laden), als <c>activatetab</c> auch auf dem
|
||||
/// Hauptport. <c>focus</c> aktiviert einen gefundenen Tab jetzt, statt ihn
|
||||
/// zu schliessen und neu zu oeffnen (Ersatzweg fuer aeltere Plugins
|
||||
/// bleibt). Dazu <see cref="IncomingCallTabTitle"/>: bei einem eingehenden
|
||||
/// Ruf holt SwyxTray den Firefox-Tab mit diesem Titel selbsttaetig nach
|
||||
/// vorn.
|
||||
/// </summary>
|
||||
public const int ProtocolVersion = 8;
|
||||
public const int ProtocolVersion = 9;
|
||||
|
||||
/// <summary>
|
||||
/// ZUR ZEIT ABGESCHALTET: Token-, Origin- und Host-Pruefung entfallen —
|
||||
@@ -99,6 +106,16 @@ internal sealed class WebSocketConfig
|
||||
|
||||
public int MaxConnections { get; set; } = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Beginnt eine Leitung zu klingeln, holt SwyxTray den ersten Firefox-Tab,
|
||||
/// dessen Titel diesen Text enthaelt (ohne Beachtung der Gross-/
|
||||
/// Kleinschreibung), ueber das Plugin nach vorn — ohne ihn neu zu laden,
|
||||
/// damit der Web-Client seine WebSocket-Verbindung behaelt. Leer = aus.
|
||||
/// Ohne Plugin oder ohne passenden Tab passiert nichts; es wird bewusst
|
||||
/// kein neuer Tab geoeffnet.
|
||||
/// </summary>
|
||||
public string IncomingCallTabTitle { get; set; } = "SwyxWeb";
|
||||
|
||||
/// <summary>
|
||||
/// Voreingestellt hoert die App nur auf 127.0.0.1 und ::1. Auf <c>true</c>
|
||||
/// bindet sie an alle Schnittstellen und ist damit aus dem lokalen Netz
|
||||
|
||||
Reference in New Issue
Block a user