feat: store created and modified time separately on database for bookmarks (#896)

* sqlite migrate script

* create time just when bookmark added and modified update if change happen

* show added and modified time in footer instead of header

* add bun.lockb that missing

* add migrate for postgres

* add pg support of created time

* change modifed to modifed_at and create to created_at in sqlite

* change modifed to modifed_at and create to created_at in postgre

* add created_at to mariadb

* fix migration file names

* better variable name and more clear code for add modified time if created and modified is not in same day

* add unittest

* add unittest to sure filters work as expected

* index for created_at and modified_at

* build new styles.css

* update swagger documents

* make styles

* change Created and Modified to CreatedAt and ModifiedAt

* fix missing Modified

* fix typo

* missing Modified

* fix typo

* make swagger

* run tests parallel

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>

* remove t.Parallel()

* remove dayjs dependency and combine two function

* better unittest name

* fix typo

* diffrnt footer style for login and content page

* use class instead of id

* back parallel

* change duplicate url

* remvoe run Parallel

* make styles

---------

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>
This commit is contained in:
Monirzadeh 2024-06-26 21:47:51 +03:30 committed by GitHub
parent a3d4a687aa
commit 4a5564d60b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 284 additions and 99 deletions

View file

@ -389,6 +389,9 @@ const docTemplate = `{
"description": "TODO: migrate outside the DTO",
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"excerpt": {
"type": "string"
},
@ -410,7 +413,7 @@ const docTemplate = `{
"imageURL": {
"type": "string"
},
"modified": {
"modifiedAt": {
"type": "string"
},
"public": {

View file

@ -378,6 +378,9 @@
"description": "TODO: migrate outside the DTO",
"type": "boolean"
},
"createdAt": {
"type": "string"
},
"excerpt": {
"type": "string"
},
@ -399,7 +402,7 @@
"imageURL": {
"type": "string"
},
"modified": {
"modifiedAt": {
"type": "string"
},
"public": {

View file

@ -90,6 +90,8 @@ definitions:
create_ebook:
description: 'TODO: migrate outside the DTO'
type: boolean
createdAt:
type: string
excerpt:
type: string
hasArchive:
@ -104,7 +106,7 @@ definitions:
type: integer
imageURL:
type: string
modified:
modifiedAt:
type: string
public:
type: integer

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
@ -424,3 +427,92 @@ func testGetAccounts(t *testing.T, db DB) {
assert.Equal(t, 0, len(accounts))
})
}
// 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)
}

View file

@ -0,0 +1 @@
ALTER TABLE bookmark RENAME COLUMN modified to created_at;

View file

@ -0,0 +1,2 @@
ALTER TABLE bookmark
MODIFY created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -0,0 +1,2 @@
ALTER TABLE bookmark
ADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

View file

@ -0,0 +1,3 @@
UPDATE bookmark
SET modified_at = COALESCE(created_at, CURRENT_TIMESTAMP)
WHERE created_at IS NOT NULL;

View file

@ -0,0 +1 @@
CREATE INDEX idx_created_at ON bookmark (created_at);

View file

@ -0,0 +1 @@
CREATE INDEX idx_modified_at ON bookmark (modified_at);

View file

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

View 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);

View file

@ -61,6 +61,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
@ -131,8 +137,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)
}
@ -145,7 +151,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)
@ -189,17 +195,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)
}
@ -211,7 +218,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)
@ -285,7 +292,8 @@ func (db *MySQLDatabase) GetBookmarks(ctx context.Context, opts GetBookmarksOpti
`excerpt`,
`author`,
`public`,
`modified`,
`created_at`,
`modified_at`,
`content <> "" has_content`}
if opts.WithContent {
@ -371,7 +379,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`
}
@ -564,7 +572,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 != "" {

View file

@ -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 != "" {

View file

@ -60,6 +60,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
@ -127,15 +128,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)
@ -193,8 +194,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 != ""
@ -202,11 +203,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)
@ -298,7 +300,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`
@ -389,7 +392,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`
}
@ -669,8 +672,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 = ?`

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

@ -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")) || {},

View file

@ -48,7 +48,7 @@
</div>
</form>
</div>
<footer>
<footer class="login-footer">
<p>$$.Version$$</p>
</footer>
</div>

View file

@ -360,6 +360,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)