Add Dock Progress Indicator and Integrate with Electron Context
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
This commit is contained in:
parent
ae8d4532c0
commit
672e3c59ac
@ -513,3 +513,104 @@ extern "C" bool setNativeWindowDragRegion(void *windowPtr, double x,
|
|||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@interface ElectrobunDockProgressView : NSView
|
||||||
|
@property(nonatomic) double progress;
|
||||||
|
@property(nonatomic) int state;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation ElectrobunDockProgressView
|
||||||
|
@synthesize progress = _progress;
|
||||||
|
@synthesize state = _state;
|
||||||
|
|
||||||
|
- (NSColor *)fillColor {
|
||||||
|
switch (self.state) {
|
||||||
|
case 1:
|
||||||
|
return [NSColor systemRedColor];
|
||||||
|
case 2:
|
||||||
|
return [NSColor systemYellowColor];
|
||||||
|
default:
|
||||||
|
if (@available(macOS 10.14, *)) {
|
||||||
|
return [NSColor controlAccentColor];
|
||||||
|
}
|
||||||
|
return [NSColor systemBlueColor];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)drawRect:(NSRect)dirtyRect {
|
||||||
|
(void)dirtyRect;
|
||||||
|
|
||||||
|
NSRect bounds = [self bounds];
|
||||||
|
[[NSApp applicationIconImage] drawInRect:bounds
|
||||||
|
fromRect:NSZeroRect
|
||||||
|
operation:NSCompositingOperationSourceOver
|
||||||
|
fraction:1.0];
|
||||||
|
|
||||||
|
CGFloat inset = round(bounds.size.width * 0.14);
|
||||||
|
CGFloat height = round(bounds.size.height * 0.11);
|
||||||
|
NSRect track =
|
||||||
|
NSMakeRect(NSMinX(bounds) + inset, NSMinY(bounds) + round(height * 0.9),
|
||||||
|
bounds.size.width - inset * 2.0, height);
|
||||||
|
CGFloat radius = height / 2.0;
|
||||||
|
|
||||||
|
NSBezierPath *trackPath = [NSBezierPath bezierPathWithRoundedRect:track
|
||||||
|
xRadius:radius
|
||||||
|
yRadius:radius];
|
||||||
|
[[NSColor colorWithWhite:0.0 alpha:0.55] setFill];
|
||||||
|
[trackPath fill];
|
||||||
|
|
||||||
|
double fraction = MIN(1.0, MAX(0.0, self.progress));
|
||||||
|
if (fraction > 0.0) {
|
||||||
|
NSRect fill = NSInsetRect(track, 1.5, 1.5);
|
||||||
|
CGFloat fillRadius = fill.size.height / 2.0;
|
||||||
|
fill.size.width = MAX(fill.size.height, fill.size.width * fraction);
|
||||||
|
[[self fillColor] setFill];
|
||||||
|
[[NSBezierPath bezierPathWithRoundedRect:fill
|
||||||
|
xRadius:fillRadius
|
||||||
|
yRadius:fillRadius] fill];
|
||||||
|
}
|
||||||
|
|
||||||
|
[[NSColor colorWithWhite:1.0 alpha:0.85] setStroke];
|
||||||
|
NSBezierPath *outline =
|
||||||
|
[NSBezierPath bezierPathWithRoundedRect:NSInsetRect(track, 0.5, 0.5)
|
||||||
|
xRadius:radius - 0.5
|
||||||
|
yRadius:radius - 0.5];
|
||||||
|
outline.lineWidth = 1.0;
|
||||||
|
[outline stroke];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
// fraction < 0 removes the progress bar; state: 0 normal, 1 error, 2 paused.
|
||||||
|
extern "C" bool setDockProgress(double fraction, int state) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
NSDockTile *dockTile = [NSApp dockTile];
|
||||||
|
if (dockTile == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fraction < 0.0) {
|
||||||
|
if ([dockTile contentView] != nil) {
|
||||||
|
[dockTile setContentView:nil];
|
||||||
|
[dockTile display];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElectrobunDockProgressView *view = nil;
|
||||||
|
if ([[dockTile contentView]
|
||||||
|
isKindOfClass:[ElectrobunDockProgressView class]]) {
|
||||||
|
view = (ElectrobunDockProgressView *)[dockTile contentView];
|
||||||
|
} else {
|
||||||
|
NSSize size = [dockTile size];
|
||||||
|
view = [[ElectrobunDockProgressView alloc]
|
||||||
|
initWithFrame:NSMakeRect(0, 0, size.width, size.height)];
|
||||||
|
[dockTile setContentView:view];
|
||||||
|
}
|
||||||
|
|
||||||
|
view.progress = fraction;
|
||||||
|
view.state = state;
|
||||||
|
[dockTile display];
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { theme, Popover } from 'antd'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
import { AuthContext } from '../context/AuthContext'
|
import { AuthContext } from '../context/AuthContext'
|
||||||
|
import { ElectronContext } from '../context/ElectronContext'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import { getStateTagInfo } from '../utils/Utils'
|
import { getStateTagInfo } from '../utils/Utils'
|
||||||
import SpotlightTooltip from './SpotlightTooltip'
|
import SpotlightTooltip from './SpotlightTooltip'
|
||||||
@ -259,6 +260,7 @@ const LiveActivities = () => {
|
|||||||
subscribeToObjectTypeUpdates,
|
subscribeToObjectTypeUpdates,
|
||||||
subscribeToObjectUpdates
|
subscribeToObjectUpdates
|
||||||
} = useContext(ApiServerContext)
|
} = useContext(ApiServerContext)
|
||||||
|
const { isElectron, setAppProgress } = useContext(ElectronContext)
|
||||||
const [watched, setWatched] = useState([])
|
const [watched, setWatched] = useState([])
|
||||||
|
|
||||||
const userId = userProfile?._id
|
const userId = userProfile?._id
|
||||||
@ -496,6 +498,26 @@ const LiveActivities = () => {
|
|||||||
|
|
||||||
useEffect(() => () => restoreLiveFavicon(), [])
|
useEffect(() => () => restoreLiveFavicon(), [])
|
||||||
|
|
||||||
|
const appProgressPercent = faviconActivity?.percent ?? null
|
||||||
|
const appProgressStatus = faviconActivity?.status ?? null
|
||||||
|
const setAppProgressRef = useRef(setAppProgress)
|
||||||
|
setAppProgressRef.current = setAppProgress
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isElectron) return
|
||||||
|
void setAppProgressRef.current({
|
||||||
|
percent: appProgressPercent,
|
||||||
|
status: appProgressStatus
|
||||||
|
})
|
||||||
|
}, [isElectron, appProgressPercent, appProgressStatus])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isElectron) return undefined
|
||||||
|
return () => {
|
||||||
|
void setAppProgressRef.current({ percent: null })
|
||||||
|
}
|
||||||
|
}, [isElectron])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const seq = ++faviconCaptureSeq.current
|
const seq = ++faviconCaptureSeq.current
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|||||||
@ -365,6 +365,15 @@ const ElectronProvider = ({ children }) => {
|
|||||||
[electronAvailable]
|
[electronAvailable]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const setAppProgress = useCallback(
|
||||||
|
async (progress) => {
|
||||||
|
if (!electronAvailable) return false
|
||||||
|
const result = await desktopBridge.setAppProgress(progress)
|
||||||
|
return result?.ok ?? false
|
||||||
|
},
|
||||||
|
[electronAvailable]
|
||||||
|
)
|
||||||
|
|
||||||
const getElectronVersion = useCallback(async () => {
|
const getElectronVersion = useCallback(async () => {
|
||||||
if (!electronAvailable) return null
|
if (!electronAvailable) return null
|
||||||
return await desktopBridge.getAppVersion()
|
return await desktopBridge.getAppVersion()
|
||||||
@ -402,6 +411,7 @@ const ElectronProvider = ({ children }) => {
|
|||||||
setToken,
|
setToken,
|
||||||
resizeSpotlightWindow,
|
resizeSpotlightWindow,
|
||||||
setSidebarViewMenu,
|
setSidebarViewMenu,
|
||||||
|
setAppProgress,
|
||||||
getElectronVersion,
|
getElectronVersion,
|
||||||
getAppEngine,
|
getAppEngine,
|
||||||
registerTabHistoryHandler,
|
registerTabHistoryHandler,
|
||||||
|
|||||||
69
src/desktop/app-progress.js
Normal file
69
src/desktop/app-progress.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { setMacDockProgress } from './macos-window-effects.js'
|
||||||
|
import {
|
||||||
|
setWindowsTaskbarProgress,
|
||||||
|
TASKBAR_STATE
|
||||||
|
} from './windows-taskbar-progress.js'
|
||||||
|
|
||||||
|
const MAC_DOCK_STATE = { normal: 0, error: 1, paused: 2 }
|
||||||
|
|
||||||
|
const reports = new Map()
|
||||||
|
|
||||||
|
function progressState(status) {
|
||||||
|
if (status === 'error') return 'error'
|
||||||
|
if (status === 'warning') return 'paused'
|
||||||
|
return 'normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProgress({ percent, status } = {}) {
|
||||||
|
const value = Number(percent)
|
||||||
|
if (percent == null || !Number.isFinite(value)) return null
|
||||||
|
return {
|
||||||
|
fraction: Math.min(100, Math.max(0, value)) / 100,
|
||||||
|
state: progressState(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDockProgress() {
|
||||||
|
let best = null
|
||||||
|
for (const report of reports.values()) {
|
||||||
|
if (report.progress && (!best || report.progress.fraction > best.fraction)) {
|
||||||
|
best = report.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMacDockProgress(
|
||||||
|
best ? best.fraction : null,
|
||||||
|
best ? MAC_DOCK_STATE[best.state] : 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAppProgress(windowId, window, progress) {
|
||||||
|
const normalized = normalizeProgress(progress)
|
||||||
|
|
||||||
|
if (normalized) {
|
||||||
|
reports.set(windowId, { window, progress: normalized })
|
||||||
|
} else {
|
||||||
|
reports.delete(windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
applyDockProgress()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return setWindowsTaskbarProgress(
|
||||||
|
window?.ptr,
|
||||||
|
normalized?.fraction ?? null,
|
||||||
|
TASKBAR_STATE[normalized?.state ?? 'normal']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAppProgress(windowId) {
|
||||||
|
if (!reports.delete(windowId)) return
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
applyDockProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,7 +45,17 @@ function resolveDylibPath() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loadedLibrary
|
||||||
|
|
||||||
function loadMacWindowEffectsLibrary() {
|
function loadMacWindowEffectsLibrary() {
|
||||||
|
if (loadedLibrary !== undefined) {
|
||||||
|
return loadedLibrary
|
||||||
|
}
|
||||||
|
loadedLibrary = openMacWindowEffectsLibrary()
|
||||||
|
return loadedLibrary
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMacWindowEffectsLibrary() {
|
||||||
const dylibPath = resolveDylibPath()
|
const dylibPath = resolveDylibPath()
|
||||||
if (!dylibPath) {
|
if (!dylibPath) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@ -77,6 +87,10 @@ function loadMacWindowEffectsLibrary() {
|
|||||||
setNativeWindowDragRegion: {
|
setNativeWindowDragRegion: {
|
||||||
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
args: [FFIType.ptr, FFIType.f64, FFIType.f64],
|
||||||
returns: FFIType.bool
|
returns: FFIType.bool
|
||||||
|
},
|
||||||
|
setDockProgress: {
|
||||||
|
args: [FFIType.f64, FFIType.i32],
|
||||||
|
returns: FFIType.bool
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -104,6 +118,28 @@ function applyTrafficLightsPosition(lib, mainWindow, attempt = 0) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fraction is 0..1, or null to remove the bar; state: 0 normal, 1 error, 2 paused.
|
||||||
|
export function setMacDockProgress(fraction, state = 0) {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaded = loadMacWindowEffectsLibrary()
|
||||||
|
if (!loaded?.lib.symbols.setDockProgress) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return loaded.lib.symbols.setDockProgress(
|
||||||
|
fraction == null ? -1 : fraction,
|
||||||
|
state
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('macOS dock progress: failed to update dock tile:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function applyMacOSWindowEffects(mainWindow) {
|
export function applyMacOSWindowEffects(mainWindow) {
|
||||||
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
|
if (process.platform !== 'darwin' || !mainWindow?.ptr) {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { BrowserView } from 'electrobun/bun'
|
import { BrowserView } from 'electrobun/bun'
|
||||||
|
import { setAppProgress } from './app-progress.js'
|
||||||
import {
|
import {
|
||||||
checkForCompletedUpdate,
|
checkForCompletedUpdate,
|
||||||
getRunningAppVersion,
|
getRunningAppVersion,
|
||||||
@ -20,7 +21,9 @@ import {
|
|||||||
} from './store.js'
|
} from './store.js'
|
||||||
import {
|
import {
|
||||||
createAppWindow,
|
createAppWindow,
|
||||||
|
getFocusedWindowId,
|
||||||
getMainWindow,
|
getMainWindow,
|
||||||
|
getWindowById,
|
||||||
getWindowSession,
|
getWindowSession,
|
||||||
getWindowState,
|
getWindowState,
|
||||||
handleWindowControl,
|
handleWindowControl,
|
||||||
@ -103,6 +106,15 @@ export function createAppRpc() {
|
|||||||
setSidebarViewMenu: async ({ sections }) => ({
|
setSidebarViewMenu: async ({ sections }) => ({
|
||||||
ok: setSidebarViewMenu(sections)
|
ok: setSidebarViewMenu(sections)
|
||||||
}),
|
}),
|
||||||
|
setAppProgress: async ({ windowId, percent, status } = {}) => {
|
||||||
|
const targetWindowId = windowId || getFocusedWindowId()
|
||||||
|
return {
|
||||||
|
ok: setAppProgress(targetWindowId, getWindowById(targetWindowId), {
|
||||||
|
percent,
|
||||||
|
status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
checkAppUpdateResult: async () =>
|
checkAppUpdateResult: async () =>
|
||||||
checkForCompletedUpdate(getMainWindow()),
|
checkForCompletedUpdate(getMainWindow()),
|
||||||
checkDuplicateInstallations: async () => checkForDuplicateInstallations(),
|
checkDuplicateInstallations: async () => checkForDuplicateInstallations(),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
import Electrobun, { BrowserWindow, Updater, Utils } from 'electrobun/bun'
|
||||||
|
import { clearAppProgress } from './app-progress.js'
|
||||||
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
|
import { applyApplicationMenu, setupApplicationMenuEvents } from './menu.js'
|
||||||
import {
|
import {
|
||||||
applyMacOSWindowEffects,
|
applyMacOSWindowEffects,
|
||||||
@ -447,6 +448,7 @@ function setupWindowEvents(window, windowId) {
|
|||||||
})
|
})
|
||||||
window.on?.('close', () => {
|
window.on?.('close', () => {
|
||||||
captureWindowLayout(windowId)
|
captureWindowLayout(windowId)
|
||||||
|
clearAppProgress(windowId)
|
||||||
windows.delete(windowId)
|
windows.delete(windowId)
|
||||||
if (focusedWindowId === windowId) {
|
if (focusedWindowId === windowId) {
|
||||||
focusedWindowId = getFocusedWindowId()
|
focusedWindowId = getFocusedWindowId()
|
||||||
|
|||||||
142
src/desktop/windows-taskbar-progress.js
Normal file
142
src/desktop/windows-taskbar-progress.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { CFunction, dlopen, FFIType, ptr, read } from 'bun:ffi'
|
||||||
|
|
||||||
|
const CLSID_TASKBAR_LIST = '56FDF344-FD6D-11D0-958A-006097C9A090'
|
||||||
|
const IID_ITASKBAR_LIST3 = 'EA1AFB91-9E28-4B86-90E9-9E9F8A5EEDAF'
|
||||||
|
const COINIT_APARTMENTTHREADED = 0x2
|
||||||
|
const CLSCTX_INPROC_SERVER = 0x1
|
||||||
|
const RPC_E_CHANGED_MODE = 0x80010106 | 0
|
||||||
|
|
||||||
|
// ITaskbarList3 vtable slots (IUnknown 0-2, ITaskbarList 3-7, ITaskbarList2 8).
|
||||||
|
const VTABLE_HR_INIT = 3
|
||||||
|
const VTABLE_SET_PROGRESS_VALUE = 9
|
||||||
|
const VTABLE_SET_PROGRESS_STATE = 10
|
||||||
|
|
||||||
|
export const TASKBAR_STATE = {
|
||||||
|
none: 0x0,
|
||||||
|
indeterminate: 0x1,
|
||||||
|
normal: 0x2,
|
||||||
|
error: 0x4,
|
||||||
|
paused: 0x8
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROGRESS_TOTAL = 1000n
|
||||||
|
|
||||||
|
let taskbar
|
||||||
|
|
||||||
|
function guidBuffer(guid) {
|
||||||
|
const hex = guid.replace(/-/g, '')
|
||||||
|
const buffer = Buffer.alloc(16)
|
||||||
|
buffer.writeUInt32LE(parseInt(hex.slice(0, 8), 16), 0)
|
||||||
|
buffer.writeUInt16LE(parseInt(hex.slice(8, 12), 16), 4)
|
||||||
|
buffer.writeUInt16LE(parseInt(hex.slice(12, 16), 16), 6)
|
||||||
|
Buffer.from(hex.slice(16), 'hex').copy(buffer, 8)
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
function vtableMethod(object, slot, args) {
|
||||||
|
const vtable = read.ptr(object, 0)
|
||||||
|
return CFunction({
|
||||||
|
ptr: read.ptr(vtable, slot * 8),
|
||||||
|
args: [FFIType.ptr, ...args],
|
||||||
|
returns: FFIType.i32
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTaskbarList() {
|
||||||
|
const ole32 = dlopen('ole32.dll', {
|
||||||
|
CoInitializeEx: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
||||||
|
CoCreateInstance: {
|
||||||
|
args: [FFIType.ptr, FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr],
|
||||||
|
returns: FFIType.i32
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const initResult = ole32.symbols.CoInitializeEx(null, COINIT_APARTMENTTHREADED)
|
||||||
|
if (initResult < 0 && initResult !== RPC_E_CHANGED_MODE) {
|
||||||
|
throw new Error(`CoInitializeEx failed (0x${(initResult >>> 0).toString(16)})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clsid = guidBuffer(CLSID_TASKBAR_LIST)
|
||||||
|
const iid = guidBuffer(IID_ITASKBAR_LIST3)
|
||||||
|
const out = new BigUint64Array(1)
|
||||||
|
const createResult = ole32.symbols.CoCreateInstance(
|
||||||
|
ptr(clsid),
|
||||||
|
null,
|
||||||
|
CLSCTX_INPROC_SERVER,
|
||||||
|
ptr(iid),
|
||||||
|
ptr(out)
|
||||||
|
)
|
||||||
|
if (createResult < 0 || !out[0]) {
|
||||||
|
throw new Error(
|
||||||
|
`CoCreateInstance(TaskbarList) failed (0x${(createResult >>> 0).toString(16)})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const object = Number(out[0])
|
||||||
|
const hrInit = vtableMethod(object, VTABLE_HR_INIT, [])
|
||||||
|
const initTaskbarResult = hrInit(object)
|
||||||
|
if (initTaskbarResult < 0) {
|
||||||
|
throw new Error(
|
||||||
|
`ITaskbarList3.HrInit failed (0x${(initTaskbarResult >>> 0).toString(16)})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
object,
|
||||||
|
setProgressValue: vtableMethod(object, VTABLE_SET_PROGRESS_VALUE, [
|
||||||
|
FFIType.ptr,
|
||||||
|
FFIType.u64,
|
||||||
|
FFIType.u64
|
||||||
|
]),
|
||||||
|
setProgressState: vtableMethod(object, VTABLE_SET_PROGRESS_STATE, [
|
||||||
|
FFIType.ptr,
|
||||||
|
FFIType.u32
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskbarList() {
|
||||||
|
if (taskbar !== undefined) {
|
||||||
|
return taskbar
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
taskbar = createTaskbarList()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Windows taskbar progress unavailable:', error)
|
||||||
|
taskbar = null
|
||||||
|
}
|
||||||
|
return taskbar
|
||||||
|
}
|
||||||
|
|
||||||
|
// fraction is 0..1, or null to remove the bar.
|
||||||
|
export function setWindowsTaskbarProgress(
|
||||||
|
hwnd,
|
||||||
|
fraction,
|
||||||
|
state = TASKBAR_STATE.normal
|
||||||
|
) {
|
||||||
|
if (process.platform !== 'win32' || !hwnd) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = getTaskbarList()
|
||||||
|
if (!list) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fraction == null) {
|
||||||
|
return list.setProgressState(list.object, hwnd, TASKBAR_STATE.none) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const completed = BigInt(
|
||||||
|
Math.round(Math.min(1, Math.max(0, fraction)) * Number(PROGRESS_TOTAL))
|
||||||
|
)
|
||||||
|
// SetProgressValue resets the state to normal, so the state goes second.
|
||||||
|
list.setProgressValue(list.object, hwnd, completed, PROGRESS_TOTAL)
|
||||||
|
return list.setProgressState(list.object, hwnd, state) >= 0
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Windows taskbar progress: failed to update:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -263,6 +263,7 @@ const electronAPI = {
|
|||||||
invokeRequest('resizeSpotlightWindow', { height }),
|
invokeRequest('resizeSpotlightWindow', { height }),
|
||||||
setSidebarViewMenu: (sections) =>
|
setSidebarViewMenu: (sections) =>
|
||||||
invokeRequest('setSidebarViewMenu', { sections }),
|
invokeRequest('setSidebarViewMenu', { sections }),
|
||||||
|
setAppProgress: (progress) => invokeRequest('setAppProgress', progress),
|
||||||
getAppVersion: () => invokeRequest('getAppVersion'),
|
getAppVersion: () => invokeRequest('getAppVersion'),
|
||||||
getAppEngine: () => invokeRequest('getAppEngine'),
|
getAppEngine: () => invokeRequest('getAppEngine'),
|
||||||
syncWindowTabs: (payload) => invokeRequest('syncWindowTabs', payload),
|
syncWindowTabs: (payload) => invokeRequest('syncWindowTabs', payload),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user