diff --git a/cmd/add.go b/cmd/add.go
index 1c98192..e5eac88 100644
--- a/cmd/add.go
+++ b/cmd/add.go
@@ -88,5 +88,6 @@ func addBookmark(url, title, excerpt string, tags []string, offline bool) (book
return book, err
}
+ bookmark.Modified = time.Now().UTC().Format("2006-01-02 15:04:05")
return bookmark, nil
}
diff --git a/cmd/serve.go b/cmd/serve.go
index 506c7f0..017750d 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -28,6 +28,7 @@ var (
router.GET("/webfonts/*filepath", serveFiles)
router.GET("/api/bookmarks", apiGetBookmarks)
router.POST("/api/bookmarks", apiInsertBookmarks)
+ router.PUT("/api/bookmarks", apiUpdateBookmarks)
// Route for panic
router.PanicHandler = func(w http.ResponseWriter, r *http.Request, arg interface{}) {
@@ -80,3 +81,25 @@ func apiInsertBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
err = json.NewEncoder(w).Encode(&book)
checkError(err)
}
+
+func apiUpdateBookmarks(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ // Decode request
+ request := model.Bookmark{}
+ err := json.NewDecoder(r.Body).Decode(&request)
+ checkError(err)
+
+ // Convert tags and ID
+ id := []string{fmt.Sprintf("%d", request.ID)}
+ tags := make([]string, len(request.Tags))
+ for i, tag := range request.Tags {
+ tags[i] = tag.Name
+ }
+
+ // Update bookmark
+ bookmarks, err := updateBookmarks(id, request.URL, request.Title, request.Excerpt, tags, false)
+ checkError(err)
+
+ // Return new saved result
+ err = json.NewEncoder(w).Encode(&bookmarks[0])
+ checkError(err)
+}
diff --git a/cmd/update.go b/cmd/update.go
index 013f652..8835a8d 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -58,109 +58,13 @@ var (
}
}
- // Read bookmarks from database
- bookmarks, err := DB.GetBookmarks(db.GetBookmarksOptions{WithContents: true}, args...)
+ // Update bookmarks
+ bookmarks, err := updateBookmarks(args, url, title, excerpt, tags, offline)
if err != nil {
cError.Println(err)
return
}
- if len(bookmarks) == 0 {
- cError.Println("No matching index found")
- return
- }
-
- if url != "" && len(bookmarks) == 1 {
- bookmarks[0].URL = url
- }
-
- // If not offline, fetch articles from internet
- if !offline {
- mutex := sync.Mutex{}
- waitGroup := sync.WaitGroup{}
-
- for i, book := range bookmarks {
- go func(pos int, book model.Bookmark) {
- waitGroup.Add(1)
- defer waitGroup.Done()
-
- article, err := readability.Parse(book.URL, 10*time.Second)
- if err == nil {
- book.Title = article.Meta.Title
- book.ImageURL = article.Meta.Image
- book.Excerpt = article.Meta.Excerpt
- book.Author = article.Meta.Author
- book.MinReadTime = article.Meta.MinReadTime
- book.MaxReadTime = article.Meta.MaxReadTime
- book.Content = article.Content
- book.HTML = article.RawContent
-
- mutex.Lock()
- bookmarks[pos] = book
- mutex.Unlock()
- }
- }(i, book)
- }
-
- waitGroup.Wait()
- }
-
- // Map the tags to be deleted
- addedTags := make(map[string]struct{})
- deletedTags := make(map[string]struct{})
- for _, tag := range tags {
- tag = strings.ToLower(tag)
- tag = strings.TrimSpace(tag)
-
- if strings.HasPrefix(tag, "-") {
- tag = strings.TrimPrefix(tag, "-")
- deletedTags[tag] = struct{}{}
- } else {
- addedTags[tag] = struct{}{}
- }
- }
-
- // Set default title, excerpt and tags
- for i := range bookmarks {
- if title != "" {
- bookmarks[i].Title = title
- }
-
- if excerpt != "" {
- bookmarks[i].Excerpt = excerpt
- }
-
- tempAddedTags := make(map[string]struct{})
- for key, value := range addedTags {
- tempAddedTags[key] = value
- }
-
- newTags := []model.Tag{}
- for _, tag := range bookmarks[i].Tags {
- if _, isDeleted := deletedTags[tag.Name]; isDeleted {
- tag.Deleted = true
- }
-
- if _, alreadyExist := addedTags[tag.Name]; alreadyExist {
- delete(tempAddedTags, tag.Name)
- }
-
- newTags = append(newTags, tag)
- }
-
- for tag := range tempAddedTags {
- newTags = append(newTags, model.Tag{Name: tag})
- }
-
- bookmarks[i].Tags = newTags
- }
-
- err = DB.UpdateBookmarks(bookmarks)
- if err != nil {
- cError.Println("Failed to update bookmarks:", err)
- return
- }
-
printBookmark(bookmarks...)
},
}
@@ -175,3 +79,107 @@ func init() {
updateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt and update ALL bookmarks")
rootCmd.AddCommand(updateCmd)
}
+
+func updateBookmarks(indices []string, url, title, excerpt string, tags []string, offline bool) ([]model.Bookmark, error) {
+ // Read bookmarks from database
+ bookmarks, err := DB.GetBookmarks(db.GetBookmarksOptions{WithContents: true}, indices...)
+ if err != nil {
+ return []model.Bookmark{}, err
+ }
+
+ if len(bookmarks) == 0 {
+ return []model.Bookmark{}, fmt.Errorf("No matching index found")
+ }
+
+ if url != "" && len(bookmarks) == 1 {
+ bookmarks[0].URL = url
+ }
+
+ // If not offline, fetch articles from internet
+ if !offline {
+ mutex := sync.Mutex{}
+ waitGroup := sync.WaitGroup{}
+
+ for i, book := range bookmarks {
+ go func(pos int, book model.Bookmark) {
+ waitGroup.Add(1)
+ defer waitGroup.Done()
+
+ article, err := readability.Parse(book.URL, 10*time.Second)
+ if err == nil {
+ book.Title = article.Meta.Title
+ book.ImageURL = article.Meta.Image
+ book.Excerpt = article.Meta.Excerpt
+ book.Author = article.Meta.Author
+ book.MinReadTime = article.Meta.MinReadTime
+ book.MaxReadTime = article.Meta.MaxReadTime
+ book.Content = article.Content
+ book.HTML = article.RawContent
+
+ mutex.Lock()
+ bookmarks[pos] = book
+ mutex.Unlock()
+ }
+ }(i, book)
+ }
+
+ waitGroup.Wait()
+ }
+
+ // Map the tags to be deleted
+ addedTags := make(map[string]struct{})
+ deletedTags := make(map[string]struct{})
+ for _, tag := range tags {
+ tag = strings.ToLower(tag)
+ tag = strings.TrimSpace(tag)
+
+ if strings.HasPrefix(tag, "-") {
+ tag = strings.TrimPrefix(tag, "-")
+ deletedTags[tag] = struct{}{}
+ } else {
+ addedTags[tag] = struct{}{}
+ }
+ }
+
+ // Set default title, excerpt and tags
+ for i := range bookmarks {
+ if title != "" {
+ bookmarks[i].Title = title
+ }
+
+ if excerpt != "" {
+ bookmarks[i].Excerpt = excerpt
+ }
+
+ tempAddedTags := make(map[string]struct{})
+ for key, value := range addedTags {
+ tempAddedTags[key] = value
+ }
+
+ newTags := []model.Tag{}
+ for _, tag := range bookmarks[i].Tags {
+ if _, isDeleted := deletedTags[tag.Name]; isDeleted {
+ tag.Deleted = true
+ }
+
+ if _, alreadyExist := addedTags[tag.Name]; alreadyExist {
+ delete(tempAddedTags, tag.Name)
+ }
+
+ newTags = append(newTags, tag)
+ }
+
+ for tag := range tempAddedTags {
+ newTags = append(newTags, model.Tag{Name: tag})
+ }
+
+ bookmarks[i].Tags = newTags
+ }
+
+ err = DB.UpdateBookmarks(bookmarks)
+ if err != nil {
+ return []model.Bookmark{}, fmt.Errorf("Failed to update bookmarks: %v", err)
+ }
+
+ return bookmarks, nil
+}
diff --git a/view/css/stylesheet.css b/view/css/stylesheet.css
index 39b0e53..a5c272e 100644
--- a/view/css/stylesheet.css
+++ b/view/css/stylesheet.css
@@ -1 +1 @@
-.header-link{border-right:1px solid #E5E5E5;color:#000;cursor:pointer;font-size:.9em;line-height:70px;overflow:hidden;padding:0 16px}.header-link:hover{color:#3F51B5}*{border-width:0;box-sizing:border-box;font-family:"Source Sans Pro",sans-serif;margin:0;padding:0;text-decoration:none}.spacer{-webkit-box-flex:1;flex:1 0}#app{background-color:#F5F5F5;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;height:auto;min-height:100vh}#app #header{background-color:#FFF;box-shadow:0 0 3px rgba(0,0,0,0.3);left:0;position:fixed;right:0;top:0;z-index:99;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap}#app #header #logo{border-left:1px solid #E5E5E5;cursor:default;flex-shrink:0;border-right:1px solid #E5E5E5;color:#000;cursor:pointer;font-size:.9em;overflow:hidden;padding:0 16px;line-height:70px;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;font-size:1.5em;font-weight:100;color:#3F51B5}#app #header #logo:hover{color:#3F51B5}#app #header #logo span{margin-right:8px}#app #header #logo:hover{background-color:#F5F5F5}#app #header #search-box{-webkit-box-align:center;align-items:center;border-right:1px solid #E5E5E5;display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:16px;width:100%}#app #header #search-box .button,#app #header #search-box input{background-color:#FFF;border:1px solid #E5E5E5;color:#000;font-size:.9em;padding:8px}#app #header #search-box .button{cursor:pointer;color:#535A60}#app #header #search-box .button:hover{color:#F44336}#app #header #search-box input{border-right:0;-webkit-box-flex:1;flex:1 0;padding:8px 16px}#app #main{margin-top:70px;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap}#app #main #new-bookmark{align-self:center;max-width:600px;width:100%;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;margin:32px 8px 20px;background-color:#FFF;outline:1px solid #E5E5E5}#app #main #new-bookmark input[type=text],#app #main #new-bookmark textarea{outline:1px solid #E5E5E5;color:#000;font-size:.9em;padding:12px 16px}#app #main #new-bookmark textarea{resize:vertical;min-height:4em;max-height:10em}#app #main #new-bookmark .button-area{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:8px}#app #main #new-bookmark .button-area a{color:#535A60;text-transform:uppercase;padding:8px;background-color:#FFF}#app #main #new-bookmark .button-area a.button{font-size:.9em;cursor:pointer}#app #main #new-bookmark .button-area a.button:hover{color:#F44336}#app #main #grid{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row;padding:4px}#app #main #grid>.column{-webkit-box-flex:1;flex:1 0;padding:12px}#app #main #grid>.column>*:not(:last-child){margin-bottom:24px}#app #main #progress-bar{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column;-webkit-box-align:center;align-items:center;padding:32px}#app #main #progress-bar i{color:#6F757A;font-size:3em}#app #main #progress-bar a{color:#3F51B5 !important;font-size:.9em}#app #main #progress-bar a:hover{color:#F44336 !important}.bookmark{background-color:#FFF;border:1px solid #E5E5E5;position:relative}.bookmark .checkbox{z-index:9;right:0;opacity:0;position:absolute;outline:1px solid #E5E5E5;color:#535A60;background-color:#FFF;width:32px;line-height:32px;text-align:center;display:block;font-size:.9em}.bookmark .checkbox:hover{color:#F44336 !important}.bookmark .bookmark-metadata{padding:16px;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap}.bookmark .bookmark-metadata .bookmark-time{color:#6F757A;font-size:.9em;margin-bottom:8px}.bookmark .bookmark-metadata .bookmark-title{color:#000;font-size:1.3em;font-weight:600}.bookmark .bookmark-metadata .bookmark-url{color:#6F757A;font-size:.9em;margin-bottom:8px;margin-bottom:0;margin-top:8px;max-height:2.6em;line-height:1.3em;text-overflow:ellipsis;overflow:hidden}.bookmark .bookmark-metadata.has-image{min-height:250px;background-position:center;background-repeat:no-repeat;background-size:cover;-webkit-box-pack:end;justify-content:flex-end;position:relative}.bookmark .bookmark-metadata.has-image::before{content:"";background-color:rgba(0,0,0,0.5);position:absolute;top:0;left:0;right:0;bottom:0;z-index:0}.bookmark .bookmark-metadata.has-image .bookmark-time,.bookmark .bookmark-metadata.has-image .bookmark-url{z-index:2;color:white;text-shadow:1px 1px 1px rgba(0,0,0,0.5)}.bookmark .bookmark-metadata.has-image .bookmark-title{z-index:2;color:white;text-shadow:1px 1px 1px rgba(0,0,0,0.5)}.bookmark .bookmark-metadata:hover .bookmark-title{text-decoration:underline}.bookmark .bookmark-excerpt{padding:16px;color:#000;border-top:1px solid #E5E5E5}.bookmark .bookmark-tags{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;padding:0 12px 12px;margin-top:-4px}.bookmark .bookmark-tags a{font-size:.9em;padding:4px;color:#3F51B5 !important}.bookmark .bookmark-tags a::before{content:"#"}.bookmark .bookmark-tags a:hover{text-decoration:underline}.bookmark .bookmark-menu{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;border-top:1px solid #E5E5E5;visibility:hidden}.bookmark .bookmark-menu a{display:block;-webkit-box-flex:1;flex:1 0;color:#535A60 !important;padding:8px;font-size:.9em;text-align:center}.bookmark .bookmark-menu a:not(:last-child){border-right:1px solid #E5E5E5}.bookmark .bookmark-menu a:hover{color:#F44336 !important}.bookmark:hover .checkbox{opacity:1}.bookmark:hover .bookmark-menu{visibility:visible}.bookmark.checked{border:0;outline:6px solid #9E9E9E}.bookmark.checked .checkbox{opacity:1;outline:0;background-color:#9E9E9E;color:white}
\ No newline at end of file
+.header-link{border-right:1px solid #E5E5E5;color:#000;cursor:pointer;font-size:.9em;line-height:70px;overflow:hidden;padding:0 16px}.header-link:hover{color:#3F51B5}*{border-width:0;box-sizing:border-box;font-family:"Source Sans Pro",sans-serif;margin:0;padding:0;text-decoration:none}.spacer{-webkit-box-flex:1;flex:1 0}#app{background-color:#F5F5F5;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;height:auto;min-height:100vh}#app #header{background-color:#FFF;box-shadow:0 0 3px rgba(0,0,0,0.3);left:0;position:fixed;right:0;top:0;z-index:99;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap}#app #header #logo{border-left:1px solid #E5E5E5;cursor:default;flex-shrink:0;border-right:1px solid #E5E5E5;color:#000;cursor:pointer;font-size:.9em;overflow:hidden;padding:0 16px;line-height:70px;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;font-size:1.5em;font-weight:100;color:#3F51B5}#app #header #logo:hover{color:#3F51B5}#app #header #logo span{margin-right:8px}#app #header #logo:hover{background-color:#F5F5F5}#app #header #search-box{-webkit-box-align:center;align-items:center;border-right:1px solid #E5E5E5;display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:16px;width:100%}#app #header #search-box .button,#app #header #search-box input{background-color:#FFF;border:1px solid #E5E5E5;color:#000;font-size:.9em;padding:8px}#app #header #search-box .button{cursor:pointer;color:#535A60}#app #header #search-box .button:hover{color:#F44336}#app #header #search-box input{border-right:0;-webkit-box-flex:1;flex:1 0;padding:8px 16px}#app #main{margin-top:70px;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap}#app #main #input-bookmark{align-self:center;max-width:600px;width:100%;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;margin:32px 8px 20px;background-color:#FFF;outline:1px solid #E5E5E5}#app #main #input-bookmark>p{color:#000;font-weight:600;text-transform:uppercase;padding:16px}#app #main #input-bookmark input[type=text],#app #main #input-bookmark textarea{outline:1px solid #E5E5E5;color:#000;font-size:.9em;padding:12px 16px}#app #main #input-bookmark textarea{resize:vertical;min-height:4em;max-height:10em}#app #main #input-bookmark .button-area{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:8px}#app #main #input-bookmark .button-area a{color:#535A60;text-transform:uppercase;padding:8px;background-color:#FFF}#app #main #input-bookmark .button-area a.button{font-size:.9em;cursor:pointer}#app #main #input-bookmark .button-area a.button:hover{color:#F44336}#app #main #grid{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row;padding:4px}#app #main #grid>.column{-webkit-box-flex:1;flex:1 0;padding:12px}#app #main #grid>.column>*:not(:last-child){margin-bottom:24px}#app #main #progress-bar{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column;-webkit-box-align:center;align-items:center;padding:32px}#app #main #progress-bar i{color:#6F757A;font-size:3em}#app #main #progress-bar a{color:#3F51B5 !important;font-size:.9em}#app #main #progress-bar a:hover{color:#F44336 !important}.bookmark{background-color:#FFF;border:1px solid #E5E5E5;position:relative}.bookmark .checkbox{z-index:9;right:0;opacity:0;position:absolute;outline:1px solid #E5E5E5;color:#535A60;background-color:#FFF;width:32px;line-height:32px;text-align:center;display:block;font-size:.9em}.bookmark .checkbox:hover{color:#F44336 !important}.bookmark .bookmark-metadata{padding:16px;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;border-bottom:1px solid #E5E5E5}.bookmark .bookmark-metadata .bookmark-time{color:#6F757A;font-size:.9em;margin-bottom:8px}.bookmark .bookmark-metadata .bookmark-title{color:#000;font-size:1.3em;font-weight:600}.bookmark .bookmark-metadata .bookmark-url{color:#6F757A;font-size:.9em;margin-bottom:8px;margin-bottom:0;margin-top:8px;max-height:2.6em;line-height:1.3em;text-overflow:ellipsis;overflow:hidden}.bookmark .bookmark-metadata.has-image{min-height:250px;background-position:center;background-repeat:no-repeat;background-size:cover;-webkit-box-pack:end;justify-content:flex-end;position:relative}.bookmark .bookmark-metadata.has-image::before{content:"";background-color:rgba(0,0,0,0.5);position:absolute;top:0;left:0;right:0;bottom:0;z-index:0}.bookmark .bookmark-metadata.has-image .bookmark-time,.bookmark .bookmark-metadata.has-image .bookmark-url{z-index:2;color:white;text-shadow:1px 1px 1px rgba(0,0,0,0.5)}.bookmark .bookmark-metadata.has-image .bookmark-title{z-index:2;color:white;text-shadow:1px 1px 1px rgba(0,0,0,0.5)}.bookmark .bookmark-metadata:hover .bookmark-title{text-decoration:underline}.bookmark .bookmark-excerpt{padding:16px 16px 0;color:#000}.bookmark .bookmark-tags{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;padding:12px 12px 0;margin-bottom:-4px}.bookmark .bookmark-tags a{font-size:.9em;padding:4px;color:#3F51B5 !important}.bookmark .bookmark-tags a::before{content:"#"}.bookmark .bookmark-tags a:hover{text-decoration:underline}.bookmark .bookmark-menu{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;border-top:1px solid #E5E5E5;visibility:hidden;margin-top:16px}.bookmark .bookmark-menu a{display:block;-webkit-box-flex:1;flex:1 0;color:#535A60 !important;padding:8px;font-size:.9em;text-align:center}.bookmark .bookmark-menu a:not(:last-child){border-right:1px solid #E5E5E5}.bookmark .bookmark-menu a:hover{color:#F44336 !important}.bookmark:hover .checkbox{opacity:1}.bookmark:hover .bookmark-menu{visibility:visible}.bookmark.checked{border:0;outline:6px solid #9E9E9E}.bookmark.checked .checkbox{opacity:1;outline:0;background-color:#9E9E9E;color:white}
\ No newline at end of file
diff --git a/view/index.html b/view/index.html
index 5a4eece..9e63566 100644
--- a/view/index.html
+++ b/view/index.html
@@ -26,19 +26,20 @@