Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Show notification on import/export using interop service #10520

Closed
Closed
13 changes: 8 additions & 5 deletions packages/app-desktop/InteropServiceHelper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import InteropService from '@joplin/lib/services/interop/InteropService';
import CommandService from '@joplin/lib/services/CommandService';
import shim from '@joplin/lib/shim';
import { ExportModuleOutputFormat, ExportOptions, FileSystemItem } from '@joplin/lib/services/interop/types';
import { ExportModule } from '@joplin/lib/services/interop/Module';
import TaskUIService from '@joplin/lib/services/TaskUIService';

import { _ } from '@joplin/lib/locale';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
Expand Down Expand Up @@ -178,7 +178,7 @@ export default class InteropServiceHelper {
public static async export(_dispatch: Function, module: ExportModule, options: ExportNoteOptions = null) {
if (!options) options = {};

let path = null;
let path: string | string[] = null;

if (module.target === 'file') {
const noteId = options.sourceNoteIds && options.sourceNoteIds.length ? options.sourceNoteIds[0] : null;
Expand All @@ -196,7 +196,8 @@ export default class InteropServiceHelper {

if (Array.isArray(path)) path = path[0];

void CommandService.instance().execute('showModalMessage', _('Exporting to "%s" as "%s" format. Please wait...', path, module.format));
const taskId = `interop-export-${path}`;
TaskUIService.onTaskStarted(taskId, _('Exporting to "%s" as "%s" format. Please wait...', path, module.format));

const exportOptions: ExportOptions = {};
exportOptions.path = path;
Expand All @@ -215,12 +216,14 @@ export default class InteropServiceHelper {
const result = await service.export(exportOptions);
// eslint-disable-next-line no-console
console.info('Export result: ', result);

TaskUIService.onTaskCompleted(taskId, _('Successfully exported from %s.', path));
} catch (error) {
console.error(error);
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
}

void CommandService.instance().execute('hideModalMessage');
TaskUIService.onTaskCompleted(taskId, _('Could not export notes.'));
}
}

}
6 changes: 6 additions & 0 deletions packages/app-desktop/gui/MainScreen/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import NotePropertiesDialog from '../NotePropertiesDialog';
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
import validateColumns from '../NoteListHeader/utils/validateColumns';
import TrashNotification from '../TrashNotification/TrashNotification';
import TaskNotification from '../TaskNotification/TaskNotification';

const PluginManager = require('@joplin/lib/services/PluginManager');
const ipcRenderer = require('electron').ipcRenderer;
Expand Down Expand Up @@ -913,13 +914,18 @@ class MainScreenComponent extends React.Component<Props, State> {

<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} themeId={this.props.themeId} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />

<TaskNotification
themeId={this.props.themeId}
/>

<TrashNotification
lastDeletion={this.props.lastDeletion}
lastDeletionNotificationTime={this.props.lastDeletionNotificationTime}
themeId={this.props.themeId}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
dispatch={this.props.dispatch as any}
/>

{messageComp}
{layoutComp}
{pluginDialog}
Expand Down
25 changes: 12 additions & 13 deletions packages/app-desktop/gui/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { EventName } from '@joplin/lib/eventManager';
import TaskUIService from '@joplin/lib/services/TaskUIService';
const packageInfo: PackageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
Expand Down Expand Up @@ -292,7 +293,7 @@ function useMenu(props: Props) {
}, []);

const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: string) => {
let path = null;
let path: string | string[] = null;

if (moduleSource === 'file') {
path = await bridge().showOpenDialog({
Expand All @@ -308,24 +309,19 @@ function useMenu(props: Props) {

if (Array.isArray(path)) path = path[0];

const modalMessage = _('Importing from "%s" as "%s" format. Please wait...', path, module.format);

void CommandService.instance().execute('showModalMessage', modalMessage);

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const errors: any[] = [];

const taskId = `interop-import-${path}`;
TaskUIService.onTaskStarted(taskId, _('Importing from "%s" as "%s" format. Please wait...', path, module.outputFormat));

const importOptions = {
path,
format: module.format,
outputFormat: module.outputFormat,
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onProgress: (status: any) => {
const statusStrings: string[] = Object.keys(status).map((key: string) => {
return `${key}: ${status[key]}`;
});

void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
onProgress: (_status: any) => {
// Nothing to be done.
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onError: (error: any) => {
Expand All @@ -337,14 +333,17 @@ function useMenu(props: Props) {

const service = InteropService.instance();
try {

const result = await service.import(importOptions);
// eslint-disable-next-line no-console
console.info('Import result: ', result);

TaskUIService.onTaskCompleted(taskId, _('Successfully imported from %s.', path));
} catch (error) {
bridge().showErrorMessageBox(error.message);
}

void CommandService.instance().execute('hideModalMessage');
TaskUIService.onTaskCompleted(taskId, _('Could not import notes.'));
}

if (errors.length) {
const response = bridge().showErrorMessageBox('There was some errors importing the notes - check the console for more details.\n\nPlease consider sending a bug report to the forum!', {
Expand Down
7 changes: 7 additions & 0 deletions packages/app-desktop/gui/NotyfContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ export default React.createContext(
new Notyf({
// Set your global Notyf configuration here
duration: 6000,
types: [
{
type: 'loading',
dismissible: true,
duration: 0,
},
],
}),
);
82 changes: 82 additions & 0 deletions packages/app-desktop/gui/TaskNotification/TaskNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useContext, useMemo, useEffect } from 'react';
import { INotyfIcon, NotyfNotification } from 'notyf';
import { themeStyle } from '@joplin/lib/theme';
import { waitForElement } from '@joplin/lib/dom';
import TaskUIService from '@joplin/lib/services/TaskUIService';
import NotyfContext from '../NotyfContext';
import { htmlentities } from '@joplin/utils/html';

interface Props {
themeId: number;
}

export default (props: Props) => {
const notyfContext = useContext(NotyfContext);

const theme = useMemo(() => {
return themeStyle(props.themeId);
}, [props.themeId]);

const notyf = useMemo(() => {
const output = notyfContext;
output.options.types = notyfContext.options.types.map(type => {
if (type.type === 'success' || type.type === 'error') {
// When type is 'success' or 'error', type.icon is always of type object (INotyfIcon):
// https://github.com/caroso1222/notyf/blob/master/src/notyf.options.ts
(type.icon as INotyfIcon).color = theme.backgroundColor5;
}

type.background = theme.backgroundColor5;
return type;
});
return output;
}, [notyfContext, theme]);

useEffect(() => {
TaskUIService.setListener(async task => {
if (task.progress === 100) {
if (task.data) {
notyf.dismiss(task.data as NotyfNotification);
}

notyf.success({
message: task.message,
dismissible: true,
});
} else if (task.data) {
const spinner: HTMLElement = await waitForElement(document, task.id);

// Remove the animation.
spinner.classList.remove('-indeterminate');
// Update the progress percentage.
spinner.style.setProperty('--percentage', `${task.progress}%`);
} else {
const options = notyf.options.types.find(type => type.type === 'loading');

let cssStyle = `color: ${options.background};`;
let className = 'loading-spinner';

if (task.progress > 0) {
// Display the current progress.
cssStyle += ` --percentage: ${task.progress}%;`;
} else {
// Show an infinite animation until there is some progress.
className += ' -indeterminate';
}

task.data = notyf.open({
type: 'loading',
message: task.message,
duration: 0,
dismissible: true,
icon: `<i class="${className}" id="${htmlentities(task.id)}" style="${htmlentities(cssStyle)}"></i>`,
});
}
});


return () => TaskUIService.setListener(undefined);
}, [notyf]);

return <div style={{ display: 'none' }}/>;
};
51 changes: 51 additions & 0 deletions packages/app-desktop/gui/TaskNotification/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
body .notyf {
color: var(--joplin-color5);
}

.notyf__toast {

> .notyf__wrapper {

> .notyf__message {
display: flex;
align-items: center;
justify-content: space-between;

> .cancel {
color: var(--joplin-color5);
text-decoration: underline;
display: flex;
align-items: center;
}

> .cancel::after {
content: '✖';
margin-left: 8px; /* Adjust the space between text and cross */
}
}

> .notyf__icon {
width: 24px;
height: 24px;

> .notyf__icon--success {
background-color: var(--joplin-color5);
/* Add an icon for success */
//background-image: url('path-to-success-icon.svg');
//background-size: cover;
}

> .notyf__icon--error {
background-color: var(--joplin-color5);
/* Add an icon for error */
//background-image: url('path-to-error-icon.svg');
//background-size: cover;
}

> .notyf__icon--loading {
background-color: var(--joplin-color5);
}
}

}
}
29 changes: 29 additions & 0 deletions packages/app-desktop/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,35 @@ a {
to {transform: rotate(360deg);}
}

.loading-spinner {
--percentage: 30%;
display: flex;
width: 20px;
height: 20px;
border-radius: 50%;
align-items: center;
justify-content: center;
background: conic-gradient(
var(--joplin-background-color, white) var(--percentage),
var(--joplin-color, black) var(--percentage)
);
}

.loading-spinner::before {
content: '';
width: 70%;
height: 70%;
border-radius: 50%;
background-color: currentColor;
}

.loading-spinner.-indeterminate {
animation: 4s linear infinite rotate;
}

@media (prefers-reduced-motion) {
.loading-spinner.-indeterminate { animation: none; }
}

*:focus-visible {
outline: 1px solid var(--joplin-color-warn);
Expand Down
Loading
Loading