mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-11-01 02:43:35 +08:00 
			
		
		
		
	feature (notification): inform user of anything happenning
This commit is contained in:
		| @ -39,7 +39,9 @@ | ||||
|             color: var(--light); | ||||
|             span.title{ | ||||
|                 position: absolute; | ||||
|                 background: var(--bg-color); | ||||
|                 background: var(--color); | ||||
|                 color: white; | ||||
|                 font-size: 0.8em; | ||||
|                 opacity: 0; | ||||
|                 transform: translateY(5px); | ||||
|                 border-radius: 2px; | ||||
|  | ||||
| @ -13,41 +13,26 @@ export class Notification extends React.Component { | ||||
|             message_text: null, | ||||
|             message_type: null | ||||
|         }; | ||||
|  | ||||
|         function TaskManager(){ | ||||
|             let jobs = []; | ||||
|             let is_running = false; | ||||
|  | ||||
|             const ret = { | ||||
|                 addJob: (job) => { | ||||
|                     jobs.push(job); | ||||
|                     if(is_running === false){ | ||||
|                         is_running = true; | ||||
|                         ret._executor(); | ||||
|                     } | ||||
|                 }, | ||||
|                 _executor: () => { | ||||
|                     let job = jobs.shift(); | ||||
|                     if(!job){ | ||||
|                         is_running = false; | ||||
|                         return Promise.resolve(); | ||||
|                     } | ||||
|                     return job().then(ret._executor); | ||||
|                 } | ||||
|             }; | ||||
|             return ret; | ||||
|         } | ||||
|         this.runner = new TaskManager(); | ||||
|         this.notification_current = null; | ||||
|         this.notification_is_first = null; | ||||
|         this.notification_is_last = null; | ||||
|     } | ||||
|  | ||||
|     componentDidMount(){ | ||||
|         notify.subscribe((_message, type) => { | ||||
|             let job = playMessage.bind(this, { | ||||
|                 text: stringify(_message), | ||||
|                 type: type | ||||
|             }); | ||||
|             this.runner.addJob(job); | ||||
|         this.runner.before_run((task, isFirst, isLast) => { | ||||
|             this.notification_current = task; | ||||
|         }); | ||||
|  | ||||
|         notify.subscribe((message, type) => { | ||||
|             this.runner.addTask(Task( | ||||
|                 this.openNotification.bind(this, {text: stringify(message), type: type}), | ||||
|                 this.closeNotification.bind(this), | ||||
|                 8000, | ||||
|                 500 | ||||
|             )); | ||||
|         }); | ||||
|  | ||||
|         function stringify(data){ | ||||
|             if(typeof data === 'object' && data.message){ | ||||
|                 return data.message; | ||||
| @ -56,52 +41,134 @@ export class Notification extends React.Component { | ||||
|             } | ||||
|             return JSON.stringify(data); | ||||
|         } | ||||
|         function playMessage(message){ | ||||
|             const displayMessage = (message) => { | ||||
|                 this.setState({ | ||||
|                     appear: true, | ||||
|                     message_text: message.text, | ||||
|                     message_type: message.type | ||||
|                 }); | ||||
|                 return Promise.resolve(message); | ||||
|             }; | ||||
|             const waitForABit = (timeout, message) => { | ||||
|                 return new Promise((done, err) => { | ||||
|                     window.setTimeout(() => { | ||||
|                         done(message); | ||||
|                     }, timeout); | ||||
|                 }); | ||||
|             }; | ||||
|             const hideMessage = (message) => { | ||||
|                 this.setState({ | ||||
|                     appear: false | ||||
|                 }); | ||||
|                 return Promise.resolve(message); | ||||
|             }; | ||||
|  | ||||
|             return displayMessage(message) | ||||
|                 .then(waitForABit.bind(this, 5000)) | ||||
|                 .then(hideMessage) | ||||
|                 .then(waitForABit.bind(this, 1000)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     close(){ | ||||
|         this.setState({ appear: false }); | ||||
|     closeNotification(){ | ||||
|         return new Promise((done ,err) => { | ||||
|             this.setState({ | ||||
|                 appear: false | ||||
|             }, done); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     openNotification(message){ | ||||
|         return new Promise((done ,err) => { | ||||
|             this.setState({ | ||||
|                 appear: true, | ||||
|                 message_text: message.text, | ||||
|                 message_type: message.type | ||||
|             }, done); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     cancelAnimation(){ | ||||
|         return this.notification_current.cancel(); | ||||
|     } | ||||
|  | ||||
|     render(){ | ||||
|         return ( | ||||
|             <NgIf cond={this.state.appear === true} className="component_notification no-select"> | ||||
|               <ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={500}> | ||||
|             <ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={100} transitionAppear={false} className="component_notification"> | ||||
|               <NgIf key={this.state.message_text+this.state.message_type+this.state.appear} cond={this.state.appear === true} className="no-select"> | ||||
|                 <div className={"component_notification--container "+(this.state.message_type || 'info')}> | ||||
|                   <div className="message"> | ||||
|                     { this.state.message_text } | ||||
|                   </div> | ||||
|                   <div className="close" onClick={this.close.bind(this)}>X</div> | ||||
|                   <div className="close" onClick={this.cancelAnimation.bind(this)}>X</div> | ||||
|                 </div> | ||||
|               </ReactCSSTransitionGroup> | ||||
|             </NgIf> | ||||
|               </NgIf> | ||||
|             </ReactCSSTransitionGroup> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| function TaskManager(){ | ||||
|     let tasks = []; | ||||
|     let is_running = false; | ||||
|     let subscriber = null; | ||||
|     let current_task = null; | ||||
|     let is_first = null; | ||||
|     let is_last = null; | ||||
|  | ||||
|     const ret ={ | ||||
|         addTask: function(task){ | ||||
|             current_task && current_task.cancel(); | ||||
|             tasks.push(task); | ||||
|             if(is_running === false){ | ||||
|                 is_running = true; | ||||
|                 ret._run(); | ||||
|             } | ||||
|         }, | ||||
|         before_run: function(fn){ | ||||
|             subscriber = fn; | ||||
|         }, | ||||
|         _run: function(){ | ||||
|             current_task = tasks.shift(); | ||||
|             is_last = tasks.length === 0; | ||||
|             if(!current_task){ | ||||
|                 is_running = false; | ||||
|                 return Promise.resolve(); | ||||
|             }else{ | ||||
|                 const mode = tasks.length > 0 ? 'minimal' : 'normal'; | ||||
|                 subscriber(current_task, mode); | ||||
|                 return current_task.run(mode).then(ret._run); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| function Task(_runCallback, _finishCallback, wait_time_before_finish, minimum_running_time){ | ||||
|     let start_date = null; | ||||
|     let done = null; | ||||
|     let promise = new Promise((_done) => { done = _done; }); | ||||
|     let timeout = null; | ||||
|  | ||||
|     const ret = { | ||||
|         run: function(mode = 'normal'){ | ||||
|             const wait = mode === 'minimal' ? minimum_running_time : wait_time_before_finish; | ||||
|             start_date = new Date(); | ||||
|  | ||||
|             new Promise((_done, err) => { | ||||
|                 timeout = window.setTimeout(() => { | ||||
|                     _done(); | ||||
|                 }, 200); | ||||
|             }) | ||||
|                 .then(_runCallback) | ||||
|                 .then(() => new Promise((_done, err) => { | ||||
|                     timeout = window.setTimeout(() => { | ||||
|                         _done(); | ||||
|                     }, wait); | ||||
|                 })) | ||||
|                 .then(() => { | ||||
|                     ret._complete(); | ||||
|                 }); | ||||
|             return promise; | ||||
|         }, | ||||
|         cancel: function(){ | ||||
|             window.clearTimeout(timeout); | ||||
|             timeout = null; | ||||
|             let elapsed_time = new Date() - start_date; | ||||
|  | ||||
|             if(elapsed_time < minimum_running_time){ | ||||
|                 window.setTimeout(() => { | ||||
|                     ret._complete(); | ||||
|                 }, minimum_running_time - elapsed_time); | ||||
|             }else{ | ||||
|                 ret._complete(); | ||||
|             } | ||||
|             return promise; | ||||
|         }, | ||||
|         _complete: function(){ | ||||
|             if(done){ | ||||
|                 _finishCallback(); | ||||
|                 done(); | ||||
|             } | ||||
|             done = null; | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
|     }; | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| @ -1,23 +1,23 @@ | ||||
| .component_notification{ | ||||
|     position: fixed; | ||||
|     bottom: 25px; | ||||
|     left: 25px; | ||||
|     bottom: 20px; | ||||
|     left: 20px; | ||||
|     right: 0; | ||||
|     font-size: 0.95em; | ||||
|     z-index: 10; | ||||
|  | ||||
|     .component_notification--container{ | ||||
|         width: 400px; | ||||
|  | ||||
|         text-align: left; | ||||
|         display: inline-block; | ||||
|         padding: 15px 25px 15px 15px; | ||||
|         padding: 15px 20px 15px 15px; | ||||
|         border-radius: 2px; | ||||
|         box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px; | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|  | ||||
|         &.info{ | ||||
|             background: rgba(0,0,0,0.6); | ||||
|             background: var(--color); | ||||
|             color: white; | ||||
|         } | ||||
|         &.error{ | ||||
| @ -40,3 +40,36 @@ | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @media (max-width: 450px){ | ||||
|     .component_notification{ | ||||
|         bottom: 0px; | ||||
|         left: 0px; | ||||
|         .component_notification--container{ | ||||
|             width: 100%; | ||||
|             box-sizing: border-box; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| .component_notification{ | ||||
|     .notification-leave{ | ||||
|         opacity: 1; | ||||
|     } | ||||
|     .notification-leave.notification-leave-active{ | ||||
|         opacity: 0; | ||||
|         transition: opacity 0.2s ease-out; | ||||
|     } | ||||
|  | ||||
|     .notification-enter{ | ||||
|         transform: translateY(50px); | ||||
|         opacity: 0; | ||||
|         display: inline-block; | ||||
|     } | ||||
|     .notification-enter.notification-enter-active{ | ||||
|         opacity: 1; | ||||
|         transform: translateY(0); | ||||
|         transition: all 0.1s ease-out; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -23,9 +23,9 @@ export function http_get(url, type = 'json'){ | ||||
|                     } | ||||
|                 }else{ | ||||
|                     if(navigator.onLine === false){ | ||||
|                         err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'}); | ||||
|                         err({status: xhr.status, code: "CONNECTION_LOST", message: 'Ooups! Looks like your internet has gone away'}); | ||||
|                     }else{ | ||||
|                         err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'}); | ||||
|                         err({status: xhr.status, message: xhr.responseText || 'Oups! Something went wrong'}); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @ -35,7 +35,6 @@ export function http_get(url, type = 'json'){ | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| export function http_post(url, data, type = 'json'){ | ||||
|     return new Promise((done, err) => { | ||||
|         var xhr = new XMLHttpRequest(); | ||||
|  | ||||
| @ -27,7 +27,8 @@ class FileSystem{ | ||||
|                                 window.setTimeout(() => done(), 2000); | ||||
|                             })) | ||||
|                             .then(() => { | ||||
|                                 return keep_pulling_from_http === true? fetch_from_http(_path) : Promise.resolve(); | ||||
|                                 if(keep_pulling_from_http === false) return Promise.resolve(); | ||||
|                                 return fetch_from_http(_path); | ||||
|                             }); | ||||
|                     }; | ||||
|                     fetch_from_http(path); | ||||
| @ -64,18 +65,18 @@ class FileSystem{ | ||||
|                 } | ||||
|                 // publish | ||||
|                 cache.put(cache.FILE_PATH, path, {results: response.results}); | ||||
|                 if(this.current_path === path) this.obs && this.obs.next(response.results); | ||||
|                 if(this.current_path === path) this.obs && this.obs.next({status: 'ok', results: response.results}); | ||||
|             }); | ||||
|         }).catch((_err) => { | ||||
|             // TODO: user is in offline mode, notify | ||||
|             console.log(_err); | ||||
|             this.obs.next(_err); | ||||
|             return Promise.reject(); | ||||
|         }); | ||||
|     } | ||||
|     _ls_from_cache(path, _record_access = false){ | ||||
|         return cache.get(cache.FILE_PATH, path, _record_access).then((_files) => { | ||||
|             if(_files && _files.results){ | ||||
|                 if(this.current_path === path){ | ||||
|                     this.obs && this.obs.next(_files.results); | ||||
|                     this.obs && this.obs.next({status: 'ok', results: _files.results}); | ||||
|                 } | ||||
|             }; | ||||
|             return Promise.resolve(); | ||||
| @ -353,4 +354,3 @@ class FileSystem{ | ||||
|  | ||||
|  | ||||
| export const Files = new FileSystem(); | ||||
| window.Files = Files; | ||||
|  | ||||
| @ -25,7 +25,7 @@ export class FilesPage extends React.Component { | ||||
|         this.resetHeight = debounce(this.resetHeight.bind(this), 100); | ||||
|         this.goToFiles = goToFiles.bind(null, this.props.history); | ||||
|         this.goToViewer = goToViewer.bind(null, this.props.history); | ||||
|         this.observers = {ls: null}; | ||||
|         this.observers = []; | ||||
|     } | ||||
|  | ||||
|     componentDidMount(){ | ||||
| @ -50,7 +50,7 @@ export class FilesPage extends React.Component { | ||||
|         this.props.unsubscribe('file.delete'); | ||||
|         this.props.unsubscribe('file.refresh'); | ||||
|         window.removeEventListener("resize", this.resetHeight); | ||||
|         if(this.observers.ls) this.observers.ls.unsubscribe(); | ||||
|         this._cleanupListeners(); | ||||
|     } | ||||
|  | ||||
|     componentWillReceiveProps(nextProps){ | ||||
| @ -72,21 +72,36 @@ export class FilesPage extends React.Component { | ||||
|  | ||||
|     onRefresh(path = this.state.path){ | ||||
|         this.resetHeight(); | ||||
|         if(this.observers.ls) this.observers.ls.unsubscribe(); | ||||
|         this.observers.ls = Files.ls(path).subscribe((files) => { | ||||
|             files = files.map((file) => { | ||||
|                 let path = this.state.path+file.name; | ||||
|                 file.link = file.type === "file" ? "/view"+path : "/files"+path+"/"; | ||||
|                 return file; | ||||
|             }); | ||||
|             this.setState({files: files, loading: false}); | ||||
|         this._cleanupListeners(); | ||||
|         const observer = Files.ls(path).subscribe((res) => { | ||||
|             if(res.status === 'ok'){ | ||||
|                 let files = res.results; | ||||
|                 files = files.map((file) => { | ||||
|                     let path = this.state.path+file.name; | ||||
|                     file.link = file.type === "file" ? "/view"+path : "/files"+path+"/"; | ||||
|                     return file; | ||||
|                 }); | ||||
|                 this.setState({files: files, loading: false}); | ||||
|             }else{ | ||||
|                 notify.send(res, 'error'); | ||||
|             } | ||||
|         }, (error) => { | ||||
|             notify.send(error, 'error'); | ||||
|             this.setState({error: error}); | ||||
|         }); | ||||
|         this.observers.push(observer); | ||||
|         this.setState({error: false}); | ||||
|     } | ||||
|  | ||||
|     _cleanupListeners(){ | ||||
|         if(this.observers.length > 0) { | ||||
|             this.observers = this.observers.filter((observer) => { | ||||
|                 observer.unsubscribe(); | ||||
|                 return false; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onCreate(path, type, file){ | ||||
|         if(type === 'file'){ | ||||
|             return Files.touch(path, file) | ||||
|  | ||||
| @ -40,9 +40,22 @@ | ||||
|         padding: 0 5px; | ||||
|         line-height: 22px; | ||||
|         white-space: nowrap; | ||||
|         span{ | ||||
|             display: inline-block; | ||||
|             width: calc(100% - 130px); | ||||
|             white-space: nowrap; | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|             vertical-align: bottom; | ||||
|             color: inherit; | ||||
|         } | ||||
|     } | ||||
|     form{ | ||||
|         display: inline; | ||||
|         display: inline-block; | ||||
|         input{ | ||||
|             border-width: 0px; | ||||
|             padding: 0 2px 0 2px; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .component_icon{ | ||||
|  | ||||
| @ -85,10 +85,9 @@ export class ViewerPage extends React.Component { | ||||
|                 this.setState({needSaving: false}); | ||||
|             }) | ||||
|             .catch((err) => { | ||||
|                 if(err && err.code === 'CANCELLED'){ | ||||
|                     notify.send(err, 'error'); | ||||
|                 } | ||||
|                 if(err && err.code === 'CANCELLED'){ return; } | ||||
|                 this.setState({isSaving: false}); | ||||
|                 notify.send(err, 'error'); | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Mickael KERJEAN
					Mickael KERJEAN