feat: implement preferences pane
This commit is contained in:
@@ -65,6 +65,7 @@ import { NotesContextMenuDirective } from './components/NotesContextMenu';
|
||||
import { NotesOptionsPanelDirective } from './components/NotesOptionsPanel';
|
||||
import { IconDirective } from './components/Icon';
|
||||
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
|
||||
import { PreferencesDirective } from './components/preferences';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
/**
|
||||
@@ -90,7 +91,7 @@ function reloadHiddenFirefoxTab(): boolean {
|
||||
const startApplication: StartApplication = async function startApplication(
|
||||
defaultSyncServerHost: string,
|
||||
bridge: Bridge,
|
||||
nextVersionSyncServerHost: string,
|
||||
nextVersionSyncServerHost: string
|
||||
) {
|
||||
if (reloadHiddenFirefoxTab()) {
|
||||
return;
|
||||
@@ -161,7 +162,8 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('notesContextMenu', NotesContextMenuDirective)
|
||||
.directive('notesOptionsPanel', NotesOptionsPanelDirective)
|
||||
.directive('icon', IconDirective)
|
||||
.directive('noteTagsContainer', NoteTagsContainerDirective);
|
||||
.directive('noteTagsContainer', NoteTagsContainerDirective)
|
||||
.directive('preferences', PreferencesDirective);
|
||||
|
||||
// Filters
|
||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||
@@ -174,10 +176,12 @@ const startApplication: StartApplication = async function startApplication(
|
||||
Object.defineProperties(window, {
|
||||
application: {
|
||||
get: () =>
|
||||
(angular
|
||||
.element(document)
|
||||
.injector()
|
||||
.get('mainApplicationGroup') as any).primaryApplication,
|
||||
(
|
||||
angular
|
||||
.element(document)
|
||||
.injector()
|
||||
.get('mainApplicationGroup') as any
|
||||
).primaryApplication,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,40 +13,59 @@ import PasswordIcon from '../../icons/ic-textbox-password.svg';
|
||||
import TrashSweepIcon from '../../icons/ic-trash-sweep.svg';
|
||||
import MoreIcon from '../../icons/ic-more.svg';
|
||||
import TuneIcon from '../../icons/ic-tune.svg';
|
||||
|
||||
import AccessibilityIcon from '../../icons/ic-accessibility.svg';
|
||||
import HelpIcon from '../../icons/ic-help.svg';
|
||||
import KeyboardIcon from '../../icons/ic-keyboard.svg';
|
||||
import ListedIcon from '../../icons/ic-listed.svg';
|
||||
import SecurityIcon from '../../icons/ic-security.svg';
|
||||
import SettingsFilledIcon from '../../icons/ic-settings-filled.svg';
|
||||
import StarIcon from '../../icons/ic-star.svg';
|
||||
import ThemesIcon from '../../icons/ic-themes.svg';
|
||||
import UserIcon from '../../icons/ic-user.svg';
|
||||
|
||||
import { toDirective } from './utils';
|
||||
|
||||
const ICONS = {
|
||||
'pencil-off': PencilOffIcon,
|
||||
'rich-text': RichTextIcon,
|
||||
'trash': TrashIcon,
|
||||
'pin': PinIcon,
|
||||
'unpin': UnpinIcon,
|
||||
'archive': ArchiveIcon,
|
||||
'unarchive': UnarchiveIcon,
|
||||
'hashtag': HashtagIcon,
|
||||
trash: TrashIcon,
|
||||
pin: PinIcon,
|
||||
unpin: UnpinIcon,
|
||||
archive: ArchiveIcon,
|
||||
unarchive: UnarchiveIcon,
|
||||
hashtag: HashtagIcon,
|
||||
'chevron-right': ChevronRightIcon,
|
||||
'restore': RestoreIcon,
|
||||
'close': CloseIcon,
|
||||
'password': PasswordIcon,
|
||||
restore: RestoreIcon,
|
||||
close: CloseIcon,
|
||||
password: PasswordIcon,
|
||||
'trash-sweep': TrashSweepIcon,
|
||||
'more': MoreIcon,
|
||||
'tune': TuneIcon,
|
||||
more: MoreIcon,
|
||||
tune: TuneIcon,
|
||||
accessibility: AccessibilityIcon,
|
||||
help: HelpIcon,
|
||||
keyboard: KeyboardIcon,
|
||||
listed: ListedIcon,
|
||||
security: SecurityIcon,
|
||||
'settings-filled': SettingsFilledIcon,
|
||||
star: StarIcon,
|
||||
themes: ThemesIcon,
|
||||
user: UserIcon,
|
||||
};
|
||||
|
||||
export type IconType = keyof typeof ICONS;
|
||||
|
||||
type Props = {
|
||||
type: keyof (typeof ICONS);
|
||||
className: string;
|
||||
}
|
||||
type: IconType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Icon: React.FC<Props> = ({ type, className }) => {
|
||||
const IconComponent = ICONS[type];
|
||||
return <IconComponent className={`sn-icon ${className}`} />;
|
||||
};
|
||||
|
||||
export const IconDirective = toDirective<Props>(
|
||||
Icon,
|
||||
{
|
||||
type: '@',
|
||||
className: '@',
|
||||
}
|
||||
);
|
||||
export const IconDirective = toDirective<Props>(Icon, {
|
||||
type: '@',
|
||||
className: '@',
|
||||
});
|
||||
|
||||
53
app/assets/javascripts/components/IconButton.tsx
Normal file
53
app/assets/javascripts/components/IconButton.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { Icon, IconType } from './Icon';
|
||||
|
||||
const ICON_BUTTON_TYPES: {
|
||||
[type: string]: { className: string };
|
||||
} = {
|
||||
normal: {
|
||||
className: '',
|
||||
},
|
||||
primary: {
|
||||
className: 'info',
|
||||
},
|
||||
};
|
||||
|
||||
export type IconButtonType = keyof typeof ICON_BUTTON_TYPES;
|
||||
|
||||
interface IconButtonProps {
|
||||
/**
|
||||
* onClick - preventDefault is handled within the component
|
||||
*/
|
||||
onClick: () => void;
|
||||
|
||||
type: IconButtonType;
|
||||
|
||||
className?: string;
|
||||
|
||||
iconType: IconType;
|
||||
}
|
||||
|
||||
/**
|
||||
* CircleButton component with an icon for SPA
|
||||
* preventDefault is already handled within the component
|
||||
*/
|
||||
export const IconButton: FunctionComponent<IconButtonProps> = ({
|
||||
onClick,
|
||||
type,
|
||||
className,
|
||||
iconType,
|
||||
}) => {
|
||||
const click = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
};
|
||||
const typeProps = ICON_BUTTON_TYPES[type];
|
||||
return (
|
||||
<button
|
||||
className={`sn-icon-button ${typeProps.className} ${className ?? ''}`}
|
||||
onClick={click}
|
||||
>
|
||||
<Icon type={iconType} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
23
app/assets/javascripts/components/PreferencesMenuItem.tsx
Normal file
23
app/assets/javascripts/components/PreferencesMenuItem.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Icon, IconType } from '@/components/Icon';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
interface PreferencesMenuItemProps {
|
||||
iconType: IconType;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const PreferencesMenuItem: FunctionComponent<PreferencesMenuItemProps> =
|
||||
({ iconType, label, selected, onClick }) => (
|
||||
<div
|
||||
className={`preferences-menu-item ${selected ? 'selected' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<Icon className="icon" type={iconType} />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
13
app/assets/javascripts/components/TitleBar.tsx
Normal file
13
app/assets/javascripts/components/TitleBar.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
export const TitleBar: FunctionComponent<{ className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
}) => <div className={`sn-titlebar ${className ?? ''}`}>{children}</div>;
|
||||
|
||||
export const Title: FunctionComponent<{ className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return <div className={`sn-title ${className ?? ''}`}>{children}</div>;
|
||||
};
|
||||
22
app/assets/javascripts/components/preferences/index.tsx
Normal file
22
app/assets/javascripts/components/preferences/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
|
||||
import { toDirective } from '../utils';
|
||||
import { PreferencesView } from './view';
|
||||
|
||||
interface WrapperProps {
|
||||
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
|
||||
}
|
||||
|
||||
const PreferencesViewWrapper: FunctionComponent<WrapperProps> = observer(
|
||||
({ appState }) => {
|
||||
if (!appState.preferences.isOpen) return null;
|
||||
return (
|
||||
<PreferencesView close={() => appState.preferences.closePreferences()} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const PreferencesDirective = toDirective<WrapperProps>(
|
||||
PreferencesViewWrapper
|
||||
);
|
||||
50
app/assets/javascripts/components/preferences/mock-state.ts
Normal file
50
app/assets/javascripts/components/preferences/mock-state.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { IconType } from '@/components/Icon';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
|
||||
interface PreferenceItem {
|
||||
icon: IconType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PreferenceListItem extends PreferenceItem {
|
||||
id: number;
|
||||
}
|
||||
|
||||
const predefinedItems: PreferenceItem[] = [
|
||||
{ label: 'General', icon: 'settings-filled' },
|
||||
{ label: 'Account', icon: 'user' },
|
||||
{ label: 'Appearance', icon: 'themes' },
|
||||
{ label: 'Security', icon: 'security' },
|
||||
{ label: 'Listed', icon: 'listed' },
|
||||
{ label: 'Shortcuts', icon: 'keyboard' },
|
||||
{ label: 'Accessibility', icon: 'accessibility' },
|
||||
{ label: 'Get a free month', icon: 'star' },
|
||||
{ label: 'Help & feedback', icon: 'help' },
|
||||
];
|
||||
|
||||
export class MockState {
|
||||
private readonly _items: PreferenceListItem[];
|
||||
private _selectedId = 0;
|
||||
|
||||
constructor(items: PreferenceItem[] = predefinedItems) {
|
||||
makeObservable<MockState, '_selectedId'>(this, {
|
||||
_selectedId: observable,
|
||||
items: computed,
|
||||
select: action,
|
||||
});
|
||||
|
||||
this._items = items.map((p, idx) => ({ ...p, id: idx }));
|
||||
this._selectedId = this._items[0].id;
|
||||
}
|
||||
|
||||
select(id: number) {
|
||||
this._selectedId = id;
|
||||
}
|
||||
|
||||
get items(): (PreferenceListItem & { selected: boolean })[] {
|
||||
return this._items.map((p) => ({
|
||||
...p,
|
||||
selected: p.id === this._selectedId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
33
app/assets/javascripts/components/preferences/pane.tsx
Normal file
33
app/assets/javascripts/components/preferences/pane.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesMenuItem } from '../PreferencesMenuItem';
|
||||
import { MockState } from './mock-state';
|
||||
|
||||
interface PreferencesMenuProps {
|
||||
store: MockState;
|
||||
}
|
||||
|
||||
const PreferencesMenu: FunctionComponent<PreferencesMenuProps> = observer(
|
||||
({ store }) => (
|
||||
<div className="h-full w-auto flex flex-col px-3 py-6">
|
||||
{store.items.map((pref) => (
|
||||
<PreferencesMenuItem
|
||||
key={pref.id}
|
||||
iconType={pref.icon}
|
||||
label={pref.label}
|
||||
selected={pref.selected}
|
||||
onClick={() => store.select(pref.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export const PreferencesPane: FunctionComponent = () => {
|
||||
const store = new MockState();
|
||||
return (
|
||||
<div className="h-full w-full flex flex-row">
|
||||
<PreferencesMenu store={store}></PreferencesMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
28
app/assets/javascripts/components/preferences/view.tsx
Normal file
28
app/assets/javascripts/components/preferences/view.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IconButton } from '@/components/IconButton';
|
||||
import { TitleBar, Title } from '@/components/TitleBar';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PreferencesPane } from './pane';
|
||||
|
||||
interface PreferencesViewProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const PreferencesView: FunctionComponent<PreferencesViewProps> = ({
|
||||
close,
|
||||
}) => (
|
||||
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
||||
<TitleBar className="items-center justify-between">
|
||||
{/* div is added so flex justify-between can center the title */}
|
||||
<div className="h-8 w-8" />
|
||||
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
type="normal"
|
||||
iconType="close"
|
||||
/>
|
||||
</TitleBar>
|
||||
<PreferencesPane />
|
||||
</div>
|
||||
);
|
||||
@@ -22,6 +22,7 @@ import { SyncState } from './sync_state';
|
||||
import { SearchOptionsState } from './search_options_state';
|
||||
import { NotesState } from './notes_state';
|
||||
import { TagsState } from './tags_state';
|
||||
import { PreferencesState } from './preferences_state';
|
||||
|
||||
export enum AppStateEvent {
|
||||
TagChanged,
|
||||
@@ -63,6 +64,7 @@ export class AppState {
|
||||
showBetaWarning: boolean;
|
||||
readonly accountMenu = new AccountMenuState();
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
readonly preferences = new PreferencesState();
|
||||
readonly noAccountWarning: NoAccountWarningState;
|
||||
readonly noteTags: NoteTagsState;
|
||||
readonly sync = new SyncState();
|
||||
@@ -89,21 +91,18 @@ export class AppState {
|
||||
async () => {
|
||||
await this.notifyEvent(AppStateEvent.ActiveEditorChanged);
|
||||
},
|
||||
this.appEventObserverRemovers,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.noteTags = new NoteTagsState(
|
||||
application,
|
||||
this,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.tags = new TagsState(
|
||||
application,
|
||||
this.appEventObserverRemovers,
|
||||
),
|
||||
this.noAccountWarning = new NoAccountWarningState(
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
(this.tags = new TagsState(application, this.appEventObserverRemovers)),
|
||||
(this.noAccountWarning = new NoAccountWarningState(
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
));
|
||||
this.searchOptions = new SearchOptionsState(
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
@@ -128,6 +127,7 @@ export class AppState {
|
||||
makeObservable(this, {
|
||||
showBetaWarning: observable,
|
||||
isSessionsModalVisible: observable,
|
||||
preferences: observable,
|
||||
|
||||
enableBetaWarning: action,
|
||||
disableBetaWarning: action,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
|
||||
export class PreferencesState {
|
||||
private _open = false;
|
||||
|
||||
constructor() {
|
||||
makeObservable<PreferencesState, '_open'>(this, {
|
||||
_open: observable,
|
||||
openPreferences: action,
|
||||
closePreferences: action,
|
||||
isOpen: computed,
|
||||
});
|
||||
}
|
||||
|
||||
openPreferences = (): void => {
|
||||
this._open = true;
|
||||
};
|
||||
|
||||
closePreferences = (): void => {
|
||||
this._open = false;
|
||||
};
|
||||
|
||||
get isOpen() {
|
||||
return this._open;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
preferences(
|
||||
app-state='self.appState'
|
||||
)
|
||||
challenge-modal(
|
||||
ng-repeat="challenge in self.challenges track by challenge.id"
|
||||
class="sk-modal"
|
||||
@@ -36,3 +39,4 @@
|
||||
notes-context-menu(
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
|
||||
@@ -33,44 +33,47 @@ const ACCOUNT_SWITCHER_ENABLED = false;
|
||||
const ACCOUNT_SWITCHER_FEATURE_KEY = 'account_switcher';
|
||||
|
||||
type DockShortcut = {
|
||||
name: string,
|
||||
component: SNComponent,
|
||||
name: string;
|
||||
component: SNComponent;
|
||||
icon: {
|
||||
type: string
|
||||
background_color: string
|
||||
border_color: string
|
||||
}
|
||||
}
|
||||
type: string;
|
||||
background_color: string;
|
||||
border_color: string;
|
||||
};
|
||||
};
|
||||
|
||||
class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
outOfSync: boolean;
|
||||
hasPasscode: boolean;
|
||||
dataUpgradeAvailable: boolean;
|
||||
dockShortcuts: DockShortcut[];
|
||||
hasAccountSwitcher: boolean;
|
||||
showBetaWarning: boolean;
|
||||
showDataUpgrade: boolean;
|
||||
}> {
|
||||
private $rootScope: ng.IRootScopeService
|
||||
private rooms: SNComponent[] = []
|
||||
private themesWithIcons: SNTheme[] = []
|
||||
private showSyncResolution = false
|
||||
private unregisterComponent: any
|
||||
private rootScopeListener1: any
|
||||
private rootScopeListener2: any
|
||||
public arbitraryStatusMessage?: string
|
||||
public user?: any
|
||||
private offline = true
|
||||
public showAccountMenu = false
|
||||
private didCheckForOffline = false
|
||||
private queueExtReload = false
|
||||
private reloadInProgress = false
|
||||
public hasError = false
|
||||
public isRefreshing = false
|
||||
public lastSyncDate?: string
|
||||
public newUpdateAvailable = false
|
||||
public dockShortcuts: DockShortcut[] = []
|
||||
public roomShowState: Partial<Record<string, boolean>> = {}
|
||||
class FooterViewCtrl extends PureViewCtrl<
|
||||
unknown,
|
||||
{
|
||||
outOfSync: boolean;
|
||||
hasPasscode: boolean;
|
||||
dataUpgradeAvailable: boolean;
|
||||
dockShortcuts: DockShortcut[];
|
||||
hasAccountSwitcher: boolean;
|
||||
showBetaWarning: boolean;
|
||||
showDataUpgrade: boolean;
|
||||
}
|
||||
> {
|
||||
private $rootScope: ng.IRootScopeService;
|
||||
private rooms: SNComponent[] = [];
|
||||
private themesWithIcons: SNTheme[] = [];
|
||||
private showSyncResolution = false;
|
||||
private unregisterComponent: any;
|
||||
private rootScopeListener1: any;
|
||||
private rootScopeListener2: any;
|
||||
public arbitraryStatusMessage?: string;
|
||||
public user?: any;
|
||||
private offline = true;
|
||||
public showAccountMenu = false;
|
||||
private didCheckForOffline = false;
|
||||
private queueExtReload = false;
|
||||
private reloadInProgress = false;
|
||||
public hasError = false;
|
||||
public isRefreshing = false;
|
||||
public lastSyncDate?: string;
|
||||
public newUpdateAvailable = false;
|
||||
public dockShortcuts: DockShortcut[] = [];
|
||||
public roomShowState: Partial<Record<string, boolean>> = {};
|
||||
private observerRemovers: Array<() => void> = [];
|
||||
private completedInitialSync = false;
|
||||
private showingDownloadStatus = false;
|
||||
@@ -117,7 +120,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
this.showAccountMenu = this.appState.accountMenu.show;
|
||||
this.setState({
|
||||
showBetaWarning: showBetaWarning,
|
||||
showDataUpgrade: !showBetaWarning
|
||||
showDataUpgrade: !showBetaWarning,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -128,7 +131,9 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
/** Enable permanently for this user so they don't lose the feature after its disabled */
|
||||
localStorage.setItem(ACCOUNT_SWITCHER_FEATURE_KEY, JSON.stringify(true));
|
||||
}
|
||||
const hasAccountSwitcher = stringValue ? JSON.parse(stringValue) : ACCOUNT_SWITCHER_ENABLED;
|
||||
const hasAccountSwitcher = stringValue
|
||||
? JSON.parse(stringValue)
|
||||
: ACCOUNT_SWITCHER_ENABLED;
|
||||
this.setState({ hasAccountSwitcher });
|
||||
}
|
||||
|
||||
@@ -148,7 +153,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
reloadUpgradeStatus() {
|
||||
this.application.checkForSecurityUpdate().then((available) => {
|
||||
this.setState({
|
||||
dataUpgradeAvailable: available
|
||||
dataUpgradeAvailable: available,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -176,19 +181,25 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
async reloadPasscodeStatus() {
|
||||
const hasPasscode = this.application.hasPasscode();
|
||||
this.setState({
|
||||
hasPasscode: hasPasscode
|
||||
hasPasscode: hasPasscode,
|
||||
});
|
||||
}
|
||||
|
||||
addRootScopeListeners() {
|
||||
this.rootScopeListener1 = this.$rootScope.$on(RootScopeMessages.ReloadExtendedData, () => {
|
||||
this.reloadExtendedData();
|
||||
});
|
||||
this.rootScopeListener2 = this.$rootScope.$on(RootScopeMessages.NewUpdateAvailable, () => {
|
||||
this.$timeout(() => {
|
||||
this.onNewUpdateAvailable();
|
||||
});
|
||||
});
|
||||
this.rootScopeListener1 = this.$rootScope.$on(
|
||||
RootScopeMessages.ReloadExtendedData,
|
||||
() => {
|
||||
this.reloadExtendedData();
|
||||
}
|
||||
);
|
||||
this.rootScopeListener2 = this.$rootScope.$on(
|
||||
RootScopeMessages.NewUpdateAvailable,
|
||||
() => {
|
||||
this.$timeout(() => {
|
||||
this.onNewUpdateAvailable();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
@@ -202,11 +213,11 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
}
|
||||
break;
|
||||
case AppStateEvent.BeganBackupDownload:
|
||||
statusService.setMessage("Saving local backup…");
|
||||
statusService.setMessage('Saving local backup…');
|
||||
break;
|
||||
case AppStateEvent.EndedBackupDownload: {
|
||||
const successMessage = "Successfully saved backup.";
|
||||
const errorMessage = "Unable to save local backup.";
|
||||
const successMessage = 'Successfully saved backup.';
|
||||
const errorMessage = 'Unable to save local backup.';
|
||||
statusService.setMessage(data.success ? successMessage : errorMessage);
|
||||
|
||||
const twoSeconds = 2000;
|
||||
@@ -237,12 +248,12 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
break;
|
||||
case ApplicationEvent.EnteredOutOfSync:
|
||||
this.setState({
|
||||
outOfSync: true
|
||||
outOfSync: true,
|
||||
});
|
||||
break;
|
||||
case ApplicationEvent.ExitedOutOfSync:
|
||||
this.setState({
|
||||
outOfSync: false
|
||||
outOfSync: false,
|
||||
});
|
||||
break;
|
||||
case ApplicationEvent.CompletedFullSync:
|
||||
@@ -290,17 +301,15 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
CollectionSort.Title,
|
||||
'asc',
|
||||
(theme: SNTheme) => {
|
||||
return (
|
||||
theme.package_info &&
|
||||
theme.package_info.dock_icon
|
||||
);
|
||||
return theme.package_info && theme.package_info.dock_icon;
|
||||
}
|
||||
);
|
||||
|
||||
this.observerRemovers.push(this.application.streamItems(
|
||||
ContentType.Component,
|
||||
async () => {
|
||||
const components = this.application.getItems(ContentType.Component) as SNComponent[];
|
||||
this.observerRemovers.push(
|
||||
this.application.streamItems(ContentType.Component, async () => {
|
||||
const components = this.application.getItems(
|
||||
ContentType.Component
|
||||
) as SNComponent[];
|
||||
this.rooms = components.filter((candidate) => {
|
||||
return candidate.area === ComponentArea.Rooms && !candidate.deleted;
|
||||
});
|
||||
@@ -308,33 +317,38 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
this.queueExtReload = false;
|
||||
this.reloadExtendedData();
|
||||
}
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
|
||||
this.observerRemovers.push(this.application.streamItems(
|
||||
ContentType.Theme,
|
||||
async () => {
|
||||
const themes = this.application.getDisplayableItems(ContentType.Theme) as SNTheme[];
|
||||
this.observerRemovers.push(
|
||||
this.application.streamItems(ContentType.Theme, async () => {
|
||||
const themes = this.application.getDisplayableItems(
|
||||
ContentType.Theme
|
||||
) as SNTheme[];
|
||||
this.themesWithIcons = themes;
|
||||
this.reloadDockShortcuts();
|
||||
}
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registerComponentHandler() {
|
||||
this.unregisterComponent = this.application.componentManager!.registerHandler({
|
||||
identifier: 'room-bar',
|
||||
areas: [ComponentArea.Rooms, ComponentArea.Modal],
|
||||
focusHandler: (component, focused) => {
|
||||
if (component.isEditor() && focused) {
|
||||
if (component.package_info?.identifier === 'org.standardnotes.standard-sheets') {
|
||||
return;
|
||||
this.unregisterComponent =
|
||||
this.application.componentManager!.registerHandler({
|
||||
identifier: 'room-bar',
|
||||
areas: [ComponentArea.Rooms, ComponentArea.Modal],
|
||||
focusHandler: (component, focused) => {
|
||||
if (component.isEditor() && focused) {
|
||||
if (
|
||||
component.package_info?.identifier ===
|
||||
'org.standardnotes.standard-sheets'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
}
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateSyncStatus() {
|
||||
@@ -354,17 +368,17 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
statusManager.setMessage('');
|
||||
}, 2000);
|
||||
} else if (stats.uploadTotalCount > 20) {
|
||||
const completionPercentage = stats.uploadCompletionCount === 0
|
||||
? 0
|
||||
: stats.uploadCompletionCount / stats.uploadTotalCount;
|
||||
const completionPercentage =
|
||||
stats.uploadCompletionCount === 0
|
||||
? 0
|
||||
: stats.uploadCompletionCount / stats.uploadTotalCount;
|
||||
|
||||
const stringPercentage = completionPercentage.toLocaleString(
|
||||
undefined,
|
||||
{ style: 'percent' }
|
||||
);
|
||||
const stringPercentage = completionPercentage.toLocaleString(undefined, {
|
||||
style: 'percent',
|
||||
});
|
||||
|
||||
statusManager.setMessage(
|
||||
`Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`,
|
||||
`Syncing ${stats.uploadTotalCount} items (${stringPercentage} complete)`
|
||||
);
|
||||
} else {
|
||||
statusManager.setMessage('');
|
||||
@@ -398,8 +412,10 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
* then closing it after a short delay.
|
||||
*/
|
||||
const extWindow = this.rooms.find((room) => {
|
||||
return room.package_info.identifier === this.application
|
||||
.getNativeExtService().extManagerId;
|
||||
return (
|
||||
room.package_info.identifier ===
|
||||
this.application.getNativeExtService().extManagerId
|
||||
);
|
||||
});
|
||||
if (!extWindow) {
|
||||
this.queueExtReload = true;
|
||||
@@ -419,11 +435,13 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
}
|
||||
|
||||
async openSecurityUpdate() {
|
||||
if (await confirmDialog({
|
||||
title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE,
|
||||
text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
|
||||
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
|
||||
})) {
|
||||
if (
|
||||
await confirmDialog({
|
||||
title: STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE,
|
||||
text: STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT,
|
||||
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
|
||||
})
|
||||
) {
|
||||
preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => {
|
||||
await this.application.upgradeProtocolVersion();
|
||||
});
|
||||
@@ -453,25 +471,27 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
|
||||
refreshData() {
|
||||
this.isRefreshing = true;
|
||||
this.application.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true
|
||||
}).then((response) => {
|
||||
this.$timeout(() => {
|
||||
this.isRefreshing = false;
|
||||
}, 200);
|
||||
if (response && response.error) {
|
||||
this.application.alertService!.alert(
|
||||
STRING_GENERIC_SYNC_ERROR
|
||||
);
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
}
|
||||
});
|
||||
this.application
|
||||
.sync({
|
||||
queueStrategy: SyncQueueStrategy.ForceSpawnNew,
|
||||
checkIntegrity: true,
|
||||
})
|
||||
.then((response) => {
|
||||
this.$timeout(() => {
|
||||
this.isRefreshing = false;
|
||||
}, 200);
|
||||
if (response && response.error) {
|
||||
this.application.alertService!.alert(STRING_GENERIC_SYNC_ERROR);
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncUpdated() {
|
||||
this.lastSyncDate = dateToLocalizedString(this.application.getLastSyncDate()!);
|
||||
this.lastSyncDate = dateToLocalizedString(
|
||||
this.application.getLastSyncDate()!
|
||||
);
|
||||
}
|
||||
|
||||
onNewUpdateAvailable() {
|
||||
@@ -480,9 +500,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
|
||||
clickedNewUpdateAnnouncement() {
|
||||
this.newUpdateAvailable = false;
|
||||
this.application.alertService!.alert(
|
||||
STRING_NEW_UPDATE_READY
|
||||
);
|
||||
this.application.alertService!.alert(STRING_NEW_UPDATE_READY);
|
||||
}
|
||||
|
||||
reloadDockShortcuts() {
|
||||
@@ -499,7 +517,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
shortcuts.push({
|
||||
name: name,
|
||||
component: theme,
|
||||
icon: icon
|
||||
icon: icon,
|
||||
} as DockShortcut);
|
||||
}
|
||||
this.setState({
|
||||
@@ -514,7 +532,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
} else {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
})
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -553,7 +571,7 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
text:
|
||||
'If you wish to go back to a stable version, make sure to sign out ' +
|
||||
'of this beta app first.<br>You can silence this warning from the ' +
|
||||
'<em>Account</em> menu.'
|
||||
'<em>Account</em> menu.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -563,6 +581,10 @@ class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
}
|
||||
this.appState.accountMenu.setShow(false);
|
||||
}
|
||||
|
||||
clickPreferences() {
|
||||
this.appState.preferences.openPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
export class FooterView extends WebDirective {
|
||||
@@ -575,7 +597,7 @@ export class FooterView extends WebDirective {
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user