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) => {
|
|
||||||
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(){
|
closeNotification(){
|
||||||
this.setState({ appear: false });
|
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(){
|
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) => {
|
||||||
files = files.map((file) => {
|
if(res.status === 'ok'){
|
||||||
let path = this.state.path+file.name;
|
let files = res.results;
|
||||||
file.link = file.type === "file" ? "/view"+path : "/files"+path+"/";
|
files = files.map((file) => {
|
||||||
return file;
|
let path = this.state.path+file.name;
|
||||||
});
|
file.link = file.type === "file" ? "/view"+path : "/files"+path+"/";
|
||||||
this.setState({files: files, loading: false});
|
return file;
|
||||||
|
});
|
||||||
|
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