mirror of
https://github.com/go-shiori/shiori.git
synced 2024-11-13 04:35:28 +08:00
Add bookmarklet
This commit is contained in:
parent
b1ae14e079
commit
4b939379e8
12 changed files with 355 additions and 25 deletions
File diff suppressed because one or more lines are too long
|
@ -40,6 +40,7 @@ func NewServeCmd(db dt.Database, dataDir string) *cobra.Command {
|
|||
router.GET("/login", hdl.serveLoginPage)
|
||||
router.GET("/bookmark/:id", hdl.serveBookmarkCache)
|
||||
router.GET("/thumb/:id", hdl.serveThumbnailImage)
|
||||
router.GET("/submit", hdl.serveSubmitPage)
|
||||
|
||||
router.POST("/api/login", hdl.apiLogin)
|
||||
router.GET("/api/bookmarks", hdl.apiGetBookmarks)
|
||||
|
|
|
@ -96,6 +96,11 @@ func (h *webHandler) apiGetTags(w http.ResponseWriter, r *http.Request, ps httpr
|
|||
|
||||
// apiInsertBookmark is handler for POST /api/bookmark
|
||||
func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
// Enable CORS for this endpoint
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
// Check token
|
||||
err := h.checkAPIToken(r)
|
||||
checkError(err)
|
||||
|
@ -157,7 +162,14 @@ func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, p
|
|||
|
||||
// Save bookmark to database
|
||||
_, err = h.db.InsertBookmark(book)
|
||||
checkError(err)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
book.ID = h.db.GetBookmarkID(book.URL)
|
||||
book.Modified = time.Now().UTC().Format("2006-01-02 15:04:05")
|
||||
fmt.Println(book.ID, book.Modified)
|
||||
_, err = h.db.UpdateBookmarks(book)
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
// Return new saved result
|
||||
err = json.NewEncoder(w).Encode(&book)
|
||||
|
|
|
@ -34,6 +34,12 @@ func (h *webHandler) serveIndexPage(w http.ResponseWriter, r *http.Request, ps h
|
|||
checkError(err)
|
||||
}
|
||||
|
||||
// serveSubmitPage is handler for GET /submit
|
||||
func (h *webHandler) serveSubmitPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
err := serveFile(w, "submit.html")
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
// serveLoginPage is handler for GET /login
|
||||
func (h *webHandler) serveLoginPage(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
// Check token
|
||||
|
|
|
@ -42,16 +42,21 @@ func newWebHandler(db dt.Database, dataDir string) (*webHandler, error) {
|
|||
func (h *webHandler) checkToken(r *http.Request) error {
|
||||
tokenCookie, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Token does not exist")
|
||||
return fmt.Errorf("Token error: Token does not exist")
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenCookie.Value, h.jwtKeyFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Token error: %v", err)
|
||||
}
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
return claims.Valid()
|
||||
err = claims.Valid()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Token error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *webHandler) checkAPIToken(r *http.Request) error {
|
||||
|
@ -59,11 +64,17 @@ func (h *webHandler) checkAPIToken(r *http.Request) error {
|
|||
request.AuthorizationHeaderExtractor,
|
||||
h.jwtKeyFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
// Try to check in cookie
|
||||
return h.checkToken(r)
|
||||
}
|
||||
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
return claims.Valid()
|
||||
err = claims.Valid()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Token error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *webHandler) jwtKeyFunc(token *jwt.Token) (interface{}, error) {
|
||||
|
|
|
@ -40,6 +40,9 @@ type Database interface {
|
|||
|
||||
// DeleteAccounts removes all record with matching usernames
|
||||
DeleteAccounts(usernames ...string) error
|
||||
|
||||
// GetBookmarkID fetchs bookmark ID based by its url
|
||||
GetBookmarkID(url string) int
|
||||
}
|
||||
|
||||
func checkError(err error) {
|
||||
|
|
|
@ -324,7 +324,7 @@ func (db *SQLiteDatabase) SearchBookmarks(orderLatest bool, keyword string, tags
|
|||
|
||||
// Set order clause
|
||||
if orderLatest {
|
||||
query += ` ORDER BY id DESC`
|
||||
query += ` ORDER BY modified DESC`
|
||||
}
|
||||
|
||||
// Fetch bookmarks
|
||||
|
@ -552,3 +552,10 @@ func (db *SQLiteDatabase) GetNewID(table string) (int, error) {
|
|||
|
||||
return tableID, nil
|
||||
}
|
||||
|
||||
// GetBookmarkID fetchs bookmark ID based by its url
|
||||
func (db *SQLiteDatabase) GetBookmarkID(url string) int {
|
||||
var bookmarkID int
|
||||
db.Get(&bookmarkID, `SELECT id FROM bookmark WHERE url = ?`, url)
|
||||
return bookmarkID
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -33,7 +33,8 @@
|
|||
</a>
|
||||
</yla-tooltip>
|
||||
<yla-tooltip placement="right" content="Add new bookmark">
|
||||
<a @click="showDialogAdd" v-show="!editMode && !loading">
|
||||
<a :href="bookmarklet" @click="showDialogAdd" v-show="!editMode && !loading">
|
||||
<span>+Shiori</span>
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</a>
|
||||
</yla-tooltip>
|
||||
|
@ -197,6 +198,7 @@
|
|||
nightMode: false,
|
||||
editMode: false,
|
||||
selected: [],
|
||||
bookmarklet: '',
|
||||
dialogTags: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
|
@ -318,7 +320,8 @@
|
|||
this.dialogTags.visible = false;
|
||||
this.loadData();
|
||||
},
|
||||
showDialogAdd() {
|
||||
showDialogAdd(e) {
|
||||
e.preventDefault();
|
||||
this.showDialog({
|
||||
title: 'New Bookmark',
|
||||
content: 'Create a new bookmark',
|
||||
|
@ -643,6 +646,42 @@
|
|||
this.listView = listView === '1';
|
||||
this.nightMode = nightMode === '1';
|
||||
|
||||
// Create bookmarklet
|
||||
var shioriURL = location.href.replace(/\/+$/g, ''),
|
||||
baseBookmarklet = `(function () {
|
||||
var shioriURL = '$SHIORI_URL',
|
||||
bookmarkURL = location.href,
|
||||
submitURL = shioriURL + '/submit?url=' + encodeURIComponent(bookmarkURL);
|
||||
|
||||
if (bookmarkURL.startsWith('https://') && !shioriURL.startsWith('https://')) {
|
||||
window.open(submitURL, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
var i = document.createElement('iframe');
|
||||
i.src = submitURL;
|
||||
i.frameBorder = '0';
|
||||
i.allowTransparency = true;
|
||||
i.style.position = 'fixed';
|
||||
i.style.top = 0;
|
||||
i.style.left = 0;
|
||||
i.style.width = '100%';
|
||||
i.style.height = '100%';
|
||||
i.style.zIndex = 99999;
|
||||
document.body.appendChild(i);
|
||||
|
||||
window.addEventListener('message', function onMessage(e) {
|
||||
if (e.origin !== shioriURL) return;
|
||||
if (e.data !== 'finished') return;
|
||||
window.removeEventListener('message', onMessage);
|
||||
document.body.removeChild(i);
|
||||
});
|
||||
}())`;
|
||||
|
||||
this.bookmarklet = 'javascript:' + baseBookmarklet
|
||||
.replace('$SHIORI_URL', shioriURL)
|
||||
.replace(/\s+/gm, ' ');
|
||||
|
||||
// Load data
|
||||
this.loadData();
|
||||
}
|
||||
|
|
|
@ -98,6 +98,12 @@ body {
|
|||
display: block;
|
||||
color: var(--colorSidebar);
|
||||
flex-shrink: 0;
|
||||
>span {
|
||||
display: block;
|
||||
height: 0;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #232323;
|
||||
|
@ -680,6 +686,22 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
#submit-page {
|
||||
background-color: transparent;
|
||||
.yla-dialog__header {
|
||||
text-align: center;
|
||||
}
|
||||
&:not(.iframe) {
|
||||
background-color: var(--bg);
|
||||
.yla-dialog__overlay {
|
||||
background-color: transparent;
|
||||
.yla-dialog {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.yla-tooltip {
|
||||
@media (max-width: 800px) {
|
||||
display: none;
|
||||
|
|
|
@ -83,12 +83,22 @@
|
|||
timeout: 10000
|
||||
})
|
||||
.then(function (response) {
|
||||
// Save token
|
||||
var token = response.data;
|
||||
Cookies.set('token', token, {
|
||||
expires: this.rememberMe ? 7 : 1
|
||||
});
|
||||
|
||||
location.href = '/';
|
||||
// Set destination URL
|
||||
var rx = /[&?]dst=([^&]+)(&|$)/g,
|
||||
match = rx.exec(location.href);
|
||||
|
||||
if (match == null) {
|
||||
location.href = '/';
|
||||
} else {
|
||||
var dst = match[1];
|
||||
location.href = decodeURIComponent(dst);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
||||
|
|
211
view/submit.html
Normal file
211
view/submit.html
Normal file
|
@ -0,0 +1,211 @@
|
|||
<!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/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-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>Submit New URL - Shiori - Bookmarks Manager</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="submit-page" class="page" :class="{night: nightMode, iframe: inIframe}">
|
||||
<yla-dialog v-bind="dialog"></yla-dialog>
|
||||
</div>
|
||||
<script>
|
||||
// Create private function
|
||||
function _inIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Register Vue component
|
||||
Vue.component('yla-dialog', new YlaDialog());
|
||||
|
||||
// Prepare axios instance
|
||||
var token = Cookies.get('token'),
|
||||
rest = axios.create();
|
||||
|
||||
rest.defaults.timeout = 60000;
|
||||
rest.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
var app = new Vue({
|
||||
el: '#submit-page',
|
||||
mixins: [new Base()],
|
||||
data: {
|
||||
targetURL: '',
|
||||
nightMode: false,
|
||||
inIframe: false,
|
||||
},
|
||||
methods: {
|
||||
showDialogLogin() {
|
||||
this.showDialog({
|
||||
title: 'Login',
|
||||
content: 'Please input username and password',
|
||||
fields: [{
|
||||
name: 'username',
|
||||
label: 'Username',
|
||||
}, {
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password'
|
||||
}],
|
||||
mainText: 'OK',
|
||||
secondText: this.inIframe ? 'Cancel' : '',
|
||||
mainClick: (data) => {
|
||||
// Validate input
|
||||
if (data.username.trim() === '') return;
|
||||
|
||||
// Send data
|
||||
this.dialog.loading = true;
|
||||
rest.post('/api/login', {
|
||||
username: data.username.trim(),
|
||||
password: data.password,
|
||||
})
|
||||
.then((response) => {
|
||||
// Save token
|
||||
var token = response.data;
|
||||
Cookies.set('token', token, {
|
||||
expires: this.rememberMe ? 7 : 1
|
||||
});
|
||||
|
||||
this.showDialogAdd();
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
||||
this.showErrorDialog(errorMsg);
|
||||
this.dialog.mainClick = () => {
|
||||
this.showDialogLogin();
|
||||
}
|
||||
});
|
||||
},
|
||||
secondClick: () => {
|
||||
window.top.postMessage('finished', '*');
|
||||
this.dialog.visible = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
showDialogAdd() {
|
||||
this.showDialog({
|
||||
title: 'New Bookmark',
|
||||
content: 'Create a new bookmark',
|
||||
fields: [{
|
||||
name: 'url',
|
||||
label: 'Url, start with http://...',
|
||||
value: this.targetURL,
|
||||
}, {
|
||||
name: 'title',
|
||||
label: 'Custom title (optional)'
|
||||
}, {
|
||||
name: 'excerpt',
|
||||
label: 'Custom excerpt (optional)',
|
||||
type: 'area'
|
||||
}, {
|
||||
name: 'tags',
|
||||
label: 'Comma separated tags (optional)'
|
||||
}, ],
|
||||
mainText: 'Save',
|
||||
secondText: this.inIframe ? 'Cancel' : '',
|
||||
mainClick: (data) => {
|
||||
// Prepare tags
|
||||
var tags = data.tags
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.split(/\s*,\s*/g)
|
||||
.filter(tag => tag !== '')
|
||||
.map(tag => {
|
||||
return {
|
||||
name: tag
|
||||
};
|
||||
});
|
||||
|
||||
// Validate input
|
||||
if (data.url.trim() === '') return;
|
||||
|
||||
// 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) => {
|
||||
if (!this.inIframe) {
|
||||
location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
this.showDialog({
|
||||
title: 'New Bookmark',
|
||||
content: 'The new bookmark has been saved successfully.',
|
||||
mainText: 'OK',
|
||||
mainClick: () => {
|
||||
window.top.postMessage('finished', '*');
|
||||
this.dialog.visible = false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
||||
if (errorMsg.startsWith('Token error:')) {
|
||||
this.showDialogLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
this.showErrorDialog(errorMsg);
|
||||
this.dialog.mainClick = () => {
|
||||
this.showDialogAdd();
|
||||
}
|
||||
});
|
||||
},
|
||||
secondClick: () => {
|
||||
window.top.postMessage('finished', '*');
|
||||
this.dialog.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Check if in iframe
|
||||
this.inIframe = _inIframe();
|
||||
|
||||
// Read config from local storage
|
||||
var nightMode = localStorage.getItem('shiori-night-mode');
|
||||
this.nightMode = nightMode === '1';
|
||||
|
||||
// Get target URL
|
||||
var rxURL = /[&?]url=([^&]+)(&|$)/g,
|
||||
match = rxURL.exec(location.href);
|
||||
|
||||
if (match != null) {
|
||||
var dst = match[1];
|
||||
this.targetURL = decodeURIComponent(dst);
|
||||
} else {
|
||||
this.targetURL = '';
|
||||
}
|
||||
|
||||
// Show dialog
|
||||
this.showDialogAdd();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in a new issue