diff --git a/client/components/breadcrumb.scss b/client/components/breadcrumb.scss index 71800b32..4ce18734 100644 --- a/client/components/breadcrumb.scss +++ b/client/components/breadcrumb.scss @@ -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; diff --git a/client/components/notification.js b/client/components/notification.js index 3d6b1971..ba7f0d8c 100644 --- a/client/components/notification.js +++ b/client/components/notification.js @@ -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 ( - - + +
{ this.state.message_text }
-
X
+
X
-
-
+ + ); } } + + + + +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; +} diff --git a/client/components/notification.scss b/client/components/notification.scss index 86069670..769d6c54 100644 --- a/client/components/notification.scss +++ b/client/components/notification.scss @@ -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; + } +} diff --git a/client/helpers/ajax.js b/client/helpers/ajax.js index 7203280d..033b3381 100644 --- a/client/helpers/ajax.js +++ b/client/helpers/ajax.js @@ -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(); diff --git a/client/model/files.js b/client/model/files.js index 9bc3ad7f..bda63d96 100644 --- a/client/model/files.js +++ b/client/model/files.js @@ -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; diff --git a/client/pages/filespage.js b/client/pages/filespage.js index bbc82ab0..9bd54fc6 100644 --- a/client/pages/filespage.js +++ b/client/pages/filespage.js @@ -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) diff --git a/client/pages/filespage/thing.scss b/client/pages/filespage/thing.scss index 09078eae..6d87b9a8 100644 --- a/client/pages/filespage/thing.scss +++ b/client/pages/filespage/thing.scss @@ -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{ diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js index e2ad664a..228b0548 100644 --- a/client/pages/viewerpage.js +++ b/client/pages/viewerpage.js @@ -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'); }); }