1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-15 23:00:36 +02:00
Files
.github
Assets
CliClient
Clipper
ElectronClient
Modules
ReactNativeClient
MarkdownEditor
android
ios
lib
components
screens
shared
CameraView.js
Dropdown.js
ItemList.js
ModalDialog.js
SafeAreaView.js
SaveIcon.png
action-button.js
app-nav.js
base-screen.js
checkbox.js
global-style.js
note-body-viewer.js
note-item.js
note-list.js
screen-header.js
select-date-time-dialog.js
side-menu-content-note.js
side-menu-content.js
side-menu.js
hooks
images
joplin-renderer
migrations
models
renderers
services
vendor
ArrayUtils.js
AsyncActionQueue.ts
BaseApplication.js
BaseModel.js
BaseSyncTarget.js
Cache.js
ClipperServer.js
CssUtils.js
DropboxApi.js
EventDispatcher.js
HtmlToMd.js
JoplinError.js
JoplinServerApi.ts
ModelCache.js
ObjectUtils.js
SyncTargetDropbox.js
SyncTargetFilesystem.js
SyncTargetMemory.js
SyncTargetNextcloud.js
SyncTargetOneDrive.js
SyncTargetOneDriveDev.js
SyncTargetRegistry.js
SyncTargetWebDAV.js
TaskQueue.js
TemplateUtils.js
WebDavApi.js
WelcomeUtils.js
database-driver-node.js
database-driver-react-native.js
database.js
dialogs.js
file-api-driver-dropbox.js
file-api-driver-local.js
file-api-driver-memory.js
file-api-driver-onedrive.js
file-api-driver-webdav.js
file-api.js
folders-screen-utils.js
fs-driver-base.js
fs-driver-dummy.js
fs-driver-node.js
fs-driver-rn.js
geolocation-node.js
geolocation-react.js
htmlUtils.js
import-enex-html-gen.js
import-enex-md-gen.js
import-enex.js
joplin-database.js
locale.js
logger.js
markJsUtils.js
markdownUtils.js
markupLanguageUtils.js
mime-utils.js
net-utils.js
onedrive-api-node-utils.js
onedrive-api.js
package.json
parameters.js
parseUri.js
path-utils.js
poor-man-intervals.js
promise-utils.js
randomClipperPort.js
react-logger.js
reducer.js
registry.js
reserved-ids.js
resourceUtils.js
shim-init-node.js
shim-init-react.js
shim.js
string-utils-common.js
string-utils.js
synchronizer.js
time-utils.js
urlUtils.js
uuid.js
welcomeAssets.js
locales
tools
.buckconfig
.flowconfig
.gitattributes
.gitignore
.watchmanconfig
PluginAssetsLoader.ts
app.json
clean_build.bat
gulpfile.js
index.android.js
index.ios.js
index.js
main.js
metro.config.js
package-lock.json
package.json
root.js
setUpQuickActions.ts
Tools
docs
patches
readme
.eslintignore
.eslintrc.js
.gitignore
.travis.yml
BUILD.md
CONTRIBUTING.md
Joplin_install_and_update.sh
LICENSE
README.md
SECURITY.md
_config.yml
appveyor.yml
gulpfile.js
joplin.code-workspace
joplin.sublime-project
package-lock.json
package.json
tsconfig.json
joplin/ReactNativeClient/lib/components/screen-header.js

511 lines
16 KiB
JavaScript
Raw Normal View History

2019-07-29 15:43:53 +02:00
const React = require('react');
2019-07-29 15:58:33 +02:00
const { connect } = require('react-redux');
2020-02-09 15:48:09 +00:00
const { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
const Icon = require('react-native-vector-icons/Ionicons').default;
const { BackButtonService } = require('lib/services/back-button.js');
const NavService = require('lib/services/NavService.js');
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js');
const { themeStyle } = require('lib/components/global-style.js');
const { Dropdown } = require('lib/components/Dropdown.js');
const { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
2017-05-16 19:57:09 +00:00
2020-02-09 14:51:12 +00:00
Icon.loadFont();
// Rather than applying a padding to the whole bar, it is applied to each
// individual component (button, picker, etc.) so that the touchable areas
// are widder and to give more room to the picker component which has a larger
// default height.
const PADDING_V = 10;
class ScreenHeaderComponent extends React.PureComponent {
2017-08-01 17:59:01 +00:00
constructor() {
super();
this.styles_ = {};
2017-07-21 22:40:02 +01:00
}
2017-07-16 00:30:54 +01:00
2017-08-01 17:59:01 +00:00
styles() {
const themeId = Setting.value('theme');
2017-08-01 17:59:01 +00:00
if (this.styles_[themeId]) return this.styles_[themeId];
this.styles_ = {};
const theme = themeStyle(themeId);
const styleObject = {
2017-08-01 17:59:01 +00:00
container: {
flexDirection: 'column',
2017-08-01 17:59:01 +00:00
backgroundColor: theme.raisedBackgroundColor,
alignItems: 'center',
shadowColor: '#000000',
2017-08-01 17:59:01 +00:00
elevation: 5,
},
divider: {
borderBottomWidth: 1,
borderColor: theme.dividerColor,
2019-07-29 15:43:53 +02:00
backgroundColor: '#0000ff',
2017-08-01 17:59:01 +00:00
},
sideMenuButton: {
flex: 1,
alignItems: 'center',
2017-08-01 17:59:01 +00:00
backgroundColor: theme.raisedBackgroundColor,
paddingLeft: theme.marginLeft,
paddingRight: 5,
marginRight: 2,
paddingTop: PADDING_V,
paddingBottom: PADDING_V,
},
iconButton: {
flex: 1,
backgroundColor: theme.raisedBackgroundColor,
paddingLeft: 15,
paddingRight: 15,
paddingTop: PADDING_V,
paddingBottom: PADDING_V,
},
saveButton: {
flex: 0,
flexDirection: 'row',
alignItems: 'center',
2017-08-01 17:59:01 +00:00
padding: 10,
borderWidth: 1,
borderColor: theme.raisedHighlightedColor,
borderRadius: 4,
marginRight: 8,
},
saveButtonText: {
textAlignVertical: 'center',
2017-08-01 17:59:01 +00:00
color: theme.raisedHighlightedColor,
fontWeight: 'bold',
2017-08-01 17:59:01 +00:00
},
savedButtonIcon: {
fontSize: 20,
color: theme.raisedHighlightedColor,
width: 18,
height: 18,
},
saveButtonIcon: {
width: 18,
height: 18,
},
contextMenuTrigger: {
fontSize: 30,
paddingLeft: 10,
2017-08-01 17:59:01 +00:00
paddingRight: theme.marginRight,
color: theme.raisedColor,
fontWeight: 'bold',
2017-08-01 17:59:01 +00:00
},
contextMenu: {
backgroundColor: theme.raisedBackgroundColor,
},
contextMenuItem: {
backgroundColor: theme.backgroundColor,
},
contextMenuItemText: {
flex: 1,
textAlignVertical: 'center',
2017-08-01 17:59:01 +00:00
paddingLeft: theme.marginLeft,
paddingRight: theme.marginRight,
paddingTop: theme.itemMarginTop,
paddingBottom: theme.itemMarginBottom,
color: theme.color,
backgroundColor: theme.backgroundColor,
fontSize: theme.fontSize,
},
titleText: {
flex: 1,
textAlignVertical: 'center',
marginLeft: 10,
2017-08-01 17:59:01 +00:00
color: theme.raisedHighlightedColor,
fontWeight: 'bold',
2017-08-01 17:59:01 +00:00
fontSize: theme.fontSize,
paddingTop: 15,
paddingBottom: 15,
2017-12-30 20:57:34 +01:00
},
warningBox: {
2019-07-29 15:43:53 +02:00
backgroundColor: '#ff9900',
flexDirection: 'row',
2017-12-30 20:57:34 +01:00
padding: theme.marginLeft,
},
2017-08-01 17:59:01 +00:00
};
2017-07-22 17:36:55 +01:00
2017-08-01 17:59:01 +00:00
styleObject.topIcon = Object.assign({}, theme.icon);
styleObject.topIcon.flex = 1;
styleObject.topIcon.textAlignVertical = 'center';
2017-08-01 17:59:01 +00:00
styleObject.topIcon.color = theme.raisedColor;
2017-07-22 23:52:24 +01:00
2017-08-01 17:59:01 +00:00
styleObject.backButton = Object.assign({}, styleObject.iconButton);
styleObject.backButton.marginRight = 1;
2017-07-16 00:30:54 +01:00
2017-08-01 17:59:01 +00:00
styleObject.backButtonDisabled = Object.assign({}, styleObject.backButton, { opacity: theme.disabledOpacity });
styleObject.saveButtonDisabled = Object.assign({}, styleObject.saveButton, { opacity: theme.disabledOpacity });
styleObject.iconButtonDisabled = Object.assign({}, styleObject.iconButton, { opacity: theme.disabledOpacity });
2017-05-16 20:25:19 +00:00
2017-08-01 17:59:01 +00:00
this.styles_[themeId] = StyleSheet.create(styleObject);
return this.styles_[themeId];
}
2017-05-16 19:57:09 +00:00
2017-06-06 20:01:43 +00:00
sideMenuButton_press() {
this.props.dispatch({ type: 'SIDE_MENU_TOGGLE' });
2017-05-24 19:27:13 +00:00
}
async backButton_press() {
if (this.props.noteSelectionEnabled) {
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
2019-07-29 15:43:53 +02:00
} else {
await BackButtonService.back();
2019-07-29 15:43:53 +02:00
}
2017-05-16 19:57:09 +00:00
}
selectAllButton_press() {
this.props.dispatch({ type: 'NOTE_SELECT_ALL_TOGGLE' });
}
2017-07-22 23:52:24 +01:00
searchButton_press() {
NavService.go('Search');
2017-07-22 23:52:24 +01:00
}
async duplicateButton_press() {
const noteIds = this.props.selectedNoteIds;
2019-10-14 01:47:21 +02:00
// Duplicate all selected notes. ensureUniqueTitle is set to true to use the
// original note's name as a root for the new unique identifier.
await Note.duplicateMultipleNotes(noteIds, { ensureUniqueTitle: true });
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
}
async deleteButton_press() {
// Dialog needs to be displayed as a child of the parent component, otherwise
// it won't be visible within the header component.
const ok = await dialogs.confirm(this.props.parentComponent, _('Delete these notes?'));
if (!ok) return;
const noteIds = this.props.selectedNoteIds;
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
await Note.batchDelete(noteIds);
}
2017-06-06 20:01:43 +00:00
menu_select(value) {
2019-07-29 15:43:53 +02:00
if (typeof value == 'function') {
2017-05-16 20:25:19 +00:00
value();
}
}
2017-07-07 18:19:24 +01:00
log_press() {
NavService.go('Log');
2017-07-07 18:19:24 +01:00
}
2017-07-10 20:16:59 +01:00
status_press() {
NavService.go('Status');
2017-07-10 20:16:59 +01:00
}
warningBox_press(event) {
NavService.go(event.screen);
}
renderWarningBox(screen, message) {
return (
<TouchableOpacity key={screen} style={this.styles().warningBox} onPress={() => this.warningBox_press({ screen: screen })} activeOpacity={0.8}>
<Text style={{ flex: 1 }}>{message}</Text>
</TouchableOpacity>
);
2017-12-30 20:57:34 +01:00
}
2017-05-16 19:57:09 +00:00
render() {
2017-07-16 00:30:54 +01:00
function sideMenuButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.sideMenuButton}>
2019-07-29 15:43:53 +02:00
<Icon name="md-menu" style={styles.topIcon} />
2017-07-16 00:30:54 +01:00
</View>
</TouchableOpacity>
);
}
function backButton(styles, onPress, disabled) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
2019-07-29 15:43:53 +02:00
<Icon name="md-arrow-back" style={styles.topIcon} />
2017-07-16 00:30:54 +01:00
</View>
</TouchableOpacity>
);
}
function saveButton(styles, onPress, disabled, show) {
if (!show) return null;
2019-07-29 15:43:53 +02:00
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
2017-07-30 23:33:54 +02:00
return (
2019-07-29 15:43:53 +02:00
<TouchableOpacity onPress={onPress} disabled={disabled} style={{ padding: 0 }}>
<View style={disabled ? styles.saveButtonDisabled : styles.saveButton}>{icon}</View>
</TouchableOpacity>
);
}
function selectAllButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
}
2017-07-22 23:52:24 +01:00
function searchButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
2019-07-29 15:43:53 +02:00
<Icon name="md-search" style={styles.topIcon} />
2017-07-22 23:52:24 +01:00
</View>
</TouchableOpacity>
);
}
function deleteButton(styles, onPress, disabled) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
2019-07-29 15:43:53 +02:00
<Icon name="md-trash" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
}
function duplicateButton(styles, onPress, disabled) {
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<View style={disabled ? styles.iconButtonDisabled : styles.iconButton}>
<Icon name="md-copy" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
}
function sortButton(styles, onPress) {
return (
<TouchableOpacity onPress={onPress}>
<View style={styles.iconButton}>
2019-07-29 15:43:53 +02:00
<Icon name="md-funnel" style={styles.topIcon} />
</View>
</TouchableOpacity>
);
}
2017-05-16 23:46:21 +02:00
let key = 0;
const menuOptionComponents = [];
2017-05-16 23:46:21 +02:00
if (!this.props.noteSelectionEnabled) {
for (let i = 0; i < this.props.menuOptions.length; i++) {
const o = this.props.menuOptions[i];
if (o.isDivider) {
2019-09-19 22:51:18 +01:00
menuOptionComponents.push(<View key={`menuOption_${key++}`} style={this.styles().divider} />);
} else {
menuOptionComponents.push(
2019-09-19 22:51:18 +01:00
<MenuOption value={o.onPress} key={`menuOption_${key++}`} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
2019-07-29 15:43:53 +02:00
</MenuOption>
);
}
}
2017-09-24 15:48:23 +01:00
if (menuOptionComponents.length) {
2019-09-19 22:51:18 +01:00
menuOptionComponents.push(<View key={`menuOption_${key++}`} style={this.styles().divider} />);
2017-09-24 15:48:23 +01:00
}
} else {
2017-09-24 15:48:23 +01:00
menuOptionComponents.push(
<MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Delete')}</Text>
2019-07-29 15:43:53 +02:00
</MenuOption>
);
menuOptionComponents.push(
<MenuOption value={() => this.duplicateButton_press()} key={'menuOption_duplicate'} style={this.styles().contextMenuItem}>
<Text style={this.styles().contextMenuItemText}>{_('Duplicate')}</Text>
</MenuOption>
);
2017-09-24 15:48:23 +01:00
}
2017-07-10 20:16:59 +01:00
const createTitleComponent = (disabled) => {
const themeId = Setting.value('theme');
const theme = themeStyle(themeId);
const folderPickerOptions = this.props.folderPickerOptions;
if (folderPickerOptions && folderPickerOptions.enabled) {
const addFolderChildren = (folders, pickerItems, indent) => {
folders.sort((a, b) => {
const aTitle = a && a.title ? a.title : '';
const bTitle = b && b.title ? b.title : '';
return aTitle.toLowerCase() < bTitle.toLowerCase() ? -1 : +1;
});
for (let i = 0; i < folders.length; i++) {
const f = folders[i];
2019-09-19 22:51:18 +01:00
pickerItems.push({ label: `${' '.repeat(indent)} ${Folder.displayTitle(f)}`, value: f.id });
pickerItems = addFolderChildren(f.children, pickerItems, indent + 1);
}
return pickerItems;
2019-07-29 15:43:53 +02:00
};
const titlePickerItems = mustSelect => {
const folders = this.props.folders.filter(f => f.id !== Folder.conflictFolderId());
let output = [];
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
const folderTree = Folder.buildTree(folders);
output = addFolderChildren(folderTree, output, 0);
return output;
2019-07-29 15:43:53 +02:00
};
2017-07-16 17:06:05 +01:00
return (
<Dropdown
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
itemHeight={35}
disabled={disabled}
labelTransform="trim"
2019-07-29 15:43:53 +02:00
selectedValue={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
itemListStyle={{
backgroundColor: theme.backgroundColor,
}}
headerStyle={{
2017-11-19 00:23:18 +00:00
color: theme.raisedHighlightedColor,
fontSize: theme.fontSize,
opacity: disabled ? theme.disabledOpacity : 1,
}}
itemStyle={{
color: theme.color,
fontSize: theme.fontSize,
}}
onValueChange={async (folderId, itemIndex) => {
// If onValueChange is specified, use this as a callback, otherwise do the default
// which is to take the selectedNoteIds from the state and move them to the
// chosen folder.
if (folderPickerOptions.onValueChange) {
folderPickerOptions.onValueChange(folderId, itemIndex);
return;
}
if (!folderId) return;
const noteIds = this.props.selectedNoteIds;
if (!noteIds.length) return;
const folder = await Folder.load(folderId);
const ok = noteIds.length > 1 ? await dialogs.confirm(this.props.parentComponent, _('Move %d notes to notebook "%s"?', noteIds.length, folder.title)) : true;
if (!ok) return;
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
}}
/>
2017-07-16 17:06:05 +01:00
);
} else {
const title = 'title' in this.props && this.props.title !== null ? this.props.title : '';
return <Text ellipsizeMode={'tail'} numberOfLines={1} style={this.styles().titleText}>{title}</Text>;
2017-07-16 17:06:05 +01:00
}
2019-07-29 15:43:53 +02:00
};
2017-07-16 17:06:05 +01:00
const warningComps = [];
if (this.props.showMissingMasterKeyMessage) warningComps.push(this.renderWarningBox('EncryptionConfig', _('Press to set the decryption password.')));
if (this.props.hasDisabledSyncItems) warningComps.push(this.renderWarningBox('Status', _('Some items cannot be synchronised. Press for more info.')));
2017-12-30 20:57:34 +01:00
const showSideMenuButton = !!this.props.showSideMenuButton && !this.props.noteSelectionEnabled;
const showSelectAllButton = this.props.noteSelectionEnabled;
const showSearchButton = !!this.props.showSearchButton && !this.props.noteSelectionEnabled;
const showContextMenuButton = this.props.showContextMenuButton !== false;
const showBackButton = !!this.props.noteSelectionEnabled || this.props.showBackButton !== false;
let backButtonDisabled = !this.props.historyCanGoBack;
2019-07-29 15:43:53 +02:00
if (this.props.noteSelectionEnabled) backButtonDisabled = false;
const headerItemDisabled = !this.props.selectedNoteIds.length > 0;
const titleComp = createTitleComponent(headerItemDisabled);
const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
const backButtonComp = !showBackButton ? null : backButton(this.styles(), () => this.backButton_press(), backButtonDisabled);
const selectAllButtonComp = !showSelectAllButton ? null : selectAllButton(this.styles(), () => this.selectAllButton_press());
const searchButtonComp = !showSearchButton ? null : searchButton(this.styles(), () => this.searchButton_press());
const deleteButtonComp = this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press(), headerItemDisabled) : null;
const duplicateButtonComp = this.props.noteSelectionEnabled ? duplicateButton(this.styles(), () => this.duplicateButton_press(), headerItemDisabled) : null;
const sortButtonComp = !this.props.noteSelectionEnabled && this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null;
const windowHeight = Dimensions.get('window').height - 50;
const contextMenuStyle = { paddingTop: PADDING_V, paddingBottom: PADDING_V };
2019-07-29 15:43:53 +02:00
// HACK: if this button is removed during selection mode, the header layout is broken, so for now just make it 1 pixel large (normally it should be hidden)
2019-07-29 15:43:53 +02:00
if (this.props.noteSelectionEnabled) contextMenuStyle.width = 1;
const menuComp =
!menuOptionComponents.length || !showContextMenuButton ? null : (
<Menu onSelect={value => this.menu_select(value)} style={this.styles().contextMenu}>
2019-07-29 15:43:53 +02:00
<MenuTrigger style={contextMenuStyle}>
<Icon name="md-more" style={this.styles().contextMenuTrigger} />
</MenuTrigger>
<MenuOptions>
<ScrollView style={{ maxHeight: windowHeight }}>{menuOptionComponents}</ScrollView>
</MenuOptions>
</Menu>
);
2017-05-16 20:25:19 +00:00
2017-05-16 19:57:09 +00:00
return (
2019-07-29 15:43:53 +02:00
<View style={this.styles().container}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{sideMenuComp}
{backButtonComp}
{saveButton(
this.styles(),
() => {
if (this.props.onSaveButtonPress) this.props.onSaveButtonPress();
},
this.props.saveButtonDisabled === true,
this.props.showSaveButton === true
)}
{titleComp}
{selectAllButtonComp}
2019-07-29 15:43:53 +02:00
{searchButtonComp}
{deleteButtonComp}
{duplicateButtonComp}
2019-07-29 15:43:53 +02:00
{sortButtonComp}
{menuComp}
2017-12-30 20:57:34 +01:00
</View>
{warningComps}
2019-07-29 15:43:53 +02:00
<DialogBox
ref={dialogbox => {
2019-07-29 15:43:53 +02:00
this.dialogbox = dialogbox;
}}
/>
2017-05-16 19:57:09 +00:00
</View>
);
}
}
2017-06-11 22:11:14 +01:00
ScreenHeaderComponent.defaultProps = {
menuOptions: [],
};
const ScreenHeader = connect(state => {
2019-07-29 15:43:53 +02:00
return {
historyCanGoBack: state.historyCanGoBack,
locale: state.settings.locale,
folders: state.folders,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
selectedNoteIds: state.selectedNoteIds,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
hasDisabledSyncItems: state.hasDisabledSyncItems,
2019-07-29 15:43:53 +02:00
};
})(ScreenHeaderComponent);
module.exports = { ScreenHeader };