You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Added search page
This commit is contained in:
		| @@ -90,8 +90,8 @@ android { | ||||
| 		applicationId "net.cozic.joplin" | ||||
| 		minSdkVersion 16 | ||||
| 		targetSdkVersion 22 | ||||
| 		versionCode 27 | ||||
| 		versionName "0.9.14" | ||||
| 		versionCode 28 | ||||
| 		versionName "0.9.15" | ||||
| 		ndk { | ||||
| 			abiFilters "armeabi-v7a", "x86" | ||||
| 		} | ||||
|   | ||||
| @@ -25,4 +25,9 @@ globalStyle.icon = { | ||||
| 	fontSize: 30, | ||||
| }; | ||||
|  | ||||
| globalStyle.lineInput = { | ||||
| 	color: globalStyle.color, | ||||
| 	backgroundColor: globalStyle.backgroundColor, | ||||
| }; | ||||
|  | ||||
| export { globalStyle } | ||||
| @@ -4,21 +4,13 @@ import { ListView, Text, TouchableHighlight, Switch, View, StyleSheet } from 're | ||||
| import { Log } from 'lib/log.js'; | ||||
| import { _ } from 'lib/locale.js'; | ||||
| import { Checkbox } from 'lib/components/checkbox.js'; | ||||
| import { NoteItem } from 'lib/components/note-item.js'; | ||||
| import { reg } from 'lib/registry.js'; | ||||
| import { Note } from 'lib/models/note.js'; | ||||
| import { time } from 'lib/time-utils.js'; | ||||
| import { globalStyle } from 'lib/components/global-style.js'; | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
| 	listItem: { | ||||
| 		flexDirection: 'row', | ||||
| 		height: 40, | ||||
| 		borderBottomWidth: 1, | ||||
| 		borderBottomColor: globalStyle.dividerColor, | ||||
| 		alignItems: 'center', | ||||
| 		paddingLeft: globalStyle.marginLeft, | ||||
| 		backgroundColor: globalStyle.backgroundColor, | ||||
| 	}, | ||||
| 	noItemMessage: { | ||||
| 		paddingLeft: globalStyle.marginLeft, | ||||
| 		paddingRight: globalStyle.marginRight, | ||||
| @@ -64,36 +56,18 @@ class ItemListComponent extends Component { | ||||
| 	listView_itemPress(itemId) {} | ||||
|  | ||||
| 	render() { | ||||
| 		let renderRow = (item) => { | ||||
| 			let onPress = () => { | ||||
| 				this.listView_itemPress(item.id); | ||||
| 			} | ||||
| 			let onLongPress = () => { | ||||
| 				this.listView_itemLongPress(item.id); | ||||
| 			} | ||||
|  | ||||
| 			const listItemStyle = { color: globalStyle.color }; | ||||
| 			const checkboxStyle = Object.assign({}, listItemStyle); | ||||
| 			if (!Number(item.is_todo)) checkboxStyle.display = 'none'; | ||||
|  | ||||
| 			const checkboxChecked = !!Number(item.todo_completed); | ||||
|  | ||||
| 			return ( | ||||
| 				<TouchableHighlight onPress={onPress} onLongPress={onLongPress} underlayColor="#0066FF"> | ||||
| 					<View style={ styles.listItem }> | ||||
| 						<Checkbox style={checkboxStyle} checked={checkboxChecked} onChange={(checked) => { this.todoCheckbox_change(item.id, checked) }}/><Text style={listItemStyle}>{item.title}</Text> | ||||
| 					</View> | ||||
| 				</TouchableHighlight> | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39 | ||||
|  | ||||
| 		if (this.state.dataSource.getRowCount()) { | ||||
| 			return ( | ||||
| 				<ListView | ||||
| 					dataSource={this.state.dataSource} | ||||
| 					renderRow={renderRow} | ||||
| 					renderRow={(note) => { | ||||
| 						return <NoteItem | ||||
| 							note={note} | ||||
| 							onPress={(note) => this.listView_itemPress(note.id) } | ||||
| 							onCheckboxChange={(note, checked) => this.todoCheckbox_change(note.id, checked) } | ||||
| 						/> }} | ||||
| 					enableEmptySections={true} | ||||
| 				/> | ||||
| 			); | ||||
|   | ||||
							
								
								
									
										65
									
								
								ReactNativeClient/lib/components/note-item.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								ReactNativeClient/lib/components/note-item.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import { connect } from 'react-redux' | ||||
| import { ListView, Text, TouchableHighlight, Switch, View, StyleSheet } from 'react-native'; | ||||
| import { Log } from 'lib/log.js'; | ||||
| import { _ } from 'lib/locale.js'; | ||||
| import { Checkbox } from 'lib/components/checkbox.js'; | ||||
| import { reg } from 'lib/registry.js'; | ||||
| import { Note } from 'lib/models/note.js'; | ||||
| import { time } from 'lib/time-utils.js'; | ||||
| import { globalStyle } from 'lib/components/global-style.js'; | ||||
|  | ||||
| const styleObject = { | ||||
| 	listItem: { | ||||
| 		flexDirection: 'row', | ||||
| 		height: 40, | ||||
| 		borderBottomWidth: 1, | ||||
| 		borderBottomColor: globalStyle.dividerColor, | ||||
| 		alignItems: 'center', | ||||
| 		paddingLeft: globalStyle.marginLeft, | ||||
| 		backgroundColor: globalStyle.backgroundColor, | ||||
| 	}, | ||||
| 	listItemText: { | ||||
| 		color: globalStyle.color, | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| const styles = StyleSheet.create(styleObject); | ||||
|  | ||||
| class NoteItemComponent extends Component { | ||||
|  | ||||
| 	noteItem_press(noteId) { | ||||
| 		this.props.dispatch({ | ||||
| 			type: 'Navigation/NAVIGATE', | ||||
| 			routeName: 'Note', | ||||
| 			noteId: noteId, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		const note = this.props.note ? this.props.note : {}; | ||||
| 		const onPress = this.props.onPress; | ||||
| 		const onLongPress = this.props.onLongPress; | ||||
| 		const onCheckboxChange = this.props.onCheckboxChange; | ||||
|  | ||||
| 		const checkboxStyle = !Number(note.is_todo) ? { display: 'none' } : { color: globalStyle.color }; | ||||
| 		const checkboxChecked = !!Number(note.todo_completed); | ||||
|  | ||||
| 		return ( | ||||
| 			<TouchableHighlight onPress={() => onPress ? onPress(note) : this.noteItem_press(note.id)} onLongPress={() => onLongPress(note)} underlayColor="#0066FF"> | ||||
| 				<View style={ styles.listItem }> | ||||
| 					<Checkbox style={checkboxStyle} checked={checkboxChecked} onChange={(checked) => { onCheckboxChange(note, checked) }}/><Text style={styles.listItemText}>{note.title}</Text> | ||||
| 				</View> | ||||
| 			</TouchableHighlight> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| const NoteItem = connect( | ||||
| 	(state) => { | ||||
| 		return {}; | ||||
| 	} | ||||
| )(NoteItemComponent) | ||||
|  | ||||
| export { NoteItem } | ||||
| @@ -40,12 +40,11 @@ let styleObject = { | ||||
| 		paddingRight: 5, | ||||
| 		marginRight: 2, | ||||
| 	}, | ||||
| 	backButton: { | ||||
| 	iconButton: { | ||||
| 		flex: 1, | ||||
| 		backgroundColor: globalStyle.backgroundColor, | ||||
| 		paddingLeft: 15, | ||||
| 		paddingRight: 15, | ||||
| 		marginRight: 1, | ||||
| 	}, | ||||
| 	saveButton: { | ||||
| 		flex: 1, | ||||
| @@ -100,6 +99,9 @@ styleObject.topIcon = Object.assign({}, globalStyle.icon); | ||||
| styleObject.topIcon.flex = 1; | ||||
| styleObject.topIcon.textAlignVertical = 'center'; | ||||
|  | ||||
| styleObject.backButton = Object.assign({}, styleObject.iconButton); | ||||
| styleObject.backButton.marginRight = 1; | ||||
|  | ||||
| styleObject.backButtonDisabled = Object.assign({}, styleObject.backButton, { opacity: globalStyle.disabledOpacity }); | ||||
| styleObject.saveButtonDisabled = Object.assign({}, styleObject.saveButton, { opacity: globalStyle.disabledOpacity }); | ||||
|  | ||||
| @@ -115,6 +117,13 @@ class ScreenHeaderComponent extends Component { | ||||
| 		this.props.dispatch({ type: 'Navigation/BACK' }); | ||||
| 	} | ||||
|  | ||||
| 	searchButton_press() { | ||||
| 		this.props.dispatch({ | ||||
| 			type: 'Navigation/NAVIGATE', | ||||
| 			routeName: 'Search', | ||||
| 		});	 | ||||
| 	} | ||||
|  | ||||
| 	menu_select(value) { | ||||
| 		if (typeof(value) == 'function') { | ||||
| 			value(); | ||||
| @@ -170,6 +179,16 @@ class ScreenHeaderComponent extends Component { | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		function searchButton(styles, onPress) { | ||||
| 			return ( | ||||
| 				<TouchableOpacity onPress={onPress}> | ||||
| 					<View style={styles.iconButton}> | ||||
| 						<Icon name='md-search' style={styles.topIcon} /> | ||||
| 					</View> | ||||
| 				</TouchableOpacity> | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		let key = 0; | ||||
| 		let menuOptionComponents = []; | ||||
| 		for (let i = 0; i < this.props.menuOptions.length; i++) { | ||||
| @@ -220,10 +239,11 @@ class ScreenHeaderComponent extends Component { | ||||
| 				{ sideMenuButton(styles, () => this.sideMenuButton_press()) } | ||||
| 				{ backButton(styles, () => this.backButton_press(), !this.props.historyCanGoBack) } | ||||
| 				{ saveButton(styles, () => { if (this.props.onSaveButtonPress) this.props.onSaveButtonPress() }, this.props.saveButtonDisabled === true, this.props.showSaveButton === true) } | ||||
| 				{ titleComp }				 | ||||
| 				{ titleComp } | ||||
| 				{ searchButton(styles, () => this.searchButton_press()) } | ||||
| 			    <Menu onSelect={(value) => this.menu_select(value)} style={styles.contextMenu}> | ||||
| 					<MenuTrigger> | ||||
| 						<Text style={styles.contextMenuTrigger}>      ⋮</Text> | ||||
| 						<Text style={styles.contextMenuTrigger}>  ⋮</Text> | ||||
| 					</MenuTrigger> | ||||
| 					<MenuOptions> | ||||
| 						{ menuOptionComponents } | ||||
|   | ||||
							
								
								
									
										147
									
								
								ReactNativeClient/lib/components/screens/search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								ReactNativeClient/lib/components/screens/search.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import { ListView, StyleSheet, View, TextInput, FlatList, TouchableHighlight } from 'react-native'; | ||||
| import { connect } from 'react-redux' | ||||
| import { ScreenHeader } from 'lib/components/screen-header.js'; | ||||
| import Icon from 'react-native-vector-icons/Ionicons'; | ||||
| import { _ } from 'lib/locale.js'; | ||||
| import { Note } from 'lib/models/note.js'; | ||||
| import { NoteItem } from 'lib/components/note-item.js'; | ||||
| import { BaseScreenComponent } from 'lib/components/base-screen.js'; | ||||
| import { globalStyle } from 'lib/components/global-style.js'; | ||||
|  | ||||
| let styles = { | ||||
| 	body: { | ||||
| 		flex: 1, | ||||
| 	}, | ||||
| 	searchContainer: { | ||||
| 		flexDirection: 'row', | ||||
| 		alignItems: 'center', | ||||
| 		borderWidth: 1, | ||||
| 		borderColor: globalStyle.dividerColor, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| styles.searchTextInput = Object.assign({}, globalStyle.lineInput); | ||||
| styles.searchTextInput.paddingLeft = globalStyle.marginLeft; | ||||
| styles.searchTextInput.flex = 1; | ||||
|  | ||||
| styles.clearIcon = Object.assign({}, globalStyle.icon); | ||||
| styles.clearIcon.color = globalStyle.colorFaded; | ||||
| styles.clearIcon.paddingRight = globalStyle.marginRight; | ||||
| styles.clearIcon.backgroundColor = globalStyle.backgroundColor; | ||||
|  | ||||
| styles = StyleSheet.create(styles); | ||||
|  | ||||
| class SearchScreenComponent extends BaseScreenComponent { | ||||
| 	 | ||||
| 	static navigationOptions(options) { | ||||
| 		return { header: null }; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.state = { | ||||
| 			query: '', | ||||
| 			notes: [], | ||||
| 		}; | ||||
| 		this.isMounted_ = false; | ||||
| 	} | ||||
|  | ||||
| 	componentDidMount() { | ||||
| 		this.setState({ query: this.props.query }); | ||||
| 		this.refreshSearch(this.props.query); | ||||
| 		this.isMounted_ = true; | ||||
| 	} | ||||
|  | ||||
| 	componentWillUnmount() { | ||||
| 		this.isMounted_ = false; | ||||
| 	} | ||||
|  | ||||
| 	componentWillReceiveProps(newProps) { | ||||
| 		let newState = {}; | ||||
| 		if ('query' in newProps) newState.query = newProps.query; | ||||
|  | ||||
| 		if (Object.getOwnPropertyNames(newState).length) { | ||||
| 			this.setState(newState); | ||||
| 			this.refreshSearch(newState.query); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	searchTextInput_submit() { | ||||
| 		const query = this.state.query.trim(); | ||||
| 		if (!query) return; | ||||
|  | ||||
| 		this.props.dispatch({ | ||||
| 			type: 'SEARCH_QUERY', | ||||
| 			query: query, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	clearButton_press() { | ||||
| 		this.props.dispatch({ | ||||
| 			type: 'SEARCH_QUERY', | ||||
| 			query: '', | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async refreshSearch(query = null) { | ||||
| 		query = query === null ? this.state.query.trim : query.trim(); | ||||
|  | ||||
| 		let notes = [] | ||||
|  | ||||
| 		if (query) { | ||||
| 			notes = await Note.previews(null, { | ||||
| 				anywherePattern: '*' + query + '*', | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		if (!this.isMounted_) return; | ||||
|  | ||||
| 		this.setState({ notes: notes }); | ||||
| 	} | ||||
|  | ||||
| 	searchTextInput_changeText(text) { | ||||
| 		this.setState({ query: text }); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return ( | ||||
| 			<View style={this.styles().screen}> | ||||
| 				<ScreenHeader navState={this.props.navigation.state} /> | ||||
| 				<View style={styles.body}> | ||||
| 					<View style={styles.searchContainer}> | ||||
| 						<TextInput | ||||
| 							style={styles.searchTextInput} | ||||
| 							underlineColorAndroid="#ffffff00"  | ||||
| 							onSubmitEditing={() => { this.searchTextInput_submit() }} | ||||
| 							onChangeText={(text) => this.searchTextInput_changeText(text) } | ||||
| 							value={this.state.query} | ||||
| 						/> | ||||
| 						<TouchableHighlight onPress={() => this.clearButton_press() }> | ||||
| 							<Icon name='md-close-circle' style={styles.clearIcon} /> | ||||
| 						</TouchableHighlight> | ||||
| 					</View> | ||||
|  | ||||
| 					<FlatList | ||||
| 						data={this.state.notes} | ||||
| 						keyExtractor={(item, index) => item.id} | ||||
| 						renderItem={(event) => <NoteItem | ||||
| 							note={event.item} | ||||
| 						/>} | ||||
| 					/> | ||||
| 				</View> | ||||
| 			</View> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| const SearchScreen = connect( | ||||
| 	(state) => { | ||||
| 		return { | ||||
| 			query: state.searchQuery, | ||||
| 		}; | ||||
| 	} | ||||
| )(SearchScreenComponent) | ||||
|  | ||||
| export { SearchScreen }; | ||||
| @@ -130,8 +130,6 @@ class SideMenuContentComponent extends Component { | ||||
|  | ||||
| 		if (items.length) items.push(<View style={{ height: 30, flex: -1 }} key='divider_1'></View>); // DIVIDER | ||||
|  | ||||
| 		const syncTitle = this.props.syncStarted ? _('Cancel sync') : _('Synchronize'); | ||||
|  | ||||
| 		let lines = Synchronizer.reportToLines(this.props.syncReport); | ||||
| 		const syncReportText = lines.join("\n"); | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import { FoldersScreen } from 'lib/components/screens/folders.js' | ||||
| import { LogScreen } from 'lib/components/screens/log.js' | ||||
| import { StatusScreen } from 'lib/components/screens/status.js' | ||||
| import { WelcomeScreen } from 'lib/components/screens/welcome.js' | ||||
| import { SearchScreen } from 'lib/components/screens/search.js' | ||||
| import { OneDriveLoginScreen } from 'lib/components/screens/onedrive-login.js' | ||||
| import { Setting } from 'lib/models/setting.js' | ||||
| import { MenuContext } from 'react-native-popup-menu'; | ||||
| @@ -52,6 +53,7 @@ let defaultState = { | ||||
| 	}, | ||||
| 	syncStarted: false, | ||||
| 	syncReport: {}, | ||||
| 	searchQuery: '', | ||||
| }; | ||||
|  | ||||
| const initialRoute = { | ||||
| @@ -298,6 +300,11 @@ const reducer = (state = defaultState, action) => { | ||||
| 				newState.syncReport = action.report; | ||||
| 				break; | ||||
|  | ||||
| 			case 'SEARCH_QUERY': | ||||
|  | ||||
| 				newState = Object.assign({}, state); | ||||
| 				newState.searchQuery = action.query.trim(); | ||||
|  | ||||
| 		} | ||||
| 	} catch (error) { | ||||
| 		error.message = 'In reducer: ' + error.message; | ||||
| @@ -420,6 +427,12 @@ async function initialize(dispatch, backButtonHandler) { | ||||
| 		reg.scheduleSync(1); | ||||
| 	}, 1000 * 60 * 5); | ||||
|  | ||||
| 	if (Setting.value('env') == 'dev') { | ||||
|  | ||||
| 	} else { | ||||
| 		reg.scheduleSync(); | ||||
| 	} | ||||
|  | ||||
| 	initializationState_ = 'done'; | ||||
|  | ||||
| 	reg.logger().info('Application initialized'); | ||||
| @@ -434,11 +447,6 @@ class AppComponent extends React.Component { | ||||
|  | ||||
| 	async componentDidMount() { | ||||
| 		await initialize(this.props.dispatch, this.backButtonHandler.bind(this)); | ||||
| 		if (Setting.value('env') == 'dev') { | ||||
|  | ||||
| 		} else { | ||||
| 			reg.scheduleSync(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	backButtonHandler() { | ||||
| @@ -481,6 +489,7 @@ class AppComponent extends React.Component { | ||||
| 			OneDriveLogin: { screen: OneDriveLoginScreen }, | ||||
| 			Log: { screen: LogScreen }, | ||||
| 			Status: { screen: StatusScreen }, | ||||
| 			Search: { screen: SearchScreen }, | ||||
| 		}; | ||||
|  | ||||
| 		return ( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user