@@ -25,14 +25,18 @@ export function setupEvents(frameworkProvider: FrameworkProvider) {
|
|||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
});
|
});
|
||||||
|
|
||||||
events?.applicationMenu.openInSettingModal(activeTab => {
|
events?.applicationMenu.openInSettingModal(({ activeTab, scrollAnchor }) => {
|
||||||
using currentWorkspace = getCurrentWorkspace(frameworkProvider);
|
using currentWorkspace = getCurrentWorkspace(frameworkProvider);
|
||||||
if (!currentWorkspace) {
|
if (!currentWorkspace) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { workspace } = currentWorkspace;
|
const { workspace } = currentWorkspace;
|
||||||
workspace.scope.get(WorkspaceDialogService).open('setting', {
|
const workspaceDialogService = workspace.scope.get(WorkspaceDialogService);
|
||||||
|
// close all other dialogs first
|
||||||
|
workspaceDialogService.closeAll();
|
||||||
|
workspaceDialogService.open('setting', {
|
||||||
activeTab: activeTab as unknown as SettingTab,
|
activeTab: activeTab as unknown as SettingTab,
|
||||||
|
scrollAnchor,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
configureDesktopApiModule,
|
configureDesktopApiModule,
|
||||||
DesktopApiService,
|
DesktopApiService,
|
||||||
} from '@affine/core/modules/desktop-api';
|
} from '@affine/core/modules/desktop-api';
|
||||||
import { configureSpellCheckSettingModule } from '@affine/core/modules/editor-setting';
|
import {
|
||||||
|
configureSpellCheckSettingModule,
|
||||||
|
configureTraySettingModule,
|
||||||
|
} from '@affine/core/modules/editor-setting';
|
||||||
import { configureFindInPageModule } from '@affine/core/modules/find-in-page';
|
import { configureFindInPageModule } from '@affine/core/modules/find-in-page';
|
||||||
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
import { configureElectronStateStorageImpls } from '@affine/core/modules/storage';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +30,7 @@ export function setupModules() {
|
|||||||
configureFindInPageModule(framework);
|
configureFindInPageModule(framework);
|
||||||
configureDesktopApiModule(framework);
|
configureDesktopApiModule(framework);
|
||||||
configureSpellCheckSettingModule(framework);
|
configureSpellCheckSettingModule(framework);
|
||||||
|
configureTraySettingModule(framework);
|
||||||
configureDesktopBackupModule(framework);
|
configureDesktopBackupModule(framework);
|
||||||
|
|
||||||
framework.impl(PopupWindowProvider, p => {
|
framework.impl(PopupWindowProvider, p => {
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export function createApplicationMenu() {
|
|||||||
label: `About ${app.getName()}`,
|
label: `About ${app.getName()}`,
|
||||||
click: async () => {
|
click: async () => {
|
||||||
await showMainWindow();
|
await showMainWindow();
|
||||||
applicationMenuSubjects.openInSettingModal$.next('about');
|
applicationMenuSubjects.openInSettingModal$.next({
|
||||||
|
activeTab: 'about',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export const applicationMenuEvents = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
// todo: properly define the active tab type
|
// todo: properly define the active tab type
|
||||||
openInSettingModal: (fn: (activeTab: string) => void) => {
|
openInSettingModal: (
|
||||||
|
fn: (props: { activeTab: string; scrollAnchor?: string }) => void
|
||||||
|
) => {
|
||||||
const sub = applicationMenuSubjects.openInSettingModal$.subscribe(fn);
|
const sub = applicationMenuSubjects.openInSettingModal$.subscribe(fn);
|
||||||
return () => {
|
return () => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
|
|||||||
@@ -3,5 +3,8 @@ import { Subject } from 'rxjs';
|
|||||||
export const applicationMenuSubjects = {
|
export const applicationMenuSubjects = {
|
||||||
newPageAction$: new Subject<'page' | 'edgeless'>(),
|
newPageAction$: new Subject<'page' | 'edgeless'>(),
|
||||||
openJournal$: new Subject<void>(),
|
openJournal$: new Subject<void>(),
|
||||||
openInSettingModal$: new Subject<string>(),
|
openInSettingModal$: new Subject<{
|
||||||
|
activeTab: string;
|
||||||
|
scrollAnchor?: string;
|
||||||
|
}>(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -342,7 +342,8 @@ function setupRecordingListeners() {
|
|||||||
status?.status === 'create-block-failed'
|
status?.status === 'create-block-failed'
|
||||||
) {
|
) {
|
||||||
// show the popup for 10s
|
// show the popup for 10s
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
|
() => {
|
||||||
// check again if current status is still ready
|
// check again if current status is still ready
|
||||||
if (
|
if (
|
||||||
(recordingStatus$.value?.status === 'create-block-success' ||
|
(recordingStatus$.value?.status === 'create-block-success' ||
|
||||||
@@ -353,7 +354,9 @@ function setupRecordingListeners() {
|
|||||||
logger.error('failed to hide recording popup', err);
|
logger.error('failed to hide recording popup', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 10_000);
|
},
|
||||||
|
status?.status === 'create-block-failed' ? 30_000 : 10_000
|
||||||
|
);
|
||||||
} else if (!status) {
|
} else if (!status) {
|
||||||
// status is removed, we should hide the popup
|
// status is removed, we should hide the popup
|
||||||
popupManager
|
popupManager
|
||||||
@@ -550,22 +553,40 @@ export async function stopRecording(id: number) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { file } = recording;
|
const { file, stream } = recording;
|
||||||
|
|
||||||
|
// First stop the audio stream to prevent more data coming in
|
||||||
|
try {
|
||||||
|
stream.stop();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to stop audio stream', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// End the file with a timeout
|
||||||
file.end();
|
file.end();
|
||||||
|
|
||||||
// Wait for file to finish writing
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await Promise.race([
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
file.on('finish', () => {
|
file.on('finish', () => {
|
||||||
// check if the file is empty
|
// check if the file is empty
|
||||||
const stats = fs.statSync(file.path);
|
const stats = fs.statSync(file.path);
|
||||||
if (stats.size === 0) {
|
if (stats.size === 0) {
|
||||||
logger.error(`Recording ${id} is empty`);
|
|
||||||
reject(new Error('Recording is empty'));
|
reject(new Error('Recording is empty'));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
file.on('error', err => {
|
||||||
|
reject(err);
|
||||||
});
|
});
|
||||||
|
}),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('File writing timeout')), 10000)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
const recordingStatus = recordingStateMachine.dispatch({
|
const recordingStatus = recordingStateMachine.dispatch({
|
||||||
type: 'STOP_RECORDING',
|
type: 'STOP_RECORDING',
|
||||||
id,
|
id,
|
||||||
@@ -591,6 +612,11 @@ export async function stopRecording(id: number) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return serializeRecordingStatus(recordingStatus);
|
return serializeRecordingStatus(recordingStatus);
|
||||||
|
} finally {
|
||||||
|
// Clean up the file stream if it's still open
|
||||||
|
if (!file.closed) {
|
||||||
|
file.destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,9 +51,17 @@ export const SpellCheckStateSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const SpellCheckStateKey = 'spellCheckState' as const;
|
export const SpellCheckStateKey = 'spellCheckState' as const;
|
||||||
// eslint-disable-next-line no-redeclare
|
// oxlint-disable-next-line no-redeclare
|
||||||
export type SpellCheckStateSchema = z.infer<typeof SpellCheckStateSchema>;
|
export type SpellCheckStateSchema = z.infer<typeof SpellCheckStateSchema>;
|
||||||
|
|
||||||
|
export const MenubarStateKey = 'menubarState' as const;
|
||||||
|
export const MenubarStateSchema = z.object({
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-redeclare
|
||||||
|
export type MenubarStateSchema = z.infer<typeof MenubarStateSchema>;
|
||||||
|
|
||||||
export const MeetingSettingsKey = 'meetingSettings' as const;
|
export const MeetingSettingsKey = 'meetingSettings' as const;
|
||||||
export const MeetingSettingsSchema = z.object({
|
export const MeetingSettingsSchema = z.object({
|
||||||
// global meeting feature control
|
// global meeting feature control
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
nativeImage,
|
nativeImage,
|
||||||
Tray,
|
Tray,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
|
import { map, shareReplay } from 'rxjs';
|
||||||
|
|
||||||
import { isMacOS } from '../../shared/utils';
|
import { isMacOS } from '../../shared/utils';
|
||||||
import { applicationMenuSubjects } from '../application-menu';
|
import { applicationMenuSubjects } from '../application-menu';
|
||||||
@@ -22,9 +23,10 @@ import {
|
|||||||
stopRecording,
|
stopRecording,
|
||||||
updateApplicationsPing$,
|
updateApplicationsPing$,
|
||||||
} from '../recording/feature';
|
} from '../recording/feature';
|
||||||
|
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||||
|
import { globalStateStorage } from '../shared-storage/storage';
|
||||||
import { getMainWindow } from '../windows-manager';
|
import { getMainWindow } from '../windows-manager';
|
||||||
import { icons } from './icons';
|
import { icons } from './icons';
|
||||||
|
|
||||||
export interface TrayMenuConfigItem {
|
export interface TrayMenuConfigItem {
|
||||||
label: string;
|
label: string;
|
||||||
click?: () => void;
|
click?: () => void;
|
||||||
@@ -81,7 +83,7 @@ function buildMenuConfig(config: TrayMenuConfig): MenuItemConstructorOptions[] {
|
|||||||
return menuConfig;
|
return menuConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrayState {
|
class TrayState implements Disposable {
|
||||||
tray: Tray | null = null;
|
tray: Tray | null = null;
|
||||||
|
|
||||||
// tray's icon
|
// tray's icon
|
||||||
@@ -94,6 +96,7 @@ class TrayState {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.icon.setTemplateImage(true);
|
this.icon.setTemplateImage(true);
|
||||||
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
// sorry, no idea on better naming
|
// sorry, no idea on better naming
|
||||||
@@ -133,17 +136,20 @@ class TrayState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRecordingMenuProvider(): TrayMenuProvider | null {
|
getRecordingMenuProvider(): TrayMenuProvider | null {
|
||||||
if (
|
if (!checkRecordingAvailable()) {
|
||||||
!checkRecordingAvailable() ||
|
|
||||||
!checkScreenRecordingPermission() ||
|
|
||||||
!MeetingsSettingsState.value.enabled
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfig = () => {
|
const getConfig = () => {
|
||||||
|
const items: TrayMenuConfig = [];
|
||||||
|
if (
|
||||||
|
checkScreenRecordingPermission() &&
|
||||||
|
MeetingsSettingsState.value.enabled
|
||||||
|
) {
|
||||||
const appGroups = appGroups$.value;
|
const appGroups = appGroups$.value;
|
||||||
const runningAppGroups = appGroups.filter(appGroup => appGroup.isRunning);
|
const runningAppGroups = appGroups.filter(
|
||||||
|
appGroup => appGroup.isRunning
|
||||||
|
);
|
||||||
|
|
||||||
const recordingStatus = recordingStatus$.value;
|
const recordingStatus = recordingStatus$.value;
|
||||||
|
|
||||||
@@ -162,7 +168,8 @@ class TrayState {
|
|||||||
startRecording(appGroup);
|
startRecording(appGroup);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
return [
|
|
||||||
|
items.push(
|
||||||
{
|
{
|
||||||
label: 'Start Recording Meeting',
|
label: 'Start Recording Meeting',
|
||||||
icon: icons.record,
|
icon: icons.record,
|
||||||
@@ -180,23 +187,15 @@ class TrayState {
|
|||||||
...appMenuItems,
|
...appMenuItems,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...appMenuItems,
|
...appMenuItems
|
||||||
{
|
);
|
||||||
label: `Meetings Settings...`,
|
} else {
|
||||||
click: async () => {
|
|
||||||
showMainWindow();
|
|
||||||
applicationMenuSubjects.openInSettingModal$.next('meetings');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordingLabel = recordingStatus.appGroup?.name
|
const recordingLabel = recordingStatus.appGroup?.name
|
||||||
? `Recording (${recordingStatus.appGroup?.name})`
|
? `Recording (${recordingStatus.appGroup?.name})`
|
||||||
: 'Recording';
|
: 'Recording';
|
||||||
|
|
||||||
// recording is either started or paused
|
// recording is either started or paused
|
||||||
return [
|
items.push(
|
||||||
{
|
{
|
||||||
label: recordingLabel,
|
label: recordingLabel,
|
||||||
icon: icons.recording,
|
icon: icons.recording,
|
||||||
@@ -210,8 +209,21 @@ class TrayState {
|
|||||||
logger.error('Failed to stop recording:', err);
|
logger.error('Failed to stop recording:', err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
label: `Meetings Settings...`,
|
||||||
|
click: () => {
|
||||||
|
showMainWindow();
|
||||||
|
applicationMenuSubjects.openInSettingModal$.next({
|
||||||
|
activeTab: 'meetings',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
];
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -237,11 +249,23 @@ class TrayState {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Menubar settings...',
|
||||||
|
click: () => {
|
||||||
|
showMainWindow();
|
||||||
|
applicationMenuSubjects.openInSettingModal$.next({
|
||||||
|
activeTab: 'appearance',
|
||||||
|
scrollAnchor: 'menubar',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: `About ${app.getName()}`,
|
label: `About ${app.getName()}`,
|
||||||
click: () => {
|
click: () => {
|
||||||
showMainWindow();
|
showMainWindow();
|
||||||
applicationMenuSubjects.openInSettingModal$.next('about');
|
applicationMenuSubjects.openInSettingModal$.next({
|
||||||
|
activeTab: 'about',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'separator',
|
'separator',
|
||||||
@@ -270,6 +294,12 @@ class TrayState {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposables: (() => void)[] = [];
|
||||||
|
|
||||||
|
[Symbol.dispose]() {
|
||||||
|
this.disposables.forEach(d => d());
|
||||||
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
if (!this.tray) {
|
if (!this.tray) {
|
||||||
this.tray = new Tray(this.icon);
|
this.tray = new Tray(this.icon);
|
||||||
@@ -287,8 +317,8 @@ class TrayState {
|
|||||||
logger.debug('App groups updated, refreshing tray menu');
|
logger.debug('App groups updated, refreshing tray menu');
|
||||||
this.update();
|
this.update();
|
||||||
});
|
});
|
||||||
beforeAppQuit(() => {
|
|
||||||
logger.info('Cleaning up tray before app quit');
|
this.disposables.push(() => {
|
||||||
this.tray?.off('click', clickHandler);
|
this.tray?.off('click', clickHandler);
|
||||||
this.tray?.destroy();
|
this.tray?.destroy();
|
||||||
appGroupsSubscription.unsubscribe();
|
appGroupsSubscription.unsubscribe();
|
||||||
@@ -311,12 +341,39 @@ class TrayState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _trayState: TrayState | undefined;
|
const TraySettingsState = {
|
||||||
|
$: globalStateStorage.watch<MenubarStateSchema>(MenubarStateKey).pipe(
|
||||||
|
map(v => MenubarStateSchema.parse(v ?? {})),
|
||||||
|
shareReplay(1)
|
||||||
|
),
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return MenubarStateSchema.parse(
|
||||||
|
globalStateStorage.get(MenubarStateKey) ?? {}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const setupTrayState = () => {
|
export const setupTrayState = () => {
|
||||||
|
let _trayState: TrayState | undefined;
|
||||||
|
if (TraySettingsState.value.enabled) {
|
||||||
|
_trayState = new TrayState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTrayState = (state: MenubarStateSchema) => {
|
||||||
|
if (state.enabled) {
|
||||||
if (!_trayState) {
|
if (!_trayState) {
|
||||||
_trayState = new TrayState();
|
_trayState = new TrayState();
|
||||||
_trayState.init();
|
|
||||||
}
|
}
|
||||||
return _trayState;
|
} else {
|
||||||
|
_trayState?.[Symbol.dispose]();
|
||||||
|
_trayState = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscription = TraySettingsState.$.subscribe(updateTrayState);
|
||||||
|
|
||||||
|
beforeAppQuit(() => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ import type { PropsWithChildren, ReactNode } from 'react';
|
|||||||
import { wrapper, wrapperDisabled } from './share.css';
|
import { wrapper, wrapperDisabled } from './share.css';
|
||||||
|
|
||||||
interface SettingWrapperProps {
|
interface SettingWrapperProps {
|
||||||
|
id?: string;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingWrapper = ({
|
export const SettingWrapper = ({
|
||||||
|
id,
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
disabled,
|
disabled,
|
||||||
}: PropsWithChildren<SettingWrapperProps>) => {
|
}: PropsWithChildren<SettingWrapperProps>) => {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(wrapper, disabled && wrapperDisabled)}>
|
<div id={id} className={clsx(wrapper, disabled && wrapperDisabled)}>
|
||||||
{title ? <div className="title">{title}</div> : null}
|
{title ? <div className="title">{title}</div> : null}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SettingWrapper,
|
SettingWrapper,
|
||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { LanguageMenu } from '@affine/core/components/affine/language-menu';
|
import { LanguageMenu } from '@affine/core/components/affine/language-menu';
|
||||||
|
import { TraySettingService } from '@affine/core/modules/editor-setting/services/tray-settings';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -58,6 +59,28 @@ export const ThemeSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MenubarSetting = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const traySettingService = useService(TraySettingService);
|
||||||
|
const { enabled } = useLiveData(traySettingService.setting$);
|
||||||
|
return (
|
||||||
|
<SettingWrapper
|
||||||
|
id="menubar"
|
||||||
|
title={t['com.affine.appearanceSettings.menubar.title']()}
|
||||||
|
>
|
||||||
|
<SettingRow
|
||||||
|
name={t['com.affine.appearanceSettings.menubar.toggle']()}
|
||||||
|
desc={t['com.affine.appearanceSettings.menubar.description']()}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={checked => traySettingService.setEnabled(checked)}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const AppearanceSettings = () => {
|
export const AppearanceSettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
@@ -150,6 +173,8 @@ export const AppearanceSettings = () => {
|
|||||||
)}
|
)}
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{BUILD_CONFIG.isElectron ? <MenubarSetting /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -143,13 +143,11 @@ export const useGeneralSettingList = (): GeneralSettingList => {
|
|||||||
|
|
||||||
interface GeneralSettingProps {
|
interface GeneralSettingProps {
|
||||||
activeTab: SettingTab;
|
activeTab: SettingTab;
|
||||||
scrollAnchor?: string;
|
|
||||||
onChangeSettingState: (settingState: SettingState) => void;
|
onChangeSettingState: (settingState: SettingState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GeneralSetting = ({
|
export const GeneralSetting = ({
|
||||||
activeTab,
|
activeTab,
|
||||||
scrollAnchor,
|
|
||||||
onChangeSettingState,
|
onChangeSettingState,
|
||||||
}: GeneralSettingProps) => {
|
}: GeneralSettingProps) => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
@@ -166,7 +164,7 @@ export const GeneralSetting = ({
|
|||||||
case 'about':
|
case 'about':
|
||||||
return <AboutAffine />;
|
return <AboutAffine />;
|
||||||
case 'plans':
|
case 'plans':
|
||||||
return <AFFiNEPricingPlans scrollAnchor={scrollAnchor} />;
|
return <AFFiNEPricingPlans />;
|
||||||
case 'billing':
|
case 'billing':
|
||||||
return <BillingSettings onChangeSettingState={onChangeSettingState} />;
|
return <BillingSettings onChangeSettingState={onChangeSettingState} />;
|
||||||
case 'experimental-features':
|
case 'experimental-features':
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { CloudPlanLayout, PlanLayout } from './layout';
|
|||||||
import { PlansSkeleton } from './skeleton';
|
import { PlansSkeleton } from './skeleton';
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
const Settings = ({ scrollAnchor }: { scrollAnchor?: string }) => {
|
const Settings = () => {
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(SubscriptionService);
|
||||||
const prices = useLiveData(subscriptionService.prices.prices$);
|
const prices = useLiveData(subscriptionService.prices.prices$);
|
||||||
|
|
||||||
@@ -24,23 +24,13 @@ const Settings = ({ scrollAnchor }: { scrollAnchor?: string }) => {
|
|||||||
return <PlansSkeleton />;
|
return <PlansSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <PlanLayout cloud={<CloudPlans />} ai={<AIPlan />} />;
|
||||||
<PlanLayout
|
|
||||||
cloud={<CloudPlans />}
|
|
||||||
ai={<AIPlan />}
|
|
||||||
scrollAnchor={scrollAnchor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AFFiNEPricingPlans = ({
|
export const AFFiNEPricingPlans = () => {
|
||||||
scrollAnchor,
|
|
||||||
}: {
|
|
||||||
scrollAnchor?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
|
<SWRErrorBoundary FallbackComponent={PlansErrorBoundary}>
|
||||||
<Settings scrollAnchor={scrollAnchor} />
|
<Settings />
|
||||||
</SWRErrorBoundary>
|
</SWRErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import {
|
|||||||
type HtmlHTMLAttributes,
|
type HtmlHTMLAttributes,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
|
|
||||||
import * as styles from './layout.css';
|
import * as styles from './layout.css';
|
||||||
|
|
||||||
@@ -69,24 +67,12 @@ export const PricingCollapsible = ({
|
|||||||
export interface PlanLayoutProps {
|
export interface PlanLayoutProps {
|
||||||
cloud?: ReactNode;
|
cloud?: ReactNode;
|
||||||
ai?: ReactNode;
|
ai?: ReactNode;
|
||||||
scrollAnchor?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlanLayout = ({ cloud, ai, scrollAnchor }: PlanLayoutProps) => {
|
export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const plansRootRef = useRef<HTMLDivElement>(null);
|
const plansRootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// TODO(@catsjuice): Need a better solution to handle this situation
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!scrollAnchor) return;
|
|
||||||
flushSync(() => {
|
|
||||||
const target = plansRootRef.current?.querySelector(`#${scrollAnchor}`);
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [scrollAnchor]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.plansLayoutRoot} ref={plansRootRef}>
|
<div className={styles.plansLayoutRoot} ref={plansRootRef}>
|
||||||
{/* TODO(@catsjuice): SettingHeader component shouldn't have margin itself */}
|
{/* TODO(@catsjuice): SettingHeader component shouldn't have margin itself */}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
|
||||||
import { AccountSetting } from './account-setting';
|
import { AccountSetting } from './account-setting';
|
||||||
import { GeneralSetting } from './general-setting';
|
import { GeneralSetting } from './general-setting';
|
||||||
@@ -39,6 +40,7 @@ import { WorkspaceSetting } from './workspace-setting';
|
|||||||
interface SettingProps extends ModalProps {
|
interface SettingProps extends ModalProps {
|
||||||
activeTab?: SettingTab;
|
activeTab?: SettingTab;
|
||||||
onCloseSetting: () => void;
|
onCloseSetting: () => void;
|
||||||
|
scrollAnchor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWorkspaceSetting = (key: string): boolean =>
|
const isWorkspaceSetting = (key: string): boolean =>
|
||||||
@@ -55,10 +57,11 @@ const CenteredLoading = () => {
|
|||||||
const SettingModalInner = ({
|
const SettingModalInner = ({
|
||||||
activeTab: initialActiveTab = 'appearance',
|
activeTab: initialActiveTab = 'appearance',
|
||||||
onCloseSetting,
|
onCloseSetting,
|
||||||
|
scrollAnchor: initialScrollAnchor,
|
||||||
}: SettingProps) => {
|
}: SettingProps) => {
|
||||||
const [settingState, setSettingState] = useState<SettingState>({
|
const [settingState, setSettingState] = useState<SettingState>({
|
||||||
activeTab: initialActiveTab,
|
activeTab: initialActiveTab,
|
||||||
scrollAnchor: undefined,
|
scrollAnchor: initialScrollAnchor,
|
||||||
});
|
});
|
||||||
const globalContextService = useService(GlobalContextService);
|
const globalContextService = useService(GlobalContextService);
|
||||||
|
|
||||||
@@ -150,6 +153,18 @@ const SettingModalInner = ({
|
|||||||
}
|
}
|
||||||
}, [isSelfhosted, settingState.activeTab]);
|
}, [isSelfhosted, settingState.activeTab]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settingState.scrollAnchor) {
|
||||||
|
flushSync(() => {
|
||||||
|
const target = modalContentRef.current?.querySelector(
|
||||||
|
`#${settingState.scrollAnchor}`
|
||||||
|
);
|
||||||
|
if (target) {
|
||||||
|
target.scrollIntoView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [settingState]);
|
||||||
return (
|
return (
|
||||||
<FrameworkScope scope={currentServer.scope}>
|
<FrameworkScope scope={currentServer.scope}>
|
||||||
<SettingSidebar
|
<SettingSidebar
|
||||||
@@ -165,7 +180,6 @@ const SettingModalInner = ({
|
|||||||
<div className={style.centerContainer}>
|
<div className={style.centerContainer}>
|
||||||
<div ref={modalContentRef} className={style.content}>
|
<div ref={modalContentRef} className={style.content}>
|
||||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||||
{}
|
|
||||||
{settingState.activeTab === 'account' &&
|
{settingState.activeTab === 'account' &&
|
||||||
loginStatus === 'authenticated' ? (
|
loginStatus === 'authenticated' ? (
|
||||||
<AccountSetting onChangeSettingState={setSettingState} />
|
<AccountSetting onChangeSettingState={setSettingState} />
|
||||||
@@ -178,7 +192,6 @@ const SettingModalInner = ({
|
|||||||
) : !isWorkspaceSetting(settingState.activeTab) ? (
|
) : !isWorkspaceSetting(settingState.activeTab) ? (
|
||||||
<GeneralSetting
|
<GeneralSetting
|
||||||
activeTab={settingState.activeTab}
|
activeTab={settingState.activeTab}
|
||||||
scrollAnchor={settingState.scrollAnchor}
|
|
||||||
onChangeSettingState={setSettingState}
|
onChangeSettingState={setSettingState}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -223,6 +236,7 @@ const SettingModalInner = ({
|
|||||||
export const SettingDialog = ({
|
export const SettingDialog = ({
|
||||||
close,
|
close,
|
||||||
activeTab,
|
activeTab,
|
||||||
|
scrollAnchor,
|
||||||
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['setting']>) => {
|
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['setting']>) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -242,7 +256,11 @@ export const SettingDialog = ({
|
|||||||
onOpenChange={() => close()}
|
onOpenChange={() => close()}
|
||||||
>
|
>
|
||||||
<Suspense fallback={<CenteredLoading />}>
|
<Suspense fallback={<CenteredLoading />}>
|
||||||
<SettingModalInner activeTab={activeTab} onCloseSetting={close} />
|
<SettingModalInner
|
||||||
|
activeTab={activeTab}
|
||||||
|
onCloseSetting={close}
|
||||||
|
scrollAnchor={scrollAnchor}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,4 +38,11 @@ export class WorkspaceDialogService extends Service {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeAll() {
|
||||||
|
const dialogs = this.dialogs$.value;
|
||||||
|
dialogs.forEach(dialog => {
|
||||||
|
this.close(dialog.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { CurrentUserDBEditorSettingProvider } from './impls/user-db';
|
|||||||
import { EditorSettingProvider } from './provider/editor-setting-provider';
|
import { EditorSettingProvider } from './provider/editor-setting-provider';
|
||||||
import { EditorSettingService } from './services/editor-setting';
|
import { EditorSettingService } from './services/editor-setting';
|
||||||
import { SpellCheckSettingService } from './services/spell-check-setting';
|
import { SpellCheckSettingService } from './services/spell-check-setting';
|
||||||
|
import { TraySettingService } from './services/tray-settings';
|
||||||
export type { FontFamily } from './schema';
|
export type { FontFamily } from './schema';
|
||||||
export { EditorSettingSchema, fontStyleOptions } from './schema';
|
export { EditorSettingSchema, fontStyleOptions } from './schema';
|
||||||
export { EditorSettingService } from './services/editor-setting';
|
export { EditorSettingService } from './services/editor-setting';
|
||||||
@@ -30,3 +31,7 @@ export function configureSpellCheckSettingModule(framework: Framework) {
|
|||||||
DesktopApiService,
|
DesktopApiService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function configureTraySettingModule(framework: Framework) {
|
||||||
|
framework.service(TraySettingService, [GlobalStateService]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type {
|
||||||
|
MenubarStateKey,
|
||||||
|
MenubarStateSchema,
|
||||||
|
} from '@affine/electron/main/shared-state-schema';
|
||||||
|
import { LiveData, Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { GlobalStateService } from '../../storage';
|
||||||
|
|
||||||
|
const MENUBAR_SETTING_KEY: typeof MenubarStateKey = 'menubarState';
|
||||||
|
|
||||||
|
export class TraySettingService extends Service {
|
||||||
|
constructor(private readonly globalStateService: GlobalStateService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
setting$ = LiveData.from(
|
||||||
|
this.globalStateService.globalState.watch<MenubarStateSchema>(
|
||||||
|
MENUBAR_SETTING_KEY
|
||||||
|
),
|
||||||
|
null
|
||||||
|
).map(v => v ?? { enabled: true });
|
||||||
|
|
||||||
|
setEnabled(enabled: boolean) {
|
||||||
|
this.globalStateService.globalState.set(MENUBAR_SETTING_KEY, {
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -952,9 +952,21 @@ export function useAFFiNEI18N(): {
|
|||||||
*/
|
*/
|
||||||
["com.affine.appearanceSettings.sidebar.title"](): string;
|
["com.affine.appearanceSettings.sidebar.title"](): string;
|
||||||
/**
|
/**
|
||||||
* `Customise your AFFiNE appearance`
|
* `Customize your AFFiNE appearance`
|
||||||
*/
|
*/
|
||||||
["com.affine.appearanceSettings.subtitle"](): string;
|
["com.affine.appearanceSettings.subtitle"](): string;
|
||||||
|
/**
|
||||||
|
* `Menubar`
|
||||||
|
*/
|
||||||
|
["com.affine.appearanceSettings.menubar.title"](): string;
|
||||||
|
/**
|
||||||
|
* `Enable menubar app`
|
||||||
|
*/
|
||||||
|
["com.affine.appearanceSettings.menubar.toggle"](): string;
|
||||||
|
/**
|
||||||
|
* `Display the menubar app in the tray for quick access to AFFiNE or meeting recordings.`
|
||||||
|
*/
|
||||||
|
["com.affine.appearanceSettings.menubar.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Theme`
|
* `Theme`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -228,7 +228,10 @@
|
|||||||
"com.affine.appearanceSettings.noisyBackground.description": "Use background noise effect on the sidebar.",
|
"com.affine.appearanceSettings.noisyBackground.description": "Use background noise effect on the sidebar.",
|
||||||
"com.affine.appearanceSettings.noisyBackground.title": "Noise background on the sidebar",
|
"com.affine.appearanceSettings.noisyBackground.title": "Noise background on the sidebar",
|
||||||
"com.affine.appearanceSettings.sidebar.title": "Sidebar",
|
"com.affine.appearanceSettings.sidebar.title": "Sidebar",
|
||||||
"com.affine.appearanceSettings.subtitle": "Customise your AFFiNE appearance",
|
"com.affine.appearanceSettings.subtitle": "Customize your AFFiNE appearance",
|
||||||
|
"com.affine.appearanceSettings.menubar.title": "Menubar",
|
||||||
|
"com.affine.appearanceSettings.menubar.toggle": "Enable menubar app",
|
||||||
|
"com.affine.appearanceSettings.menubar.description": "Display the menubar app in the tray for quick access to AFFiNE or meeting recordings.",
|
||||||
"com.affine.appearanceSettings.theme.title": "Theme",
|
"com.affine.appearanceSettings.theme.title": "Theme",
|
||||||
"com.affine.appearanceSettings.title": "Appearance settings",
|
"com.affine.appearanceSettings.title": "Appearance settings",
|
||||||
"com.affine.appearanceSettings.translucentUI.description": "Use transparency effect on the sidebar.",
|
"com.affine.appearanceSettings.translucentUI.description": "Use transparency effect on the sidebar.",
|
||||||
|
|||||||
Reference in New Issue
Block a user