mirror of
https://github.com/go-shiori/shiori.git
synced 2024-09-20 06:56:10 +08:00
Merge branch 'master' into fmartingr/issue657
This commit is contained in:
commit
a91515ca2b
|
@ -1,6 +1,11 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
git:
|
||||
ignore_tags:
|
||||
- "{{ if not .IsNightly }}*-rc*{{ end }}"
|
||||
|
||||
builds:
|
||||
- binary: shiori
|
||||
env:
|
||||
|
@ -26,6 +31,7 @@ builds:
|
|||
goarch: arm
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
|
||||
archives:
|
||||
- id: shiori
|
||||
name_template: >-
|
||||
|
@ -47,8 +53,10 @@ archives:
|
|||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
groups:
|
||||
|
@ -79,5 +87,6 @@ changelog:
|
|||
exclude:
|
||||
- "^deps:"
|
||||
- "^chore\\(deps\\):"
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
|
|
|
@ -520,6 +520,9 @@ const docTemplate = `{
|
|||
"description": "TODO: migrate outside the DTO",
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"excerpt": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -541,7 +544,7 @@ const docTemplate = `{
|
|||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"modified": {
|
||||
"modifiedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"public": {
|
||||
|
|
|
@ -509,6 +509,9 @@
|
|||
"description": "TODO: migrate outside the DTO",
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"excerpt": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -530,7 +533,7 @@
|
|||
"imageURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"modified": {
|
||||
"modifiedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"public": {
|
||||
|
|
|
@ -99,6 +99,8 @@ definitions:
|
|||
create_ebook:
|
||||
description: 'TODO: migrate outside the DTO'
|
||||
type: boolean
|
||||
createdAt:
|
||||
type: string
|
||||
excerpt:
|
||||
type: string
|
||||
hasArchive:
|
||||
|
@ -113,7 +115,7 @@ definitions:
|
|||
type: integer
|
||||
imageURL:
|
||||
type: string
|
||||
modified:
|
||||
modifiedAt:
|
||||
type: string
|
||||
public:
|
||||
type: integer
|
||||
|
|
|
@ -62,7 +62,7 @@ func exportHandler(cmd *cobra.Command, args []string) {
|
|||
|
||||
for _, book := range bookmarks {
|
||||
// Create Unix timestamp for bookmark
|
||||
modifiedTime, err := time.Parse(model.DatabaseDateFormat, book.Modified)
|
||||
modifiedTime, err := time.Parse(model.DatabaseDateFormat, book.ModifiedAt)
|
||||
if err != nil {
|
||||
modifiedTime = time.Now()
|
||||
}
|
||||
|
|
|
@ -136,10 +136,10 @@ func importHandler(cmd *cobra.Command, args []string) {
|
|||
|
||||
// Add item to list
|
||||
bookmark := model.BookmarkDTO{
|
||||
URL: url,
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
Modified: modifiedDate.Format(model.DatabaseDateFormat),
|
||||
URL: url,
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
ModifiedAt: modifiedDate.Format(model.DatabaseDateFormat),
|
||||
}
|
||||
|
||||
mapURL[url] = struct{}{}
|
||||
|
|
|
@ -94,10 +94,10 @@ func pocketHandler(cmd *cobra.Command, args []string) {
|
|||
|
||||
// Add item to list
|
||||
bookmark := model.BookmarkDTO{
|
||||
URL: url,
|
||||
Title: title,
|
||||
Modified: modified.Format(model.DatabaseDateFormat),
|
||||
Tags: tags,
|
||||
URL: url,
|
||||
Title: title,
|
||||
ModifiedAt: modified.Format(model.DatabaseDateFormat),
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
mapURL[url] = struct{}{}
|
||||
|
|
|
@ -62,6 +62,12 @@ func GenerateEbook(deps *dependencies.Dependencies, req ProcessRequest, dstPath
|
|||
|
||||
ebook.SetTitle(book.Title)
|
||||
ebook.SetAuthor(book.Author)
|
||||
if deps.Domains.Storage.FileExists(bookmarkThumbnailPath) {
|
||||
// TODO: Use `deps.Domains.Storage` to retrieve the file.
|
||||
absoluteCoverPath := fp.Join(deps.Config.Storage.DataDir, bookmarkThumbnailPath)
|
||||
coverPath, _ := ebook.AddImage(absoluteCoverPath, "cover.jpg")
|
||||
ebook.SetCover(coverPath, "")
|
||||
}
|
||||
ebook.SetDescription(book.Excerpt)
|
||||
_, err = ebook.AddSection(`<h1 style="text-align:center"> `+book.Title+` </h1>`+book.HTML+lastline, book.Title, "", "")
|
||||
if err != nil {
|
||||
|
|
|
@ -120,6 +120,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
}
|
||||
|
||||
book.HasContent = book.Content != ""
|
||||
book.ModifiedAt = ""
|
||||
}
|
||||
|
||||
// Save article image to local disk
|
||||
|
@ -137,6 +138,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
}
|
||||
if err == nil {
|
||||
book.ImageURL = fp.Join("/", "bookmark", strID, "thumb")
|
||||
book.ModifiedAt = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -154,6 +156,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
return book, true, errors.Wrap(err, "failed to create ebook")
|
||||
}
|
||||
book.HasEbook = true
|
||||
book.ModifiedAt = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,6 +189,7 @@ func ProcessBookmark(deps *dependencies.Dependencies, req ProcessRequest) (book
|
|||
}
|
||||
|
||||
book.HasArchive = true
|
||||
book.ModifiedAt = ""
|
||||
}
|
||||
|
||||
return book, false, nil
|
||||
|
|
|
@ -3,6 +3,7 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-shiori/shiori/internal/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -15,19 +16,21 @@ type testDatabaseFactory func(t *testing.T, ctx context.Context) (DB, error)
|
|||
func testDatabase(t *testing.T, dbFactory testDatabaseFactory) {
|
||||
tests := map[string]databaseTestCase{
|
||||
// Bookmarks
|
||||
"testBookmarkAutoIncrement": testBookmarkAutoIncrement,
|
||||
"testCreateBookmark": testCreateBookmark,
|
||||
"testCreateBookmarkWithContent": testCreateBookmarkWithContent,
|
||||
"testCreateBookmarkTwice": testCreateBookmarkTwice,
|
||||
"testCreateBookmarkWithTag": testCreateBookmarkWithTag,
|
||||
"testCreateTwoDifferentBookmarks": testCreateTwoDifferentBookmarks,
|
||||
"testUpdateBookmark": testUpdateBookmark,
|
||||
"testUpdateBookmarkWithContent": testUpdateBookmarkWithContent,
|
||||
"testGetBookmark": testGetBookmark,
|
||||
"testGetBookmarkNotExistent": testGetBookmarkNotExistent,
|
||||
"testGetBookmarks": testGetBookmarks,
|
||||
"testGetBookmarksWithSQLCharacters": testGetBookmarksWithSQLCharacters,
|
||||
"testGetBookmarksCount": testGetBookmarksCount,
|
||||
"testBookmarkAutoIncrement": testBookmarkAutoIncrement,
|
||||
"testCreateBookmark": testCreateBookmark,
|
||||
"testCreateBookmarkWithContent": testCreateBookmarkWithContent,
|
||||
"testCreateBookmarkTwice": testCreateBookmarkTwice,
|
||||
"testCreateBookmarkWithTag": testCreateBookmarkWithTag,
|
||||
"testCreateTwoDifferentBookmarks": testCreateTwoDifferentBookmarks,
|
||||
"testUpdateBookmark": testUpdateBookmark,
|
||||
"testUpdateBookmarkUpdatesModifiedTime": testUpdateBookmarkUpdatesModifiedTime,
|
||||
"testGetBoomarksWithTimeFilters": testGetBoomarksWithTimeFilters,
|
||||
"testUpdateBookmarkWithContent": testUpdateBookmarkWithContent,
|
||||
"testGetBookmark": testGetBookmark,
|
||||
"testGetBookmarkNotExistent": testGetBookmarkNotExistent,
|
||||
"testGetBookmarks": testGetBookmarks,
|
||||
"testGetBookmarksWithSQLCharacters": testGetBookmarksWithSQLCharacters,
|
||||
"testGetBookmarksCount": testGetBookmarksCount,
|
||||
// Tags
|
||||
"testCreateTag": testCreateTag,
|
||||
"testCreateTags": testCreateTags,
|
||||
|
@ -500,3 +503,92 @@ func testListAccountsWithPassword(t *testing.T, db DB) {
|
|||
require.NotEmpty(t, acc.Password)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider using `t.Parallel()` once we have automated database tests spawning databases using testcontainers.
|
||||
func testUpdateBookmarkUpdatesModifiedTime(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book := model.BookmarkDTO{
|
||||
URL: "https://github.com/go-shiori/shiori",
|
||||
Title: "shiori",
|
||||
}
|
||||
|
||||
resultBook, err := db.SaveBookmarks(ctx, true, book)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
updatedBook := resultBook[0]
|
||||
updatedBook.Title = "modified"
|
||||
updatedBook.ModifiedAt = ""
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
resultUpdatedBooks, err := db.SaveBookmarks(ctx, false, updatedBook)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
assert.NotEqual(t, resultBook[0].ModifiedAt, resultUpdatedBooks[0].ModifiedAt)
|
||||
assert.Equal(t, resultBook[0].CreatedAt, resultUpdatedBooks[0].CreatedAt)
|
||||
assert.Equal(t, resultBook[0].CreatedAt, resultBook[0].ModifiedAt)
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
|
||||
assert.Equal(t, updatedBook.Title, resultUpdatedBooks[0].Title, "Saved bookmark must have updated Title")
|
||||
}
|
||||
|
||||
// TODO: Consider using `t.Parallel()` once we have automated database tests spawning databases using testcontainers.
|
||||
func testGetBoomarksWithTimeFilters(t *testing.T, db DB) {
|
||||
ctx := context.TODO()
|
||||
|
||||
book1 := model.BookmarkDTO{
|
||||
URL: "https://github.com/go-shiori/shiori/one",
|
||||
Title: "Added First but Modified Last",
|
||||
}
|
||||
book2 := model.BookmarkDTO{
|
||||
URL: "https://github.com/go-shiori/shiori/second",
|
||||
Title: "Added Last but Modified First",
|
||||
}
|
||||
|
||||
// create two new bookmark
|
||||
resultBook1, err := db.SaveBookmarks(ctx, true, book1)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
time.Sleep(1 * time.Second)
|
||||
resultBook2, err := db.SaveBookmarks(ctx, true, book2)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
// update those bookmarks
|
||||
updatedBook1 := resultBook1[0]
|
||||
updatedBook1.Title = "Added First but Modified Last Updated Title"
|
||||
updatedBook1.ModifiedAt = ""
|
||||
|
||||
updatedBook2 := resultBook2[0]
|
||||
updatedBook2.Title = "Last Added but modified First Updated Title"
|
||||
updatedBook2.ModifiedAt = ""
|
||||
|
||||
// modified bookmark2 first after one second modified bookmark1
|
||||
resultUpdatedBook2, err := db.SaveBookmarks(ctx, false, updatedBook2)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
time.Sleep(1 * time.Second)
|
||||
resultUpdatedBook1, err := db.SaveBookmarks(ctx, false, updatedBook1)
|
||||
assert.NoError(t, err, "Save bookmarks must not fail")
|
||||
|
||||
// get diffrent filteter combination
|
||||
booksOrderByLastAdded, err := db.GetBookmarks(ctx, GetBookmarksOptions{
|
||||
IDs: []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
|
||||
OrderMethod: 1,
|
||||
})
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
booksOrderByLastModified, err := db.GetBookmarks(ctx, GetBookmarksOptions{
|
||||
IDs: []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
|
||||
OrderMethod: 2,
|
||||
})
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
booksOrderById, err := db.GetBookmarks(ctx, GetBookmarksOptions{
|
||||
IDs: []int{resultUpdatedBook1[0].ID, resultUpdatedBook2[0].ID},
|
||||
OrderMethod: 0,
|
||||
})
|
||||
assert.NoError(t, err, "Get bookmarks must not fail")
|
||||
|
||||
// Check Last Added
|
||||
assert.Equal(t, booksOrderByLastAdded[0].Title, updatedBook2.Title)
|
||||
// Check Last Modified
|
||||
assert.Equal(t, booksOrderByLastModified[0].Title, updatedBook1.Title)
|
||||
// Second id should be 2 if order them by id
|
||||
assert.Equal(t, booksOrderById[1].ID, 2)
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE bookmark RENAME COLUMN modified to created_at;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE bookmark
|
||||
MODIFY created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE bookmark
|
||||
ADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE bookmark
|
||||
SET modified_at = COALESCE(created_at, CURRENT_TIMESTAMP)
|
||||
WHERE created_at IS NOT NULL;
|
|
@ -0,0 +1 @@
|
|||
CREATE INDEX idx_created_at ON bookmark (created_at);
|
|
@ -0,0 +1 @@
|
|||
CREATE INDEX idx_modified_at ON bookmark (modified_at);
|
|
@ -0,0 +1,16 @@
|
|||
-- Rename "modified" column to "created_at"
|
||||
ALTER TABLE bookmark
|
||||
RENAME COLUMN modified to created_at;
|
||||
|
||||
-- Add the "modified_at" column to the bookmark table
|
||||
ALTER TABLE bookmark
|
||||
ADD COLUMN modified_at TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- Update the "modified_at" column with the value from the "created_at" column if it is not null
|
||||
UPDATE bookmark
|
||||
SET modified_at = COALESCE(created_at, CURRENT_TIMESTAMP)
|
||||
WHERE created_at IS NOT NULL;
|
||||
|
||||
-- Index for "created_at" "modified_at""
|
||||
CREATE INDEX idx_created_at ON bookmark(created_at);
|
||||
CREATE INDEX idx_modified_at ON bookmark(modified_at);
|
12
internal/database/migrations/sqlite/0004_created_time.up.sql
Normal file
12
internal/database/migrations/sqlite/0004_created_time.up.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
ALTER TABLE bookmark
|
||||
RENAME COLUMN modified to created_at;
|
||||
|
||||
ALTER TABLE bookmark
|
||||
ADD COLUMN modified_at TEXT NULL;
|
||||
|
||||
UPDATE bookmark
|
||||
SET modified_at = bookmark.created_at
|
||||
WHERE created_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX idx_created_at ON bookmark(created_at);
|
||||
CREATE INDEX idx_modified_at ON bookmark(modified_at);
|
|
@ -60,6 +60,12 @@ var mysqlMigrations = []migration{
|
|||
|
||||
return nil
|
||||
}),
|
||||
newFileMigration("0.7.0", "0.8.0", "mysql/0005_rename_to_created_at"),
|
||||
newFileMigration("0.8.0", "0.8.1", "mysql/0006_change_created_at_settings"),
|
||||
newFileMigration("0.8.1", "0.8.2", "mysql/0007_add_modified_at"),
|
||||
newFileMigration("0.8.2", "0.8.3", "mysql/0008_set_modified_at_equal_created_at"),
|
||||
newFileMigration("0.8.3", "0.8.4", "mysql/0009_index_for_created_at"),
|
||||
newFileMigration("0.8.4", "0.8.5", "mysql/0010_index_for_modified_at"),
|
||||
}
|
||||
|
||||
// MySQLDatabase is implementation of Database interface
|
||||
|
@ -130,8 +136,8 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
(url, title, excerpt, author, public, content, html, modified_at, created_at)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -144,7 +150,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
public = ?,
|
||||
content = ?,
|
||||
html = ?,
|
||||
modified = ?
|
||||
modified_at = ?
|
||||
WHERE id = ?`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -188,17 +194,18 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
}
|
||||
|
||||
// Set modified time
|
||||
if book.Modified == "" {
|
||||
book.Modified = modifiedTime
|
||||
if book.ModifiedAt == "" {
|
||||
book.ModifiedAt = modifiedTime
|
||||
}
|
||||
|
||||
// Save bookmark
|
||||
var err error
|
||||
if create {
|
||||
book.CreatedAt = modifiedTime
|
||||
var res sql.Result
|
||||
res, err = stmtInsertBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.CreatedAt)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -210,7 +217,7 @@ func (db *MySQLDatabase) SaveBookmarks(ctx context.Context, create bool, bookmar
|
|||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified, book.ID)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -284,7 +291,8 @@ func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpti
|
|||
`excerpt`,
|
||||
`author`,
|
||||
`public`,
|
||||
`modified`,
|
||||
`created_at`,
|
||||
`modified_at`,
|
||||
`content <> "" has_content`}
|
||||
|
||||
if opts.WithContent {
|
||||
|
@ -370,7 +378,7 @@ func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpti
|
|||
case ByLastAdded:
|
||||
query += ` ORDER BY id DESC`
|
||||
case ByLastModified:
|
||||
query += ` ORDER BY modified DESC`
|
||||
query += ` ORDER BY modified_at DESC`
|
||||
default:
|
||||
query += ` ORDER BY id`
|
||||
}
|
||||
|
@ -563,7 +571,7 @@ func (db *MySQLDatabase) GetBookmark(ctx context.Context, id int, url string) (m
|
|||
args := []interface{}{id}
|
||||
query := `SELECT
|
||||
id, url, title, excerpt, author, public,
|
||||
content, html, modified, content <> '' has_content
|
||||
content, html, modified_at, created_at, content <> '' has_content
|
||||
FROM bookmark WHERE id = ?`
|
||||
|
||||
if url != "" {
|
||||
|
|
|
@ -57,6 +57,7 @@ var postgresMigrations = []migration{
|
|||
|
||||
return nil
|
||||
}),
|
||||
newFileMigration("0.3.0", "0.4.0", "postgres/0002_created_time"),
|
||||
}
|
||||
|
||||
// PGDatabase is implementation of Database interface
|
||||
|
@ -128,8 +129,8 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks
|
|||
if err := db.withTx(ctx, func(tx *sqlx.Tx) error {
|
||||
// Prepare statement
|
||||
stmtInsertBook, err := tx.Preparex(`INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, content, html, modified)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
(url, title, excerpt, author, public, content, html, modified_at, created_at)
|
||||
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -143,7 +144,7 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks
|
|||
public = $5,
|
||||
content = $6,
|
||||
html = $7,
|
||||
modified = $8
|
||||
modified_at = $8
|
||||
WHERE id = $9`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -187,20 +188,21 @@ func (db *PGDatabase) SaveBookmarks(ctx context.Context, create bool, bookmarks
|
|||
}
|
||||
|
||||
// Set modified time
|
||||
if book.Modified == "" {
|
||||
book.Modified = modifiedTime
|
||||
if book.ModifiedAt == "" {
|
||||
book.ModifiedAt = modifiedTime
|
||||
}
|
||||
|
||||
// Save bookmark
|
||||
var err error
|
||||
if create {
|
||||
book.CreatedAt = modifiedTime
|
||||
err = stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified).Scan(&book.ID)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.CreatedAt).Scan(&book.ID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author,
|
||||
book.Public, book.Content, book.HTML, book.Modified, book.ID)
|
||||
book.Public, book.Content, book.HTML, book.ModifiedAt, book.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -270,7 +272,8 @@ func (db *PGDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions
|
|||
`excerpt`,
|
||||
`author`,
|
||||
`public`,
|
||||
`modified`,
|
||||
`created_at`,
|
||||
`modified_at`,
|
||||
`content <> '' has_content`}
|
||||
|
||||
if opts.WithContent {
|
||||
|
@ -359,7 +362,7 @@ func (db *PGDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOptions
|
|||
case ByLastAdded:
|
||||
query += ` ORDER BY id DESC`
|
||||
case ByLastModified:
|
||||
query += ` ORDER BY modified DESC`
|
||||
query += ` ORDER BY modified_at DESC`
|
||||
default:
|
||||
query += ` ORDER BY id`
|
||||
}
|
||||
|
@ -570,7 +573,7 @@ func (db *PGDatabase) GetBookmark(ctx context.Context, id int, url string) (mode
|
|||
args := []interface{}{id}
|
||||
query := `SELECT
|
||||
id, url, title, excerpt, author, public,
|
||||
content, html, modified, content <> '' has_content
|
||||
content, html, modified_at, created_at, content <> '' has_content
|
||||
FROM bookmark WHERE id = $1`
|
||||
|
||||
if url != "" {
|
||||
|
|
|
@ -59,6 +59,7 @@ var sqliteMigrations = []migration{
|
|||
}),
|
||||
newFileMigration("0.3.0", "0.4.0", "sqlite/0002_denormalize_content"),
|
||||
newFileMigration("0.4.0", "0.5.0", "sqlite/0003_uniq_id"),
|
||||
newFileMigration("0.5.0", "0.6.0", "sqlite/0004_created_time"),
|
||||
}
|
||||
|
||||
// SQLiteDatabase is implementation of Database interface
|
||||
|
@ -126,15 +127,15 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma
|
|||
// Prepare statement
|
||||
|
||||
stmtInsertBook, err := tx.PreparexContext(ctx, `INSERT INTO bookmark
|
||||
(url, title, excerpt, author, public, modified, has_content)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?) RETURNING id`)
|
||||
(url, title, excerpt, author, public, modified_at, has_content, created_at)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?) RETURNING id`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
stmtUpdateBook, err := tx.PreparexContext(ctx, `UPDATE bookmark SET
|
||||
url = ?, title = ?, excerpt = ?, author = ?,
|
||||
public = ?, modified = ?, has_content = ?
|
||||
public = ?, modified_at = ?, has_content = ?
|
||||
WHERE id = ?`)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -192,8 +193,8 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma
|
|||
}
|
||||
|
||||
// Set modified time
|
||||
if book.Modified == "" {
|
||||
book.Modified = modifiedTime
|
||||
if book.ModifiedAt == "" {
|
||||
book.ModifiedAt = modifiedTime
|
||||
}
|
||||
|
||||
hasContent := book.Content != ""
|
||||
|
@ -201,11 +202,12 @@ func (db *SQLiteDatabase) SaveBookmarks(ctx context.Context, create bool, bookma
|
|||
// Create or update bookmark
|
||||
var err error
|
||||
if create {
|
||||
book.CreatedAt = modifiedTime
|
||||
err = stmtInsertBook.QueryRowContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent).Scan(&book.ID)
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.ModifiedAt, hasContent, book.CreatedAt).Scan(&book.ID)
|
||||
} else {
|
||||
_, err = stmtUpdateBook.ExecContext(ctx,
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.Modified, hasContent, book.ID)
|
||||
book.URL, book.Title, book.Excerpt, book.Author, book.Public, book.ModifiedAt, hasContent, book.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
|
@ -297,7 +299,8 @@ func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpt
|
|||
b.excerpt,
|
||||
b.author,
|
||||
b.public,
|
||||
b.modified,
|
||||
b.created_at,
|
||||
b.modified_at,
|
||||
b.has_content
|
||||
FROM bookmark b
|
||||
WHERE 1`
|
||||
|
@ -388,7 +391,7 @@ func (db *SQLiteDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpt
|
|||
case ByLastAdded:
|
||||
query += ` ORDER BY b.id DESC`
|
||||
case ByLastModified:
|
||||
query += ` ORDER BY b.modified DESC`
|
||||
query += ` ORDER BY b.modified_at DESC`
|
||||
default:
|
||||
query += ` ORDER BY b.id`
|
||||
}
|
||||
|
@ -668,8 +671,8 @@ func (db *SQLiteDatabase) DeleteBookmarks(ctx context.Context, ids ...int) error
|
|||
func (db *SQLiteDatabase) GetBookmark(ctx context.Context, id int, url string) (model.BookmarkDTO, bool, error) {
|
||||
args := []interface{}{id}
|
||||
query := `SELECT
|
||||
b.id, b.url, b.title, b.excerpt, b.author, b.public, b.modified,
|
||||
bc.content, bc.html, b.has_content
|
||||
b.id, b.url, b.title, b.excerpt, b.author, b.public, b.modified_at,
|
||||
bc.content, bc.html, b.has_content, b.created_at
|
||||
FROM bookmark b
|
||||
LEFT JOIN bookmark_content bc ON bc.docid = b.id
|
||||
WHERE b.id = ?`
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:build linux || windows || darwin
|
||||
// +build linux windows darwin
|
||||
//go:build linux || windows || darwin || freebsd
|
||||
// +build linux windows darwin freebsd
|
||||
|
||||
package database
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ type BookmarkDTO struct {
|
|||
Excerpt string `db:"excerpt" json:"excerpt"`
|
||||
Author string `db:"author" json:"author"`
|
||||
Public int `db:"public" json:"public"`
|
||||
Modified string `db:"modified" json:"modified"`
|
||||
CreatedAt string `db:"created_at" json:"createdAt"`
|
||||
ModifiedAt string `db:"modified_at" json:"modifiedAt"`
|
||||
Content string `db:"content" json:"-"`
|
||||
HTML string `db:"html" json:"html,omitempty"`
|
||||
ImageURL string `db:"image_url" json:"imageURL"`
|
||||
|
|
File diff suppressed because one or more lines are too long
1
internal/view/assets/js/dayjs.min.js
vendored
1
internal/view/assets/js/dayjs.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -410,7 +410,7 @@ export default {
|
|||
},
|
||||
{
|
||||
name: "makePublic",
|
||||
label: "Make archive publicly available",
|
||||
label: "Make bookmark publicly available",
|
||||
type: "check",
|
||||
value: this.appOptions.MakePublic,
|
||||
},
|
||||
|
@ -517,7 +517,7 @@ export default {
|
|||
},
|
||||
{
|
||||
name: "makePublic",
|
||||
label: "Make archive publicly available",
|
||||
label: "Make bookmark publicly available",
|
||||
type: "check",
|
||||
value: book.public >= 1,
|
||||
},
|
||||
|
|
|
@ -45,7 +45,7 @@ var template = `
|
|||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="appOptions.MakePublic" @change="saveSetting">
|
||||
Make archive publicly available by default
|
||||
Make bookmark publicly available by default
|
||||
</label>
|
||||
</details>
|
||||
<details v-if="activeAccount.owner" open class="setting-group" id="setting-accounts">
|
||||
|
|
|
@ -165,7 +165,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
// Display bookmark menu items on small/medium screens
|
||||
@media (max-width: 1024px) {
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
@ -5,10 +5,57 @@ select {
|
|||
color: var(--color);
|
||||
}
|
||||
|
||||
footer {
|
||||
.login-footer {
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
.content-footer {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
max-width: 840px;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--contentBg);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: var(--colorLink);
|
||||
&:nth-child(1) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&[v-cloak] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
a {
|
||||
padding: 0 4px;
|
||||
color: var(--color);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--main);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
border-width: 0;
|
||||
box-sizing: border-box;
|
||||
|
@ -384,18 +431,6 @@ a {
|
|||
flex-flow: column;
|
||||
align-items: center;
|
||||
|
||||
#metadata {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: var(--colorLink);
|
||||
|
||||
&[v-cloak] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#title {
|
||||
padding: 8px 0;
|
||||
grid-column-start: 1;
|
||||
|
@ -406,22 +441,6 @@ a {
|
|||
hyphens: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#links {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
a {
|
||||
padding: 0 4px;
|
||||
color: var(--color);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content {
|
||||
|
@ -491,7 +510,6 @@ a {
|
|||
|
||||
#page-home {
|
||||
> .empty-message {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
font-size: 1em;
|
||||
background-color: var(--contentBg);
|
||||
|
|
|
@ -16,16 +16,14 @@
|
|||
|
||||
<link href="assets/css/style.css" rel="stylesheet">
|
||||
|
||||
<script src="assets/js/dayjs.min.js"></script>
|
||||
<script src="assets/js/vue.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="content-scene">
|
||||
<div id="header">
|
||||
<p id="metadata" v-cloak>Added {{localtime()}}</p>
|
||||
<p id="title" dir="auto">$$.Book.Title$$</p>
|
||||
<div id="links">
|
||||
<div class="links">
|
||||
<a href="$$.Book.URL$$" target="_blank" rel="noopener noreferrer">View Original</a>
|
||||
$$if .Book.HasArchive$$
|
||||
<a href="bookmark/$$.Book.ID$$/archive">View Archive</a>
|
||||
|
@ -38,6 +36,9 @@
|
|||
<div id="content" dir="auto" v-pre>
|
||||
$$.HTML$$
|
||||
</div>
|
||||
<footer class="content-footer">
|
||||
<p class="metadata">{{ createdModifiedTime() }} </p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
|
@ -48,15 +49,21 @@
|
|||
el: '#content-scene',
|
||||
mixins: [basePage],
|
||||
data: {
|
||||
modified: "$$.Book.Modified$$"
|
||||
created: "$$.Book.CreatedAt$$"
|
||||
},
|
||||
methods: {
|
||||
localtime() {
|
||||
var strTime = this.modified.replace(" ", "T");
|
||||
if (!strTime.endsWith("Z")) {
|
||||
strTime += "Z";
|
||||
}
|
||||
return dayjs(strTime).format("D MMMM YYYY, HH:mm:ss");
|
||||
createdModifiedTime() {
|
||||
const strCreatedTime = "$$.Book.CreatedAt$$".replace(" ", "T") + ("$$.Book.CreatedAt$$".endsWith("Z") ? "" : "Z");
|
||||
const strModifiedTime = "$$.Book.ModifiedAt$$".replace(" ", "T") + ("$$.Book.ModifiedAt$$".endsWith("Z") ? "" : "Z");
|
||||
|
||||
const createdDate = new Date(strCreatedTime);
|
||||
const modifiedDate = new Date(strModifiedTime);
|
||||
|
||||
if (createdDate.toDateString() === modifiedDate.toDateString()) {
|
||||
return `Added ${createdDate.getDate()} ${createdDate.toLocaleString('default', { month: 'long' })} ${createdDate.getFullYear()}`;
|
||||
} else {
|
||||
return `Added ${createdDate.getDate()} ${createdDate.toLocaleString('default', { month: 'long' })} ${createdDate.getFullYear()} | Last Modified ${modifiedDate.getDate()} ${modifiedDate.toLocaleString('default', {month: 'long'})} ${modifiedDate.getFullYear()}`;
|
||||
}
|
||||
},
|
||||
loadSetting() {
|
||||
var opts = JSON.parse(localStorage.getItem("shiori-account")) || {},
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<footer>
|
||||
<footer class="login-footer">
|
||||
<p>$$.Version$$</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
@ -359,6 +359,9 @@ func (h *Handler) ApiUpdateBookmark(w http.ResponseWriter, r *http.Request, ps h
|
|||
}
|
||||
}
|
||||
|
||||
// Set bookmark modified
|
||||
book.ModifiedAt = ""
|
||||
|
||||
// Update database
|
||||
res, err := h.DB.SaveBookmarks(ctx, false, book)
|
||||
checkError(err)
|
||||
|
|
|
@ -12,8 +12,7 @@ case `uname -o` in
|
|||
GNU/Linux)
|
||||
# Detect support of avx2 in linux hosts
|
||||
if ! grep -q avx2 /proc/cpuinfo; then
|
||||
BUN="sde -chip-check-disable -- bun"
|
||||
echo "Your CPU does not support avx2 so we use sde, for more information please look at https://github.com/oven-sh/bun/issues/762#issuecomment-1186505847"
|
||||
echo "It seems that your CPU does not support AVX2, if you experience long build times (>1m) ensure that you use bun's baseline builds. More information at https://github.com/oven-sh/bun/issues/67"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
|
Loading…
Reference in a new issue