shiori/view/index.html
2018-05-21 13:28:08 +07:00

396 lines
No EOL
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="/css/fontawesome.css">
<link rel="stylesheet" href="/css/source-sans-pro.css">
<link rel="stylesheet" href="/css/yla-dialog.css">
<link rel="stylesheet" href="/css/yla-tooltip.css">
<link rel="stylesheet" href="/css/stylesheet.css">
<script src="/js/vue.js"></script>
<script src="/js/axios.js"></script>
<script src="/js/js-cookie.js"></script>
<script src="/js/component/yla-tooltip.js"></script>
<script src="/js/component/yla-dialog.js"></script>
<script src="/js/page/base.js"></script>
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/res/favicon-16x16.png" sizes="16x16" />
<title>Shiori - Bookmarks Manager</title>
</head>
<body>
<div id="index-page">
<div id="sidebar">
<p id="logo"></p>
<yla-tooltip placement="right" content="Reload data">
<a @click="reloadData">
<i class="fas fa-sync-alt fa-fw" :class="loading && 'fa-spin'"></i>
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Add new bookmark">
<a @click="showDialogAdd">
<i class="fas fa-plus fa-fw"></i>
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Batch edit">
<a>
<i class="fas fa-pencil-alt fa-fw"></i>
</a>
</yla-tooltip>
<div class="spacer"></div>
<yla-tooltip placement="right" content="Toggle night mode">
<a>
<i class="fas fa-moon fa-fw"></i>
</a>
</yla-tooltip>
<yla-tooltip placement="right" content="Log out">
<a>
<i class="fas fa-sign-out-alt fa-fw"></i>
</a>
</yla-tooltip>
</div>
<div id="body">
<div id="header">
<input type="text" placeholder="Search bookmarks by url, tags, title or content">
<a>
<i class="fas fa-search fa-fw"></i>
</a>
</div>
<div id="grid">
<div class="pagination-box" v-if="maxPage > 0">
<p>Page</p>
<input type="text" placeholder="1" :value="page+1" @focus="$event.target.select()" @keyup.enter="changePage($event.target.value-1)">
<p>{{maxPage+1}}</p>
<div class="spacer"></div>
<a v-if="page > 1" title="Go to first page" @click="changePage(0)">
<i class="fas fa-fw fa-angle-double-left"></i>
</a>
<a v-if="page > 0" title="Go to previous page" @click="changePage(page-1)">
<i class="fa fa-fw fa-angle-left"></i>
</a>
<a v-if="page < maxPage" title="Go to next page" @click="changePage(page+1)">
<i class="fa fa-fw fa-angle-right"></i>
</a>
<a v-if="page < maxPage - 1" title="Go to last page" @click="changePage(maxPage)">
<i class="fas fa-fw fa-angle-double-right"></i>
</a>
</div>
<div class="bookmark" v-for="(book, idx) in visibleBookmarks">
<a class="bookmark-content" :href="book.hasContent ? '/bookmark/'+book.id : null" :title="book.hasContent ? 'View cache' : null" target="_blank">
<img v-if="book.imageURL !== ''" :src="book.imageURL">
<p class="title">{{book.title}}</p>
<p class="excerpt" v-if="book.imageURL === ''">{{book.excerpt}}</p>
</a>
<div class="bookmark-menu">
<a class="url" title="View original" :href="book.url" target="_blank">
{{getHostname(book.url)}}
</a>
<a title="Edit bookmark" @click="showDialogEdit(idx)">
<i class="fas fa-pencil-alt"></i>
</a>
<a title="Delete bookmark" @click="showDialogDelete([idx])">
<i class="fas fa-trash-alt"></i>
</a>
<a title="Update cache" @click="showDialogUpdateCache([idx])">
<i class="fas fa-cloud-download-alt"></i>
</a>
</div>
</div>
<div class="pagination-box" v-if="maxPage > 0">
<p>Page</p>
<input type="text" placeholder="1" :value="page+1" @focus="$event.target.select()" @keyup.enter="changePage($event.target.value-1)">
<p>{{maxPage+1}}</p>
<div class="spacer"></div>
<a v-if="page > 1" title="Go to first page" @click="changePage(0)">
<i class="fas fa-fw fa-angle-double-left"></i>
</a>
<a v-if="page > 0" title="Go to previous page" @click="changePage(page-1)">
<i class="fa fa-fw fa-angle-left"></i>
</a>
<a v-if="page < maxPage" title="Go to next page" @click="changePage(page+1)">
<i class="fa fa-fw fa-angle-right"></i>
</a>
<a v-if="page < maxPage - 1" title="Go to last page" @click="changePage(maxPage)">
<i class="fas fa-fw fa-angle-double-right"></i>
</a>
</div>
<div id="grid-padding"></div>
</div>
</div>
<yla-dialog v-bind="dialog"></yla-dialog>
</div>
<script>
// Define global variable
var pageSize = 30;
// Prepare axios instance
var token = Cookies.get('token'),
rest = axios.create();
rest.defaults.timeout = 60000;
rest.defaults.headers.common['Authorization'] = 'Bearer ' + token;
// Register Vue component
Vue.component('yla-dialog', new YlaDialog());
Vue.component('yla-tooltip', new YlaTooltip());
new Vue({
el: '#index-page',
mixins: [new Base()],
data: {
loading: false,
bookmarks: [],
page: 0,
maxPage: 0,
},
computed: {
visibleBookmarks() {
var start = this.page * pageSize,
finish = start + pageSize;
return this.bookmarks.slice(start, finish);
}
},
methods: {
loadData() {
if (this.loading) return;
// Fetch data
this.loading = true;
rest.get('/api/bookmarks')
.then((response) => {
this.loading = false;
this.bookmarks = response.data;
this.page = 0;
this.maxPage = Math.ceil(this.bookmarks.length / pageSize) - 1;
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.loading = false;
this.showErrorDialog(errorMsg);
});
},
reloadData() {},
changePage(target) {
target = parseInt(target, 10) || 0;
if (target >= this.maxPage) this.page = this.maxPage;
else if (target <= 0) this.page = 0;
else this.page = target;
document.getElementById('grid').scrollTop = 0;
},
showDialogAdd() {
this.showDialog({
title: 'New Bookmark',
content: 'Create a new bookmark',
fields: [{
name: 'url',
label: 'Url, start with http://...',
}, {
name: 'tags',
label: 'Space separated tags (optional)'
}, {
name: 'title',
label: 'Custom title (optional)'
}, {
name: 'excerpt',
label: 'Custom excerpt (optional)',
type: 'area'
}],
mainText: 'OK',
secondText: 'Cancel',
mainClick: (data) => {
// Prepare tags
var tags = data.tags
.toLowerCase()
.split(/\s+/g).map(tag => {
return {
name: tag
};
});
// Send data
this.dialog.loading = true;
rest.post('/api/bookmarks', {
url: data.url.trim(),
title: data.title.trim(),
excerpt: data.excerpt.trim(),
tags: tags
})
.then((response) => {
this.dialog.loading = false;
this.dialog.visible = false;
this.bookmarks.splice(0, 0, response.data);
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
});
}
});
},
showDialogEdit(idx) {
var book = JSON.parse(JSON.stringify(this.bookmarks[idx])),
strTags = book.tags.map(tag => tag.name).join(' ');
this.showDialog({
title: 'Edit Bookmark',
content: 'Edit the bookmark\'s data',
showLabel: true,
fields: [{
name: 'title',
label: 'Title',
value: book.title,
}, {
name: 'excerpt',
label: 'Excerpt',
type: 'area',
value: book.excerpt,
}, {
name: 'tags',
label: 'Tags',
value: strTags,
}],
mainText: 'OK',
secondText: 'Cancel',
mainClick: (data) => {
// Validate input
if (data.title.trim() === '') return;
// Prepare tags
var tags = data.tags
.toLowerCase()
.split(/\s+/g).map(tag => {
return {
name: tag
};
});
// Set new data
book.title = data.title.trim();
book.excerpt = data.excerpt.trim();
book.tags = tags;
// Send data
this.dialog.loading = true;
rest.put('/api/bookmarks', book)
.then((response) => {
this.dialog.loading = false;
this.dialog.visible = false;
this.bookmarks.splice(idx, 1, response.data);
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
});
}
});
},
showDialogDelete(indices) {
// Check and prepare indices
if (!(indices instanceof Array)) return;
if (indices.length === 0) return;
indices.sort();
// Create title andd content
var title = "Delete Bookmarks",
content = "Delete the selected bookmarks ? This action is irreversible.";
if (indices.length === 1) {
title = "Delete Bookmark";
content = "Are you sure ? This action is irreversible.";
}
// Get list of bookmark ID
var listID = [];
for (var i = 0; i < indices.length; i++) {
listID.push(this.bookmarks[indices[i]].id);
}
// Show dialog
this.showDialog({
title: title,
content: content,
mainText: 'Yes',
secondText: 'No',
mainClick: () => {
this.dialog.loading = true;
rest.delete('/api/bookmarks/', {
data: listID
})
.then((response) => {
this.dialog.loading = false;
this.dialog.visible = false;
for (var i = indices.length - 1; i >= 0; i--) {
this.bookmarks.splice(indices[i], 1);
}
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
});
}
});
},
showDialogUpdateCache(indices) {
// Check and prepare indices
if (!(indices instanceof Array)) return;
if (indices.length === 0) return;
indices.sort();
// Get list of bookmark ID
var listID = [];
for (var i = 0; i < indices.length; i++) {
listID.push(this.bookmarks[indices[i]].id);
}
// Show dialog
this.showDialog({
title: 'Update Cache',
content: 'Update cache for selected bookmarks ? This action is irreversible.',
mainText: 'Yes',
secondText: 'No',
mainClick: () => {
this.dialog.loading = true;
rest.put('/api/cache/', listID)
.then((response) => {
this.dialog.loading = false;
this.dialog.visible = false;
response.data.forEach(book => {
for (var i = 0; i < indices.length; i++) {
var idx = indices[i];
if (book.id === this.bookmarks[idx].id) {
this.bookmarks.splice(idx, 1, book);
break;
}
}
});
})
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
});
}
});
},
getHostname(url) {
parser = document.createElement('a');
parser.href = url;
return parser.hostname.replace(/^www\./g, '');
}
},
mounted() {
this.loadData();
}
});
</script>
</body>
</html>