mirror of
https://github.com/go-shiori/shiori.git
synced 2025-02-22 15:06:04 +08:00
Implement batch delete
This commit is contained in:
parent
124c2845e9
commit
0f4ab6aa38
4 changed files with 168 additions and 44 deletions
14
cmd/serve.go
14
cmd/serve.go
|
@ -29,7 +29,7 @@ var (
|
||||||
router.GET("/api/bookmarks", apiGetBookmarks)
|
router.GET("/api/bookmarks", apiGetBookmarks)
|
||||||
router.POST("/api/bookmarks", apiInsertBookmarks)
|
router.POST("/api/bookmarks", apiInsertBookmarks)
|
||||||
router.PUT("/api/bookmarks", apiUpdateBookmarks)
|
router.PUT("/api/bookmarks", apiUpdateBookmarks)
|
||||||
router.DELETE("/api/bookmarks/:id", apiDeleteBookmarks)
|
router.DELETE("/api/bookmarks", apiDeleteBookmarks)
|
||||||
|
|
||||||
// Route for panic
|
// Route for panic
|
||||||
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
|
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
|
||||||
|
@ -107,11 +107,13 @@ func apiUpdateBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
|
||||||
|
|
||||||
func apiDeleteBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
func apiDeleteBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
// Decode request
|
// Decode request
|
||||||
id := ps.ByName("id")
|
request := []string{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||||||
// Delete bookmarks
|
|
||||||
_, _, err := DB.DeleteBookmarks(id)
|
|
||||||
checkError(err)
|
checkError(err)
|
||||||
|
|
||||||
fmt.Fprint(w, id)
|
// Delete bookmarks
|
||||||
|
_, _, err = DB.DeleteBookmarks(request...)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
fmt.Fprint(w, request)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
152
view/index.html
152
view/index.html
|
@ -16,6 +16,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="header">
|
<div id="header">
|
||||||
|
<template v-if="checkedBookmarks.length === 0">
|
||||||
<a id="logo" href="/">
|
<a id="logo" href="/">
|
||||||
<span>栞</span>shiori</a>
|
<span>栞</span>shiori</a>
|
||||||
<div id="search-box">
|
<div id="search-box">
|
||||||
|
@ -25,16 +26,31 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="header-menu" v-if="!loading">
|
<div id="header-menu" v-if="!loading">
|
||||||
<a href="#" @click="loadData">
|
<a @click="loadData">
|
||||||
<i class="fas fa-sync fa-fw"></i>
|
<i class="fas fa-cloud fa-fw"></i> Reload
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<template v-if="!loading && error === ''">
|
<template v-if="!loading && error === ''">
|
||||||
<div id="input-bookmark" v-if="!loading">
|
<div id="input-bookmark">
|
||||||
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
|
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
|
||||||
<input type="text" ref="inputURL" v-model="inputBookmark.url" placeholder="URL for the bookmark">
|
<input type="text" ref="inputURL" v-model="inputBookmark.url" placeholder="URL for the bookmark" @focus="clearSelectedBookmarks">
|
||||||
<template v-if="inputBookmark.url !== ''">
|
<template v-if="inputBookmark.url !== ''">
|
||||||
<input type="text" v-model="inputBookmark.title" placeholder="Custom bookmark title (optional)">
|
<input type="text" v-model="inputBookmark.title" placeholder="Custom bookmark title (optional)">
|
||||||
<input type="text" v-model="inputBookmark.tags" placeholder="Space separated tags for this bookmark (optional)">
|
<input type="text" v-model="inputBookmark.tags" placeholder="Space separated tags for this bookmark (optional)">
|
||||||
|
@ -52,30 +68,33 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div id="grid" v-if="!loading">
|
<div id="grid">
|
||||||
<div v-for="column in gridColumns" class="column">
|
<div v-for="column in gridColumns" class="column">
|
||||||
<div v-for="item in column" class="bookmark" :ref="'bookmark-'+item.index">
|
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
|
||||||
<a href="#" class="checkbox">
|
<a class="checkbox" @click="toggleBookmarkCheck(item.index)">
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
</a>
|
</a>
|
||||||
<a class="bookmark-metadata" :class="{'has-image':item.imageURL !== ''}" :style="bookmarkImage(item)" :href="item.url">
|
<a class="bookmark-metadata" target="_blank" :class="{'has-image':item.imageURL !== ''}" :style="bookmarkImage(item)" :href="item.url">
|
||||||
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
|
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
|
||||||
<p class="bookmark-title">{{item.title}}</p>
|
<p class="bookmark-title">{{item.title}}</p>
|
||||||
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
|
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
|
||||||
</a>
|
</a>
|
||||||
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
|
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
|
||||||
<div v-if="item.tags.length > 0" class="bookmark-tags">
|
<div v-if="item.tags.length > 0" class="bookmark-tags">
|
||||||
<a v-for="tag in item.tags" href="#">{{tag.name}}</a>
|
<a v-for="tag in item.tags">{{tag.name}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="bookmark-menu">
|
<div class="bookmark-menu">
|
||||||
<a href="#" @click="editBookmark(item.index)">
|
<a @click="updateBookmark(item.index)">
|
||||||
<i class="fas fa-pencil-alt"></i>
|
<i class="fas fa-sync"></i> Update
|
||||||
</a>
|
</a>
|
||||||
<a href="#" @click="deleteBookmark(item.index)">
|
<a @click="editBookmark(item.index)">
|
||||||
<i class="far fa-trash-alt"></i>
|
<i class="fas fa-pencil-alt"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a href="#">
|
<a @click="deleteBookmarks([item.index])">
|
||||||
<i class="fas fa-history"></i>
|
<i class="far fa-trash-alt"></i> Delete
|
||||||
|
</a>
|
||||||
|
<a>
|
||||||
|
<i class="fas fa-history"></i> Cache
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,6 +134,7 @@
|
||||||
error: "",
|
error: "",
|
||||||
loading: true,
|
loading: true,
|
||||||
bookmarks: [],
|
bookmarks: [],
|
||||||
|
checkedBookmarks: [],
|
||||||
inputBookmark: {
|
inputBookmark: {
|
||||||
index: -1,
|
index: -1,
|
||||||
id: -1,
|
id: -1,
|
||||||
|
@ -184,7 +204,10 @@
|
||||||
var idx = app.inputBookmark.index;
|
var idx = app.inputBookmark.index;
|
||||||
|
|
||||||
if (idx === -1) app.bookmarks.unshift(response.data);
|
if (idx === -1) app.bookmarks.unshift(response.data);
|
||||||
else app.bookmarks.splice(idx, 1, 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();
|
app.clearInputBookmark();
|
||||||
})
|
})
|
||||||
|
@ -210,31 +233,58 @@
|
||||||
this.inputBookmark.excerpt = bookmark.excerpt;
|
this.inputBookmark.excerpt = bookmark.excerpt;
|
||||||
|
|
||||||
this.$nextTick(function () {
|
this.$nextTick(function () {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
app.$refs.inputURL.focus();
|
app.$refs.inputURL.focus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteBookmark: function (idx) {
|
deleteBookmarks: function (indices) {
|
||||||
var bookmark = this.bookmarks[idx];
|
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.visible = true;
|
||||||
this.dialog.isError = false;
|
this.dialog.isError = false;
|
||||||
this.dialog.loading = false;
|
this.dialog.loading = false;
|
||||||
this.dialog.title = "Delete Bookmark";
|
this.dialog.title = title;
|
||||||
this.dialog.content = "Delete <b>\"" + bookmark.title.trim() + "\"</b> from bookmarks ? This action is irreversible.";
|
this.dialog.content = content;
|
||||||
this.dialog.mainChoice = "Yes";
|
this.dialog.mainChoice = "Yes";
|
||||||
this.dialog.secondChoice = "No";
|
this.dialog.secondChoice = "No";
|
||||||
this.dialog.mainAction = function () {
|
this.dialog.mainAction = function () {
|
||||||
app.dialog.loading = true;
|
app.dialog.loading = true;
|
||||||
|
|
||||||
instance.delete('/api/bookmarks/' + bookmark.id)
|
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) {
|
.then(function (response) {
|
||||||
app.dialog.loading = false;
|
app.dialog.loading = false;
|
||||||
app.dialog.visible = false;
|
app.dialog.visible = false;
|
||||||
app.bookmarks.splice(idx, 1);
|
|
||||||
|
|
||||||
var scrollIdx = idx === 1 ? 1 : idx - 1;
|
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 () {
|
app.$nextTick(function () {
|
||||||
app.$refs['bookmark-' + scrollIdx][0].scrollIntoView();
|
var el = app.$refs['bookmark-' + scrollIdx];
|
||||||
|
if (el) el[0].scrollIntoView();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
@ -245,11 +295,63 @@
|
||||||
this.dialog.secondAction = function () {
|
this.dialog.secondAction = function () {
|
||||||
app.dialog.visible = false;
|
app.dialog.visible = false;
|
||||||
app.$nextTick(function () {
|
app.$nextTick(function () {
|
||||||
app.$refs['bookmark-' + idx][0].scrollIntoView();
|
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 () {
|
clearInputBookmark: function () {
|
||||||
var idx = this.inputBookmark.index;
|
var idx = this.inputBookmark.index;
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,14 @@
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
|
#n-selected {
|
||||||
|
line-height: @headerHeight;
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: @fontLightColor;
|
||||||
|
flex: 1 0;
|
||||||
|
border-right: 1px solid @border;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
#logo {
|
#logo {
|
||||||
border-left: 1px solid @border;
|
border-left: 1px solid @border;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
@ -84,6 +92,12 @@
|
||||||
color: @linkColor;
|
color: @linkColor;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-right: 1px solid @border;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @main;
|
color: @main;
|
||||||
background-color: @appBg;
|
background-color: @appBg;
|
||||||
|
@ -148,6 +162,7 @@
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @accent !important;
|
color: @accent !important;
|
||||||
|
@ -222,6 +237,7 @@
|
||||||
padding: 12px 12px 0;
|
padding: 12px 12px 0;
|
||||||
margin-bottom: -4px;
|
margin-bottom: -4px;
|
||||||
a {
|
a {
|
||||||
|
cursor: pointer;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
color: @main !important;
|
color: @main !important;
|
||||||
|
@ -240,12 +256,16 @@
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
a {
|
a {
|
||||||
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
color: @linkColor !important;
|
color: @linkColor !important;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
border-right: 1px solid @border;
|
border-right: 1px solid @border;
|
||||||
}
|
}
|
||||||
|
@ -263,7 +283,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.checked {
|
&.checked {
|
||||||
border: 0;
|
border: 1px solid @borderDark;
|
||||||
outline: 6px solid @borderDark;
|
outline: 6px solid @borderDark;
|
||||||
.checkbox {
|
.checkbox {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
Loading…
Reference in a new issue