mirror of
https://github.com/go-shiori/shiori.git
synced 2025-02-21 22:43:22 +08:00
Implement update archive in web interface
This commit is contained in:
parent
36e88a2e7a
commit
a040b85ee2
5 changed files with 260 additions and 76 deletions
|
@ -75,7 +75,7 @@ export default {
|
|||
this.$emit("delete", this.eventItem);
|
||||
},
|
||||
updateBookmark() {
|
||||
this.$emit("update", this.id, this.index);
|
||||
this.$emit("update", this.eventItem);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,7 +24,8 @@ var template = `
|
|||
:listMode="displayOptions.listMode"
|
||||
@tagClicked="filterTag"
|
||||
@delete="showDialogDelete"
|
||||
@edit="showDialogEdit">
|
||||
@edit="showDialogEdit"
|
||||
@update="showDialogUpdateArchive">
|
||||
</bookmark-item>
|
||||
<div class="pagination-box" v-if="maxPage > 0">
|
||||
<p>Page</p>
|
||||
|
@ -287,70 +288,6 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
showDialogDelete(items) {
|
||||
// Check and filter items
|
||||
if (typeof items !== "object") return;
|
||||
if (!Array.isArray(items)) items = [items];
|
||||
|
||||
items = items.filter(item => {
|
||||
var id = (typeof item.id === "number") ? item.id : 0,
|
||||
index = (typeof item.index === "number") ? item.index : -1;
|
||||
|
||||
return id > 0 && index > -1;
|
||||
});
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
// Split ids and indices
|
||||
var ids = items.map(item => item.id),
|
||||
indices = items.map(item => item.index).sort((a, b) => b - a);
|
||||
|
||||
// Create title and content
|
||||
var title = "Delete Bookmarks",
|
||||
content = "Delete the selected bookmarks ? This action is irreversible.";
|
||||
|
||||
if (items.length === 1) {
|
||||
title = "Delete Bookmark";
|
||||
content = "Are you sure ? This action is irreversible.";
|
||||
}
|
||||
|
||||
// Show dialog
|
||||
this.showDialog({
|
||||
title: title,
|
||||
content: content,
|
||||
mainText: "Yes",
|
||||
secondText: "No",
|
||||
mainClick: () => {
|
||||
this.dialog.loading = true;
|
||||
fetch("/api/bookmarks", {
|
||||
method: "delete",
|
||||
body: JSON.stringify(ids),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw response;
|
||||
return response;
|
||||
})
|
||||
.then(() => {
|
||||
this.dialog.loading = false;
|
||||
this.dialog.visible = false;
|
||||
indices.forEach(index => this.bookmarks.splice(index, 1))
|
||||
|
||||
if (this.bookmarks.length < 20) {
|
||||
this.loadData(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.dialog.loading = false;
|
||||
err.text().then(msg => {
|
||||
this.showErrorDialog(`${msg} (${err.status})`);
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
showDialogEdit(item) {
|
||||
// Check the item
|
||||
if (typeof item !== "object") return;
|
||||
|
@ -434,6 +371,122 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
showDialogDelete(items) {
|
||||
// Check and filter items
|
||||
if (typeof items !== "object") return;
|
||||
if (!Array.isArray(items)) items = [items];
|
||||
|
||||
items = items.filter(item => {
|
||||
var id = (typeof item.id === "number") ? item.id : 0,
|
||||
index = (typeof item.index === "number") ? item.index : -1;
|
||||
|
||||
return id > 0 && index > -1;
|
||||
});
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
// Split ids and indices
|
||||
var ids = items.map(item => item.id),
|
||||
indices = items.map(item => item.index).sort((a, b) => b - a);
|
||||
|
||||
// Create title and content
|
||||
var title = "Delete Bookmarks",
|
||||
content = "Delete the selected bookmarks ? This action is irreversible.";
|
||||
|
||||
if (items.length === 1) {
|
||||
title = "Delete Bookmark";
|
||||
content = "Are you sure ? This action is irreversible.";
|
||||
}
|
||||
|
||||
// Show dialog
|
||||
this.showDialog({
|
||||
title: title,
|
||||
content: content,
|
||||
mainText: "Yes",
|
||||
secondText: "No",
|
||||
mainClick: () => {
|
||||
this.dialog.loading = true;
|
||||
fetch("/api/bookmarks", {
|
||||
method: "delete",
|
||||
body: JSON.stringify(ids),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw response;
|
||||
return response;
|
||||
})
|
||||
.then(() => {
|
||||
this.dialog.loading = false;
|
||||
this.dialog.visible = false;
|
||||
indices.forEach(index => this.bookmarks.splice(index, 1))
|
||||
|
||||
if (this.bookmarks.length < 20) {
|
||||
this.loadData(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.dialog.loading = false;
|
||||
err.text().then(msg => {
|
||||
this.showErrorDialog(`${msg} (${err.status})`);
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
showDialogUpdateArchive(items) {
|
||||
// Check and filter items
|
||||
if (typeof items !== "object") return;
|
||||
if (!Array.isArray(items)) items = [items];
|
||||
|
||||
items = items.filter(item => {
|
||||
var id = (typeof item.id === "number") ? item.id : 0,
|
||||
index = (typeof item.index === "number") ? item.index : -1;
|
||||
|
||||
return id > 0 && index > -1;
|
||||
});
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
// Show dialog
|
||||
var ids = items.map(item => item.id);
|
||||
|
||||
this.showDialog({
|
||||
title: 'Update Archive',
|
||||
content: 'Update archive for selected bookmarks ? This action is irreversible.',
|
||||
mainText: 'Yes',
|
||||
secondText: 'No',
|
||||
mainClick: () => {
|
||||
this.dialog.loading = true;
|
||||
fetch("/api/archive", {
|
||||
method: "put",
|
||||
body: JSON.stringify(ids),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) throw response;
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
this.dialog.loading = false;
|
||||
this.dialog.visible = false;
|
||||
json.forEach(book => {
|
||||
var item = items.find(el => el.id === book.id);
|
||||
this.bookmarks.splice(item.index, 1, book);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.dialog.loading = false;
|
||||
err.text().then(msg => {
|
||||
this.showErrorDialog(`${msg} (${err.status})`);
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var stateWatcher = (e) => {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -11,6 +11,7 @@ import (
|
|||
fp "path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-shiori/go-readability"
|
||||
|
@ -263,11 +264,11 @@ func (h *handler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
book = results[0]
|
||||
|
||||
// Save article image to local disk
|
||||
imgPath := fp.Join(h.DataDir, "thumb", fmt.Sprintf("%d", book.ID))
|
||||
strID := strconv.Itoa(book.ID)
|
||||
imgPath := fp.Join(h.DataDir, "thumb", strID)
|
||||
for _, imageURL := range imageURLs {
|
||||
err = downloadBookImage(imageURL, imgPath, time.Minute)
|
||||
if err == nil {
|
||||
strID := strconv.Itoa(book.ID)
|
||||
book.ImageURL = path.Join("/", "thumb", strID)
|
||||
break
|
||||
}
|
||||
|
@ -329,7 +330,7 @@ func (h *handler) apiUpdateBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
bookmarks, err := h.DB.GetBookmarks(filter)
|
||||
checkError(err)
|
||||
if len(bookmarks) == 0 {
|
||||
panic(fmt.Errorf("no bookmark with matching index"))
|
||||
panic(fmt.Errorf("no bookmark with matching ids"))
|
||||
}
|
||||
|
||||
// Set new bookmark data
|
||||
|
@ -369,3 +370,133 @@ func (h *handler) apiUpdateBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
err = json.NewEncoder(w).Encode(&newBook)
|
||||
checkError(err)
|
||||
}
|
||||
|
||||
// apiUpdateArchive is handler for PUT /api/archive
|
||||
func (h *handler) apiUpdateArchive(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
// Make sure session still valid
|
||||
err := h.validateSession(r)
|
||||
checkError(err)
|
||||
|
||||
// Decode request
|
||||
ids := []int{}
|
||||
err = json.NewDecoder(r.Body).Decode(&ids)
|
||||
checkError(err)
|
||||
|
||||
// Get existing bookmark from database
|
||||
filter := database.GetBookmarksOptions{
|
||||
IDs: ids,
|
||||
}
|
||||
|
||||
bookmarks, err := h.DB.GetBookmarks(filter)
|
||||
checkError(err)
|
||||
if len(bookmarks) == 0 {
|
||||
panic(fmt.Errorf("no bookmark with matching ids"))
|
||||
}
|
||||
|
||||
// For web interface, let's limit to max 20 IDs to update.
|
||||
// This is done to prevent the REST request from client took too long to finish.
|
||||
if len(bookmarks) > 20 {
|
||||
panic(fmt.Errorf("max 20 bookmarks to update"))
|
||||
}
|
||||
|
||||
// Fetch data from internet
|
||||
mx := sync.RWMutex{}
|
||||
wg := sync.WaitGroup{}
|
||||
chDone := make(chan struct{})
|
||||
chProblem := make(chan int, 10)
|
||||
semaphore := make(chan struct{}, 10)
|
||||
|
||||
for i, book := range bookmarks {
|
||||
wg.Add(1)
|
||||
|
||||
go func(i int, book model.Bookmark) {
|
||||
// Make sure to finish the WG
|
||||
defer wg.Done()
|
||||
|
||||
// Register goroutine to semaphore
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
|
||||
// Download article
|
||||
resp, err := httpClient.Get(book.URL)
|
||||
if err != nil {
|
||||
chProblem <- book.ID
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
article, err := readability.FromReader(resp.Body, book.URL)
|
||||
if err != nil {
|
||||
chProblem <- book.ID
|
||||
return
|
||||
}
|
||||
|
||||
book.Author = article.Byline
|
||||
book.Content = article.TextContent
|
||||
book.HTML = article.Content
|
||||
book.HasContent = book.Content != ""
|
||||
|
||||
if article.Title != "" {
|
||||
book.Title = article.Title
|
||||
}
|
||||
|
||||
if article.Excerpt != "" {
|
||||
book.Excerpt = article.Excerpt
|
||||
}
|
||||
|
||||
// Get image for thumbnail and save it to local disk
|
||||
var imageURLs []string
|
||||
if article.Image != "" {
|
||||
imageURLs = append(imageURLs, article.Image)
|
||||
}
|
||||
|
||||
if article.Favicon != "" {
|
||||
imageURLs = append(imageURLs, article.Favicon)
|
||||
}
|
||||
|
||||
// Save article image to local disk
|
||||
strID := strconv.Itoa(book.ID)
|
||||
imgPath := fp.Join(h.DataDir, "thumb", strID)
|
||||
for _, imageURL := range imageURLs {
|
||||
err = downloadBookImage(imageURL, imgPath, time.Minute)
|
||||
if err == nil {
|
||||
book.ImageURL = path.Join("/", "thumb", strID)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Update list of bookmarks
|
||||
mx.Lock()
|
||||
bookmarks[i] = book
|
||||
mx.Unlock()
|
||||
}(i, book)
|
||||
}
|
||||
|
||||
// Receive all problematic bookmarks
|
||||
idWithProblems := []int{}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-chDone:
|
||||
return
|
||||
case id := <-chProblem:
|
||||
idWithProblems = append(idWithProblems, id)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait until all download finished
|
||||
wg.Wait()
|
||||
close(chDone)
|
||||
|
||||
// Update database
|
||||
_, err = h.DB.SaveBookmarks(bookmarks...)
|
||||
checkError(err)
|
||||
|
||||
// Return new saved result
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(&bookmarks)
|
||||
checkError(err)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func ServeApp(DB database.DB, dataDir string, port int) error {
|
|||
router.POST("/api/bookmarks", hdl.apiInsertBookmark)
|
||||
router.DELETE("/api/bookmarks", hdl.apiDeleteBookmark)
|
||||
router.PUT("/api/bookmarks", hdl.apiUpdateBookmark)
|
||||
// router.PUT("/api/cache", hdl.apiUpdateCache)
|
||||
router.PUT("/api/archive", hdl.apiUpdateArchive)
|
||||
// router.PUT("/api/bookmarks/tags", hdl.apiUpdateBookmarkTags)
|
||||
|
||||
// Route for panic
|
||||
|
|
Loading…
Reference in a new issue