perf: reduce initial bundle size via lazy-loading

- Lazy-load locale data: only en-GB statically imported, all 26 others
  use dynamic import() loaded during idle time
- Remove core-js/es/object polyfill (Object.fromEntries supported
  since Chrome 73+, Android 9 is EOL)
- Defer right-panel content with @defer (prefetch on idle) blocks,
  moving markdown, schedule, and task-detail to lazy chunks
- Lazy-load bottom panel via dynamic import()
- Lazy-load Formly: remove FormlyConfigModule from bootstrap, each
  dialog component imports it directly; dialog openers use dynamic
  import() so Formly is only loaded when a dialog opens
- Extract _openSyncInitialCfgDialog helper to DRY up duplicate import
- Guard bottom sheet and shortcut dialog opens against race conditions
  after async import
This commit is contained in:
Johannes Millan 2026-03-10 12:13:42 +01:00
parent 14ed231a1e
commit d989c064f3
21 changed files with 175 additions and 157 deletions

View file

@ -10,7 +10,6 @@ import { ShepherdService } from '../../features/shepherd/shepherd.service';
import { TourId } from '../../features/shepherd/shepherd-steps.const';
import { T } from '../../t.const';
import { LS } from '../../core/persistence/storage-keys.const';
import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component';
import { getGithubErrorUrl } from '../../core/error-handler/global-error-handler.util';
import { DialogPromptComponent } from '../../ui/dialog-prompt/dialog-prompt.component';
import {
@ -498,7 +497,9 @@ export class MagicNavConfigService {
}
// Simple action handlers
private _openCreateProject(): void {
private async _openCreateProject(): Promise<void> {
const { DialogCreateProjectComponent } =
await import('../../features/project/dialogs/create-project/dialog-create-project.component');
this._matDialog
.open(DialogCreateProjectComponent, { restoreFocus: true })
.afterClosed()

View file

@ -9,7 +9,6 @@ import { LayoutService } from '../layout/layout.service';
import { TaskService } from '../../features/tasks/task.service';
import { MatDialog } from '@angular/material/dialog';
import { DialogAddNoteComponent } from '../../features/note/dialog-add-note/dialog-add-note.component';
import { DialogCreateProjectComponent } from '../../features/project/dialogs/create-project/dialog-create-project.component';
import { IPC } from '../../../../electron/shared-with-frontend/ipc-events.const';
import { UiHelperService } from '../../features/ui-helper/ui-helper.service';
import { WorkContextService } from '../../features/work-context/work-context.service';
@ -156,15 +155,19 @@ export class ShortcutService {
ev.preventDefault();
} else if (checkKeyCombo(ev, keys.addNewProject)) {
if (this._matDialog.openDialogs.length === 0) {
this._matDialog
.open(DialogCreateProjectComponent, { restoreFocus: true })
.afterClosed()
.subscribe((newProjectId: string | undefined) => {
if (newProjectId) {
this._router.navigate([`project/${newProjectId}/tasks`]);
}
});
ev.preventDefault();
const { DialogCreateProjectComponent } =
await import('../../features/project/dialogs/create-project/dialog-create-project.component');
if (this._matDialog.openDialogs.length === 0) {
this._matDialog
.open(DialogCreateProjectComponent, { restoreFocus: true })
.afterClosed()
.subscribe((newProjectId: string | undefined) => {
if (newProjectId) {
this._router.navigate([`project/${newProjectId}/tasks`]);
}
});
}
}
} else if (checkKeyCombo(ev, keys.addNewNote)) {
if (this._matDialog.openDialogs.length === 0) {

View file

@ -28,10 +28,7 @@ import { Store } from '@ngrx/store';
import { TaskSharedActions } from '../../root-store/meta/task-shared.actions';
import { TaskWithSubTasks } from '../../features/tasks/task.model';
import { firstValueFrom } from 'rxjs';
import {
DialogWorkContextSettingsComponent,
WorkContextSettingsDialogData,
} from '../../features/work-context/dialog-work-context-settings/dialog-work-context-settings.component';
import type { WorkContextSettingsDialogData } from '../../features/work-context/dialog-work-context-settings/dialog-work-context-settings.component';
@Component({
selector: 'work-context-menu',
@ -238,6 +235,8 @@ export class WorkContextMenuComponent implements OnInit {
this._tagService.getTagById$(this.contextId).pipe(first()),
);
const { DialogWorkContextSettingsComponent } =
await import('../../features/work-context/dialog-work-context-settings/dialog-work-context-settings.component');
this._matDialog.open(DialogWorkContextSettingsComponent, {
restoreFocus: true,
backdropClass: 'cdk-overlay-transparent-backdrop',

View file

@ -1,29 +1,4 @@
import localeEnUS from '@angular/common/locales/en';
import localeEnGB from '@angular/common/locales/en-GB';
import localeEs from '@angular/common/locales/es';
import localeDe from '@angular/common/locales/de';
import localeAr from '@angular/common/locales/ar';
import localeCs from '@angular/common/locales/cs';
import localeFa from '@angular/common/locales/fa';
import localeFr from '@angular/common/locales/fr';
import localeJa from '@angular/common/locales/ja';
import localeKo from '@angular/common/locales/ko';
import localeRu from '@angular/common/locales/ru';
import localeSk from '@angular/common/locales/sk';
import localeTr from '@angular/common/locales/tr';
import localeZh from '@angular/common/locales/zh';
import localeIt from '@angular/common/locales/it';
import localePl from '@angular/common/locales/pl';
import localePt from '@angular/common/locales/pt';
import localeNl from '@angular/common/locales/nl';
import localeNb from '@angular/common/locales/nb';
import localeHr from '@angular/common/locales/hr';
import localeUk from '@angular/common/locales/uk';
import localeId from '@angular/common/locales/id';
import localeFi from '@angular/common/locales/fi';
import localeSv from '@angular/common/locales/sv';
import localeRo from '@angular/common/locales/ro';
import localeRoMD from '@angular/common/locales/ro-MD';
/**
* All of available app languages
@ -102,51 +77,62 @@ export const DateTimeLocales = {
export type DateTimeLocale = (typeof DateTimeLocales)[keyof typeof DateTimeLocales];
export const LocalesImports: Record<keyof typeof DateTimeLocales, unknown> = {
en: localeEnGB,
en_gb: localeEnGB,
en_us: localeEnUS,
tr_tr: localeTr,
de_de: localeDe,
de: localeDe,
fr_fr: localeFr,
es_es: localeEs,
es: localeEs,
it_it: localeIt,
pt_br: localePt,
ru_ru: localeRu,
ru: localeRu,
zh_cn: localeZh,
ja_jp: localeJa,
ja: localeJa,
ko_kr: localeKo,
ko: localeKo,
zh_tw: localeZh,
ar: localeAr,
cs_cz: localeCs,
cs: localeCs,
fa: localeFa,
fi: localeFi,
fr: localeFr,
id: localeId,
it: localeIt,
pl: localePl,
pt: localePt,
nl: localeNl,
nb: localeNb,
hr: localeHr,
uk_ua: localeUk,
uk: localeUk,
sk_sk: localeSk,
sk: localeSk,
sv: localeSv,
tr: localeTr,
zh: localeZh,
ro: localeRo,
ro_ro: localeRo,
ro_md: localeRoMD,
/**
* Maps locale keys to dynamic import functions for lazy loading.
* Only the default locale (en-GB) is statically imported; all others
* are loaded on demand to reduce the initial bundle size.
*/
export const LocaleImportFns: Record<
keyof typeof DateTimeLocales,
() => Promise<{ default: unknown }>
> = {
en: () => Promise.resolve({ default: localeEnGB }),
en_gb: () => Promise.resolve({ default: localeEnGB }),
en_us: () => import('@angular/common/locales/en'),
tr_tr: () => import('@angular/common/locales/tr'),
de_de: () => import('@angular/common/locales/de'),
de: () => import('@angular/common/locales/de'),
fr_fr: () => import('@angular/common/locales/fr'),
es_es: () => import('@angular/common/locales/es'),
es: () => import('@angular/common/locales/es'),
it_it: () => import('@angular/common/locales/it'),
pt_br: () => import('@angular/common/locales/pt'),
ru_ru: () => import('@angular/common/locales/ru'),
ru: () => import('@angular/common/locales/ru'),
zh_cn: () => import('@angular/common/locales/zh'),
ja_jp: () => import('@angular/common/locales/ja'),
ja: () => import('@angular/common/locales/ja'),
ko_kr: () => import('@angular/common/locales/ko'),
ko: () => import('@angular/common/locales/ko'),
zh_tw: () => import('@angular/common/locales/zh'),
ar: () => import('@angular/common/locales/ar'),
cs_cz: () => import('@angular/common/locales/cs'),
cs: () => import('@angular/common/locales/cs'),
fa: () => import('@angular/common/locales/fa'),
fi: () => import('@angular/common/locales/fi'),
fr: () => import('@angular/common/locales/fr'),
id: () => import('@angular/common/locales/id'),
it: () => import('@angular/common/locales/it'),
pl: () => import('@angular/common/locales/pl'),
pt: () => import('@angular/common/locales/pt'),
nl: () => import('@angular/common/locales/nl'),
nb: () => import('@angular/common/locales/nb'),
hr: () => import('@angular/common/locales/hr'),
uk_ua: () => import('@angular/common/locales/uk'),
uk: () => import('@angular/common/locales/uk'),
sk_sk: () => import('@angular/common/locales/sk'),
sk: () => import('@angular/common/locales/sk'),
sv: () => import('@angular/common/locales/sv'),
tr: () => import('@angular/common/locales/tr'),
zh: () => import('@angular/common/locales/zh'),
ro: () => import('@angular/common/locales/ro'),
ro_ro: () => import('@angular/common/locales/ro'),
ro_md: () => import('@angular/common/locales/ro-MD'),
};
/** Default locale data, statically imported for instant availability */
export const DEFAULT_LOCALE_DATA = localeEnGB;
export const DEFAULT_LANGUAGE = LanguageCode.en;
export const DEFAULT_LOCALE = DateTimeLocales.en_gb;
export const DEFAULT_FIRST_DAY_OF_WEEK = 1; // monday

View file

@ -8,7 +8,7 @@ import {
output,
signal,
} from '@angular/core';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { BoardCfg } from '../boards.model';
import { BOARDS_FORM } from '../boards-form.const';
import { UntypedFormGroup } from '@angular/forms';
@ -24,7 +24,7 @@ import { DEFAULT_BOARD_CFG, DEFAULT_PANEL_CFG } from '../boards.const';
@Component({
selector: 'board-edit',
standalone: true,
imports: [FormlyModule, MatButton, MatIcon, TranslatePipe],
imports: [FormlyConfigModule, MatButton, MatIcon, TranslatePipe],
templateUrl: './board-edit.component.html',
styleUrl: './board-edit.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,

View file

@ -5,7 +5,8 @@ import {
input,
output,
} from '@angular/core';
import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { GlobalConfigSectionKey } from '../global-config.model';
import { ProjectCfgFormKey } from '../../project/project.model';
@ -19,7 +20,7 @@ import { Log } from '../../../core/log';
templateUrl: './config-form.component.html',
styleUrls: ['./config-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [FormsModule, ReactiveFormsModule, FormlyModule],
imports: [FormsModule, ReactiveFormsModule, FormlyConfigModule],
})
export class ConfigFormComponent {
T: typeof T = T;

View file

@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatDialogRef, MatDialogTitle, MatDialogContent } from '@angular/material/dialog';
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { GlobalConfigService } from '../../config/global-config.service';
import { T } from '../../../t.const';
import { PomodoroConfig } from '../../config/global-config.model';
@ -52,7 +53,7 @@ const POMODORO_DURATION_FIELDS: FormlyFieldConfig[] = [
standalone: true,
imports: [
ReactiveFormsModule,
FormlyModule,
FormlyConfigModule,
MatDialogTitle,
MatDialogContent,
TranslatePipe,

View file

@ -43,7 +43,7 @@ import { JiraAdditionalCfgComponent } from '../providers/jira/jira-view-componen
import { HelpSectionComponent } from '../../../ui/help-section/help-section.component';
import { TranslatePipe } from '@ngx-translate/core';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { IS_ANDROID_WEB_VIEW } from '../../../util/is-android-web-view';
@ -72,7 +72,7 @@ import { ISSUE_PROVIDER_COMMON_FORM_FIELDS } from '../common-issue-form-stuff.co
HelpSectionComponent,
TranslatePipe,
MatSlideToggle,
FormlyModule,
FormlyConfigModule,
MatDialogActions,
MatButton,
MatIcon,

View file

@ -14,7 +14,8 @@ import {
} from '@angular/material/dialog';
import { Project, ProjectCopy } from '../../project.model';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../../ui/formly-config.module';
import { ProjectService } from '../../project.service';
import { DEFAULT_PROJECT } from '../../project.const';
import { JiraCfg } from '../../../issue/providers/jira/jira.model';
@ -48,7 +49,7 @@ import { Log } from '../../../../core/log';
FormsModule,
ReactiveFormsModule,
MatDialogContent,
FormlyModule,
FormlyConfigModule,
MatDialogActions,
MatButton,
TranslatePipe,

View file

@ -1,20 +1,30 @@
@if (panelContent() === 'ISSUE_PANEL') {
<issue-panel [@slideInFromRight]></issue-panel>
@defer (prefetch on idle) {
<issue-panel [@slideInFromRight]></issue-panel>
}
} @else if (panelContent() === 'NOTES') {
<notes [@slideInFromRight]></notes>
@defer (prefetch on idle) {
<notes [@slideInFromRight]></notes>
}
} @else if (panelContent() === 'PLUGIN') {
@for (key of pluginPanelKeys(); track key) {
<plugin-panel-container [@slideInFromRight]></plugin-panel-container>
@defer (prefetch on idle) {
<plugin-panel-container [@slideInFromRight]></plugin-panel-container>
}
}
<!-- Task content comes last so we can avoid an extra effect to unset selected task -->
} @else if (panelContent() === 'SCHEDULE_DAY_PANEL') {
<schedule-day-panel [@slideInFromRight]></schedule-day-panel>
@defer (prefetch on idle) {
<schedule-day-panel [@slideInFromRight]></schedule-day-panel>
}
} @else {
@if (selectedTaskWithDelayForNone(); as task) {
<task-detail-panel
[@taskDetailPanelTaskChange]="task.id"
[@.disabled]="isDisableTaskPanelAni()"
[task]="task"
></task-detail-panel>
@defer (prefetch on idle) {
<task-detail-panel
[@taskDetailPanelTaskChange]="task.id"
[@.disabled]="isDisableTaskPanelAni()"
[task]="task"
></task-detail-panel>
}
}
}

View file

@ -37,7 +37,6 @@ import {
import { isInputElement } from '../../util/dom-element';
import { BottomPanelStateService } from '../../core-ui/bottom-panel-state.service';
import { slideRightPanelAni } from './slide-right-panel-out.ani';
import { BottomPanelContainerComponent } from '../bottom-panel/bottom-panel-container.component';
import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet';
import { PanelContentService } from '../panels/panel-content.service';
// Right panel resize constants
@ -155,7 +154,7 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy {
private readonly _boundOnPointerUp = this._handlePointerUp.bind(this);
private readonly _boundOnWindowResize = this._throttledWindowResize.bind(this);
private _bottomSheet = inject(MatBottomSheet);
private _bottomSheetRef: MatBottomSheetRef<BottomPanelContainerComponent> | null = null;
private _bottomSheetRef: MatBottomSheetRef | null = null;
private _bottomSheetSubscription: Subscription | null = null;
// Track listener state to prevent double attachment/removal
@ -218,21 +217,29 @@ export class RightPanelComponent implements AfterViewInit, OnDestroy {
if (hasContent && !this._bottomSheetRef) {
// Open bottom sheet
this._bottomSheetRef = this._bottomSheet.open(BottomPanelContainerComponent, {
hasBackdrop: true,
closeOnNavigation: false,
panelClass: 'bottom-panel-sheet',
// Let CSS handle positioning and height
});
import('../bottom-panel/bottom-panel-container.component').then((m) => {
// Re-check: conditions may have changed while chunk was loading
if (this._bottomSheetRef) {
return;
}
this._bottomSheetRef = this._bottomSheet.open(
m.BottomPanelContainerComponent,
{
hasBackdrop: true,
closeOnNavigation: false,
panelClass: 'bottom-panel-sheet',
},
);
// Handle bottom sheet dismissal
this._bottomSheetSubscription = this._bottomSheetRef
.afterDismissed()
.subscribe(() => {
this._bottomSheetRef = null;
this._bottomSheetSubscription = null;
this.close();
});
// Handle bottom sheet dismissal
this._bottomSheetSubscription = this._bottomSheetRef
.afterDismissed()
.subscribe(() => {
this._bottomSheetRef = null;
this._bottomSheetSubscription = null;
this.close();
});
});
} else if (!hasContent && this._bottomSheetRef) {
// Close bottom sheet
this._bottomSheetRef.dismiss();

View file

@ -14,7 +14,8 @@ import {
SimpleCounterType,
} from '../simple-counter.model';
import { T } from '../../../t.const';
import { FormlyFieldConfig, FormlyFormOptions, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { adjustToDialogFormlyForm } from '../../../util/adjust-to-dialog-formly-form';
import { SIMPLE_COUNTER_FORM } from '../../config/form-cfgs/simple-counter-form.const';
@ -38,7 +39,7 @@ import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confir
TranslatePipe,
ReactiveFormsModule,
FormsModule,
FormlyModule,
FormlyConfigModule,
],
})
export class DialogSimpleCounterEditSettingsComponent {

View file

@ -21,7 +21,8 @@ import {
TaskRepeatCfg,
TaskRepeatCfgCopy,
} from '../task-repeat-cfg.model';
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { UntypedFormGroup } from '@angular/forms';
import {
TASK_REPEAT_CFG_ADVANCED_FORM_CFG,
@ -63,7 +64,7 @@ import { CollapsibleComponent } from '../../../ui/collapsible/collapsible.compon
TranslatePipe,
MatDialogContent,
HelpSectionComponent,
FormlyModule,
FormlyConfigModule,
ChipListInputComponent,
MatDialogActions,
MatButton,

View file

@ -70,7 +70,6 @@ import { CHRONO_SUGGESTIONS } from './add-task-bar.const';
import { TaskRepeatCfgService } from '../../task-repeat-cfg/task-repeat-cfg.service';
import { DEFAULT_TASK_REPEAT_CFG } from '../../task-repeat-cfg/task-repeat-cfg.model';
import { getQuickSettingUpdates } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/get-quick-setting-updates';
import { DialogEditTaskRepeatCfgComponent } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ShortSyntaxTag, shortSyntaxToTags } from './short-syntax-to-tags';
import { DEFAULT_PROJECT_COLOR } from '../../work-context/work-context.const';
@ -791,7 +790,9 @@ export class AddTaskBarComponent implements AfterViewInit, OnInit, OnDestroy {
.getByIdOnce$(taskId)
.pipe(timeout(1000), takeUntilDestroyed(this._destroyRef))
.subscribe({
next: (task) => {
next: async (task) => {
const { DialogEditTaskRepeatCfgComponent } =
await import('../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component');
this._matDialog.open(DialogEditTaskRepeatCfgComponent, {
data: { task, defaultRemindOption: remindOption },
});

View file

@ -35,7 +35,6 @@ import { PanDirective, PanEvent } from '../../../ui/swipe-gesture/pan.directive'
import { TaskAttachmentService } from '../task-attachment/task-attachment.service';
import { DialogEditTaskAttachmentComponent } from '../task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component';
import { swirlAnimation } from '../../../ui/animations/swirl-in-out.ani';
import { DialogEditTaskRepeatCfgComponent } from '../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component';
import { ProjectService } from '../../project/project.service';
import { Project } from '../../project/project.model';
import { _MISSING_PROJECT_ } from '../../project/project.const';
@ -389,7 +388,9 @@ export class TaskComponent implements OnDestroy, AfterViewInit {
});
}
editTaskRepeatCfg(): void {
async editTaskRepeatCfg(): Promise<void> {
const { DialogEditTaskRepeatCfgComponent } =
await import('../../task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component');
this._matDialog
.open(DialogEditTaskRepeatCfgComponent, {
data: {

View file

@ -7,7 +7,8 @@ import {
MatDialogTitle,
} from '@angular/material/dialog';
import { ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { T } from '../../../t.const';
import { Project } from '../../project/project.model';
import { Tag } from '../../tag/tag.model';
@ -34,7 +35,7 @@ export interface WorkContextSettingsDialogData {
MatDialogTitle,
ReactiveFormsModule,
MatDialogContent,
FormlyModule,
FormlyConfigModule,
MatDialogActions,
MatButton,
TranslatePipe,

View file

@ -18,7 +18,6 @@ import { T } from '../../../t.const';
import { SYNC_FORM } from '../../../features/config/form-cfgs/sync-form.const';
import { FormGroup } from '@angular/forms';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { SyncConfig } from '../../../features/config/global-config.model';
import { SyncProviderId } from '../../../op-log/sync-providers/provider.const';
@ -46,7 +45,6 @@ import { isOnline } from '../../../util/is-online';
MatIcon,
TranslatePipe,
FormlyConfigModule,
FormlyModule,
],
})
export class DialogSyncInitialCfgComponent implements AfterViewInit {

View file

@ -51,7 +51,6 @@ import { DialogConflictResolutionResult } from './sync.model';
import { DialogSyncConflictComponent } from './dialog-sync-conflict/dialog-sync-conflict.component';
import { ReminderService } from '../../features/reminder/reminder.service';
import { DialogSyncInitialCfgComponent } from './dialog-sync-initial-cfg/dialog-sync-initial-cfg.component';
import { DialogHandleDecryptErrorComponent } from './dialog-handle-decrypt-error/dialog-handle-decrypt-error.component';
import { DialogEnterEncryptionPasswordComponent } from './dialog-enter-encryption-password/dialog-enter-encryption-password.component';
import {
@ -449,14 +448,14 @@ export class SyncWrapperService {
msg: T.F.SYNC.S.AUTH_TOKEN_REJECTED,
translateParams: { reason: error.message },
type: 'ERROR',
actionFn: async () => this._matDialog.open(DialogSyncInitialCfgComponent),
actionFn: () => this._openSyncInitialCfgDialog(),
actionStr: T.F.SYNC.S.BTN_CONFIGURE,
});
} else {
this._snackService.open({
msg: T.F.SYNC.S.INCOMPLETE_CFG,
type: 'ERROR',
actionFn: async () => this._matDialog.open(DialogSyncInitialCfgComponent),
actionFn: () => this._openSyncInitialCfgDialog(),
actionStr: T.F.SYNC.S.BTN_CONFIGURE,
});
}
@ -693,6 +692,12 @@ export class SyncWrapperService {
* Handle incoherent timestamps dialog with proper async error handling.
* Uses fire-and-forget pattern but logs errors instead of swallowing them.
*/
private async _openSyncInitialCfgDialog(): Promise<void> {
const { DialogSyncInitialCfgComponent } =
await import('./dialog-sync-initial-cfg/dialog-sync-initial-cfg.component');
this._matDialog.open(DialogSyncInitialCfgComponent);
}
private _handleIncoherentTimestampsDialog(): void {
this._openSyncErrorDialog({ type: 'incoherent-timestamps' });
}

View file

@ -9,7 +9,8 @@ import {
import { MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { FormlyConfigModule } from '../../../ui/formly-config.module';
import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
import { JSONSchema7 } from 'json-schema';
import { PluginManifest } from '../../plugin-api.model';
@ -123,7 +124,7 @@ interface PluginConfigData {
MatProgressSpinner,
MatError,
ReactiveFormsModule,
FormlyModule,
FormlyConfigModule,
],
})
export class PluginConfigDialogComponent {

View file

@ -11,7 +11,11 @@ import { registerLocaleData } from '@angular/common';
import { environment } from './environments/environment';
import { IS_ELECTRON } from './app/app.constants';
import { DEFAULT_LANGUAGE, LocalesImports } from './app/core/locale.constants';
import {
DEFAULT_LANGUAGE,
DEFAULT_LOCALE_DATA,
LocaleImportFns,
} from './app/core/locale.constants';
import { IS_ANDROID_WEB_VIEW } from './app/util/is-android-web-view';
import { androidInterface } from './app/features/android/android-interface';
import { IS_IOS_NATIVE, IS_NATIVE_PLATFORM } from './app/util/is-native-platform';
@ -30,7 +34,6 @@ import {
MatDateFormats,
DateAdapter,
} from '@angular/material/core';
import { FormlyConfigModule } from './app/ui/formly-config.module';
import { markedOptionsFactory } from './app/ui/marked-options-factory';
import { MaterialCssVarsModule } from 'angular-material-css-vars';
import { MatSidenavModule } from '@angular/material/sidenav';
@ -96,7 +99,6 @@ bootstrapApplication(AppComponent, {
importProvidersFrom(
FeatureStoresModule,
MatNativeDateModule,
FormlyConfigModule,
MarkdownModule.forRoot({
markedOptions: {
provide: MARKED_OPTIONS,
@ -251,27 +253,24 @@ bootstrapApplication(AppComponent, {
// Initialize touch fix for Material menus
initializeMatMenuTouchFix();
// Register default locale immediately for fast startup
registerLocaleData(LocalesImports[DEFAULT_LANGUAGE], DEFAULT_LANGUAGE);
// Register default locale immediately (statically imported, no network fetch)
registerLocaleData(DEFAULT_LOCALE_DATA, DEFAULT_LANGUAGE);
// Defer other locales to idle time for better initial load performance
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => {
Object.keys(LocalesImports).forEach((locale) => {
if (locale !== DEFAULT_LANGUAGE) {
registerLocaleData(LocalesImports[locale], locale);
}
});
// Lazily load and register remaining locales during idle time
const registerRemainingLocales = (): void => {
Object.keys(LocaleImportFns).forEach((locale) => {
if (locale !== DEFAULT_LANGUAGE) {
LocaleImportFns[locale as keyof typeof LocaleImportFns]().then((m) => {
registerLocaleData(m.default, locale);
});
}
});
};
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => registerRemainingLocales());
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => {
Object.keys(LocalesImports).forEach((locale) => {
if (locale !== DEFAULT_LANGUAGE) {
registerLocaleData(LocalesImports[locale], locale);
}
});
}, 0);
setTimeout(() => registerRemainingLocales(), 0);
}
// TODO make asset caching work for electron

View file

@ -20,7 +20,8 @@
/* IE9, IE10 and IE11 requires all of the following polyfills. **/
//import 'core-js/es/symbol';
import 'core-js/es/object'; // Support for Chrome mobile <= 69, which is the default browser for Android <= 9, especially the following error: TypeError: Object.fromEntries is not a function
// core-js/es/object removed: Object.fromEntries is supported since Chrome 73+ (2019).
// Android 9 (Chrome 69) is EOL and no longer targeted.
/*import 'core-js/es/function';
import 'core-js/es/parse-int';
import 'core-js/es/parse-float';