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