2018-02-11 22:00:56 +08:00
<!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" >
2018-05-18 15:07:15 +08:00
< link rel = "stylesheet" href = "/css/yla-dialog.css" >
< link rel = "stylesheet" href = "/css/yla-tooltip.css" >
< link rel = "stylesheet" href = "/css/stylesheet.css" >
2018-02-11 22:00:56 +08:00
< script src = "/js/vue.js" > < / script >
< script src = "/js/axios.js" > < / script >
2018-02-22 17:48:36 +08:00
< script src = "/js/js-cookie.js" > < / script >
2018-05-18 15:07:15 +08:00
< script src = "/js/component/yla-tooltip.js" > < / script >
< script src = "/js/component/yla-dialog.js" > < / script >
< script src = "/js/page/base.js" > < / script >
2018-02-26 14:43:17 +08:00
< 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" / >
2018-02-11 22:00:56 +08:00
< title > Shiori - Bookmarks Manager< / title >
< / head >
< body >
2018-05-21 21:26:18 +08:00
< div id = "index-page" :class = "{night: nightMode}" >
2018-05-18 15:07:15 +08:00
< div id = "sidebar" >
< p id = "logo" > 栞< / p >
< yla-tooltip placement = "right" content = "Reload data" >
2018-05-21 14:28:08 +08:00
< a @ click = "reloadData" >
< i class = "fas fa-sync-alt fa-fw" :class = "loading && 'fa-spin'" > < / i >
2018-05-18 15:07:15 +08:00
< / 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 >
2018-05-21 17:37:48 +08:00
< yla-tooltip placement = "right" :content = "listView ? 'Switch to grid view' : 'Switch to list view'" >
< a @ click = "toggleListView" >
< i class = "fas fa-fw" :class = "listView ? 'fa-th-large' : 'fa-th-list'" > < / i >
< / a >
< / yla-tooltip >
2018-05-18 15:07:15 +08:00
< yla-tooltip placement = "right" content = "Toggle night mode" >
2018-05-21 21:26:18 +08:00
< a @ click = "toggleNightMode" >
< i class = "fas fa-fw" :class = "nightMode ? 'fa-moon' : 'fa-sun'" > < / i >
2018-05-18 15:07:15 +08:00
< / a >
< / yla-tooltip >
< yla-tooltip placement = "right" content = "Log out" >
< a >
< i class = "fas fa-sign-out-alt fa-fw" > < / i >
< / a >
< / yla-tooltip >
2018-02-11 22:00:56 +08:00
< / div >
2018-05-18 15:07:15 +08:00
< div id = "body" >
< div id = "header" >
2018-05-21 15:03:08 +08:00
< input type = "text" v-model . trim = "search" placeholder = "Search bookmarks by url, tags, title or content" @ focus = "$event.target.select()" @ keyup . enter = "loadData" >
< a title = "Search" @ click = "loadData" >
2018-05-18 15:07:15 +08:00
< i class = "fas fa-search fa-fw" > < / i >
< / a >
2018-02-14 12:41:43 +08:00
< / div >
2018-05-21 17:37:48 +08:00
< div id = "content" ref = "content" >
< div id = "grid" :class = "{list: listView}" >
< 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 >
2018-05-18 15:07:15 +08:00
< / a >
2018-05-21 17:37:48 +08:00
< a v-if = "page > 0" title = "Go to previous page" @ click = "changePage(page-1)" >
< i class = "fa fa-fw fa-angle-left" > < / i >
2018-05-18 15:07:15 +08:00
< / a >
2018-05-21 17:37:48 +08:00
< a v-if = "page < maxPage" title = "Go to next page" @ click = "changePage(page+1)" >
< i class = "fa fa-fw fa-angle-right" > < / i >
2018-05-18 15:07:15 +08:00
< / a >
2018-05-21 17:37:48 +08:00
< a v-if = "page < maxPage - 1" title = "Go to last page" @ click = "changePage(maxPage)" >
< i class = "fas fa-fw fa-angle-double-right" > < / i >
2018-05-18 15:07:15 +08:00
< / a >
< / div >
2018-05-21 17:37:48 +08:00
< 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 >
2018-03-10 11:39:38 +08:00
< / div >
< / div >
< / div >
2018-05-18 15:07:15 +08:00
< yla-dialog v-bind = "dialog" > < / yla-dialog >
2018-02-11 22:00:56 +08:00
< / div >
< script >
2018-05-21 14:28:08 +08:00
// Define global variable
var pageSize = 30;
2018-05-18 15:07:15 +08:00
// Prepare axios instance
2018-02-22 17:48:36 +08:00
var token = Cookies.get('token'),
2018-05-18 15:07:15 +08:00
rest = axios.create();
2018-02-22 17:48:36 +08:00
2018-05-18 17:18:38 +08:00
rest.defaults.timeout = 60000;
2018-05-18 15:07:15 +08:00
rest.defaults.headers.common['Authorization'] = 'Bearer ' + token;
2018-02-11 22:00:56 +08:00
2018-05-18 15:07:15 +08:00
// Register Vue component
Vue.component('yla-dialog', new YlaDialog());
Vue.component('yla-tooltip', new YlaTooltip());
new Vue({
el: '#index-page',
mixins: [new Base()],
2018-02-11 22:00:56 +08:00
data: {
2018-02-23 15:30:58 +08:00
loading: false,
2018-02-12 22:06:53 +08:00
bookmarks: [],
2018-05-21 15:03:08 +08:00
search: '',
2018-05-21 14:28:08 +08:00
page: 0,
maxPage: 0,
2018-05-21 17:37:48 +08:00
listView: false,
nightMode: false
2018-05-21 14:28:08 +08:00
},
computed: {
visibleBookmarks() {
var start = this.page * pageSize,
finish = start + pageSize;
return this.bookmarks.slice(start, finish);
}
2018-02-11 22:00:56 +08:00
},
methods: {
2018-05-18 15:07:15 +08:00
loadData() {
2018-02-24 15:01:52 +08:00
if (this.loading) return;
2018-05-21 15:03:08 +08:00
// Parse search query
var rxTags = /(^|\s+)#(\S+)/g,
keyword = this.search.replace(rxTags, ' ').trim().replace(/\s+/g, ' '),
tags = [];
while ((result = rxTags.exec(this.search)) !== null) {
tags.push(result[2]);
}
2018-02-23 15:30:58 +08:00
// Fetch data
2018-02-12 22:06:53 +08:00
this.loading = true;
2018-05-21 15:03:08 +08:00
rest.get('/api/bookmarks', {
params: {
keyword: keyword,
tags: tags.join(' ')
}
})
2018-05-18 15:07:15 +08:00
.then((response) => {
this.loading = false;
this.bookmarks = response.data;
2018-05-21 14:28:08 +08:00
this.page = 0;
this.maxPage = Math.ceil(this.bookmarks.length / pageSize) - 1;
2018-05-21 17:37:48 +08:00
this.$refs.content.scrollTop = 0;
2018-02-12 22:06:53 +08:00
})
2018-05-18 15:07:15 +08:00
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.loading = false;
this.showErrorDialog(errorMsg);
2018-02-14 12:41:43 +08:00
});
},
2018-05-21 17:37:48 +08:00
reloadData() {
if (this.loading) return;
this.search = '';
this.loadData();
},
2018-05-21 14:28:08 +08:00
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;
2018-05-21 17:37:48 +08:00
this.$refs.content.scrollTop = 0;
},
toggleListView() {
this.listView = !this.listView;
this.$refs.content.scrollTop = 0;
localStorage.setItem('shiori-list-view', this.listView ? '1' : '0');
2018-05-21 14:28:08 +08:00
},
2018-05-21 21:26:18 +08:00
toggleNightMode() {
this.nightMode = !this.nightMode;
localStorage.setItem('shiori-night-mode', this.nightMode ? '1' : '0');
},
2018-05-18 15:07:15 +08:00
showDialogAdd() {
this.showDialog({
title: 'New Bookmark',
2018-05-19 14:36:51 +08:00
content: 'Create a new bookmark',
2018-05-18 15:07:15 +08:00
fields: [{
name: 'url',
2018-05-19 14:36:51 +08:00
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'
2018-05-18 15:07:15 +08:00
}],
mainText: 'OK',
secondText: 'Cancel',
mainClick: (data) => {
2018-05-19 14:36:51 +08:00
// Prepare tags
var tags = data.tags
.toLowerCase()
.split(/\s+/g).map(tag => {
return {
name: tag
};
});
// Send data
2018-05-18 15:07:15 +08:00
this.dialog.loading = true;
rest.post('/api/bookmarks', {
2018-05-19 14:36:51 +08:00
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);
});
}
});
},
2018-05-19 16:28:17 +08:00
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,
2018-05-19 17:11:18 +08:00
}, {
name: 'tags',
label: 'Tags',
value: strTags,
2018-05-19 16:28:17 +08:00
}],
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);
});
}
});
},
2018-05-19 14:36:51 +08:00
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 + + ) {
2018-05-19 23:43:15 +08:00
listID.push(this.bookmarks[indices[i]].id);
2018-05-19 14:36:51 +08:00
}
// Show dialog
this.showDialog({
title: title,
content: content,
mainText: 'Yes',
secondText: 'No',
mainClick: () => {
this.dialog.loading = true;
rest.delete('/api/bookmarks/', {
data: listID
2018-03-07 16:51:47 +08:00
})
2018-05-18 15:07:15 +08:00
.then((response) => {
this.dialog.loading = false;
this.dialog.visible = false;
2018-05-19 14:36:51 +08:00
for (var i = indices.length - 1; i >= 0; i--) {
this.bookmarks.splice(indices[i], 1);
}
2018-03-07 16:51:47 +08:00
})
2018-05-18 15:07:15 +08:00
.catch((error) => {
var errorMsg = (error.response ? error.response.data : error.message).trim();
this.showErrorDialog(errorMsg);
2018-03-07 16:51:47 +08:00
});
2018-05-18 15:07:15 +08:00
}
2018-02-13 17:14:08 +08:00
});
2018-05-18 15:07:15 +08:00
},
2018-05-19 17:11:18 +08:00
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 + + ) {
2018-05-19 23:43:15 +08:00
listID.push(this.bookmarks[indices[i]].id);
2018-05-19 17:11:18 +08:00
}
// 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);
});
}
});
},
2018-05-18 15:07:15 +08:00
getHostname(url) {
parser = document.createElement('a');
parser.href = url;
2018-05-19 14:36:51 +08:00
return parser.hostname.replace(/^www\./g, '');
2018-02-13 17:14:08 +08:00
}
},
2018-05-18 15:07:15 +08:00
mounted() {
2018-05-21 17:37:48 +08:00
// Read config from local storage
var listView = localStorage.getItem('shiori-list-view'),
nightMode = localStorage.getItem('shiori-night-mode');
this.listView = listView === '1';
this.nightMode = nightMode === '1';
// Load data
2018-02-11 22:00:56 +08:00
this.loadData();
}
2018-05-18 15:07:15 +08:00
});
2018-02-11 22:00:56 +08:00
< / script >
< / body >
2018-03-10 11:39:38 +08:00
< / html >