shiori/view/index.html

567 lines
26 KiB
HTML
Raw Normal View History

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/stylesheet.css">
<link rel="stylesheet" href="/css/fontawesome.css">
<link rel="stylesheet" href="/css/source-sans-pro.css">
<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-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-02-17 22:49:16 +08:00
<div id="main-page">
2018-02-11 22:00:56 +08:00
<div id="header">
2018-02-17 16:51:43 +08:00
<template v-if="checkedBookmarks.length === 0">
<a id="logo" href="/">
<span></span>shiori</a>
<div id="search-box">
2018-02-23 15:30:58 +08:00
<input type="text" name="keyword" v-model.trim="search.query" @keyup.enter="loadData" placeholder="Search url, tags, title or content">
<a class="button" @click="loadData">
2018-02-17 16:51:43 +08:00
<i class="fas fa-search fa-fw"></i>
</a>
</div>
<div id="header-menu" v-if="!loading">
2018-02-23 15:30:58 +08:00
<a @click="reloadData">
2018-02-25 22:39:46 +08:00
<i class="fas fa-cloud fa-fw"></i>
<span>Reload</span>
2018-02-17 16:51:43 +08:00
</a>
2018-02-17 17:48:55 +08:00
<a @click="toggleImage">
2018-02-25 22:39:46 +08:00
<i class="fas fa-fw" :class="showImage ? 'fa-eye-slash' : 'fa-eye'"></i>
<span>{{showImage ? 'Hide image' : 'Show image'}}</span>
2018-02-17 17:48:55 +08:00
</a>
2018-02-22 17:48:36 +08:00
<a @click="logout">
2018-02-25 22:39:46 +08:00
<i class="fas fa-sign-out-alt fa-fw"></i>
<span>Logout</span>
2018-02-22 17:48:36 +08:00
</a>
2018-02-17 16:51:43 +08:00
</div>
</template>
<template v-else>
<p id="n-selected">{{checkedBookmarks.length}} selected</p>
<div id="header-menu">
<a @click="clearSelectedBookmarks">
2018-02-25 22:39:46 +08:00
<i class="fas fa-fw fa-ban"></i>
<span>Cancel</span>
2018-02-17 16:51:43 +08:00
</a>
<a @click="selectAllBookmarks">
2018-02-25 22:39:46 +08:00
<i class="fas fa-fw fa-check-square"></i>
<span>Select all</span>
2018-02-17 16:51:43 +08:00
</a>
<a @click="deleteBookmarks(checkedBookmarks)">
2018-02-25 22:39:46 +08:00
<i class="fas fa-fw fa-trash"></i>
<span>Delete</span>
2018-02-17 16:51:43 +08:00
</a>
</div>
</template>
2018-02-11 22:00:56 +08:00
</div>
<div id="main">
2018-02-14 19:50:53 +08:00
<template v-if="!loading && error === ''">
2018-02-17 16:51:43 +08:00
<div id="input-bookmark">
2018-02-14 19:50:53 +08:00
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
2018-02-25 22:39:46 +08:00
<input type="text" ref="inputURL" v-model.trim="inputBookmark.url" placeholder="URL for the new bookmark" @focus="clearSelectedBookmarks">
2018-02-14 19:50:53 +08:00
<template v-if="inputBookmark.url !== ''">
2018-02-22 17:48:36 +08:00
<input type="text" v-model.trim="inputBookmark.title" placeholder="Custom bookmark title (optional)">
<input type="text" v-model.trim="inputBookmark.tags" placeholder="Space separated tags for this bookmark (optional)">
<textarea name="excerpt" v-model.trim="inputBookmark.excerpt" placeholder="Excerpt for this bookmark (optional)"></textarea>
2018-02-14 19:50:53 +08:00
<p v-if="inputBookmark.error !== ''" class="error-message">{{inputBookmark.error}}</p>
<div class="button-area">
<div class="spacer"></div>
<a v-if="inputBookmark.loading">
<i class="fas fa-fw fa-spinner fa-spin"></i>
2018-02-12 22:06:53 +08:00
</a>
2018-02-14 19:50:53 +08:00
<template v-else>
<a class="button" @click="clearInputBookmark">Cancel</a>
<a class="button" @click="saveBookmark">Done</a>
</template>
</div>
</template>
</div>
2018-02-23 15:30:58 +08:00
<div v-if="search.query !== '' && !loading" id="search-parameter">
<a v-if="search.keyword !== ''" @click="removeSearchParam(search.keyword)">{{search.keyword}}</a>
<a v-for="tag in search.tags" @click="removeSearchParam('#'+tag)">#{{tag}}</a>
</div>
2018-02-17 16:51:43 +08:00
<div id="grid">
<div v-for="column in gridColumns" class="column" :style="{maxWidth: columnWidth}">
2018-02-17 16:51:43 +08:00
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
<a class="checkbox" @click="toggleBookmarkCheck(item.index)">
2018-02-14 19:50:53 +08:00
<i class="fas fa-check"></i>
2018-02-12 22:06:53 +08:00
</a>
2018-02-17 17:48:55 +08:00
<a class="bookmark-metadata" target="_blank" :class="{'has-image':bookmarkImage(item) !== ''}" :style="bookmarkImage(item)" :href="item.url">
2018-02-14 19:50:53 +08:00
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
<p class="bookmark-title">{{item.title}}</p>
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
2018-02-12 22:06:53 +08:00
</a>
2018-02-14 19:50:53 +08:00
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
<div v-if="item.tags.length > 0" class="bookmark-tags">
2018-02-24 15:01:52 +08:00
<a v-for="tag in item.tags" @click="searchTag(tag.name)">{{tag.name}}</a>
2018-02-14 19:50:53 +08:00
</div>
<div class="bookmark-menu">
2018-02-17 16:51:43 +08:00
<a @click="updateBookmark(item.index)">
2018-02-25 22:39:46 +08:00
<i class="fas fa-sync"></i>
<span>Update</span>
2018-02-17 16:51:43 +08:00
</a>
<a @click="editBookmark(item.index)">
2018-02-25 22:39:46 +08:00
<i class="fas fa-pencil-alt"></i>
<span>Edit</span>
2018-02-14 19:50:53 +08:00
</a>
2018-02-17 16:51:43 +08:00
<a @click="deleteBookmarks([item.index])">
2018-02-25 22:39:46 +08:00
<i class="far fa-trash-alt"></i>
<span>Delete</span>
2018-02-14 19:50:53 +08:00
</a>
2018-02-17 22:49:16 +08:00
<a :href="'/bookmark/'+item.id" target="_blank">
2018-02-25 22:39:46 +08:00
<i class="fas fa-history"></i>
<span>Cache</span>
2018-02-14 19:50:53 +08:00
</a>
</div>
2018-02-11 22:00:56 +08:00
</div>
</div>
2018-02-12 22:06:53 +08:00
</div>
2018-02-14 19:50:53 +08:00
</template>
<div v-if="loading || error !== ''" id="message-bar">
2018-02-12 22:06:53 +08:00
<i v-if="loading" class="fas fa-fw fa-spinner fa-spin"></i>
2018-02-14 19:50:53 +08:00
<p v-if="error !== ''" class="error-message">{{error}}</p>
2018-02-11 22:00:56 +08:00
</div>
</div>
2018-02-14 12:41:43 +08:00
<div v-if="dialog.visible" id="dialog-overlay">
<div id="dialog">
2018-02-14 19:50:53 +08:00
<p id="dialog-title" :class="{'error-message': dialog.isError}">{{dialog.title}}</p>
2018-02-14 12:41:43 +08:00
<p v-html="dialog.content" id="dialog-content"></p>
<div id="dialog-button">
<div class="spacer"></div>
<a v-if="dialog.loading">
<i class="fas fa-fw fa-spinner fa-spin"></i>
</a>
<template v-else>
<a class="button" @click="dialog.secondAction">{{dialog.secondChoice}}</a>
<a class="button" @click="dialog.mainAction">{{dialog.mainChoice}}</a>
</template>
</div>
</div>
</div>
2018-02-11 22:00:56 +08:00
</div>
<script>
2018-02-22 17:48:36 +08:00
var token = Cookies.get('token'),
instance = axios.create();
2018-02-14 19:50:53 +08:00
instance.defaults.timeout = 10000;
2018-02-22 17:48:36 +08:00
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token;
2018-02-11 22:00:56 +08:00
var app = new Vue({
2018-02-17 22:49:16 +08:00
el: '#main-page',
2018-02-11 22:00:56 +08:00
data: {
windowWidth: 0,
2018-02-14 19:50:53 +08:00
error: "",
2018-02-23 15:30:58 +08:00
loading: false,
2018-02-12 22:06:53 +08:00
bookmarks: [],
2018-02-17 16:51:43 +08:00
checkedBookmarks: [],
2018-02-17 17:48:55 +08:00
showImage: true,
2018-02-23 15:30:58 +08:00
search: {
query: "",
keyword: "",
tags: []
},
2018-02-13 17:14:08 +08:00
inputBookmark: {
index: -1,
id: -1,
2018-02-12 22:06:53 +08:00
url: "",
title: "",
tags: "",
excerpt: "",
2018-02-14 19:50:53 +08:00
error: "",
2018-02-12 22:06:53 +08:00
loading: false
2018-02-14 12:41:43 +08:00
},
dialog: {
visible: false,
loading: false,
2018-02-14 19:50:53 +08:00
isError: false,
2018-02-14 12:41:43 +08:00
title: '',
content: '',
mainChoice: '',
secondChoice: '',
mainAction: function () {},
secondAction: function () {}
2018-02-12 22:06:53 +08:00
}
2018-02-11 22:00:56 +08:00
},
methods: {
2018-02-24 15:01:52 +08:00
searchTag: function (tag) {
if (this.loading) return;
var newTag = '#' + tag;
if (this.search.query.indexOf(newTag) === -1) {
this.search.query += ' ' + newTag;
this.search.query = this.search.query.trim().replace(/\s+/g, ' ');
this.loadData();
}
},
2018-02-23 15:30:58 +08:00
removeSearchParam: function (param) {
if (this.loading) return;
this.search.query = this.search.query.replace(param, ' ').trim().replace(/\s+/g, ' ');
this.loadData();
},
reloadData: function () {
if (this.loading) return;
this.search.query = '';
this.loadData();
},
2018-02-11 22:00:56 +08:00
loadData: function () {
2018-02-23 15:30:58 +08:00
if (this.loading) return;
// Parse search query
var rxTags = /(^|\s+)#(\S+)/g,
tags = [];
while ((result = rxTags.exec(this.search.query)) !== null) {
tags.push(result[2]);
}
var keyword = this.search.query.replace(/(^|\s+)#(\S+)/g, ' ').trim().replace(/\s+/g, ' ');
// Fetch data
2018-02-14 19:50:53 +08:00
this.error = '';
2018-02-12 22:06:53 +08:00
this.loading = true;
2018-02-23 15:30:58 +08:00
this.search.tags = tags;
this.search.keyword = keyword;
instance.get('/api/bookmarks', {
params: {
keyword: this.search.keyword,
tags: this.search.tags.join(" ")
}
})
2018-02-11 22:00:56 +08:00
.then(function (response) {
2018-02-12 22:06:53 +08:00
app.loading = false;
2018-02-11 22:00:56 +08:00
app.bookmarks = response.data;
})
.catch(function (error) {
2018-02-14 19:50:53 +08:00
var errorMsg = error.response ? error.response.data : error.message;
2018-02-12 22:06:53 +08:00
app.loading = false;
2018-02-14 19:50:53 +08:00
app.error = errorMsg.trim();
2018-02-12 22:06:53 +08:00
});
},
saveBookmark: function () {
2018-02-13 17:14:08 +08:00
if (this.inputBookmark.loading) return;
this.inputBookmark.loading = true;
2018-02-12 22:06:53 +08:00
2018-02-13 17:14:08 +08:00
if (this.inputBookmark.url === "") return;
2018-02-12 22:06:53 +08:00
2018-02-26 18:00:14 +08:00
var idx = this.inputBookmark.index,
tags = this.inputBookmark.tags.replace(/\s+/g, " "),
newTags = tags === "" ? [] : listTag = tags.split(/\s+/g),
finalTags = [];
2018-02-12 22:06:53 +08:00
2018-02-26 18:00:14 +08:00
if (idx !== -1) {
var oldTags = this.bookmarks[idx].tags;
for (var i = 0; i < oldTags.length; i++) {
if (newTags.indexOf(oldTags[i].name) === -1) {
finalTags.push({
name: '-' + oldTags[i].name
})
}
}
}
for (var i = 0; i < newTags.length; i++) {
finalTags.push({
2018-02-12 22:06:53 +08:00
name: listTag[i]
});
}
2018-02-13 17:14:08 +08:00
instance.request({
method: this.inputBookmark.id === -1 ? 'post' : 'put',
url: '/api/bookmarks',
timeout: 15000,
data: {
id: this.inputBookmark.id,
url: this.inputBookmark.url,
title: this.inputBookmark.title,
excerpt: this.inputBookmark.excerpt,
2018-02-26 18:00:14 +08:00
tags: finalTags
2018-02-13 17:14:08 +08:00
}
2018-02-12 22:06:53 +08:00
})
.then(function (response) {
2018-02-13 17:14:08 +08:00
if (idx === -1) app.bookmarks.unshift(response.data);
2018-02-17 16:51:43 +08:00
else {
app.bookmarks.splice(idx, 1, response.data);
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
}
2018-02-13 17:14:08 +08:00
app.clearInputBookmark();
2018-02-12 22:06:53 +08:00
})
.catch(function (error) {
2018-02-14 19:50:53 +08:00
var errorMsg = error.response ? error.response.data : error.message;
app.inputBookmark.loading = false;
app.inputBookmark.error = errorMsg.trim();
2018-02-11 22:00:56 +08:00
});
},
2018-02-13 17:14:08 +08:00
editBookmark: function (idx) {
var bookmark = this.bookmarks[idx],
tags = [];
for (var i = 0; i < bookmark.tags.length; i++) {
tags.push(bookmark.tags[i].name);
}
this.inputBookmark.index = idx;
this.inputBookmark.id = bookmark.id;
this.inputBookmark.url = bookmark.url;
this.inputBookmark.title = bookmark.title;
this.inputBookmark.tags = tags.join(" ");
this.inputBookmark.excerpt = bookmark.excerpt;
this.$nextTick(function () {
2018-02-17 16:51:43 +08:00
window.scrollTo(0, 0);
2018-02-13 17:14:08 +08:00
app.$refs.inputURL.focus();
});
2018-02-12 22:06:53 +08:00
},
2018-02-17 16:51:43 +08:00
deleteBookmarks: function (indices) {
var title = "Delete Bookmarks",
content = "Delete the selected bookmark(s) ? This action is irreversible.",
smallestIndex = 1;
if (indices.length === 0) return;
else if (indices.length === 1) {
var bookmark = this.bookmarks[indices[0]];
smallestIndex = indices[0];
title = "Delete Bookmark";
content = "Delete <b>\"" + bookmark.title.trim() + "\"</b> from bookmarks ? This action is irreversible.";
} else {
indices.sort();
smallestIndex = indices[indices.length - 1];
}
2018-02-14 12:41:43 +08:00
this.dialog.visible = true;
2018-02-14 19:50:53 +08:00
this.dialog.isError = false;
2018-02-14 12:41:43 +08:00
this.dialog.loading = false;
2018-02-17 16:51:43 +08:00
this.dialog.title = title;
this.dialog.content = content;
2018-02-14 12:41:43 +08:00
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.loading = true;
2018-02-17 16:51:43 +08:00
var listId = [];
for (var i = 0; i < indices.length; i++) {
listId.push('' + app.bookmarks[indices[i]].id);
}
instance.delete('/api/bookmarks/', {
data: listId
})
2018-02-14 12:41:43 +08:00
.then(function (response) {
app.dialog.loading = false;
app.dialog.visible = false;
2018-02-17 16:51:43 +08:00
for (var i = indices.length - 1; i >= 0; i--) {
app.bookmarks.splice(indices[i], 1);
}
app.clearSelectedBookmarks();
var scrollIdx = smallestIndex === 1 ? 1 : smallestIndex - 1;
2018-02-14 12:41:43 +08:00
app.$nextTick(function () {
2018-02-26 18:31:20 +08:00
var el = app.$refs['bookmark-' + smallestIndex][0];
if (el) el.scrollIntoView();
else window.scrollTo(0, 0);
2018-02-14 12:41:43 +08:00
});
})
.catch(function (error) {
2018-02-14 19:50:53 +08:00
var errorMsg = error.response ? error.response.data : error.message;
app.showDialogError("Error Deleting Bookmark", errorMsg.trim());
2018-02-14 12:41:43 +08:00
});
};
this.dialog.secondAction = function () {
app.dialog.visible = false;
app.$nextTick(function () {
2018-02-17 16:51:43 +08:00
app.$refs['bookmark-' + smallestIndex][0].scrollIntoView();
2018-02-14 12:41:43 +08:00
});
};
},
2018-02-17 16:51:43 +08:00
updateBookmark: function (idx) {
var bookmark = this.bookmarks[idx];
this.dialog.visible = true;
this.dialog.isError = false;
this.dialog.loading = false;
this.dialog.title = "Update Bookmark";
this.dialog.content = "Update data of <b>\"" + bookmark.title.trim() + "\"</b> ? This action is irreversible.";
this.dialog.mainChoice = "Yes";
this.dialog.secondChoice = "No";
this.dialog.mainAction = function () {
app.dialog.loading = true;
instance.put('/api/bookmarks', {
id: bookmark.id,
}, {
timeout: 15000,
})
.then(function (response) {
app.dialog.loading = false;
app.dialog.visible = false;
app.bookmarks.splice(idx, 1, response.data);
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
})
.catch(function (error) {
var errorMsg = error.response ? error.response.data : error.message;
app.showDialogError("Error Updating Bookmark", errorMsg.trim());
});
};
this.dialog.secondAction = function () {
app.dialog.visible = false;
app.$nextTick(function () {
app.$refs['bookmark-' + idx][0].scrollIntoView();
});
};
},
toggleBookmarkCheck: function (idx) {
var checkedIdx = this.checkedBookmarks.indexOf(idx);
if (checkedIdx !== -1) this.checkedBookmarks.splice(checkedIdx, 1);
else this.checkedBookmarks.push(idx);
},
selectAllBookmarks: function () {
this.clearSelectedBookmarks();
for (var i = 0; i < this.bookmarks.length; i++) {
this.checkedBookmarks.push(i);
}
},
clearSelectedBookmarks: function () {
this.checkedBookmarks.splice(0, this.checkedBookmarks.length);
},
isBookmarkChecked: function (idx) {
return this.checkedBookmarks.indexOf(idx) !== -1;
},
2018-02-14 12:41:43 +08:00
clearInputBookmark: function () {
var idx = this.inputBookmark.index;
this.inputBookmark.index = -1;
this.inputBookmark.id = -1;
this.inputBookmark.url = "";
this.inputBookmark.title = "";
this.inputBookmark.tags = "";
this.inputBookmark.excerpt = "";
2018-02-14 19:50:53 +08:00
this.inputBookmark.error = "";
2018-02-14 12:41:43 +08:00
this.inputBookmark.loading = false;
if (idx !== -1) app.$nextTick(function () {
var bookmarkItem = app.$refs['bookmark-' + idx];
bookmarkItem[0].scrollIntoView();
})
},
2018-02-11 22:00:56 +08:00
bookmarkTime: function (book) {
var time = book.modified,
readTime = "",
finalBookmarkTime = "";
if (book.maxReadTime === 0) {
readTime = "";
} else if (book.minReadTime === book.maxReadTime) {
readTime = book.minReadTime + " min read";
} else {
readTime = book.minReadTime + "-" + book.maxReadTime + " min read";
}
finalBookmarkTime = "Updated " + time;
2018-02-12 22:06:53 +08:00
if (readTime !== "") finalBookmarkTime += " \u00B7 " + readTime;
2018-02-11 22:00:56 +08:00
return finalBookmarkTime;
},
2018-02-17 17:48:55 +08:00
toggleImage: function () {
this.showImage = !this.showImage;
if (this.showImage) localStorage.setItem('show-image', '');
else localStorage.removeItem('show-image');
},
2018-02-11 22:00:56 +08:00
bookmarkImage: function (book) {
2018-02-17 17:48:55 +08:00
if (!this.showImage) return "";
2018-02-12 22:06:53 +08:00
if (book.imageURL === "") return "";
return "background-image: url(" + book.imageURL + ")";
},
getDomainURL: function (url) {
var hostname;
if (url.indexOf("://") > -1) {
hostname = url.split('/')[2];
} else {
hostname = url.split('/')[0];
}
hostname = hostname.split(':')[0];
hostname = hostname.split('?')[0];
return hostname;
2018-02-14 19:50:53 +08:00
},
showDialogError: function (title, msg) {
this.dialog.isError = true;
this.dialog.visible = true;
this.dialog.loading = false;
this.dialog.title = title;
this.dialog.content = msg;
this.dialog.mainChoice = "OK"
this.dialog.secondChoice = ""
this.dialog.mainAction = function () {
app.dialog.visible = false;
}
this.dialog.secondAction = function () {}
2018-02-22 17:48:36 +08:00
},
logout: function () {
Cookies.remove('token');
location.href = '/login';
2018-02-11 22:00:56 +08:00
}
},
computed: {
gridColumns: function () {
var nColumn = Math.round(this.windowWidth / 500),
finalContent = [],
currentColumn = 0;
for (var i = 0; i < nColumn; i++) {
finalContent.push([]);
}
for (var i = 0; i < this.bookmarks.length; i++) {
var bookmark = this.bookmarks[i];
2018-02-13 17:14:08 +08:00
bookmark.index = i;
2018-02-11 22:00:56 +08:00
finalContent[currentColumn].push(bookmark);
currentColumn += 1;
if (currentColumn >= nColumn) currentColumn = 0;
}
return finalContent;
2018-02-23 15:30:58 +08:00
},
columnWidth: function () {
var nColumn = Math.round(this.windowWidth / 500),
percent = Math.round(100 / nColumn * 1000) / 1000;
return percent + "%";
}
2018-02-11 22:00:56 +08:00
},
2018-02-13 17:14:08 +08:00
watch: {
'inputBookmark.url': function (newURL) {
if (newURL === "") this.clearInputBookmark();
else this.$nextTick(function () {
app.$refs.inputURL.focus();
});
}
},
2018-02-11 22:00:56 +08:00
mounted: function () {
2018-02-17 17:48:55 +08:00
this.showImage = localStorage.getItem('show-image') !== null;
2018-02-11 22:00:56 +08:00
this.windowWidth = window.innerWidth;
2018-02-17 17:48:55 +08:00
2018-02-11 22:00:56 +08:00
window.addEventListener('resize', function () {
app.windowWidth = window.innerWidth;
})
this.loadData();
}
})
</script>
</body>
</html>