mirror of
https://github.com/go-shiori/shiori.git
synced 2024-11-16 14:16:29 +08:00
746 lines
No EOL
34 KiB
HTML
746 lines
No EOL
34 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" class="page" :class="{night: options.nightMode}">
|
|
<div id="sidebar">
|
|
<p id="logo">栞</p>
|
|
<yla-tooltip placement="right" content="Reload data">
|
|
<a @click="reloadData" v-show="!editMode">
|
|
<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 :href="bookmarklet" @click="showDialogAdd" v-show="!editMode && !loading">
|
|
<span>+Shiori</span>
|
|
<i class="fas fa-plus fa-fw"></i>
|
|
</a>
|
|
</yla-tooltip>
|
|
<yla-tooltip placement="right" :content="editMode ? 'Cancel batch edit' : 'Batch edit'">
|
|
<a @click="toggleEditMode" v-show="!loading">
|
|
<i class="fas fa-fw" :class="editMode ? 'fa-times' : 'fa-pencil-alt'"></i>
|
|
</a>
|
|
</yla-tooltip>
|
|
<yla-tooltip placement="right" content="Show tags">
|
|
<a @click="showDialogTags" v-show="!editMode && !loading">
|
|
<i class="fas fa-fw fa-tags"></i>
|
|
</a>
|
|
</yla-tooltip>
|
|
<div class="spacer"></div>
|
|
<yla-tooltip placement="right" content="Options">
|
|
<a @click="showDialogOptions">
|
|
<i class="fas fa-fw fa-cog"></i>
|
|
</a>
|
|
</yla-tooltip>
|
|
<yla-tooltip placement="right" content="Log out">
|
|
<a @click="showDialogLogout">
|
|
<i class="fas fa-sign-out-alt fa-fw"></i>
|
|
</a>
|
|
</yla-tooltip>
|
|
</div>
|
|
<div id="body">
|
|
<div id="header" class="header" v-if="!editMode">
|
|
<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">
|
|
<i class="fas fa-search fa-fw"></i>
|
|
</a>
|
|
</div>
|
|
<div id="batch-edit" class="header" v-if="editMode">
|
|
<p>{{selected.length}} Items selected</p>
|
|
<a :class="{disabled: selected.length === 0}" @click="showDialogDelete(selected)">
|
|
<i class="fas fa-fw fa-trash-alt"></i>
|
|
<span>Delete</span>
|
|
</a>
|
|
<a :class="{disabled: selected.length === 0}" @click="showDialogAddTags(selected)">
|
|
<i class="fas fa-fw fa-tags"></i>
|
|
<span>Add tags</span>
|
|
</a>
|
|
<a :class="{disabled: selected.length === 0}" @click="showDialogUpdateCache(selected)">
|
|
<i class="fas fa-fw fa-cloud-download-alt"></i>
|
|
<span>Update cache</span>
|
|
</a>
|
|
<a id="cancel-edit" @click="toggleEditMode">
|
|
<i class="fas fa-fw fa-times"></i>
|
|
</a>
|
|
</div>
|
|
<div id="grid" :class="{list: options.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)" :disabled="editMode">
|
|
<p>{{maxPage+1}}</p>
|
|
<div class="spacer"></div>
|
|
<template v-if="!editMode">
|
|
<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>
|
|
</template>
|
|
</div>
|
|
<div class="bookmark" v-for="(book, idx) in visibleBookmarks" :class="{selected: isSelected(idx)}">
|
|
<a class="bookmark-selector" v-if="editMode" @click="toggleSelection(idx)"></a>
|
|
<a class="bookmark-link" :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>
|
|
<p v-show="options.showBookmarkID" class="id">{{book.id}}</p>
|
|
</a>
|
|
<div class="bookmark-tags" v-if="book.tags.length > 0">
|
|
<a v-for="tag in book.tags" @click="filterTag(tag.name)">{{tag.name}}</a>
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<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)" :disabled="editMode">
|
|
<p>{{maxPage+1}}</p>
|
|
<div class="spacer"></div>
|
|
<template v-if="!editMode">
|
|
<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>
|
|
</template>
|
|
</div>
|
|
<div id="grid-padding"></div>
|
|
</div>
|
|
</div>
|
|
<yla-dialog id="dialog-tags" v-bind="dialogTags">
|
|
<a v-for="tag in tags" @click="filterTag(tag.name)">
|
|
<span>{{tag.name}}</span>
|
|
<span>{{tag.nBookmarks}}</span>
|
|
</a>
|
|
</yla-dialog>
|
|
<yla-dialog id="dialog-options" v-bind="dialogOptions">
|
|
<a @click="toggleListView">
|
|
<i class="fa-fw" :class="options.listView ? 'fas fa-check-square' : 'far fa-square'"></i>Use list view
|
|
</a>
|
|
<a @click="toggleNightMode">
|
|
<i class="fa-fw" :class="options.nightMode ? 'fas fa-check-square' : 'far fa-square'"></i>Use night mode
|
|
</a>
|
|
<a @click="toggleBookmarkID">
|
|
<i class="fa-fw" :class="options.showBookmarkID ? 'fas fa-check-square' : 'far fa-square'"></i>Show bookmark's ID
|
|
</a>
|
|
</yla-dialog>
|
|
<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() {
|
|
return {
|
|
loading: false,
|
|
tags: [],
|
|
bookmarks: [],
|
|
search: '',
|
|
page: 0,
|
|
maxPage: 0,
|
|
editMode: false,
|
|
selected: [],
|
|
bookmarklet: '',
|
|
options: {
|
|
listView: false,
|
|
nightMode: false,
|
|
showBookmarkID: false,
|
|
},
|
|
dialogTags: {
|
|
visible: false,
|
|
loading: false,
|
|
title: 'Existing Tags',
|
|
mainText: 'Cancel',
|
|
mainClick: () => {
|
|
this.dialogTags.visible = false;
|
|
},
|
|
},
|
|
dialogOptions: {
|
|
visible: false,
|
|
title: 'Options',
|
|
mainText: 'OK',
|
|
mainClick: () => {
|
|
this.dialogOptions.visible = false;
|
|
},
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
visibleBookmarks() {
|
|
var start = this.page * pageSize,
|
|
finish = start + pageSize;
|
|
|
|
return this.bookmarks.slice(start, finish);
|
|
}
|
|
},
|
|
methods: {
|
|
loadData() {
|
|
if (this.loading) return;
|
|
|
|
// Parse search query
|
|
var rxTagA = /['"]#([^'"]+)['"]/g,
|
|
rxTagB = /(^|\s+)#(\S+)/g,
|
|
keyword = this.search,
|
|
tags = [];
|
|
|
|
// Fetch tag A first
|
|
while ((result = rxTagA.exec(keyword)) !== null) {
|
|
tags.push(result[1]);
|
|
}
|
|
|
|
// Clear tag A from keyword
|
|
keyword = keyword.replace(rxTagA, '');
|
|
|
|
// Fetch tag B
|
|
while ((result = rxTagB.exec(keyword)) !== null) {
|
|
tags.push(result[2]);
|
|
}
|
|
|
|
// Clear tag B from keyword and clean it
|
|
keyword = keyword.replace(rxTagB, '').trim().replace(/\s+/g, ' ');
|
|
|
|
// Fetch data
|
|
this.loading = true;
|
|
rest.get('/api/bookmarks', {
|
|
params: {
|
|
keyword: keyword,
|
|
tags: tags.join(',')
|
|
}
|
|
})
|
|
.then((response) => {
|
|
this.page = 0;
|
|
this.bookmarks = response.data;
|
|
this.maxPage = Math.ceil(this.bookmarks.length / pageSize) - 1;
|
|
window.scrollTo(0, 0);
|
|
|
|
return rest.get('/api/tags');
|
|
})
|
|
.then((response) => {
|
|
this.tags = response.data;
|
|
this.loading = false;
|
|
})
|
|
.catch((error) => {
|
|
this.loading = false;
|
|
|
|
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
},
|
|
reloadData() {
|
|
if (this.loading) return;
|
|
this.search = '';
|
|
this.loadData();
|
|
},
|
|
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;
|
|
|
|
window.scrollTo(0, 0);
|
|
},
|
|
toggleListView() {
|
|
this.options.listView = !this.options.listView;
|
|
window.scrollTo(0, 0);
|
|
localStorage.setItem('shiori-list-view', this.options.listView ? '1' : '0');
|
|
},
|
|
toggleNightMode() {
|
|
this.options.nightMode = !this.options.nightMode;
|
|
localStorage.setItem('shiori-night-mode', this.options.nightMode ? '1' : '0');
|
|
},
|
|
toggleBookmarkID() {
|
|
this.options.showBookmarkID = !this.options.showBookmarkID;
|
|
localStorage.setItem('shiori-show-id', this.options.showBookmarkID ? '1' : '0');
|
|
},
|
|
toggleEditMode() {
|
|
this.editMode = !this.editMode;
|
|
this.selected = [];
|
|
},
|
|
toggleSelection(idx) {
|
|
var pos = this.selected.indexOf(idx);
|
|
if (pos === -1) this.selected.push(idx);
|
|
else this.selected.splice(pos, 1);
|
|
},
|
|
isSelected(idx) {
|
|
return this.selected.indexOf(idx) > -1;
|
|
},
|
|
filterTag(tag) {
|
|
// Prepare variable
|
|
var rxSpace = /\s+/g,
|
|
searchTag = rxSpace.test(tag) ? '"#' + tag + '"' : '#' + tag;
|
|
|
|
// Check if tag already exist in search
|
|
rxTag = new RegExp(searchTag, 'g');
|
|
if (rxTag.test(this.search)) return;
|
|
|
|
// Create new search query
|
|
var newSearch = this.search
|
|
.replace(rxTag, '')
|
|
.replace(rxSpace, ' ')
|
|
.trim();
|
|
|
|
// Load data
|
|
this.search = (newSearch + ' ' + searchTag).trim();
|
|
this.dialogTags.visible = false;
|
|
this.loadData();
|
|
},
|
|
showDialogAdd(e) {
|
|
e.preventDefault();
|
|
this.showDialog({
|
|
title: 'New Bookmark',
|
|
content: 'Create a new bookmark',
|
|
fields: [{
|
|
name: 'url',
|
|
label: 'Url, start with http://...',
|
|
}, {
|
|
name: 'title',
|
|
label: 'Custom title (optional)'
|
|
}, {
|
|
name: 'excerpt',
|
|
label: 'Custom excerpt (optional)',
|
|
type: 'area'
|
|
}, {
|
|
name: 'tags',
|
|
label: 'Comma separated tags (optional)',
|
|
separator: ',',
|
|
dictionary: this.tags.map(tag => tag.name)
|
|
}, ],
|
|
mainText: 'OK',
|
|
secondText: '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
|
|
};
|
|
});
|
|
|
|
// 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();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogEdit(idx) {
|
|
idx += this.page * pageSize;
|
|
|
|
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()
|
|
.replace(/\s+/g, ' ')
|
|
.split(/\s*,\s*/g)
|
|
.filter(tag => tag !== '')
|
|
.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();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogDelete(indices) {
|
|
// Check and prepare indices
|
|
if (!(indices instanceof Array)) return;
|
|
if (indices.length === 0) return;
|
|
indices.sort();
|
|
|
|
// Set real indices value
|
|
indices = indices.map(item => item + this.page * pageSize)
|
|
|
|
// Create title and 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.selected = [];
|
|
this.editMode = false;
|
|
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();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogUpdateCache(indices) {
|
|
// Check and prepare indices
|
|
if (!(indices instanceof Array)) return;
|
|
if (indices.length === 0) return;
|
|
indices.sort();
|
|
|
|
// Set real indices value
|
|
indices = indices.map(item => item + this.page * pageSize)
|
|
|
|
// 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.selected = [];
|
|
this.editMode = false;
|
|
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();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogAddTags(indices) {
|
|
// Check and prepare indices
|
|
if (!(indices instanceof Array)) return;
|
|
if (indices.length === 0) return;
|
|
indices.sort();
|
|
|
|
// Set real indices value
|
|
indices = indices.map(item => item + this.page * pageSize)
|
|
|
|
// Get list of bookmark ID
|
|
var listID = [];
|
|
for (var i = 0; i < indices.length; i++) {
|
|
listID.push(this.bookmarks[indices[i]].id);
|
|
}
|
|
|
|
this.showDialog({
|
|
title: 'Add New Tags',
|
|
content: 'Add new tags to selected bookmarks',
|
|
fields: [{
|
|
name: 'tags',
|
|
label: 'Comma separated tags',
|
|
value: '',
|
|
}],
|
|
mainText: 'OK',
|
|
secondText: 'Cancel',
|
|
mainClick: (data) => {
|
|
// Validate input
|
|
var tags = data.tags
|
|
.toLowerCase()
|
|
.replace(/\s+/g, ' ')
|
|
.split(/\s*,\s*/g)
|
|
.filter(tag => tag !== '')
|
|
.map(tag => {
|
|
return {
|
|
name: tag
|
|
};
|
|
});
|
|
|
|
if (tags.length === 0) return;
|
|
|
|
// Send data
|
|
this.dialog.loading = true;
|
|
rest.put('/api/bookmarks/tags', {
|
|
ids: listID,
|
|
tags: tags,
|
|
})
|
|
.then((response) => {
|
|
this.selected = [];
|
|
this.editMode = false;
|
|
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();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogTags() {
|
|
this.dialogTags.visible = true;
|
|
this.dialogTags.loading = true;
|
|
rest.get('/api/tags', {
|
|
timeout: 5000
|
|
})
|
|
.then((response) => {
|
|
this.tags = response.data;
|
|
this.dialogTags.loading = false;
|
|
})
|
|
.catch((error) => {
|
|
this.dialogTags.loading = false;
|
|
this.dialogTags.visible = false;
|
|
|
|
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
|
if (errorMsg.startsWith('Token error')) this.showDialogSessionExpired(errorMsg);
|
|
else this.showErrorDialog(errorMsg);
|
|
});
|
|
},
|
|
showDialogLogout() {
|
|
this.showDialog({
|
|
title: 'Log Out',
|
|
content: 'Do you want to log out from shiori ?',
|
|
mainText: 'Yes',
|
|
secondText: 'No',
|
|
mainClick: () => {
|
|
Cookies.remove('token');
|
|
location.href = '/login';
|
|
}
|
|
});
|
|
},
|
|
showDialogSessionExpired(msg) {
|
|
this.showDialog({
|
|
title: 'Error',
|
|
content: msg + '. Please login again.',
|
|
mainText: 'OK',
|
|
mainClick: () => {
|
|
Cookies.remove('token');
|
|
location.href = '/login';
|
|
}
|
|
});
|
|
},
|
|
showDialogOptions() {
|
|
this.dialogOptions.visible = true;
|
|
},
|
|
getHostname(url) {
|
|
parser = document.createElement('a');
|
|
parser.href = url;
|
|
return parser.hostname.replace(/^www\./g, '');
|
|
}
|
|
},
|
|
mounted() {
|
|
// Read config from local storage
|
|
var listView = localStorage.getItem('shiori-list-view'),
|
|
nightMode = localStorage.getItem('shiori-night-mode'),
|
|
showBookmarkID = localStorage.getItem('shiori-show-id');
|
|
|
|
this.options.listView = listView === '1';
|
|
this.options.nightMode = nightMode === '1';
|
|
this.options.showBookmarkID = showBookmarkID === '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();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |