Implement update archive in web interface

This commit is contained in:
Radhi Fadlillah 2019-05-30 13:58:58 +07:00
parent 36e88a2e7a
commit a040b85ee2
5 changed files with 260 additions and 76 deletions

View file

@ -75,7 +75,7 @@ export default {
this.$emit("delete", this.eventItem);
},
updateBookmark() {
this.$emit("update", this.id, this.index);
this.$emit("update", this.eventItem);
}
}
}

View file

@ -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

View file

@ -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)
}

View file

@ -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