(feat): improve tray icon
(qol): update todo
This commit is contained in:
parent
92e5003989
commit
943203eeaf
10 changed files with 205 additions and 15 deletions
|
|
@ -58,8 +58,12 @@
|
|||
"to": "web"
|
||||
},
|
||||
{
|
||||
"from": "build/icons/icon.png",
|
||||
"to": "icons/icon.png"
|
||||
"from": "build/icons",
|
||||
"to": "icons",
|
||||
"filter": [
|
||||
"32x32.png",
|
||||
"icon.png"
|
||||
]
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ import {
|
|||
import { checkForUpdates } from "./updates.js";
|
||||
import {
|
||||
ipcChannels,
|
||||
type DesktopCallStatus,
|
||||
type DesktopScreenShareAudioOutput,
|
||||
type DesktopScreenShareCapabilities,
|
||||
} from "../shared/ipc.js";
|
||||
import { initTray } from "./tray.js";
|
||||
import { initTray, setTrayCallStatus } from "./tray.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const verbose = process.argv.includes("--verbose");
|
||||
|
|
@ -189,6 +190,25 @@ function registerIpc() {
|
|||
);
|
||||
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
|
||||
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
|
||||
ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => {
|
||||
if (
|
||||
typeof status !== "object" ||
|
||||
status === null ||
|
||||
typeof (status as DesktopCallStatus).inCall !== "boolean" ||
|
||||
typeof (status as DesktopCallStatus).speaking !== "boolean" ||
|
||||
((status as DesktopCallStatus).iconDataUrl !== undefined &&
|
||||
(typeof (status as DesktopCallStatus).iconDataUrl !== "string" ||
|
||||
!(status as DesktopCallStatus).iconDataUrl?.startsWith(
|
||||
"data:image/png;base64,",
|
||||
) ||
|
||||
(status as DesktopCallStatus).iconDataUrl!.length > 16_384))
|
||||
) {
|
||||
throw new Error("Invalid call status.");
|
||||
}
|
||||
|
||||
const { inCall, iconDataUrl } = status as DesktopCallStatus;
|
||||
setTrayCallStatus(inCall, iconDataUrl);
|
||||
});
|
||||
ipcMain.handle(ipcChannels.minimizeWindow, () => {
|
||||
verboseLog("window:minimize");
|
||||
mainWindow?.minimize();
|
||||
|
|
@ -311,7 +331,7 @@ async function start() {
|
|||
verboseLog("app ready");
|
||||
registerIpc();
|
||||
registerDisplayMediaHandler();
|
||||
initTray();
|
||||
initTray(() => mainWindow);
|
||||
await createWindow();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Tray, Menu } from "electron";
|
||||
import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
|
|
@ -7,19 +7,56 @@ let tray: Tray | null = null;
|
|||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export function initTray() {
|
||||
const iconPath = path.join(__dirname, "../../build/icons/32x32.png");
|
||||
function getTrayIconPath(filename: string) {
|
||||
if (app.isPackaged) return path.join(process.resourcesPath, "icons", filename);
|
||||
return path.resolve(__dirname, "../../build/icons", filename);
|
||||
}
|
||||
|
||||
tray = new Tray(iconPath); // keep reference alive
|
||||
export function setTrayCallStatus(
|
||||
inCall: boolean,
|
||||
iconDataUrl?: string,
|
||||
) {
|
||||
if (!tray) return;
|
||||
|
||||
if (inCall && iconDataUrl) {
|
||||
const image = nativeImage.createFromDataURL(iconDataUrl);
|
||||
if (!image.isEmpty()) {
|
||||
tray.setImage(image);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tray.setImage(getTrayIconPath("32x32.png"));
|
||||
}
|
||||
|
||||
export function initTray(getMainWindow: () => BrowserWindow | null) {
|
||||
tray = new Tray(getTrayIconPath("32x32.png")); // keep reference alive
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: "Restart", type: "normal" },
|
||||
{ label: "Quit", type: "normal" },
|
||||
{
|
||||
label: "Restart",
|
||||
type: "normal",
|
||||
click: () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
{ label: "Quit", type: "normal", click: () => app.quit() },
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
tray.on("click", (event) => {
|
||||
console.log(event);
|
||||
tray.on("click", () => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) return;
|
||||
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js";
|
||||
import {
|
||||
ipcChannels,
|
||||
type DesktopCallStatus,
|
||||
type DesktopScreenShareSource,
|
||||
} from "../shared/ipc.js";
|
||||
|
||||
function windowAction(channel: string) {
|
||||
return () => ipcRenderer.invoke(channel);
|
||||
|
|
@ -27,6 +31,21 @@ const desktopApi = {
|
|||
updates: {
|
||||
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
|
||||
},
|
||||
call: {
|
||||
setStatus: (status: DesktopCallStatus) => {
|
||||
if (
|
||||
typeof status?.inCall !== "boolean" ||
|
||||
typeof status?.speaking !== "boolean" ||
|
||||
(status.iconDataUrl !== undefined &&
|
||||
(typeof status.iconDataUrl !== "string" ||
|
||||
!status.iconDataUrl.startsWith("data:image/png;base64,")))
|
||||
) {
|
||||
return Promise.reject(new Error("Invalid call status."));
|
||||
}
|
||||
|
||||
return ipcRenderer.invoke(ipcChannels.setCallStatus, status);
|
||||
},
|
||||
},
|
||||
window: {
|
||||
minimize: () => windowAction(ipcChannels.minimizeWindow),
|
||||
maximize: () => windowAction(ipcChannels.maximizeWindow),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ export type DesktopScreenShareCapabilities = {
|
|||
hasReliableSystemAudio: boolean;
|
||||
};
|
||||
|
||||
export type DesktopCallStatus = {
|
||||
inCall: boolean;
|
||||
speaking: boolean;
|
||||
iconDataUrl?: string;
|
||||
};
|
||||
|
||||
export type ReleaseArtifact = {
|
||||
name: string;
|
||||
platform: string;
|
||||
|
|
@ -55,4 +61,5 @@ export const ipcChannels = {
|
|||
closeWindow: "window:close",
|
||||
getVersion: "app:getVersion",
|
||||
checkForUpdates: "updates:checkForUpdates",
|
||||
setCallStatus: "call:setStatus",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ import Login from "@/routes/screens/login";
|
|||
|
||||
import CallPopout from "@tensamin/call/popout";
|
||||
import ChatContext from "@tensamin/chat/context";
|
||||
import { useInitializeCall } from "@tensamin/call/store";
|
||||
import { useCall, useInitializeCall } from "@tensamin/call/store";
|
||||
import { useIsSpeaking } from "@tensamin/call/speakingState";
|
||||
import { Provider as MTPProvider } from "@tensamin/mtp";
|
||||
import UserProvider from "@tensamin/user/context";
|
||||
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
|
||||
|
|
@ -274,8 +275,94 @@ function AppShell() {
|
|||
);
|
||||
}
|
||||
|
||||
function createCallTrayIcon(color: string, speaking: boolean) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return undefined;
|
||||
|
||||
context.globalAlpha = speaking ? 1 : 0.55;
|
||||
context.fillStyle = color;
|
||||
context.beginPath();
|
||||
context.arc(16, 16, 13, 0, Math.PI * 2);
|
||||
context.fill();
|
||||
|
||||
if (speaking) {
|
||||
context.globalAlpha = 0.3;
|
||||
context.fillStyle = "#ffffff";
|
||||
context.fill();
|
||||
}
|
||||
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
function CallInit() {
|
||||
return useInitializeCall();
|
||||
useInitializeCall();
|
||||
|
||||
const { load } = useStorage();
|
||||
const {
|
||||
themeColor,
|
||||
themePalette,
|
||||
themePrimaryColor,
|
||||
themePolarity,
|
||||
themeTint,
|
||||
themeCustomCss,
|
||||
} = useTheme();
|
||||
const [localUserId, setLocalUserId] = useState(-1);
|
||||
const [primaryColor, setPrimaryColor] = useState("");
|
||||
const inCall = useCall((state) => state.state === "open");
|
||||
const speaking = useIsSpeaking(localUserId);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
load("user_id").then((userId) => {
|
||||
if (active) setLocalUserId(userId);
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
setPrimaryColor(
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--primary")
|
||||
.trim(),
|
||||
);
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [
|
||||
themeColor,
|
||||
themeCustomCss,
|
||||
themePalette,
|
||||
themePolarity,
|
||||
themePrimaryColor,
|
||||
themeTint,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const iconDataUrl = primaryColor
|
||||
? createCallTrayIcon(primaryColor, speaking)
|
||||
: undefined;
|
||||
|
||||
void window.tensaminDesktop?.call
|
||||
?.setStatus?.({
|
||||
inCall,
|
||||
speaking: inCall && speaking,
|
||||
iconDataUrl: inCall ? iconDataUrl : undefined,
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error("Failed to update desktop call status", error);
|
||||
});
|
||||
}, [inCall, primaryColor, speaking]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
|
|
|
|||
Loading…
Reference in a new issue