mirror of
https://github.com/go-shiori/shiori.git
synced 2024-11-16 22:25:13 +08:00
487 lines
No EOL
22 KiB
HTML
487 lines
No EOL
22 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/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>
|
|
<script src="/js/js-cookie.js"></script>
|
|
<title>Shiori - Bookmarks Manager</title>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="main-page">
|
|
<div id="header">
|
|
<template v-if="checkedBookmarks.length === 0">
|
|
<a id="logo" href="/">
|
|
<span>栞</span>shiori</a>
|
|
<div id="search-box">
|
|
<input type="text" name="keyword" id="input-search" placeholder="Search tags, title or content">
|
|
<a class="button">
|
|
<i class="fas fa-search fa-fw"></i>
|
|
</a>
|
|
</div>
|
|
<div id="header-menu" v-if="!loading">
|
|
<a @click="loadData">
|
|
<i class="fas fa-cloud fa-fw"></i> Reload
|
|
</a>
|
|
<a @click="toggleImage">
|
|
<i class="fas fa-fw" :class="showImage ? 'fa-eye-slash' : 'fa-eye'"></i> {{showImage ? 'Hide image' : 'Show image'}}
|
|
</a>
|
|
<a @click="logout">
|
|
<i class="fas fa-sign-out-alt fa-fw"></i> Logout
|
|
</a>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<p id="n-selected">{{checkedBookmarks.length}} selected</p>
|
|
<div id="header-menu">
|
|
<a @click="clearSelectedBookmarks">
|
|
<i class="fas fa-fw fa-ban"></i> Cancel
|
|
</a>
|
|
<a @click="selectAllBookmarks">
|
|
<i class="fas fa-fw fa-check-square"></i> Select all
|
|
</a>
|
|
<a @click="deleteBookmarks(checkedBookmarks)">
|
|
<i class="fas fa-fw fa-trash"></i> Delete
|
|
</a>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div id="main">
|
|
<template v-if="!loading && error === ''">
|
|
<div id="input-bookmark">
|
|
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
|
|
<input type="text" ref="inputURL" v-model.trim="inputBookmark.url" placeholder="URL for the bookmark" @focus="clearSelectedBookmarks">
|
|
<template v-if="inputBookmark.url !== ''">
|
|
<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>
|
|
<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>
|
|
</a>
|
|
<template v-else>
|
|
<a class="button" @click="clearInputBookmark">Cancel</a>
|
|
<a class="button" @click="saveBookmark">Done</a>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div id="grid">
|
|
<div v-for="column in gridColumns" class="column">
|
|
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
|
|
<a class="checkbox" @click="toggleBookmarkCheck(item.index)">
|
|
<i class="fas fa-check"></i>
|
|
</a>
|
|
<a class="bookmark-metadata" target="_blank" :class="{'has-image':bookmarkImage(item) !== ''}" :style="bookmarkImage(item)" :href="item.url">
|
|
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
|
|
<p class="bookmark-title">{{item.title}}</p>
|
|
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
|
|
</a>
|
|
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
|
|
<div v-if="item.tags.length > 0" class="bookmark-tags">
|
|
<a v-for="tag in item.tags">{{tag.name}}</a>
|
|
</div>
|
|
<div class="bookmark-menu">
|
|
<a @click="updateBookmark(item.index)">
|
|
<i class="fas fa-sync"></i> Update
|
|
</a>
|
|
<a @click="editBookmark(item.index)">
|
|
<i class="fas fa-pencil-alt"></i> Edit
|
|
</a>
|
|
<a @click="deleteBookmarks([item.index])">
|
|
<i class="far fa-trash-alt"></i> Delete
|
|
</a>
|
|
<a :href="'/bookmark/'+item.id" target="_blank">
|
|
<i class="fas fa-history"></i> Cache
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div v-if="loading || error !== ''" id="message-bar">
|
|
<i v-if="loading" class="fas fa-fw fa-spinner fa-spin"></i>
|
|
<p v-if="error !== ''" class="error-message">{{error}}</p>
|
|
</div>
|
|
</div>
|
|
<div v-if="dialog.visible" id="dialog-overlay">
|
|
<div id="dialog">
|
|
<p id="dialog-title" :class="{'error-message': dialog.isError}">{{dialog.title}}</p>
|
|
<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>
|
|
</div>
|
|
<script>
|
|
var token = Cookies.get('token'),
|
|
instance = axios.create();
|
|
|
|
instance.defaults.timeout = 10000;
|
|
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
|
|
|
var app = new Vue({
|
|
el: '#main-page',
|
|
data: {
|
|
windowWidth: 0,
|
|
error: "",
|
|
loading: true,
|
|
bookmarks: [],
|
|
checkedBookmarks: [],
|
|
showImage: true,
|
|
inputBookmark: {
|
|
index: -1,
|
|
id: -1,
|
|
url: "",
|
|
title: "",
|
|
tags: "",
|
|
excerpt: "",
|
|
error: "",
|
|
loading: false
|
|
},
|
|
dialog: {
|
|
visible: false,
|
|
loading: false,
|
|
isError: false,
|
|
title: '',
|
|
content: '',
|
|
mainChoice: '',
|
|
secondChoice: '',
|
|
mainAction: function () {},
|
|
secondAction: function () {}
|
|
}
|
|
},
|
|
methods: {
|
|
loadData: function () {
|
|
this.error = '';
|
|
this.loading = true;
|
|
instance.get('/api/bookmarks')
|
|
.then(function (response) {
|
|
app.loading = false;
|
|
app.bookmarks = response.data;
|
|
})
|
|
.catch(function (error) {
|
|
var errorMsg = error.response ? error.response.data : error.message;
|
|
app.loading = false;
|
|
app.error = errorMsg.trim();
|
|
});
|
|
},
|
|
saveBookmark: function () {
|
|
if (this.inputBookmark.loading) return;
|
|
this.inputBookmark.loading = true;
|
|
|
|
if (this.inputBookmark.url === "") return;
|
|
|
|
var tags = this.inputBookmark.tags.replace(/\s+/g, " "),
|
|
listTag = tags === "" ? [] : listTag = tags.split(/\s+/g),
|
|
finalTag = [];
|
|
|
|
for (var i = 0; i < listTag.length; i++) {
|
|
finalTag.push({
|
|
name: listTag[i]
|
|
});
|
|
}
|
|
|
|
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,
|
|
tags: finalTag
|
|
}
|
|
})
|
|
.then(function (response) {
|
|
var idx = app.inputBookmark.index;
|
|
|
|
if (idx === -1) app.bookmarks.unshift(response.data);
|
|
else {
|
|
app.bookmarks.splice(idx, 1, response.data);
|
|
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
|
|
}
|
|
|
|
app.clearInputBookmark();
|
|
})
|
|
.catch(function (error) {
|
|
var errorMsg = error.response ? error.response.data : error.message;
|
|
app.inputBookmark.loading = false;
|
|
app.inputBookmark.error = errorMsg.trim();
|
|
});
|
|
},
|
|
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 () {
|
|
window.scrollTo(0, 0);
|
|
app.$refs.inputURL.focus();
|
|
});
|
|
},
|
|
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];
|
|
}
|
|
|
|
this.dialog.visible = true;
|
|
this.dialog.isError = false;
|
|
this.dialog.loading = false;
|
|
this.dialog.title = title;
|
|
this.dialog.content = content;
|
|
this.dialog.mainChoice = "Yes";
|
|
this.dialog.secondChoice = "No";
|
|
this.dialog.mainAction = function () {
|
|
app.dialog.loading = true;
|
|
|
|
var listId = [];
|
|
for (var i = 0; i < indices.length; i++) {
|
|
listId.push('' + app.bookmarks[indices[i]].id);
|
|
}
|
|
|
|
instance.delete('/api/bookmarks/', {
|
|
data: listId
|
|
})
|
|
.then(function (response) {
|
|
app.dialog.loading = false;
|
|
app.dialog.visible = false;
|
|
|
|
for (var i = indices.length - 1; i >= 0; i--) {
|
|
app.bookmarks.splice(indices[i], 1);
|
|
}
|
|
app.clearSelectedBookmarks();
|
|
|
|
var scrollIdx = smallestIndex === 1 ? 1 : smallestIndex - 1;
|
|
app.$nextTick(function () {
|
|
var el = app.$refs['bookmark-' + scrollIdx];
|
|
if (el) el[0].scrollIntoView();
|
|
});
|
|
})
|
|
.catch(function (error) {
|
|
var errorMsg = error.response ? error.response.data : error.message;
|
|
app.showDialogError("Error Deleting Bookmark", errorMsg.trim());
|
|
});
|
|
};
|
|
this.dialog.secondAction = function () {
|
|
app.dialog.visible = false;
|
|
app.$nextTick(function () {
|
|
app.$refs['bookmark-' + smallestIndex][0].scrollIntoView();
|
|
});
|
|
};
|
|
|
|
},
|
|
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;
|
|
},
|
|
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 = "";
|
|
this.inputBookmark.error = "";
|
|
this.inputBookmark.loading = false;
|
|
|
|
if (idx !== -1) app.$nextTick(function () {
|
|
var bookmarkItem = app.$refs['bookmark-' + idx];
|
|
bookmarkItem[0].scrollIntoView();
|
|
})
|
|
},
|
|
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;
|
|
if (readTime !== "") finalBookmarkTime += " \u00B7 " + readTime;
|
|
|
|
return finalBookmarkTime;
|
|
},
|
|
toggleImage: function () {
|
|
this.showImage = !this.showImage;
|
|
if (this.showImage) localStorage.setItem('show-image', '');
|
|
else localStorage.removeItem('show-image');
|
|
},
|
|
bookmarkImage: function (book) {
|
|
if (!this.showImage) return "";
|
|
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;
|
|
},
|
|
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 () {}
|
|
},
|
|
logout: function () {
|
|
Cookies.remove('token');
|
|
location.href = '/login';
|
|
}
|
|
},
|
|
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];
|
|
bookmark.index = i;
|
|
finalContent[currentColumn].push(bookmark);
|
|
|
|
currentColumn += 1;
|
|
if (currentColumn >= nColumn) currentColumn = 0;
|
|
}
|
|
|
|
return finalContent;
|
|
}
|
|
},
|
|
watch: {
|
|
'inputBookmark.url': function (newURL) {
|
|
if (newURL === "") this.clearInputBookmark();
|
|
else this.$nextTick(function () {
|
|
app.$refs.inputURL.focus();
|
|
});
|
|
}
|
|
},
|
|
mounted: function () {
|
|
this.showImage = localStorage.getItem('show-image') !== null;
|
|
this.windowWidth = window.innerWidth;
|
|
|
|
window.addEventListener('resize', function () {
|
|
app.windowWidth = window.innerWidth;
|
|
})
|
|
|
|
this.loadData();
|
|
}
|
|
})
|
|
</script>
|
|
</body>
|
|
|
|
</html> |