mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 00:28:33 +00:00
feat: disable high power consumption without charger (#14281)
Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
@@ -6,10 +6,10 @@ import { I18nProvider } from '@affine/core/modules/i18n';
|
|||||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import { FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
import { FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
||||||
import { Suspense } from 'react';
|
import { Suspense, useEffect } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import { setupEffects } from './effects';
|
import { setupEffects, useIsOnBattery } from './effects';
|
||||||
import { DesktopLanguageSync } from './language-sync';
|
import { DesktopLanguageSync } from './language-sync';
|
||||||
import { DesktopThemeSync } from './theme-sync';
|
import { DesktopThemeSync } from './theme-sync';
|
||||||
|
|
||||||
@@ -40,6 +40,15 @@ const future = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
const isOnBattery = useIsOnBattery();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.classList.toggle('on-battery', isOnBattery);
|
||||||
|
return () => {
|
||||||
|
document.body.classList.remove('on-battery');
|
||||||
|
};
|
||||||
|
}, [isOnBattery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<FrameworkRoot framework={frameworkProvider}>
|
<FrameworkRoot framework={frameworkProvider}>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { setupEvents } from './events';
|
import { setupEvents } from './events';
|
||||||
import { setupModules } from './modules';
|
import { setupModules } from './modules';
|
||||||
|
import { setupPowerSourceStore } from './power';
|
||||||
import { setupStoreManager } from './store-manager';
|
import { setupStoreManager } from './store-manager';
|
||||||
|
|
||||||
export function setupEffects() {
|
export function setupEffects() {
|
||||||
const { framework, frameworkProvider } = setupModules();
|
const { framework, frameworkProvider } = setupModules();
|
||||||
setupStoreManager(framework);
|
setupStoreManager(framework);
|
||||||
setupEvents(frameworkProvider);
|
setupEvents(frameworkProvider);
|
||||||
|
setupPowerSourceStore();
|
||||||
return { framework, frameworkProvider };
|
return { framework, frameworkProvider };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { useIsOnBattery } from './power';
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
|
type Listener = () => void;
|
||||||
|
|
||||||
|
let snapshot = false;
|
||||||
|
let teardown: (() => void) | null = null;
|
||||||
|
const listeners = new Set<Listener>();
|
||||||
|
|
||||||
|
function emitChange() {
|
||||||
|
listeners.forEach(listener => listener());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePowerSourceChange(isOnBattery: boolean) {
|
||||||
|
if (snapshot === isOnBattery) return;
|
||||||
|
snapshot = isOnBattery;
|
||||||
|
emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSubscribed() {
|
||||||
|
if (teardown) return;
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const subscribePowerSource = window.__apis?.events?.power?.['power-source'];
|
||||||
|
if (typeof subscribePowerSource !== 'function') return;
|
||||||
|
|
||||||
|
const unsubscribe = subscribePowerSource(handlePowerSourceChange);
|
||||||
|
teardown = typeof unsubscribe === 'function' ? unsubscribe : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(listener: Listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
if (listeners.size === 1) {
|
||||||
|
ensureSubscribed();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
if (listeners.size === 0) {
|
||||||
|
teardown?.();
|
||||||
|
teardown = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupPowerSourceStore() {
|
||||||
|
ensureSubscribed();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsOnBatterySnapshot() {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIsOnBattery() {
|
||||||
|
return useSyncExternalStore(subscribe, getIsOnBatterySnapshot, () => false);
|
||||||
|
}
|
||||||
@@ -23,3 +23,12 @@ html[data-active='true']:has([data-translucent='true']) {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable animations and transitions when on battery.
|
||||||
|
* We use :not(.playwright-test) to ensure tests (which rely on animations) don't break.
|
||||||
|
*/
|
||||||
|
body.on-battery:not(.playwright-test) * {
|
||||||
|
animation-duration: 0ms !important;
|
||||||
|
transition-duration: 0ms !important;
|
||||||
|
}
|
||||||
|
|||||||
16
packages/frontend/apps/electron-renderer/src/global.d.ts
vendored
Normal file
16
packages/frontend/apps/electron-renderer/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { apis, events } from '@affine/electron-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the global Window interface to include AFFiNE's
|
||||||
|
* Electron bridge APIs and event emitters.
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__apis?: {
|
||||||
|
apis: typeof apis;
|
||||||
|
events: typeof events;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ipcMain, webContents } from 'electron';
|
import { ipcMain, powerMonitor, webContents } from 'electron';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AFFINE_EVENT_CHANNEL_NAME,
|
AFFINE_EVENT_CHANNEL_NAME,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { applicationMenuEvents } from './application-menu';
|
import { applicationMenuEvents } from './application-menu';
|
||||||
import { beforeAppQuit } from './cleanup';
|
import { beforeAppQuit } from './cleanup';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
import { powerEvents } from './power';
|
||||||
import { recordingEvents } from './recording';
|
import { recordingEvents } from './recording';
|
||||||
import { sharedStorageEvents } from './shared-storage';
|
import { sharedStorageEvents } from './shared-storage';
|
||||||
import { uiEvents } from './ui/events';
|
import { uiEvents } from './ui/events';
|
||||||
@@ -20,6 +21,7 @@ export const allEvents = {
|
|||||||
sharedStorage: sharedStorageEvents,
|
sharedStorage: sharedStorageEvents,
|
||||||
recording: recordingEvents,
|
recording: recordingEvents,
|
||||||
popup: popupEvents,
|
popup: popupEvents,
|
||||||
|
power: powerEvents,
|
||||||
};
|
};
|
||||||
|
|
||||||
const subscriptions = new Map<number, Set<string>>();
|
const subscriptions = new Map<number, Set<string>>();
|
||||||
@@ -71,6 +73,13 @@ export function registerEvents() {
|
|||||||
if (typeof channel !== 'string') return;
|
if (typeof channel !== 'string') return;
|
||||||
if (action === 'subscribe') {
|
if (action === 'subscribe') {
|
||||||
addSubscription(event.sender, channel);
|
addSubscription(event.sender, channel);
|
||||||
|
if (channel === 'power:power-source') {
|
||||||
|
event.sender.send(
|
||||||
|
AFFINE_EVENT_CHANNEL_NAME,
|
||||||
|
channel,
|
||||||
|
powerMonitor.isOnBatteryPower()
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
removeSubscription(event.sender, channel);
|
removeSubscription(event.sender, channel);
|
||||||
}
|
}
|
||||||
|
|||||||
29
packages/frontend/apps/electron/src/main/power/index.ts
Normal file
29
packages/frontend/apps/electron/src/main/power/index.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { powerMonitor } from 'electron';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Power-related event handlers for the Electron main process.
|
||||||
|
*/
|
||||||
|
export const powerEvents = {
|
||||||
|
/**
|
||||||
|
* Subscribes to system power source changes.
|
||||||
|
* Emits the initial state immediately upon subscription.
|
||||||
|
* @param emit - Callback function to send power state to the renderer.
|
||||||
|
* @returns A cleanup function to remove listeners from powerMonitor.
|
||||||
|
*/
|
||||||
|
'power-source': (emit: (isOnBattery: boolean) => void) => {
|
||||||
|
// emit initial state
|
||||||
|
emit(powerMonitor.isOnBatteryPower());
|
||||||
|
|
||||||
|
const onBattery = () => emit(true);
|
||||||
|
const onAC = () => emit(false);
|
||||||
|
|
||||||
|
powerMonitor.on('on-battery', onBattery);
|
||||||
|
powerMonitor.on('on-ac', onAC);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
return () => {
|
||||||
|
powerMonitor.off('on-battery', onBattery);
|
||||||
|
powerMonitor.off('on-ac', onAC);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
4
packages/frontend/native/index.d.ts
vendored
4
packages/frontend/native/index.d.ts
vendored
@@ -19,10 +19,10 @@ export declare class ApplicationStateChangedSubscriber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export declare class AudioCaptureSession {
|
export declare class AudioCaptureSession {
|
||||||
stop(): void
|
|
||||||
get sampleRate(): number
|
get sampleRate(): number
|
||||||
get channels(): number
|
get channels(): number
|
||||||
get actualSampleRate(): number
|
get actualSampleRate(): number
|
||||||
|
stop(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class ShareableContent {
|
export declare class ShareableContent {
|
||||||
@@ -31,9 +31,9 @@ export declare class ShareableContent {
|
|||||||
constructor()
|
constructor()
|
||||||
static applications(): Array<ApplicationInfo>
|
static applications(): Array<ApplicationInfo>
|
||||||
static applicationWithProcessId(processId: number): ApplicationInfo | null
|
static applicationWithProcessId(processId: number): ApplicationInfo | null
|
||||||
static isUsingMicrophone(processId: number): boolean
|
|
||||||
static tapAudio(processId: number, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioCaptureSession
|
static tapAudio(processId: number, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioCaptureSession
|
||||||
static tapGlobalAudio(excludedProcesses: Array<ApplicationInfo> | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioCaptureSession
|
static tapGlobalAudio(excludedProcesses: Array<ApplicationInfo> | undefined | null, audioStreamCallback: ((err: Error | null, arg: Float32Array) => void)): AudioCaptureSession
|
||||||
|
static isUsingMicrophone(processId: number): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise<Float32Array>
|
export declare function decodeAudio(buf: Uint8Array, destSampleRate?: number | undefined | null, filename?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise<Float32Array>
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ export const test = baseTest.extend<{
|
|||||||
await use(page);
|
await use(page);
|
||||||
},
|
},
|
||||||
context: async ({ context }, use) => {
|
context: async ({ context }, use) => {
|
||||||
|
// Force-mark the body so global.css knows we are in a test.
|
||||||
|
// This keeps animations ON (0.1s) for tests, but OFF (0ms) for battery users.
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.body.classList.add('playwright-test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// workaround for skipping onboarding redirect on the web
|
// workaround for skipping onboarding redirect on the web
|
||||||
await skipOnboarding(context);
|
await skipOnboarding(context);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user