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); |             color: var(--light); | ||||||
|             span.title{ |             span.title{ | ||||||
|                 position: absolute; |                 position: absolute; | ||||||
|                 background: var(--bg-color); |                 background: var(--color); | ||||||
|  |                 color: white; | ||||||
|  |                 font-size: 0.8em; | ||||||
|                 opacity: 0; |                 opacity: 0; | ||||||
|                 transform: translateY(5px); |                 transform: translateY(5px); | ||||||
|                 border-radius: 2px; |                 border-radius: 2px; | ||||||
|  | |||||||
| @ -13,41 +13,26 @@ export class Notification extends React.Component { | |||||||
|             message_text: null, |             message_text: null, | ||||||
|             message_type: 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.runner = new TaskManager(); | ||||||
|  |         this.notification_current = null; | ||||||
|  |         this.notification_is_first = null; | ||||||
|  |         this.notification_is_last = null; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     componentDidMount(){ |     componentDidMount(){ | ||||||
|         notify.subscribe((_message, type) => { |         this.runner.before_run((task, isFirst, isLast) => { | ||||||
|             let job = playMessage.bind(this, { |             this.notification_current = task; | ||||||
|                 text: stringify(_message), |  | ||||||
|                 type: type |  | ||||||
|         }); |         }); | ||||||
|             this.runner.addJob(job); |  | ||||||
|  |         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){ |         function stringify(data){ | ||||||
|             if(typeof data === 'object' && data.message){ |             if(typeof data === 'object' && data.message){ | ||||||
|                 return data.message; |                 return data.message; | ||||||
| @ -56,52 +41,134 @@ export class Notification extends React.Component { | |||||||
|             } |             } | ||||||
|             return JSON.stringify(data); |             return JSON.stringify(data); | ||||||
|         } |         } | ||||||
|         function playMessage(message){ |     } | ||||||
|             const displayMessage = (message) => { |  | ||||||
|  |     closeNotification(){ | ||||||
|  |         return new Promise((done ,err) => { | ||||||
|  |             this.setState({ | ||||||
|  |                 appear: false | ||||||
|  |             }, done); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     openNotification(message){ | ||||||
|  |         return new Promise((done ,err) => { | ||||||
|             this.setState({ |             this.setState({ | ||||||
|                 appear: true, |                 appear: true, | ||||||
|                 message_text: message.text, |                 message_text: message.text, | ||||||
|                 message_type: message.type |                 message_type: message.type | ||||||
|  |             }, done); | ||||||
|         }); |         }); | ||||||
|                 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(){ |     cancelAnimation(){ | ||||||
|         this.setState({ appear: false }); |         return this.notification_current.cancel(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(){ |     render(){ | ||||||
|         return ( |         return ( | ||||||
|             <NgIf cond={this.state.appear === true} className="component_notification no-select"> |             <ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={100} transitionAppear={false} className="component_notification"> | ||||||
|               <ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={500}> |               <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={"component_notification--container "+(this.state.message_type || 'info')}> | ||||||
|                   <div className="message"> |                   <div className="message"> | ||||||
|                     { this.state.message_text } |                     { this.state.message_text } | ||||||
|                   </div> |                   </div> | ||||||
|                   <div className="close" onClick={this.close.bind(this)}>X</div> |                   <div className="close" onClick={this.cancelAnimation.bind(this)}>X</div> | ||||||
|                 </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{ | .component_notification{ | ||||||
|     position: fixed; |     position: fixed; | ||||||
|     bottom: 25px; |     bottom: 20px; | ||||||
|     left: 25px; |     left: 20px; | ||||||
|     right: 0; |     right: 0; | ||||||
|     font-size: 0.95em; |     font-size: 0.95em; | ||||||
|  |     z-index: 10; | ||||||
|  |  | ||||||
|     .component_notification--container{ |     .component_notification--container{ | ||||||
|         width: 400px; |         width: 400px; | ||||||
|  |  | ||||||
|         text-align: left; |         text-align: left; | ||||||
|         display: inline-block; |         display: inline-block; | ||||||
|         padding: 15px 25px 15px 15px; |         padding: 15px 20px 15px 15px; | ||||||
|         border-radius: 2px; |         border-radius: 2px; | ||||||
|         box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px; |         box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px; | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|  |  | ||||||
|         &.info{ |         &.info{ | ||||||
|             background: rgba(0,0,0,0.6); |             background: var(--color); | ||||||
|             color: white; |             color: white; | ||||||
|         } |         } | ||||||
|         &.error{ |         &.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{ |                 }else{ | ||||||
|                     if(navigator.onLine === false){ |                     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{ |                     }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'){ | export function http_post(url, data, type = 'json'){ | ||||||
|     return new Promise((done, err) => { |     return new Promise((done, err) => { | ||||||
|         var xhr = new XMLHttpRequest(); |         var xhr = new XMLHttpRequest(); | ||||||
|  | |||||||
| @ -27,7 +27,8 @@ class FileSystem{ | |||||||
|                                 window.setTimeout(() => done(), 2000); |                                 window.setTimeout(() => done(), 2000); | ||||||
|                             })) |                             })) | ||||||
|                             .then(() => { |                             .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); |                     fetch_from_http(path); | ||||||
| @ -64,18 +65,18 @@ class FileSystem{ | |||||||
|                 } |                 } | ||||||
|                 // publish |                 // publish | ||||||
|                 cache.put(cache.FILE_PATH, path, {results: response.results}); |                 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) => { |         }).catch((_err) => { | ||||||
|             // TODO: user is in offline mode, notify |             this.obs.next(_err); | ||||||
|             console.log(_err); |             return Promise.reject(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|     _ls_from_cache(path, _record_access = false){ |     _ls_from_cache(path, _record_access = false){ | ||||||
|         return cache.get(cache.FILE_PATH, path, _record_access).then((_files) => { |         return cache.get(cache.FILE_PATH, path, _record_access).then((_files) => { | ||||||
|             if(_files && _files.results){ |             if(_files && _files.results){ | ||||||
|                 if(this.current_path === path){ |                 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(); |             return Promise.resolve(); | ||||||
| @ -353,4 +354,3 @@ class FileSystem{ | |||||||
|  |  | ||||||
|  |  | ||||||
| export const Files = new 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.resetHeight = debounce(this.resetHeight.bind(this), 100); | ||||||
|         this.goToFiles = goToFiles.bind(null, this.props.history); |         this.goToFiles = goToFiles.bind(null, this.props.history); | ||||||
|         this.goToViewer = goToViewer.bind(null, this.props.history); |         this.goToViewer = goToViewer.bind(null, this.props.history); | ||||||
|         this.observers = {ls: null}; |         this.observers = []; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     componentDidMount(){ |     componentDidMount(){ | ||||||
| @ -50,7 +50,7 @@ export class FilesPage extends React.Component { | |||||||
|         this.props.unsubscribe('file.delete'); |         this.props.unsubscribe('file.delete'); | ||||||
|         this.props.unsubscribe('file.refresh'); |         this.props.unsubscribe('file.refresh'); | ||||||
|         window.removeEventListener("resize", this.resetHeight); |         window.removeEventListener("resize", this.resetHeight); | ||||||
|         if(this.observers.ls) this.observers.ls.unsubscribe(); |         this._cleanupListeners(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     componentWillReceiveProps(nextProps){ |     componentWillReceiveProps(nextProps){ | ||||||
| @ -72,21 +72,36 @@ export class FilesPage extends React.Component { | |||||||
|  |  | ||||||
|     onRefresh(path = this.state.path){ |     onRefresh(path = this.state.path){ | ||||||
|         this.resetHeight(); |         this.resetHeight(); | ||||||
|         if(this.observers.ls) this.observers.ls.unsubscribe(); |         this._cleanupListeners(); | ||||||
|         this.observers.ls = Files.ls(path).subscribe((files) => { |         const observer = Files.ls(path).subscribe((res) => { | ||||||
|  |             if(res.status === 'ok'){ | ||||||
|  |                 let files = res.results; | ||||||
|                 files = files.map((file) => { |                 files = files.map((file) => { | ||||||
|                     let path = this.state.path+file.name; |                     let path = this.state.path+file.name; | ||||||
|                     file.link = file.type === "file" ? "/view"+path : "/files"+path+"/"; |                     file.link = file.type === "file" ? "/view"+path : "/files"+path+"/"; | ||||||
|                     return file; |                     return file; | ||||||
|                 }); |                 }); | ||||||
|                 this.setState({files: files, loading: false}); |                 this.setState({files: files, loading: false}); | ||||||
|  |             }else{ | ||||||
|  |                 notify.send(res, 'error'); | ||||||
|  |             } | ||||||
|         }, (error) => { |         }, (error) => { | ||||||
|             notify.send(error, 'error'); |             notify.send(error, 'error'); | ||||||
|             this.setState({error: error}); |             this.setState({error: error}); | ||||||
|         }); |         }); | ||||||
|  |         this.observers.push(observer); | ||||||
|         this.setState({error: false}); |         this.setState({error: false}); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     _cleanupListeners(){ | ||||||
|  |         if(this.observers.length > 0) { | ||||||
|  |             this.observers = this.observers.filter((observer) => { | ||||||
|  |                 observer.unsubscribe(); | ||||||
|  |                 return false; | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     onCreate(path, type, file){ |     onCreate(path, type, file){ | ||||||
|         if(type === 'file'){ |         if(type === 'file'){ | ||||||
|             return Files.touch(path, file) |             return Files.touch(path, file) | ||||||
|  | |||||||
| @ -40,9 +40,22 @@ | |||||||
|         padding: 0 5px; |         padding: 0 5px; | ||||||
|         line-height: 22px; |         line-height: 22px; | ||||||
|         white-space: nowrap; |         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{ |     form{ | ||||||
|         display: inline; |         display: inline-block; | ||||||
|  |         input{ | ||||||
|  |             border-width: 0px; | ||||||
|  |             padding: 0 2px 0 2px; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .component_icon{ |     .component_icon{ | ||||||
|  | |||||||
| @ -85,10 +85,9 @@ export class ViewerPage extends React.Component { | |||||||
|                 this.setState({needSaving: false}); |                 this.setState({needSaving: false}); | ||||||
|             }) |             }) | ||||||
|             .catch((err) => { |             .catch((err) => { | ||||||
|                 if(err && err.code === 'CANCELLED'){ |                 if(err && err.code === 'CANCELLED'){ return; } | ||||||
|                     notify.send(err, 'error'); |  | ||||||
|                 } |  | ||||||
|                 this.setState({isSaving: false}); |                 this.setState({isSaving: false}); | ||||||
|  |                 notify.send(err, 'error'); | ||||||
|             }); |             }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Mickael KERJEAN
					Mickael KERJEAN