From 874e27bb7ec2eb9ffbbf3acbc44ff036304f0c6a Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 13 Mar 2025 09:31:06 +0100 Subject: [PATCH 01/19] feat: add tag search api --- internal/database/database_tags.go | 6 ++ internal/database/database_tags_test.go | 68 +++++++++++++- internal/http/handlers/api/v1/tags.go | 15 ++- internal/http/handlers/api/v1/tags_test.go | 104 +++++++++++++++++++++ internal/model/database.go | 1 + internal/model/tag.go | 15 +++ internal/model/tag_test.go | 64 +++++++++++++ 7 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 internal/model/tag_test.go diff --git a/internal/database/database_tags.go b/internal/database/database_tags.go index e1afd3ab2..53d4392fb 100644 --- a/internal/database/database_tags.go +++ b/internal/database/database_tags.go @@ -50,6 +50,12 @@ func (db *dbbase) GetTags(ctx context.Context, opts model.DBListTagsOptions) ([] sb.Where(sb.IsNotNull("t.id")) } + // Add search condition if search term is provided + if opts.Search != "" { + // Note: Search and BookmarkID filtering are mutually exclusive as per requirements + sb.Where(sb.Like("t.name", "%"+opts.Search+"%")) + } + if opts.OrderBy == model.DBTagOrderByTagName { sb.OrderBy("t.name") } diff --git a/internal/database/database_tags_test.go b/internal/database/database_tags_test.go index d368fd46f..b6586f0eb 100644 --- a/internal/database/database_tags_test.go +++ b/internal/database/database_tags_test.go @@ -184,7 +184,69 @@ func testGetTagsFunction(t *testing.T, db model.DB) { assert.Equal(t, "web", fetchedTags[3].Name) }) - // Test 6: Get tags for a non-existent bookmark + // Test 6: Get tags with search term + t.Run("GetTagsWithSearch", func(t *testing.T) { + // Search for tags containing "go" + fetchedTags, err := db.GetTags(ctx, model.DBListTagsOptions{ + Search: "go", + }) + require.NoError(t, err) + + // Should return only the golang tag + assert.Len(t, fetchedTags, 1) + assert.Equal(t, "golang", fetchedTags[0].Name) + + // Search for tags containing "a" + fetchedTags, err = db.GetTags(ctx, model.DBListTagsOptions{ + Search: "a", + }) + require.NoError(t, err) + + // Should return database and possibly other tags containing "a" + assert.GreaterOrEqual(t, len(fetchedTags), 1) + + // Create a map of tag names for easier checking + tagNames := make(map[string]bool) + for _, tag := range fetchedTags { + tagNames[tag.Name] = true + } + + // Verify database is in the results + assert.True(t, tagNames["database"], "Tag 'database' should be present") + + // Search for non-existent tag + fetchedTags, err = db.GetTags(ctx, model.DBListTagsOptions{ + Search: "nonexistent", + }) + require.NoError(t, err) + assert.Len(t, fetchedTags, 0) + }) + + // Test 7: Search and bookmark ID are mutually exclusive + t.Run("SearchAndBookmarkIDMutuallyExclusive", func(t *testing.T) { + // This test is just to document the behavior, as the validation happens at the model level + // The database layer will prioritize the bookmark ID filter if both are provided + fetchedTags, err := db.GetTags(ctx, model.DBListTagsOptions{ + Search: "go", + BookmarkID: savedBookmarks[0].ID, + }) + require.NoError(t, err) + + // Should return tags for the bookmark, not the search + // The number of tags may vary depending on the database implementation + assert.NotEmpty(t, fetchedTags, "Should return at least one tag for the bookmark") + + // Create a map of tag names for easier checking + tagNames := make(map[string]bool) + for _, tag := range fetchedTags { + tagNames[tag.Name] = true + } + + // Verify golang is in the results (it's associated with the first bookmark) + assert.True(t, tagNames["golang"], "Tag 'golang' should be present") + }) + + // Test 8: Get tags for a non-existent bookmark t.Run("GetTagsForNonExistentBookmark", func(t *testing.T) { fetchedTags, err := db.GetTags(ctx, model.DBListTagsOptions{ BookmarkID: 9999, // Non-existent ID @@ -195,7 +257,7 @@ func testGetTagsFunction(t *testing.T, db model.DB) { assert.Empty(t, fetchedTags) }) - // Test 7: Get tags for a bookmark with no tags + // Test 9: Get tags for a bookmark with no tags t.Run("GetTagsForBookmarkWithNoTags", func(t *testing.T) { // Create a bookmark with no tags bookmarkWithNoTags := model.BookmarkDTO{ @@ -217,7 +279,7 @@ func testGetTagsFunction(t *testing.T, db model.DB) { assert.Empty(t, fetchedTags) }) - // Test 8: Get tags with combined options (order + count) + // Test 10: Get tags with combined options (order + count) t.Run("GetTagsWithCombinedOptions", func(t *testing.T) { fetchedTags, err := db.GetTags(ctx, model.DBListTagsOptions{ WithBookmarkCount: true, diff --git a/internal/http/handlers/api/v1/tags.go b/internal/http/handlers/api/v1/tags.go index 06d654792..b708ab254 100644 --- a/internal/http/handlers/api/v1/tags.go +++ b/internal/http/handlers/api/v1/tags.go @@ -17,6 +17,7 @@ import ( // @Produce json // @Param with_bookmark_count query boolean false "Include bookmark count for each tag" // @Param bookmark_id query integer false "Filter tags by bookmark ID" +// @Param search query string false "Search tags by name" // @Success 200 {array} model.TagDTO // @Failure 403 {object} nil "Authentication required" // @Failure 500 {object} nil "Internal server error" @@ -28,6 +29,7 @@ func HandleListTags(deps model.Dependencies, c model.WebContext) { // Parse query parameters withBookmarkCount := c.Request().URL.Query().Get("with_bookmark_count") == "true" + search := c.Request().URL.Query().Get("search") var bookmarkID int if bookmarkIDStr := c.Request().URL.Query().Get("bookmark_id"); bookmarkIDStr != "" { @@ -39,11 +41,20 @@ func HandleListTags(deps model.Dependencies, c model.WebContext) { } } - tags, err := deps.Domains().Tags().ListTags(c.Request().Context(), model.ListTagsOptions{ + // Create options and validate + opts := model.ListTagsOptions{ WithBookmarkCount: withBookmarkCount, BookmarkID: bookmarkID, OrderBy: model.DBTagOrderByTagName, - }) + Search: search, + } + + if err := opts.IsValid(); err != nil { + response.SendError(c, http.StatusBadRequest, err.Error(), nil) + return + } + + tags, err := deps.Domains().Tags().ListTags(c.Request().Context(), opts) if err != nil { deps.Logger().WithError(err).Error("failed to get tags") response.SendInternalServerError(c) diff --git a/internal/http/handlers/api/v1/tags_test.go b/internal/http/handlers/api/v1/tags_test.go index f55e37732..1aa815e16 100644 --- a/internal/http/handlers/api/v1/tags_test.go +++ b/internal/http/handlers/api/v1/tags_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-shiori/shiori/internal/model" "github.com/go-shiori/shiori/internal/testutil" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -149,6 +150,109 @@ func TestHandleListTags(t *testing.T) { } require.True(t, found, "The tag associated with the bookmark should be in the response") }) + + t.Run("search parameter", func(t *testing.T) { + _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) + + // Create test tags with different names + tags := []model.Tag{ + {Name: "golang"}, + {Name: "python"}, + {Name: "javascript"}, + } + createdTags, err := deps.Database().CreateTags(ctx, tags...) + require.NoError(t, err) + require.Len(t, createdTags, 3) + + // Test searching for "go" + w := testutil.PerformRequest( + deps, + HandleListTags, + "GET", + "/api/v1/tags", + testutil.WithFakeUser(), + testutil.WithRequestQueryParam("search", "go"), + ) + require.Equal(t, http.StatusOK, w.Code) + + response, err := testutil.NewTestResponseFromReader(w.Body) + require.NoError(t, err) + response.AssertOk(t) + + // Verify the response contains only the golang tag + var tags1 []model.TagDTO + responseData, err := json.Marshal(response.Response.GetMessage()) + require.NoError(t, err) + err = json.Unmarshal(responseData, &tags1) + require.NoError(t, err) + + require.Len(t, tags1, 1) + assert.Equal(t, "golang", tags1[0].Name) + + // Test searching for "on" + w = testutil.PerformRequest( + deps, + HandleListTags, + "GET", + "/api/v1/tags", + testutil.WithFakeUser(), + testutil.WithRequestQueryParam("search", "on"), + ) + require.Equal(t, http.StatusOK, w.Code) + + response, err = testutil.NewTestResponseFromReader(w.Body) + require.NoError(t, err) + response.AssertOk(t) + + // Verify the response contains tags with "on" in their name + var tags2 []model.TagDTO + responseData, err = json.Marshal(response.Response.GetMessage()) + require.NoError(t, err) + err = json.Unmarshal(responseData, &tags2) + require.NoError(t, err) + + // Should have at least one tag + require.NotEmpty(t, tags2) + + // Create a map of tag names for easier checking + tagNames := make(map[string]bool) + for _, tag := range tags2 { + tagNames[tag.Name] = true + } + + // Verify python is in the results + assert.True(t, tagNames["python"], "Tag 'python' should be present") + }) + + t.Run("search and bookmark_id parameters together", func(t *testing.T) { + _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) + + // Create a test bookmark + bookmark := testutil.GetValidBookmark() + bookmarks, err := deps.Database().SaveBookmarks(ctx, true, *bookmark) + require.NoError(t, err) + require.Len(t, bookmarks, 1) + bookmarkID := bookmarks[0].ID + + // Test using both search and bookmark_id parameters + w := testutil.PerformRequest( + deps, + HandleListTags, + "GET", + "/api/v1/tags", + testutil.WithFakeUser(), + testutil.WithRequestQueryParam("search", "go"), + testutil.WithRequestQueryParam("bookmark_id", strconv.Itoa(bookmarkID)), + ) + require.Equal(t, http.StatusBadRequest, w.Code) + + response, err := testutil.NewTestResponseFromReader(w.Body) + require.NoError(t, err) + response.AssertNotOk(t) + + // Verify the error message + assert.Contains(t, response.Response.Message, "search and bookmark ID filtering cannot be used together") + }) } func TestHandleGetTag(t *testing.T) { diff --git a/internal/model/database.go b/internal/model/database.go index 81d9d52b0..8d89c7015 100644 --- a/internal/model/database.go +++ b/internal/model/database.go @@ -138,4 +138,5 @@ type DBListTagsOptions struct { BookmarkID int WithBookmarkCount bool OrderBy DBTagOrderBy + Search string } diff --git a/internal/model/tag.go b/internal/model/tag.go index c6f84f9c6..eebc8fb35 100644 --- a/internal/model/tag.go +++ b/internal/model/tag.go @@ -1,5 +1,9 @@ package model +import ( + "errors" +) + // BookmarkTag is the relationship between a bookmark and a tag. type BookmarkTag struct { BookmarkID int `db:"bookmark_id"` @@ -40,4 +44,15 @@ type ListTagsOptions struct { BookmarkID int WithBookmarkCount bool OrderBy DBTagOrderBy + Search string +} + +// IsValid validates the ListTagsOptions. +// Returns an error if the options are invalid, nil otherwise. +// Currently, it checks that Search and BookmarkID are not used together. +func (o ListTagsOptions) IsValid() error { + if o.Search != "" && o.BookmarkID > 0 { + return errors.New("search and bookmark ID filtering cannot be used together") + } + return nil } diff --git a/internal/model/tag_test.go b/internal/model/tag_test.go new file mode 100644 index 000000000..55785e490 --- /dev/null +++ b/internal/model/tag_test.go @@ -0,0 +1,64 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListTagsOptions_IsValid(t *testing.T) { + tests := []struct { + name string + options ListTagsOptions + wantErr bool + }{ + { + name: "valid options with search", + options: ListTagsOptions{ + Search: "test", + WithBookmarkCount: true, + OrderBy: DBTagOrderByTagName, + }, + wantErr: false, + }, + { + name: "valid options with bookmark ID", + options: ListTagsOptions{ + BookmarkID: 123, + WithBookmarkCount: true, + OrderBy: DBTagOrderByTagName, + }, + wantErr: false, + }, + { + name: "invalid options with both search and bookmark ID", + options: ListTagsOptions{ + Search: "test", + BookmarkID: 123, + WithBookmarkCount: true, + OrderBy: DBTagOrderByTagName, + }, + wantErr: true, + }, + { + name: "valid options with neither search nor bookmark ID", + options: ListTagsOptions{ + WithBookmarkCount: true, + OrderBy: DBTagOrderByTagName, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.options.IsValid() + if tt.wantErr { + assert.Error(t, err) + assert.Contains(t, err.Error(), "search and bookmark ID filtering cannot be used together") + } else { + assert.NoError(t, err) + } + }) + } +} From 03372a14abc5544c36f784240d4d71c0a2f47eea Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 13 Mar 2025 11:06:37 +0100 Subject: [PATCH 02/19] feat: add apis to add/remove bookmark tags --- internal/database/database_tags.go | 124 ++++ internal/domains/bookmark_tags_test.go | 127 ++++ internal/domains/bookmarks.go | 53 ++ internal/domains/tags.go | 5 + .../handlers/api/v1/bookmark_tags_test.go | 661 ++++++++++++++++++ internal/http/handlers/api/v1/bookmarks.go | 166 ++++- internal/http/server.go | 13 + internal/model/database.go | 12 + internal/model/domains.go | 4 + internal/model/errors.go | 8 +- 10 files changed, 1169 insertions(+), 4 deletions(-) create mode 100644 internal/domains/bookmark_tags_test.go create mode 100644 internal/http/handlers/api/v1/bookmark_tags_test.go diff --git a/internal/database/database_tags.go b/internal/database/database_tags.go index 53d4392fb..518e829a2 100644 --- a/internal/database/database_tags.go +++ b/internal/database/database_tags.go @@ -8,6 +8,7 @@ import ( "github.com/go-shiori/shiori/internal/model" "github.com/huandu/go-sqlbuilder" + "github.com/jmoiron/sqlx" ) // GetTags returns a list of tags from the database. @@ -73,3 +74,126 @@ func (db *dbbase) GetTags(ctx context.Context, opts model.DBListTagsOptions) ([] return tags, nil } + +// AddTagToBookmark adds a tag to a bookmark +func (db *dbbase) AddTagToBookmark(ctx context.Context, bookmarkID int, tagID int) error { + // Insert the bookmark-tag association + insertSb := db.Flavor().NewInsertBuilder() + insertSb.InsertInto("bookmark_tag") + insertSb.Cols("bookmark_id", "tag_id") + insertSb.Values(bookmarkID, tagID) + + insertQuery, insertArgs := insertSb.Build() + insertQuery = db.WriterDB().Rebind(insertQuery) + + if err := db.withTx(ctx, func(tx *sqlx.Tx) error { + // First check if the association already exists using sqlbuilder + selectSb := db.Flavor().NewSelectBuilder() + selectSb.Select("1") + selectSb.From("bookmark_tag") + selectSb.Where( + selectSb.And( + selectSb.Equal("bookmark_id", bookmarkID), + selectSb.Equal("tag_id", tagID), + ), + ) + + selectQuery, selectArgs := selectSb.Build() + selectQuery = db.ReaderDB().Rebind(selectQuery) + + var exists int + err := tx.QueryRowContext(ctx, selectQuery, selectArgs...).Scan(&exists) + + if err != nil && err != sql.ErrNoRows { + return fmt.Errorf("failed to check if tag is already associated: %w", err) + } + + // If it doesn't exist, insert it + if err == sql.ErrNoRows { + _, err = tx.ExecContext(ctx, insertQuery, insertArgs...) + if err != nil { + return fmt.Errorf("failed to add tag to bookmark: %w", err) + } + } + + return nil + }); err != nil { + return err + } + + return nil +} + +// RemoveTagFromBookmark removes a tag from a bookmark +func (db *dbbase) RemoveTagFromBookmark(ctx context.Context, bookmarkID int, tagID int) error { + // Delete the bookmark-tag association + deleteSb := db.Flavor().NewDeleteBuilder() + deleteSb.DeleteFrom("bookmark_tag") + deleteSb.Where( + deleteSb.And( + deleteSb.Equal("bookmark_id", bookmarkID), + deleteSb.Equal("tag_id", tagID), + ), + ) + + query, args := deleteSb.Build() + query = db.WriterDB().Rebind(query) + + if err := db.withTx(ctx, func(tx *sqlx.Tx) error { + _, err := tx.ExecContext(ctx, query, args...) + if err != nil { + return fmt.Errorf("failed to remove tag from bookmark: %w", err) + } + return nil + }); err != nil { + return err + } + + return nil +} + +// TagExists checks if a tag with the given ID exists in the database +func (db *dbbase) TagExists(ctx context.Context, tagID int) (bool, error) { + sb := db.Flavor().NewSelectBuilder() + sb.Select("1") + sb.From("tag") + sb.Where(sb.Equal("id", tagID)) + sb.Limit(1) + + query, args := sb.Build() + query = db.ReaderDB().Rebind(query) + + var exists int + err := db.ReaderDB().QueryRowContext(ctx, query, args...).Scan(&exists) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, fmt.Errorf("failed to check if tag exists: %w", err) + } + + return true, nil +} + +// BookmarkExists checks if a bookmark with the given ID exists in the database +func (db *dbbase) BookmarkExists(ctx context.Context, bookmarkID int) (bool, error) { + sb := db.Flavor().NewSelectBuilder() + sb.Select("1") + sb.From("bookmark") + sb.Where(sb.Equal("id", bookmarkID)) + sb.Limit(1) + + query, args := sb.Build() + query = db.ReaderDB().Rebind(query) + + var exists int + err := db.ReaderDB().QueryRowContext(ctx, query, args...).Scan(&exists) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, fmt.Errorf("failed to check if bookmark exists: %w", err) + } + + return true, nil +} diff --git a/internal/domains/bookmark_tags_test.go b/internal/domains/bookmark_tags_test.go new file mode 100644 index 000000000..bce6f1c8c --- /dev/null +++ b/internal/domains/bookmark_tags_test.go @@ -0,0 +1,127 @@ +package domains_test + +import ( + "context" + "testing" + + "github.com/go-shiori/shiori/internal/model" + "github.com/go-shiori/shiori/internal/testutil" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBookmarkTagOperations(t *testing.T) { + ctx := context.Background() + logger := logrus.New() + + // Setup using the test configuration and dependencies + _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) + bookmarksDomain := deps.Domains().Bookmarks() + tagsDomain := deps.Domains().Tags() + db := deps.Database() + + // Create a test bookmark + bookmark := model.BookmarkDTO{ + URL: "https://example.com/bookmark-tags-test", + Title: "Bookmark Tags Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + // Create a test tag + tagDTO := model.TagDTO{ + Tag: model.Tag{ + Name: "test-tag", + }, + } + createdTag, err := tagsDomain.CreateTag(ctx, tagDTO) + require.NoError(t, err) + tagID := createdTag.ID + + // Test BookmarkExists + t.Run("BookmarkExists", func(t *testing.T) { + // Test with existing bookmark + exists, err := bookmarksDomain.BookmarkExists(ctx, bookmarkID) + require.NoError(t, err) + assert.True(t, exists, "Bookmark should exist") + + // Test with non-existent bookmark + exists, err = bookmarksDomain.BookmarkExists(ctx, 9999) + require.NoError(t, err) + assert.False(t, exists, "Non-existent bookmark should not exist") + }) + + // Test TagExists + t.Run("TagExists", func(t *testing.T) { + // Test with existing tag + exists, err := tagsDomain.TagExists(ctx, tagID) + require.NoError(t, err) + assert.True(t, exists, "Tag should exist") + + // Test with non-existent tag + exists, err = tagsDomain.TagExists(ctx, 9999) + require.NoError(t, err) + assert.False(t, exists, "Non-existent tag should not exist") + }) + + // Test AddTagToBookmark + t.Run("AddTagToBookmark", func(t *testing.T) { + // Add tag to bookmark + err := bookmarksDomain.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was added by listing tags for the bookmark + tags, err := tagsDomain.ListTags(ctx, model.ListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1, "Should have exactly one tag") + assert.Equal(t, tagID, tags[0].ID, "Tag ID should match") + assert.Equal(t, "test-tag", tags[0].Name, "Tag name should match") + + // Test adding the same tag again (should not error) + err = bookmarksDomain.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err, "Adding the same tag again should not error") + + // Test adding tag to non-existent bookmark + err = bookmarksDomain.AddTagToBookmark(ctx, 9999, tagID) + require.Error(t, err) + assert.ErrorIs(t, err, model.ErrBookmarkNotFound, "Should return bookmark not found error") + + // Test adding non-existent tag to bookmark + err = bookmarksDomain.AddTagToBookmark(ctx, bookmarkID, 9999) + require.Error(t, err) + assert.ErrorIs(t, err, model.ErrTagNotFound, "Should return tag not found error") + }) + + // Test RemoveTagFromBookmark + t.Run("RemoveTagFromBookmark", func(t *testing.T) { + // Remove tag from bookmark + err := bookmarksDomain.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was removed by listing tags for the bookmark + tags, err := tagsDomain.ListTags(ctx, model.ListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 0, "Should have no tags after removal") + + // Test removing a tag that's not associated with the bookmark (should not error) + err = bookmarksDomain.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err, "Removing a tag that's not associated should not error") + + // Test removing tag from non-existent bookmark + err = bookmarksDomain.RemoveTagFromBookmark(ctx, 9999, tagID) + require.Error(t, err) + assert.ErrorIs(t, err, model.ErrBookmarkNotFound, "Should return bookmark not found error") + + // Test removing non-existent tag from bookmark + err = bookmarksDomain.RemoveTagFromBookmark(ctx, bookmarkID, 9999) + require.Error(t, err) + assert.ErrorIs(t, err, model.ErrTagNotFound, "Should return tag not found error") + }) +} diff --git a/internal/domains/bookmarks.go b/internal/domains/bookmarks.go index 37a269098..de6367bb9 100644 --- a/internal/domains/bookmarks.go +++ b/internal/domains/bookmarks.go @@ -113,6 +113,59 @@ func (d *BookmarksDomain) BulkUpdateBookmarkTags(ctx context.Context, bookmarkID return nil } +// AddTagToBookmark adds a tag to a bookmark +func (d *BookmarksDomain) AddTagToBookmark(ctx context.Context, bookmarkID int, tagID int) error { + // Check if bookmark exists + exists, err := d.BookmarkExists(ctx, bookmarkID) + if err != nil { + return err + } + if !exists { + return model.ErrBookmarkNotFound + } + + // Check if tag exists + exists, err = d.deps.Domains().Tags().TagExists(ctx, tagID) + if err != nil { + return err + } + if !exists { + return model.ErrTagNotFound + } + + // Add tag to bookmark + return d.deps.Database().AddTagToBookmark(ctx, bookmarkID, tagID) +} + +// RemoveTagFromBookmark removes a tag from a bookmark +func (d *BookmarksDomain) RemoveTagFromBookmark(ctx context.Context, bookmarkID int, tagID int) error { + // Check if bookmark exists + exists, err := d.BookmarkExists(ctx, bookmarkID) + if err != nil { + return err + } + if !exists { + return model.ErrBookmarkNotFound + } + + // Check if tag exists + exists, err = d.deps.Domains().Tags().TagExists(ctx, tagID) + if err != nil { + return err + } + if !exists { + return model.ErrTagNotFound + } + + // Remove tag from bookmark + return d.deps.Database().RemoveTagFromBookmark(ctx, bookmarkID, tagID) +} + +// BookmarkExists checks if a bookmark with the given ID exists +func (d *BookmarksDomain) BookmarkExists(ctx context.Context, id int) (bool, error) { + return d.deps.Database().BookmarkExists(ctx, id) +} + func NewBookmarksDomain(deps model.Dependencies) *BookmarksDomain { return &BookmarksDomain{ deps: deps, diff --git a/internal/domains/tags.go b/internal/domains/tags.go index 5e9dd15ea..2c595bcc8 100644 --- a/internal/domains/tags.go +++ b/internal/domains/tags.go @@ -75,3 +75,8 @@ func (d *tagsDomain) DeleteTag(ctx context.Context, id int) error { return nil } + +// TagExists checks if a tag with the given ID exists +func (d *tagsDomain) TagExists(ctx context.Context, id int) (bool, error) { + return d.deps.Database().TagExists(ctx, id) +} diff --git a/internal/http/handlers/api/v1/bookmark_tags_test.go b/internal/http/handlers/api/v1/bookmark_tags_test.go new file mode 100644 index 000000000..43d48d97f --- /dev/null +++ b/internal/http/handlers/api/v1/bookmark_tags_test.go @@ -0,0 +1,661 @@ +package api_v1_test + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + "testing" + + api_v1 "github.com/go-shiori/shiori/internal/http/handlers/api/v1" + "github.com/go-shiori/shiori/internal/model" + "github.com/go-shiori/shiori/internal/testutil" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Define the BookmarkTagPayload struct to match the one in the API +type bookmarkTagPayload struct { + TagID int `json:"tag_id"` +} + +func TestBookmarkTagsAPI(t *testing.T) { + ctx := context.Background() + logger := logrus.New() + + // Setup using the test configuration and dependencies + _, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger) + db := deps.Database() + + // Create a test bookmark + bookmark := model.BookmarkDTO{ + URL: "https://example.com/api-tags-test", + Title: "API Tags Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + // Create a test tag + tag := model.Tag{ + Name: "api-test-tag", + } + createdTags, err := db.CreateTags(ctx, tag) + require.NoError(t, err) + require.Len(t, createdTags, 1) + tagID := createdTags[0].ID + + // Test authentication requirements + t.Run("AuthenticationRequirements", func(t *testing.T) { + // Test unauthenticated user for GetBookmarkTags + t.Run("UnauthenticatedUserGetTags", func(t *testing.T) { + rec := testutil.PerformRequest( + deps, + api_v1.HandleGetBookmarkTags, + http.MethodGet, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + ) + + require.Equal(t, http.StatusUnauthorized, rec.Code) + }) + + // Test unauthenticated user for AddTagToBookmark + t.Run("UnauthenticatedUserAddTag", func(t *testing.T) { + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusUnauthorized, rec.Code) + }) + + // Test non-admin user for AddTagToBookmark (which requires admin) + t.Run("NonAdminUserAddTag", func(t *testing.T) { + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeUser(), // Regular user, not admin + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Just check the status code since the response might vary + require.Equal(t, http.StatusForbidden, rec.Code) + }) + + // Test unauthenticated user for RemoveTagFromBookmark + t.Run("UnauthenticatedUserRemoveTag", func(t *testing.T) { + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleRemoveTagFromBookmark, + http.MethodDelete, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusUnauthorized, rec.Code) + }) + }) + + // Test BulkUpdateBookmarkTags + t.Run("BulkUpdateBookmarkTags", func(t *testing.T) { + // Define the payload struct + type bulkUpdatePayload struct { + BookmarkIDs []int `json:"bookmark_ids"` + TagIDs []int `json:"tag_ids"` + } + + // Test successful bulk update + t.Run("SuccessfulBulkUpdate", func(t *testing.T) { + payload := bulkUpdatePayload{ + BookmarkIDs: []int{bookmarkID}, + TagIDs: []int{tagID}, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleBulkUpdateBookmarkTags, + http.MethodPut, + "/api/v1/bookmarks/bulk/tags", + testutil.WithFakeAdmin(), + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusOK, rec.Code) + + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertOk(t) + }) + + // Test unauthenticated user + t.Run("UnauthenticatedUser", func(t *testing.T) { + payload := bulkUpdatePayload{ + BookmarkIDs: []int{bookmarkID}, + TagIDs: []int{tagID}, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleBulkUpdateBookmarkTags, + http.MethodPut, + "/api/v1/bookmarks/bulk/tags", + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusUnauthorized, rec.Code) + }) + + // Test invalid request payload + t.Run("InvalidRequestPayload", func(t *testing.T) { + invalidPayload := []byte(`{"bookmark_ids": "invalid", "tag_ids": [1]}`) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleBulkUpdateBookmarkTags, + http.MethodPut, + "/api/v1/bookmarks/bulk/tags", + testutil.WithFakeAdmin(), + testutil.WithBody(string(invalidPayload)), + ) + + require.Equal(t, http.StatusBadRequest, rec.Code) + + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Invalid request payload") + }) + + // Test empty bookmark IDs + t.Run("EmptyBookmarkIDs", func(t *testing.T) { + payload := bulkUpdatePayload{ + BookmarkIDs: []int{}, + TagIDs: []int{tagID}, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleBulkUpdateBookmarkTags, + http.MethodPut, + "/api/v1/bookmarks/bulk/tags", + testutil.WithFakeAdmin(), + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusBadRequest, rec.Code) + + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "bookmark_ids should not be empty") + }) + + // Test empty tag IDs + t.Run("EmptyTagIDs", func(t *testing.T) { + payload := bulkUpdatePayload{ + BookmarkIDs: []int{bookmarkID}, + TagIDs: []int{}, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleBulkUpdateBookmarkTags, + http.MethodPut, + "/api/v1/bookmarks/bulk/tags", + testutil.WithFakeAdmin(), + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusBadRequest, rec.Code) + + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "tag_ids should not be empty") + }) + + // Test bookmark not found + t.Run("BookmarkNotFound", func(t *testing.T) { + payload := bulkUpdatePayload{ + BookmarkIDs: []int{9999}, // Non-existent bookmark ID + TagIDs: []int{tagID}, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleBulkUpdateBookmarkTags, + http.MethodPut, + "/api/v1/bookmarks/bulk/tags", + testutil.WithFakeAdmin(), + testutil.WithBody(string(payloadBytes)), + ) + + require.Equal(t, http.StatusInternalServerError, rec.Code) + + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Failed to update bookmarks") + }) + }) + + // Test GetBookmarkTags + t.Run("GetBookmarkTags", func(t *testing.T) { + // Add a tag to the bookmark first + err := db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Create a request to get the tags + rec := testutil.PerformRequest( + deps, + api_v1.HandleGetBookmarkTags, + http.MethodGet, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + ) + + // Check the response + require.Equal(t, http.StatusOK, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertOk(t) + + // Extract tags from the response + var tags []model.TagDTO + tagsData, err := json.Marshal(testResp.Response.GetMessage()) + require.NoError(t, err) + err = json.Unmarshal(tagsData, &tags) + require.NoError(t, err) + + // Verify the tags + require.Len(t, tags, 1) + assert.Equal(t, tagID, tags[0].ID) + assert.Equal(t, "api-test-tag", tags[0].Name) + }) + + // Test AddTagToBookmark + t.Run("AddTagToBookmark", func(t *testing.T) { + // Remove the tag first to ensure a clean state + err := db.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Create a request to add the tag + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusCreated, rec.Code) + + // Verify the tag was added + tags, err := deps.Domains().Tags().ListTags(ctx, model.ListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1) + assert.Equal(t, tagID, tags[0].ID) + }) + + // Test RemoveTagFromBookmark + t.Run("RemoveTagFromBookmark", func(t *testing.T) { + // Add the tag first to ensure it exists + err := db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Create a request to remove the tag + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleRemoveTagFromBookmark, + http.MethodDelete, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusOK, rec.Code) + + // Verify the tag was removed + tags, err := deps.Domains().Tags().ListTags(ctx, model.ListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 0) + }) + + // Test error cases + t.Run("ErrorCases", func(t *testing.T) { + // Test non-existent bookmark + t.Run("NonExistentBookmark", func(t *testing.T) { + // Create a request to get tags for a non-existent bookmark + rec := testutil.PerformRequest( + deps, + api_v1.HandleGetBookmarkTags, + http.MethodGet, + "/api/v1/bookmarks/9999/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", "9999"), + ) + + // Check the response + require.Equal(t, http.StatusNotFound, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Bookmark not found") + }) + + // Test non-existent tag + t.Run("NonExistentTag", func(t *testing.T) { + // Create a request to add a non-existent tag + payload := bookmarkTagPayload{ + TagID: 9999, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusNotFound, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Tag not found") + }) + + // Test non-existent bookmark for AddTagToBookmark + t.Run("NonExistentBookmarkForAddTag", func(t *testing.T) { + // Create a request to add a tag to a non-existent bookmark + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/9999/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", "9999"), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusNotFound, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Bookmark not found") + }) + + // Test non-existent bookmark for RemoveTagFromBookmark + t.Run("NonExistentBookmarkForRemoveTag", func(t *testing.T) { + // Create a request to remove a tag from a non-existent bookmark + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleRemoveTagFromBookmark, + http.MethodDelete, + "/api/v1/bookmarks/9999/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", "9999"), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusNotFound, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Bookmark not found") + }) + + // Test non-existent tag for RemoveTagFromBookmark + t.Run("NonExistentTagForRemoveTag", func(t *testing.T) { + // Create a request to remove a non-existent tag + payload := bookmarkTagPayload{ + TagID: 9999, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleRemoveTagFromBookmark, + http.MethodDelete, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusNotFound, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Tag not found") + }) + + // Test invalid bookmark ID + t.Run("InvalidBookmarkID", func(t *testing.T) { + // Create a request with an invalid bookmark ID + rec := testutil.PerformRequest( + deps, + api_v1.HandleGetBookmarkTags, + http.MethodGet, + "/api/v1/bookmarks/invalid/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", "invalid"), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Invalid bookmark ID") + }) + + // Test invalid payload + t.Run("InvalidPayload", func(t *testing.T) { + // Create a request with an invalid payload + invalidPayload := []byte(`{"tag_id": "invalid"}`) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(invalidPayload)), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Invalid request payload") + }) + + // Test zero tag ID + t.Run("ZeroTagID", func(t *testing.T) { + // Create a request with a zero tag ID + payload := bookmarkTagPayload{ + TagID: 0, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "tag_id should be a positive integer") + }) + + // Test negative tag ID + t.Run("NegativeTagID", func(t *testing.T) { + // Create a request with a negative tag ID + payload := bookmarkTagPayload{ + TagID: -1, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "tag_id should be a positive integer") + }) + + // Test validation for RemoveTagFromBookmark + t.Run("RemoveTagValidation", func(t *testing.T) { + // Create a request with a zero tag ID + payload := bookmarkTagPayload{ + TagID: 0, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleRemoveTagFromBookmark, + http.MethodDelete, + "/api/v1/bookmarks/"+strconv.Itoa(bookmarkID)+"/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", strconv.Itoa(bookmarkID)), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "tag_id should be a positive integer") + }) + }) +} diff --git a/internal/http/handlers/api/v1/bookmarks.go b/internal/http/handlers/api/v1/bookmarks.go index 68d24eb00..437039f73 100644 --- a/internal/http/handlers/api/v1/bookmarks.go +++ b/internal/http/handlers/api/v1/bookmarks.go @@ -2,6 +2,7 @@ package api_v1 import ( "encoding/json" + "errors" "fmt" "net/http" "strconv" @@ -176,6 +177,169 @@ func (p *bulkUpdateBookmarkTagsPayload) IsValid() error { return nil } +// HandleGetBookmarkTags gets the tags for a bookmark +// +// @Summary Get tags for a bookmark. +// @Tags Auth +// @securityDefinitions.apikey ApiKeyAuth +// @Produce json +// @Success 200 {array} model.TagDTO +// @Failure 403 {object} nil "Token not provided/invalid" +// @Failure 404 {object} nil "Bookmark not found" +// @Router /api/v1/bookmarks/{id}/tags [get] +func HandleGetBookmarkTags(deps model.Dependencies, c model.WebContext) { + if err := middleware.RequireLoggedInUser(deps, c); err != nil { + response.SendError(c, http.StatusForbidden, err.Error(), nil) + return + } + + bookmarkID, err := strconv.Atoi(c.Request().PathValue("id")) + if err != nil { + response.SendError(c, http.StatusBadRequest, "Invalid bookmark ID", nil) + return + } + + // Check if bookmark exists + exists, err := deps.Domains().Bookmarks().BookmarkExists(c.Request().Context(), bookmarkID) + if err != nil { + response.SendError(c, http.StatusInternalServerError, "Failed to check if bookmark exists", nil) + return + } + if !exists { + response.SendError(c, http.StatusNotFound, "Bookmark not found", nil) + return + } + + // Get bookmark to retrieve its tags + tags, err := deps.Domains().Tags().ListTags(c.Request().Context(), model.ListTagsOptions{ + BookmarkID: bookmarkID, + }) + if err != nil { + response.SendError(c, http.StatusInternalServerError, "Failed to get bookmark tags", nil) + return + } + + response.Send(c, http.StatusOK, tags) +} + +// bookmarkTagPayload is used for both adding and removing tags from bookmarks +type bookmarkTagPayload struct { + TagID int `json:"tag_id" validate:"required"` +} + +func (p *bookmarkTagPayload) IsValid() error { + if p.TagID <= 0 { + return fmt.Errorf("tag_id should be a positive integer") + } + return nil +} + +// HandleAddTagToBookmark adds a tag to a bookmark +// +// @Summary Add a tag to a bookmark. +// @Tags Auth +// @securityDefinitions.apikey ApiKeyAuth +// @Param payload body bookmarkTagPayload true "Add Tag Payload" +// @Produce json +// @Success 200 {object} nil +// @Failure 403 {object} nil "Token not provided/invalid" +// @Failure 404 {object} nil "Bookmark or tag not found" +// @Router /api/v1/bookmarks/{id}/tags [post] +func HandleAddTagToBookmark(deps model.Dependencies, c model.WebContext) { + if err := middleware.RequireLoggedInAdmin(deps, c); err != nil { + response.SendError(c, http.StatusForbidden, err.Error(), nil) + return + } + + bookmarkID, err := strconv.Atoi(c.Request().PathValue("id")) + if err != nil { + response.SendError(c, http.StatusBadRequest, "Invalid bookmark ID", nil) + return + } + + // Parse request payload + var payload bookmarkTagPayload + if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil { + response.SendError(c, http.StatusBadRequest, "Invalid request payload", nil) + return + } + + if err := payload.IsValid(); err != nil { + response.SendError(c, http.StatusBadRequest, err.Error(), nil) + return + } + + // Add tag to bookmark + err = deps.Domains().Bookmarks().AddTagToBookmark(c.Request().Context(), bookmarkID, payload.TagID) + if err != nil { + if errors.Is(err, model.ErrBookmarkNotFound) { + response.SendError(c, http.StatusNotFound, "Bookmark not found", nil) + return + } + if errors.Is(err, model.ErrTagNotFound) { + response.SendError(c, http.StatusNotFound, "Tag not found", nil) + return + } + response.SendError(c, http.StatusInternalServerError, "Failed to add tag to bookmark", nil) + return + } + + response.Send(c, http.StatusCreated, nil) +} + +// HandleRemoveTagFromBookmark removes a tag from a bookmark +// +// @Summary Remove a tag from a bookmark. +// @Tags Auth +// @securityDefinitions.apikey ApiKeyAuth +// @Param payload body bookmarkTagPayload true "Remove Tag Payload" +// @Produce json +// @Success 200 {object} nil +// @Failure 403 {object} nil "Token not provided/invalid" +// @Failure 404 {object} nil "Bookmark not found" +// @Router /api/v1/bookmarks/{id}/tags [delete] +func HandleRemoveTagFromBookmark(deps model.Dependencies, c model.WebContext) { + if err := middleware.RequireLoggedInUser(deps, c); err != nil { + response.SendError(c, http.StatusForbidden, err.Error(), nil) + return + } + + bookmarkID, err := strconv.Atoi(c.Request().PathValue("id")) + if err != nil { + response.SendError(c, http.StatusBadRequest, "Invalid bookmark ID", nil) + return + } + + // Parse request payload + var payload bookmarkTagPayload + if err := json.NewDecoder(c.Request().Body).Decode(&payload); err != nil { + response.SendError(c, http.StatusBadRequest, "Invalid request payload", nil) + return + } + + if err := payload.IsValid(); err != nil { + response.SendError(c, http.StatusBadRequest, err.Error(), nil) + return + } + + // Remove tag from bookmark + err = deps.Domains().Bookmarks().RemoveTagFromBookmark(c.Request().Context(), bookmarkID, payload.TagID) + if err != nil { + if errors.Is(err, model.ErrBookmarkNotFound) { + response.SendError(c, http.StatusNotFound, "Bookmark not found", nil) + return + } + if errors.Is(err, model.ErrTagNotFound) { + response.SendError(c, http.StatusNotFound, "Tag not found", nil) + return + } + response.SendError(c, http.StatusInternalServerError, "Failed to remove tag from bookmark", nil) + return + } + + response.Send(c, http.StatusOK, nil) +} + // HandleBulkUpdateBookmarkTags updates the tags for multiple bookmarks // // @Summary Bulk update tags for multiple bookmarks. @@ -209,7 +373,7 @@ func HandleBulkUpdateBookmarkTags(deps model.Dependencies, c model.WebContext) { // Use the domain method to update bookmark tags err := deps.Domains().Bookmarks().BulkUpdateBookmarkTags(c.Request().Context(), payload.BookmarkIDs, payload.TagIDs) if err != nil { - if err == model.ErrBookmarkNotFound { + if errors.Is(err, model.ErrBookmarkNotFound) { response.SendError(c, http.StatusNotFound, "No bookmarks found", nil) return } diff --git a/internal/http/server.go b/internal/http/server.go index 0c97fe2e6..16da2a4ce 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -167,6 +167,19 @@ func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Dependencies) api_v1.HandleBulkUpdateBookmarkTags, globalMiddleware..., )) + // Bookmark tags endpoints + s.mux.HandleFunc("GET /api/v1/bookmarks/{id}/tags", ToHTTPHandler(deps, + api_v1.HandleGetBookmarkTags, + globalMiddleware..., + )) + s.mux.HandleFunc("POST /api/v1/bookmarks/{id}/tags", ToHTTPHandler(deps, + api_v1.HandleAddTagToBookmark, + globalMiddleware..., + )) + s.mux.HandleFunc("DELETE /api/v1/bookmarks/{id}/tags", ToHTTPHandler(deps, + api_v1.HandleRemoveTagFromBookmark, + globalMiddleware..., + )) s.server = &http.Server{ Addr: fmt.Sprintf("%s%d", cfg.Http.Address, cfg.Http.Port), diff --git a/internal/model/database.go b/internal/model/database.go index 8d89c7015..068b00b63 100644 --- a/internal/model/database.go +++ b/internal/model/database.go @@ -89,6 +89,18 @@ type DB interface { // BulkUpdateBookmarkTags updates tags for multiple bookmarks. // It ensures that all bookmarks and tags exist before proceeding. BulkUpdateBookmarkTags(ctx context.Context, bookmarkIDs []int, tagIDs []int) error + + // AddTagToBookmark adds a tag to a bookmark + AddTagToBookmark(ctx context.Context, bookmarkID int, tagID int) error + + // RemoveTagFromBookmark removes a tag from a bookmark + RemoveTagFromBookmark(ctx context.Context, bookmarkID int, tagID int) error + + // TagExists checks if a tag with the given ID exists in the database + TagExists(ctx context.Context, tagID int) (bool, error) + + // BookmarkExists checks if a bookmark with the given ID exists in the database + BookmarkExists(ctx context.Context, bookmarkID int) (bool, error) } // DBOrderMethod is the order method for getting bookmarks diff --git a/internal/model/domains.go b/internal/model/domains.go index 519869210..fe8ee91b1 100644 --- a/internal/model/domains.go +++ b/internal/model/domains.go @@ -18,6 +18,9 @@ type BookmarksDomain interface { GetBookmarks(ctx context.Context, ids []int) ([]BookmarkDTO, error) UpdateBookmarkCache(ctx context.Context, bookmark BookmarkDTO, keepMetadata bool, skipExist bool) (*BookmarkDTO, error) BulkUpdateBookmarkTags(ctx context.Context, bookmarkIDs []int, tagIDs []int) error + AddTagToBookmark(ctx context.Context, bookmarkID int, tagID int) error + RemoveTagFromBookmark(ctx context.Context, bookmarkID int, tagID int) error + BookmarkExists(ctx context.Context, id int) (bool, error) } type AuthDomain interface { @@ -53,4 +56,5 @@ type TagsDomain interface { GetTag(ctx context.Context, id int) (TagDTO, error) UpdateTag(ctx context.Context, tag TagDTO) (TagDTO, error) DeleteTag(ctx context.Context, id int) error + TagExists(ctx context.Context, id int) (bool, error) } diff --git a/internal/model/errors.go b/internal/model/errors.go index 72ab34fbd..167942cd2 100644 --- a/internal/model/errors.go +++ b/internal/model/errors.go @@ -5,7 +5,9 @@ import "errors" var ( ErrBookmarkNotFound = errors.New("bookmark not found") ErrBookmarkInvalidID = errors.New("invalid bookmark ID") - ErrUnauthorized = errors.New("unauthorized user") - ErrNotFound = errors.New("not found") - ErrAlreadyExists = errors.New("already exists") + ErrTagNotFound = errors.New("tag not found") + + ErrUnauthorized = errors.New("unauthorized user") + ErrNotFound = errors.New("not found") + ErrAlreadyExists = errors.New("already exists") ) From d488337ce80706c3f0b8857c615205ec32dd7dbf Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 13 Mar 2025 11:06:59 +0100 Subject: [PATCH 03/19] chore: removed debug logger --- internal/database/database_tags.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/database/database_tags.go b/internal/database/database_tags.go index 518e829a2..1d97088dd 100644 --- a/internal/database/database_tags.go +++ b/internal/database/database_tags.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "fmt" - "log/slog" "github.com/go-shiori/shiori/internal/model" "github.com/huandu/go-sqlbuilder" @@ -64,8 +63,6 @@ func (db *dbbase) GetTags(ctx context.Context, opts model.DBListTagsOptions) ([] query, args := sb.Build() query = db.ReaderDB().Rebind(query) - slog.Info("GetTags query", "query", query, "args", args) - tags := []model.TagDTO{} err := db.ReaderDB().SelectContext(ctx, &tags, query, args...) if err != nil && err != sql.ErrNoRows { From 58cc91a2f6de9ecd715608a4cfb4d74558a922d8 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 13 Mar 2025 11:07:12 +0100 Subject: [PATCH 04/19] docs: updated swagger --- docs/swagger/docs.go | 107 ++++++++++++++++++++++++++++++++++++++ docs/swagger/swagger.json | 107 ++++++++++++++++++++++++++++++++++++++ docs/swagger/swagger.yaml | 69 ++++++++++++++++++++++++ 3 files changed, 283 insertions(+) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index b62baad67..476d918da 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -384,6 +384,96 @@ const docTemplate = `{ } } }, + "/api/v1/bookmarks/{id}/tags": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Get tags for a bookmark.", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.TagDTO" + } + } + }, + "403": { + "description": "Token not provided/invalid" + }, + "404": { + "description": "Bookmark not found" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Add a tag to a bookmark.", + "parameters": [ + { + "description": "Add Tag Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_v1.bookmarkTagPayload" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Token not provided/invalid" + }, + "404": { + "description": "Bookmark or tag not found" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Remove a tag from a bookmark.", + "parameters": [ + { + "description": "Remove Tag Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_v1.bookmarkTagPayload" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Token not provided/invalid" + }, + "404": { + "description": "Bookmark not found" + } + } + } + }, "/api/v1/system/info": { "get": { "description": "Get general system information like Shiori version, database, and OS", @@ -429,6 +519,12 @@ const docTemplate = `{ "description": "Filter tags by bookmark ID", "name": "bookmark_id", "in": "query" + }, + { + "type": "string", + "description": "Search tags by name", + "name": "search", + "in": "query" } ], "responses": { @@ -612,6 +708,17 @@ const docTemplate = `{ } }, "definitions": { + "api_v1.bookmarkTagPayload": { + "type": "object", + "required": [ + "tag_id" + ], + "properties": { + "tag_id": { + "type": "integer" + } + } + }, "api_v1.bulkUpdateBookmarkTagsPayload": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 1d8d36fbd..27695018f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -373,6 +373,96 @@ } } }, + "/api/v1/bookmarks/{id}/tags": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Get tags for a bookmark.", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.TagDTO" + } + } + }, + "403": { + "description": "Token not provided/invalid" + }, + "404": { + "description": "Bookmark not found" + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Add a tag to a bookmark.", + "parameters": [ + { + "description": "Add Tag Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_v1.bookmarkTagPayload" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Token not provided/invalid" + }, + "404": { + "description": "Bookmark or tag not found" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "Remove a tag from a bookmark.", + "parameters": [ + { + "description": "Remove Tag Payload", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api_v1.bookmarkTagPayload" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "403": { + "description": "Token not provided/invalid" + }, + "404": { + "description": "Bookmark not found" + } + } + } + }, "/api/v1/system/info": { "get": { "description": "Get general system information like Shiori version, database, and OS", @@ -418,6 +508,12 @@ "description": "Filter tags by bookmark ID", "name": "bookmark_id", "in": "query" + }, + { + "type": "string", + "description": "Search tags by name", + "name": "search", + "in": "query" } ], "responses": { @@ -601,6 +697,17 @@ } }, "definitions": { + "api_v1.bookmarkTagPayload": { + "type": "object", + "required": [ + "tag_id" + ], + "properties": { + "tag_id": { + "type": "integer" + } + } + }, "api_v1.bulkUpdateBookmarkTagsPayload": { "type": "object", "required": [ diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 4a578ab58..0c7d7592e 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -1,4 +1,11 @@ definitions: + api_v1.bookmarkTagPayload: + properties: + tag_id: + type: integer + required: + - tag_id + type: object api_v1.bulkUpdateBookmarkTagsPayload: properties: bookmark_ids: @@ -360,6 +367,64 @@ paths: summary: Refresh a token for an account tags: - Auth + /api/v1/bookmarks/{id}/tags: + delete: + parameters: + - description: Remove Tag Payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/api_v1.bookmarkTagPayload' + produces: + - application/json + responses: + "200": + description: OK + "403": + description: Token not provided/invalid + "404": + description: Bookmark not found + summary: Remove a tag from a bookmark. + tags: + - Auth + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.TagDTO' + type: array + "403": + description: Token not provided/invalid + "404": + description: Bookmark not found + summary: Get tags for a bookmark. + tags: + - Auth + post: + parameters: + - description: Add Tag Payload + in: body + name: payload + required: true + schema: + $ref: '#/definitions/api_v1.bookmarkTagPayload' + produces: + - application/json + responses: + "200": + description: OK + "403": + description: Token not provided/invalid + "404": + description: Bookmark or tag not found + summary: Add a tag to a bookmark. + tags: + - Auth /api/v1/bookmarks/bulk/tags: put: parameters: @@ -450,6 +515,10 @@ paths: in: query name: bookmark_id type: integer + - description: Search tags by name + in: query + name: search + type: string produces: - application/json responses: From 02554f7b9de05adb6405e6bb4c7c6e946dc2d7ce Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 13 Mar 2025 13:19:20 +0100 Subject: [PATCH 05/19] test: added tests --- .cursorrules | 7 + internal/database/database_tags_test.go | 326 ++++++++++++++++++++++++ internal/database/database_test.go | 27 +- 3 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..6089fc807 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,7 @@ +# Shiori Test Commands + +# Run the entire test suite +make unittest + +# Run SQLite database tests only +go test -timeout 10s -count=1 -tags test_sqlite_only ./internal/database diff --git a/internal/database/database_tags_test.go b/internal/database/database_tags_test.go index b6586f0eb..990d8dd1f 100644 --- a/internal/database/database_tags_test.go +++ b/internal/database/database_tags_test.go @@ -304,3 +304,329 @@ func testGetTagsFunction(t *testing.T, db model.DB) { assert.Equal(t, int64(1), fetchedTags[3].BookmarkCount) }) } + +// testTagBookmarkOperations tests the tag-bookmark relationship operations +func testTagBookmarkOperations(t *testing.T, db model.DB) { + ctx := context.TODO() + + // Create test data + // 1. Create a test bookmark + bookmark := model.BookmarkDTO{ + URL: "https://example.com/tag-operations-test", + Title: "Tag Operations Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + // 2. Create a test tag + tag := model.Tag{ + Name: "tag-operations-test", + } + createdTags, err := db.CreateTags(ctx, tag) + require.NoError(t, err) + require.Len(t, createdTags, 1) + tagID := createdTags[0].ID + + // Test BookmarkExists function + t.Run("BookmarkExists", func(t *testing.T) { + // Test with existing bookmark + exists, err := db.BookmarkExists(ctx, bookmarkID) + require.NoError(t, err) + assert.True(t, exists, "Bookmark should exist") + + // Test with non-existent bookmark + exists, err = db.BookmarkExists(ctx, 9999) + require.NoError(t, err) + assert.False(t, exists, "Non-existent bookmark should return false") + }) + + // Test TagExists function + t.Run("TagExists", func(t *testing.T) { + // Test with existing tag + exists, err := db.TagExists(ctx, tagID) + require.NoError(t, err) + assert.True(t, exists, "Tag should exist") + + // Test with non-existent tag + exists, err = db.TagExists(ctx, 9999) + require.NoError(t, err) + assert.False(t, exists, "Non-existent tag should return false") + }) + + // Test AddTagToBookmark function + t.Run("AddTagToBookmark", func(t *testing.T) { + // Add tag to bookmark + err := db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was added by fetching tags for the bookmark + tags, err := db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1) + assert.Equal(t, tagID, tags[0].ID) + assert.Equal(t, "tag-operations-test", tags[0].Name) + + // Test adding the same tag again (should not error) + err = db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify no duplicate was created + tags, err = db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1) + }) + + // Test RemoveTagFromBookmark function + t.Run("RemoveTagFromBookmark", func(t *testing.T) { + // First ensure the tag is associated with the bookmark + tags, err := db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1, "Tag should be associated with bookmark before removal test") + + // Remove tag from bookmark + err = db.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was removed + tags, err = db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + assert.Len(t, tags, 0, "Tag should be removed from bookmark") + + // Test removing a tag that's not associated (should not error) + err = db.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Test removing a tag from a non-existent bookmark (should not error) + err = db.RemoveTagFromBookmark(ctx, 9999, tagID) + require.NoError(t, err) + + // Test removing a non-existent tag from a bookmark (should not error) + err = db.RemoveTagFromBookmark(ctx, bookmarkID, 9999) + require.NoError(t, err) + }) + + // Test edge cases + t.Run("EdgeCases", func(t *testing.T) { + // Test adding a tag to a non-existent bookmark + // This should not error at the database layer since we're not checking existence there + err := db.AddTagToBookmark(ctx, 9999, tagID) + // The test might fail depending on foreign key constraints in the database + // If it fails, that's acceptable behavior, but we're not explicitly testing for it + if err != nil { + t.Logf("Adding tag to non-existent bookmark failed as expected: %v", err) + } + + // Test adding a non-existent tag to a bookmark + // This should not error at the database layer since we're not checking existence there + err = db.AddTagToBookmark(ctx, bookmarkID, 9999) + // The test might fail depending on foreign key constraints in the database + // If it fails, that's acceptable behavior, but we're not explicitly testing for it + if err != nil { + t.Logf("Adding non-existent tag to bookmark failed as expected: %v", err) + } + }) +} + +// testTagExists tests the TagExists function +func testTagExists(t *testing.T, db model.DB) { + ctx := context.TODO() + + // Create a test tag + tag := model.Tag{ + Name: "tag-exists-test", + } + createdTags, err := db.CreateTags(ctx, tag) + require.NoError(t, err) + require.Len(t, createdTags, 1) + tagID := createdTags[0].ID + + // Test with existing tag + exists, err := db.TagExists(ctx, tagID) + require.NoError(t, err) + assert.True(t, exists, "Tag should exist") + + // Test with non-existent tag + exists, err = db.TagExists(ctx, 9999) + require.NoError(t, err) + assert.False(t, exists, "Non-existent tag should return false") +} + +// testBookmarkExists tests the BookmarkExists function +func testBookmarkExists(t *testing.T, db model.DB) { + ctx := context.TODO() + + // Create a test bookmark + bookmark := model.BookmarkDTO{ + URL: "https://example.com/bookmark-exists-test", + Title: "Bookmark Exists Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + // Test with existing bookmark + exists, err := db.BookmarkExists(ctx, bookmarkID) + require.NoError(t, err) + assert.True(t, exists, "Bookmark should exist") + + // Test with non-existent bookmark + exists, err = db.BookmarkExists(ctx, 9999) + require.NoError(t, err) + assert.False(t, exists, "Non-existent bookmark should return false") +} + +// testAddTagToBookmark tests the AddTagToBookmark function +func testAddTagToBookmark(t *testing.T, db model.DB) { + ctx := context.TODO() + + // Create test data + bookmark := model.BookmarkDTO{ + URL: "https://example.com/add-tag-test", + Title: "Add Tag Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + tag := model.Tag{ + Name: "add-tag-test", + } + createdTags, err := db.CreateTags(ctx, tag) + require.NoError(t, err) + require.Len(t, createdTags, 1) + tagID := createdTags[0].ID + + // Add tag to bookmark + err = db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was added by fetching tags for the bookmark + tags, err := db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1) + assert.Equal(t, tagID, tags[0].ID) + assert.Equal(t, "add-tag-test", tags[0].Name) + + // Test adding the same tag again (should not error) + err = db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify no duplicate was created + tags, err = db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1) +} + +// testRemoveTagFromBookmark tests the RemoveTagFromBookmark function +func testRemoveTagFromBookmark(t *testing.T, db model.DB) { + ctx := context.TODO() + + // Create test data + bookmark := model.BookmarkDTO{ + URL: "https://example.com/remove-tag-test", + Title: "Remove Tag Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + tag := model.Tag{ + Name: "remove-tag-test", + } + createdTags, err := db.CreateTags(ctx, tag) + require.NoError(t, err) + require.Len(t, createdTags, 1) + tagID := createdTags[0].ID + + // Add tag to bookmark first + err = db.AddTagToBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was added + tags, err := db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + require.Len(t, tags, 1, "Tag should be associated with bookmark before removal test") + + // Remove tag from bookmark + err = db.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Verify tag was removed + tags, err = db.GetTags(ctx, model.DBListTagsOptions{ + BookmarkID: bookmarkID, + }) + require.NoError(t, err) + assert.Len(t, tags, 0, "Tag should be removed from bookmark") + + // Test removing a tag that's not associated (should not error) + err = db.RemoveTagFromBookmark(ctx, bookmarkID, tagID) + require.NoError(t, err) + + // Test removing a tag from a non-existent bookmark (should not error) + err = db.RemoveTagFromBookmark(ctx, 9999, tagID) + require.NoError(t, err) + + // Test removing a non-existent tag from a bookmark (should not error) + err = db.RemoveTagFromBookmark(ctx, bookmarkID, 9999) + require.NoError(t, err) +} + +// testTagBookmarkEdgeCases tests edge cases for tag-bookmark operations +func testTagBookmarkEdgeCases(t *testing.T, db model.DB) { + ctx := context.TODO() + + // Create test data + bookmark := model.BookmarkDTO{ + URL: "https://example.com/edge-cases-test", + Title: "Edge Cases Test", + } + savedBookmarks, err := db.SaveBookmarks(ctx, true, bookmark) + require.NoError(t, err) + require.Len(t, savedBookmarks, 1) + bookmarkID := savedBookmarks[0].ID + + tag := model.Tag{ + Name: "edge-cases-test", + } + createdTags, err := db.CreateTags(ctx, tag) + require.NoError(t, err) + require.Len(t, createdTags, 1) + tagID := createdTags[0].ID + + // Test adding a tag to a non-existent bookmark + // This should not error at the database layer since we're not checking existence there + err = db.AddTagToBookmark(ctx, 9999, tagID) + // The test might fail depending on foreign key constraints in the database + // If it fails, that's acceptable behavior, but we're not explicitly testing for it + if err != nil { + t.Logf("Adding tag to non-existent bookmark failed as expected: %v", err) + } + + // Test adding a non-existent tag to a bookmark + // This should not error at the database layer since we're not checking existence there + err = db.AddTagToBookmark(ctx, bookmarkID, 9999) + // The test might fail depending on foreign key constraints in the database + // If it fails, that's acceptable behavior, but we're not explicitly testing for it + if err != nil { + t.Logf("Adding non-existent tag to bookmark failed as expected: %v", err) + } +} diff --git a/internal/database/database_test.go b/internal/database/database_test.go index eb32d087b..263c86653 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -34,18 +34,23 @@ func testDatabase(t *testing.T, dbFactory testDatabaseFactory) { "testGetBookmarksCount": testGetBookmarksCount, "testSaveBookmark": testSaveBookmark, "testBulkUpdateBookmarkTags": testBulkUpdateBookmarkTags, + "testBookmarkExists": testBookmarkExists, // Tags - "testCreateTag": testCreateTag, - "testCreateTags": testCreateTags, - "testGetTags": testGetTags, - "testGetTagsFunction": testGetTagsFunction, - // "testGetTagsBookmarkCount": testGetTagsBookmarkCount, - "testGetTag": testGetTag, - "testGetTagNotExistent": testGetTagNotExistent, - "testUpdateTag": testUpdateTag, - "testRenameTag": testRenameTag, - "testDeleteTag": testDeleteTag, - "testDeleteTagNotExistent": testDeleteTagNotExistent, + "testCreateTag": testCreateTag, + "testCreateTags": testCreateTags, + "testTagExists": testTagExists, + "testGetTags": testGetTags, + "testGetTagsFunction": testGetTagsFunction, + "testGetTag": testGetTag, + "testGetTagNotExistent": testGetTagNotExistent, + "testUpdateTag": testUpdateTag, + "testRenameTag": testRenameTag, + "testDeleteTag": testDeleteTag, + "testDeleteTagNotExistent": testDeleteTagNotExistent, + "testAddTagToBookmark": testAddTagToBookmark, + "testRemoveTagFromBookmark": testRemoveTagFromBookmark, + "testTagBookmarkEdgeCases": testTagBookmarkEdgeCases, + "testTagBookmarkOperations": testTagBookmarkOperations, // Accounts "testCreateAccount": testCreateAccount, "testCreateDuplicateAccount": testCreateDuplicateAccount, From 745d69e247856a08b0289dbdbaed9b80d10603e9 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Thu, 13 Mar 2025 18:33:09 +0100 Subject: [PATCH 06/19] test: invalid ids --- .../handlers/api/v1/bookmark_tags_test.go | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/internal/http/handlers/api/v1/bookmark_tags_test.go b/internal/http/handlers/api/v1/bookmark_tags_test.go index 43d48d97f..c836797bb 100644 --- a/internal/http/handlers/api/v1/bookmark_tags_test.go +++ b/internal/http/handlers/api/v1/bookmark_tags_test.go @@ -546,6 +546,64 @@ func TestBookmarkTagsAPI(t *testing.T) { testResp.AssertMessageEquals(t, "Invalid bookmark ID") }) + // Test invalid bookmark ID for AddTagToBookmark + t.Run("InvalidBookmarkIDForAddTag", func(t *testing.T) { + // Create a request with an invalid bookmark ID + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleAddTagToBookmark, + http.MethodPost, + "/api/v1/bookmarks/invalid/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", "invalid"), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Invalid bookmark ID") + }) + + // Test invalid bookmark ID for RemoveTagFromBookmark + t.Run("InvalidBookmarkIDForRemoveTag", func(t *testing.T) { + // Create a request with an invalid bookmark ID + payload := bookmarkTagPayload{ + TagID: tagID, + } + payloadBytes, err := json.Marshal(payload) + require.NoError(t, err) + + rec := testutil.PerformRequest( + deps, + api_v1.HandleRemoveTagFromBookmark, + http.MethodDelete, + "/api/v1/bookmarks/invalid/tags", + testutil.WithFakeAdmin(), + testutil.WithRequestPathValue("id", "invalid"), + testutil.WithBody(string(payloadBytes)), + ) + + // Check the response + require.Equal(t, http.StatusBadRequest, rec.Code) + + // Parse the response + testResp, err := testutil.NewTestResponseFromRecorder(rec) + require.NoError(t, err) + testResp.AssertNotOk(t) + testResp.AssertMessageEquals(t, "Invalid bookmark ID") + }) + // Test invalid payload t.Run("InvalidPayload", func(t *testing.T) { // Create a request with an invalid payload From ae1ce6a3b517c22ba4b4935ee07c1970a3775029 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 08:48:24 +0100 Subject: [PATCH 07/19] feat: webapp v2 --- Makefile | 7 +- internal/cmd/server.go | 5 + internal/config/config.go | 6 + internal/http/handlers/bookmark_test.go | 6 +- internal/http/handlers/frontend.go | 21 +- internal/http/handlers/frontend_test.go | 2 +- internal/http/server.go | 2 +- internal/http/templates/templates.go | 20 +- webapp/.editorconfig | 9 + webapp/.gitattributes | 1 + webapp/.gitignore | 30 + webapp/.prettierrc.json | 6 + webapp/README.md | 45 + webapp/bun.lock | 1072 +++++++++++++++++ webapp/embed.go | 11 + webapp/env.d.ts | 1 + webapp/eslint.config.ts | 30 + webapp/index.html | 14 + webapp/package.json | 47 + webapp/public/favicon.ico | Bin 0 -> 4286 bytes webapp/src/App.vue | 11 + webapp/src/assets/main.css | 23 + webapp/src/components/HelloWorld.vue | 41 + webapp/src/components/TheWelcome.vue | 94 ++ webapp/src/components/WelcomeItem.vue | 87 ++ .../components/__tests__/HelloWorld.spec.ts | 11 + webapp/src/components/icons/IconCommunity.vue | 7 + .../components/icons/IconDocumentation.vue | 7 + webapp/src/components/icons/IconEcosystem.vue | 7 + webapp/src/components/icons/IconSupport.vue | 7 + webapp/src/components/icons/IconTooling.vue | 19 + webapp/src/components/layout/AppLayout.vue | 48 + webapp/src/components/layout/Sidebar.vue | 80 ++ webapp/src/components/layout/TopBar.vue | 45 + webapp/src/main.ts | 14 + webapp/src/router/index.ts | 71 ++ webapp/src/stores/counter.ts | 12 + webapp/src/views/AboutView.vue | 15 + webapp/src/views/ArchiveView.vue | 17 + webapp/src/views/HomeView.vue | 66 + webapp/src/views/LoginView.vue | 68 ++ webapp/src/views/SettingsView.vue | 17 + webapp/src/views/TagsView.vue | 17 + webapp/tailwind.config.js | 12 + webapp/tsconfig.app.json | 12 + webapp/tsconfig.json | 14 + webapp/tsconfig.node.json | 19 + webapp/tsconfig.vitest.json | 11 + webapp/vite.config.ts | 23 + webapp/vitest.config.ts | 14 + 50 files changed, 2208 insertions(+), 16 deletions(-) create mode 100644 webapp/.editorconfig create mode 100644 webapp/.gitattributes create mode 100644 webapp/.gitignore create mode 100644 webapp/.prettierrc.json create mode 100644 webapp/README.md create mode 100644 webapp/bun.lock create mode 100644 webapp/embed.go create mode 100644 webapp/env.d.ts create mode 100644 webapp/eslint.config.ts create mode 100644 webapp/index.html create mode 100644 webapp/package.json create mode 100644 webapp/public/favicon.ico create mode 100644 webapp/src/App.vue create mode 100644 webapp/src/assets/main.css create mode 100644 webapp/src/components/HelloWorld.vue create mode 100644 webapp/src/components/TheWelcome.vue create mode 100644 webapp/src/components/WelcomeItem.vue create mode 100644 webapp/src/components/__tests__/HelloWorld.spec.ts create mode 100644 webapp/src/components/icons/IconCommunity.vue create mode 100644 webapp/src/components/icons/IconDocumentation.vue create mode 100644 webapp/src/components/icons/IconEcosystem.vue create mode 100644 webapp/src/components/icons/IconSupport.vue create mode 100644 webapp/src/components/icons/IconTooling.vue create mode 100644 webapp/src/components/layout/AppLayout.vue create mode 100644 webapp/src/components/layout/Sidebar.vue create mode 100644 webapp/src/components/layout/TopBar.vue create mode 100644 webapp/src/main.ts create mode 100644 webapp/src/router/index.ts create mode 100644 webapp/src/stores/counter.ts create mode 100644 webapp/src/views/AboutView.vue create mode 100644 webapp/src/views/ArchiveView.vue create mode 100644 webapp/src/views/HomeView.vue create mode 100644 webapp/src/views/LoginView.vue create mode 100644 webapp/src/views/SettingsView.vue create mode 100644 webapp/src/views/TagsView.vue create mode 100644 webapp/tailwind.config.js create mode 100644 webapp/tsconfig.app.json create mode 100644 webapp/tsconfig.json create mode 100644 webapp/tsconfig.node.json create mode 100644 webapp/tsconfig.vitest.json create mode 100644 webapp/vite.config.ts create mode 100644 webapp/vitest.config.ts diff --git a/Makefile b/Makefile index 28f2ed753..18a0481a9 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,12 @@ clean: ## Runs server for local development .PHONY: run-server run-server: generate - GIN_MODE=$(GIN_MODE) SHIORI_DEVELOPMENT=$(SHIORI_DEVELOPMENT) SHIORI_DIR=$(SHIORI_DIR) SHIORI_HTTP_SECRET_KEY=shiori SHIORI_HTTP_SERVE_SWAGGER=true go run main.go server --log-level debug + GIN_MODE=$(GIN_MODE) SHIORI_DEVELOPMENT=$(SHIORI_DEVELOPMENT) go run main.go server --log-level debug + +## Runs server for local development with v2 web UI +.PHONY: run-server-v2 +run-server-v2: generate + GIN_MODE=$(GIN_MODE) SHIORI_DEVELOPMENT=$(SHIORI_DEVELOPMENT) SHIORI_HTTP_SERVE_WEB_UI_V2=true go run main.go server --log-level debug ## Generate swagger docs .PHONY: swagger diff --git a/internal/cmd/server.go b/internal/cmd/server.go index 48f42b703..852f9241c 100644 --- a/internal/cmd/server.go +++ b/internal/cmd/server.go @@ -24,6 +24,7 @@ func newServerCommand() *cobra.Command { cmd.Flags().StringP("webroot", "r", "/", "Root path that used by server") cmd.Flags().Bool("access-log", false, "Print out a non-standard access log") cmd.Flags().Bool("serve-web-ui", true, "Serve static files from the webroot path") + cmd.Flags().Bool("experimental-serve-web-ui-v2", false, "Serve static files from the webapp path") cmd.Flags().String("secret-key", "", "Secret key used for encrypting session data") return cmd @@ -45,6 +46,7 @@ func newServerCommandHandler() func(cmd *cobra.Command, args []string) { rootPath, _ := cmd.Flags().GetString("webroot") accessLog, _ := cmd.Flags().GetBool("access-log") serveWebUI, _ := cmd.Flags().GetBool("serve-web-ui") + serveWebUIV2, _ := cmd.Flags().GetBool("experimental-serve-web-ui-v2") secretKey, _ := cmd.Flags().GetBytesHex("secret-key") cfg, dependencies := initShiori(ctx, cmd) @@ -81,6 +83,9 @@ func newServerCommandHandler() func(cmd *cobra.Command, args []string) { setIfFlagChanged("secret-key", cmd.Flags(), cfg, func(cfg *config.Config) { cfg.Http.SecretKey = secretKey }) + setIfFlagChanged("experimental-serve-web-ui-v2", cmd.Flags(), cfg, func(cfg *config.Config) { + cfg.Http.ServeWebUIV2 = serveWebUIV2 + }) dependencies.Logger().Infof("Starting Shiori v%s", model.BuildVersion) diff --git a/internal/config/config.go b/internal/config/config.go index e70f00284..bc260ce48 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -55,6 +55,7 @@ type HttpConfig struct { RootPath string `env:"HTTP_ROOT_PATH,default=/"` AccessLog bool `env:"HTTP_ACCESS_LOG,default=True"` ServeWebUI bool `env:"HTTP_SERVE_WEB_UI,default=True"` + ServeWebUIV2 bool `env:"HTTP_SERVE_WEB_UI_V2,default=False"` ServeSwagger bool `env:"HTTP_SERVE_SWAGGER,default=False"` SecretKey []byte `env:"HTTP_SECRET_KEY"` // Fiber Specific @@ -84,6 +85,10 @@ func (c *HttpConfig) IsValid() error { return fmt.Errorf("root path should end with a slash") } + if c.ServeWebUIV2 && !c.ServeWebUI { + return fmt.Errorf("You need to enable serving the Web UI to use the experimental Web UI v2") + } + return nil } @@ -139,6 +144,7 @@ func (c *Config) DebugConfiguration(logger *logrus.Logger) { logger.Debugf(" SHIORI_HTTP_ROOT_PATH: %s", c.Http.RootPath) logger.Debugf(" SHIORI_HTTP_ACCESS_LOG: %t", c.Http.AccessLog) logger.Debugf(" SHIORI_HTTP_SERVE_WEB_UI: %t", c.Http.ServeWebUI) + logger.Debugf(" SHIORI_HTTP_SERVE_WEB_UI_V2: %t", c.Http.ServeWebUIV2) logger.Debugf(" SHIORI_HTTP_SECRET_KEY: %d characters", len(c.Http.SecretKey)) logger.Debugf(" SHIORI_HTTP_BODY_LIMIT: %d", c.Http.BodyLimit) logger.Debugf(" SHIORI_HTTP_READ_TIMEOUT: %s", c.Http.ReadTimeout) diff --git a/internal/http/handlers/bookmark_test.go b/internal/http/handlers/bookmark_test.go index 462753e0e..f301ece73 100644 --- a/internal/http/handlers/bookmark_test.go +++ b/internal/http/handlers/bookmark_test.go @@ -17,7 +17,7 @@ func TestGetBookmark(t *testing.T) { logger := logrus.New() _, deps := testutil.GetTestConfigurationAndDependencies(t, context.TODO(), logger) - err := templates.SetupTemplates() + err := templates.SetupTemplates(deps.Config()) require.NoError(t, err) // Create a private and a public bookmark to use in tests @@ -75,7 +75,7 @@ func TestBookmarkContentHandler(t *testing.T) { logger := logrus.New() _, deps := testutil.GetTestConfigurationAndDependencies(t, context.Background(), logger) - err := templates.SetupTemplates() + err := templates.SetupTemplates(deps.Config()) require.NoError(t, err) bookmark := testutil.GetValidBookmark() @@ -105,7 +105,7 @@ func TestBookmarkFileHandlers(t *testing.T) { logger := logrus.New() _, deps := testutil.GetTestConfigurationAndDependencies(t, context.Background(), logger) - err := templates.SetupTemplates() + err := templates.SetupTemplates(deps.Config()) require.NoError(t, err) bookmark := testutil.GetValidBookmark() diff --git a/internal/http/handlers/frontend.go b/internal/http/handlers/frontend.go index 217d50afe..280d3f513 100644 --- a/internal/http/handlers/frontend.go +++ b/internal/http/handlers/frontend.go @@ -8,19 +8,27 @@ import ( "github.com/go-shiori/shiori/internal/http/response" "github.com/go-shiori/shiori/internal/model" views "github.com/go-shiori/shiori/internal/view" + webapp "github.com/go-shiori/shiori/webapp" ) type assetsFS struct { http.FileSystem + serveWebUIV2 bool } func (fs assetsFS) Open(name string) (http.File, error) { - return fs.FileSystem.Open(path.Join("assets", name)) + pathJoin := "assets" + if fs.serveWebUIV2 { + pathJoin = "dist/assets" + } + + return fs.FileSystem.Open(path.Join(pathJoin, name)) } -func newAssetsFS(fs embed.FS) http.FileSystem { +func newAssetsFS(fs embed.FS, serveWebUIV2 bool) http.FileSystem { return assetsFS{ - FileSystem: http.FS(fs), + FileSystem: http.FS(fs), + serveWebUIV2: serveWebUIV2, } } @@ -38,6 +46,9 @@ func HandleFrontend(deps model.Dependencies, c model.WebContext) { // HandleAssets serves static assets func HandleAssets(deps model.Dependencies, c model.WebContext) { - fs := newAssetsFS(views.Assets) - http.StripPrefix("/assets/", http.FileServer(fs)).ServeHTTP(c.ResponseWriter(), c.Request()) + fs := views.Assets + if deps.Config().Http.ServeWebUIV2 { + fs = webapp.Assets + } + http.StripPrefix("/assets/", http.FileServer(newAssetsFS(fs, deps.Config().Http.ServeWebUIV2))).ServeHTTP(c.ResponseWriter(), c.Request()) } diff --git a/internal/http/handlers/frontend_test.go b/internal/http/handlers/frontend_test.go index 9ae7d0cbf..eadac6936 100644 --- a/internal/http/handlers/frontend_test.go +++ b/internal/http/handlers/frontend_test.go @@ -15,7 +15,7 @@ func TestHandleFrontend(t *testing.T) { logger := logrus.New() _, deps := testutil.GetTestConfigurationAndDependencies(t, context.Background(), logger) - err := templates.SetupTemplates() + err := templates.SetupTemplates(deps.Config()) require.NoError(t, err) t.Run("serves index page", func(t *testing.T) { diff --git a/internal/http/server.go b/internal/http/server.go index 16da2a4ce..11700fec7 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -27,7 +27,7 @@ type HttpServer struct { func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Dependencies) (*HttpServer, error) { s.mux = http.NewServeMux() - if err := templates.SetupTemplates(); err != nil { + if err := templates.SetupTemplates(cfg); err != nil { return nil, fmt.Errorf("failed to setup templates: %w", err) } diff --git a/internal/http/templates/templates.go b/internal/http/templates/templates.go index f4b75fa18..bd92767ad 100644 --- a/internal/http/templates/templates.go +++ b/internal/http/templates/templates.go @@ -5,7 +5,9 @@ import ( "html/template" "io" + "github.com/go-shiori/shiori/internal/config" views "github.com/go-shiori/shiori/internal/view" + webapp "github.com/go-shiori/shiori/webapp" ) const ( @@ -16,11 +18,21 @@ const ( var templates *template.Template // SetupTemplates initializes the templates for the webserver -func SetupTemplates() error { +func SetupTemplates(config *config.Config) error { var err error + fs := views.Templates + + globs := []string{"*.html"} + + if config.Http.ServeWebUIV2 { + fs = webapp.Templates + globs = []string{"**/*.html"} + } + templates, err = template.New("html"). Delims(leftTemplateDelim, rightTemplateDelim). - ParseFS(views.Templates, "*.html") + ParseFS(fs, globs...) + if err != nil { return fmt.Errorf("failed to parse templates: %w", err) } @@ -30,9 +42,7 @@ func SetupTemplates() error { // RenderTemplate renders a template with the given data func RenderTemplate(w io.Writer, name string, data any) error { if templates == nil { - if err := SetupTemplates(); err != nil { - return fmt.Errorf("failed to setup templates: %w", err) - } + return fmt.Errorf("templates not initialized") } return templates.ExecuteTemplate(w, name, data) } diff --git a/webapp/.editorconfig b/webapp/.editorconfig new file mode 100644 index 000000000..5a5809dbe --- /dev/null +++ b/webapp/.editorconfig @@ -0,0 +1,9 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +end_of_line = lf +max_line_length = 100 diff --git a/webapp/.gitattributes b/webapp/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/webapp/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/webapp/.gitignore b/webapp/.gitignore new file mode 100644 index 000000000..8ee54e8d3 --- /dev/null +++ b/webapp/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/webapp/.prettierrc.json b/webapp/.prettierrc.json new file mode 100644 index 000000000..29a2402ef --- /dev/null +++ b/webapp/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/webapp/README.md b/webapp/README.md new file mode 100644 index 000000000..fde3c9b0d --- /dev/null +++ b/webapp/README.md @@ -0,0 +1,45 @@ +# . + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +bun install +``` + +### Compile and Hot-Reload for Development + +```sh +bun dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +bun run build +``` + +### Run Unit Tests with [Vitest](https://vitest.dev/) + +```sh +bun test:unit +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +bun lint +``` diff --git a/webapp/bun.lock b/webapp/bun.lock new file mode 100644 index 000000000..fdd00a031 --- /dev/null +++ b/webapp/bun.lock @@ -0,0 +1,1072 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "shiori", + "dependencies": { + "@tailwindcss/vite": "^4.0.14", + "@vueuse/core": "^13.0.0", + "pinia": "^3.0.1", + "vue": "^3.5.13", + "vue-router": "4", + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/jsdom": "^21.1.7", + "@types/node": "^22.13.9", + "@types/vue-router": "^2.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "@vitest/eslint-plugin": "^1.1.36", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.5.0", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.7.0", + "eslint": "^9.21.0", + "eslint-plugin-vue": "~10.0.0", + "jiti": "^2.4.2", + "jsdom": "^26.0.0", + "npm-run-all2": "^7.0.2", + "prettier": "3.5.3", + "tailwindcss": "^4.0.14", + "typescript": "~5.8.0", + "vite": "^6.2.1", + "vite-plugin-vue-devtools": "^7.7.2", + "vitest": "^3.0.8", + "vue-tsc": "^2.2.8", + }, + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.2", "@csstools/css-color-parser": "^3.0.8", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA=="], + + "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + + "@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="], + + "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], + + "@babel/generator": ["@babel/generator@7.26.10", "", { "dependencies": { "@babel/parser": "^7.26.10", "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.26.5", "", { "dependencies": { "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.26.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.26.9", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], + + "@babel/helpers": ["@babel/helpers@7.26.10", "", { "dependencies": { "@babel/template": "^7.26.9", "@babel/types": "^7.26.10" } }, "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g=="], + + "@babel/parser": ["@babel/parser@7.26.10", "", { "dependencies": { "@babel/types": "^7.26.10" }, "bin": "./bin/babel-parser.js" }, "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA=="], + + "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-syntax-decorators": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g=="], + + "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg=="], + + "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A=="], + + "@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.26.8", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw=="], + + "@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="], + + "@babel/traverse": ["@babel/traverse@7.26.10", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A=="], + + "@babel/types": ["@babel/types@7.26.10", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.2", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.8", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.2" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.4", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.3" } }, "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.3", "", {}, "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.1", "", { "os": "android", "cpu": "arm" }, "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.1", "", { "os": "android", "cpu": "arm64" }, "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.1", "", { "os": "android", "cpu": "x64" }, "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.1", "", { "os": "linux", "cpu": "arm" }, "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.1", "", { "os": "linux", "cpu": "none" }, "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.1", "", { "os": "none", "cpu": "arm64" }, "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.1", "", { "os": "none", "cpu": "x64" }, "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.5.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.1.0", "", {}, "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA=="], + + "@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="], + + "@eslint/js": ["@eslint/js@9.22.0", "", {}, "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="], + + "@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.35.0", "", { "os": "android", "cpu": "arm64" }, "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.35.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.35.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.35.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.35.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.35.0", "", { "os": "linux", "cpu": "none" }, "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.35.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.35.0", "", { "os": "linux", "cpu": "none" }, "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.35.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Pim1T8rXOri+0HmV4CdKSGrqcBWX0d1HoPnQ0uw0bdp1aP5SdQVNBy8LjYncvnLgu3fnnCt17xjWGd4cqh8/hA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QysqXzYiDvQWfUiTm8XmJNO2zm9yC9P/2Gkrwg2dH9cxotQzunBHYr6jk4SujCTqnfGxduOmQcI7c2ryuW8XVg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.35.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.35.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.0.14", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.14" } }, "sha512-Ux9NbFkKWYE4rfUFz6M5JFLs/GEYP6ysxT8uSyPn6aTbh2K3xDE1zz++eVK4Vwx799fzMF8CID9sdHn4j/Ab8w=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.14", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.14", "@tailwindcss/oxide-darwin-arm64": "4.0.14", "@tailwindcss/oxide-darwin-x64": "4.0.14", "@tailwindcss/oxide-freebsd-x64": "4.0.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.14", "@tailwindcss/oxide-linux-arm64-musl": "4.0.14", "@tailwindcss/oxide-linux-x64-gnu": "4.0.14", "@tailwindcss/oxide-linux-x64-musl": "4.0.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.14", "@tailwindcss/oxide-win32-x64-msvc": "4.0.14" } }, "sha512-M8VCNyO/NBi5vJ2cRcI9u8w7Si+i76a7o1vveoGtbbjpEYJZYiyc7f2VGps/DqawO56l3tImIbq2OT/533jcrA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.14", "", { "os": "android", "cpu": "arm64" }, "sha512-VBFKC2rFyfJ5J8lRwjy6ub3rgpY186kAcYgiUr8ArR8BAZzMruyeKJ6mlsD22Zp5ZLcPW/FXMasJiJBx0WsdQg=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-U3XOwLrefGr2YQZ9DXasDSNWGPZBCh8F62+AExBEDMLDfvLLgI/HDzY8Oq8p/JtqkAY38sWPOaNnRwEGKU5Zmg=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-V5AjFuc3ndWGnOi1d379UsODb0TzAS2DYIP/lwEbfvafUaD2aNZIcbwJtYu2DQqO2+s/XBvDVA+w4yUyaewRwg=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tXvtxbaZfcPfqBwW3f53lTcyH6EDT+1eT7yabwcfcxTs+8yTPqxsDUhrqe9MrnEzpNkd+R/QAjJapfd4tjWdLg=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.14", "", { "os": "linux", "cpu": "arm" }, "sha512-cSeLNWWqIWeSTmBntQvyY2/2gcLX8rkPFfDDTQVF8qbRcRMVPLxBvFVJyfSAYRNch6ZyVH2GI6dtgALOBDpdNA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-bwDWLBalXFMDItcSXzFk6y7QKvj6oFlaY9vM+agTlwFL1n1OhDHYLZkSjaYsh6KCeG0VB0r7H8PUJVOM1LRZyg=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.14", "", { "os": "linux", "cpu": "x64" }, "sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.14", "", { "os": "linux", "cpu": "x64" }, "sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.14", "", { "os": "win32", "cpu": "x64" }, "sha512-rNXXMDJfCJLw/ZaFTOLOHoGULxyXfh2iXTGiChFiYTSgKBKQHIGEpV0yn5N25WGzJJ+VBnRjHzlmDqRV+d//oQ=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.0.14", "", { "dependencies": { "@tailwindcss/node": "4.0.14", "@tailwindcss/oxide": "4.0.14", "lightningcss": "1.29.2", "tailwindcss": "4.0.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-y69ztPTRFy+13EPS/7dEFVl7q2Goh1pQueVO8IfGeyqSpcx/joNJXFk0lLhMgUbF0VFJotwRSb9ZY7Xoq3r26Q=="], + + "@tsconfig/node22": ["@tsconfig/node22@22.0.0", "", {}, "sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/vue-router": ["@types/vue-router@2.0.0", "", { "dependencies": { "vue-router": "*" } }, "sha512-E454lQ6tp9ftVWdZ8VGZpRcIV4YeqVAcx/uifl3P1GGwscYsxOFdYfgIuKasKO0Fm6Np2JM/L378D3bcRQE9hg=="], + + "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.26.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/type-utils": "8.26.1", "@typescript-eslint/utils": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.26.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1" } }, "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.26.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.26.1", "@typescript-eslint/utils": "8.26.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.26.1", "", {}, "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.26.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.1", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ=="], + + "@vitest/eslint-plugin": ["@vitest/eslint-plugin@1.1.37", "", { "peerDependencies": { "@typescript-eslint/utils": "^8.24.0", "eslint": ">= 8.57.0", "typescript": ">= 5.0.0", "vitest": "*" }, "optionalPeers": ["typescript", "vitest"] }, "sha512-cnlBV8zr0oaBu1Vk6ruqWzpPzFCtwY0yuwUQpNIeFOUl3UhXVwNUoOYfWkZzeToGuNBaXvIsr6/RgHrXiHXqXA=="], + + "@vitest/expect": ["@vitest/expect@3.0.8", "", { "dependencies": { "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ=="], + + "@vitest/mocker": ["@vitest/mocker@3.0.8", "", { "dependencies": { "@vitest/spy": "3.0.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.0.8", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg=="], + + "@vitest/runner": ["@vitest/runner@3.0.8", "", { "dependencies": { "@vitest/utils": "3.0.8", "pathe": "^2.0.3" } }, "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.0.8", "", { "dependencies": { "@vitest/pretty-format": "3.0.8", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A=="], + + "@vitest/spy": ["@vitest/spy@3.0.8", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q=="], + + "@vitest/utils": ["@vitest/utils@3.0.8", "", { "dependencies": { "@vitest/pretty-format": "3.0.8", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q=="], + + "@volar/language-core": ["@volar/language-core@2.4.12", "", { "dependencies": { "@volar/source-map": "2.4.12" } }, "sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA=="], + + "@volar/source-map": ["@volar/source-map@2.4.12", "", {}, "sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw=="], + + "@volar/typescript": ["@volar/typescript@2.4.12", "", { "dependencies": { "@volar/language-core": "2.4.12", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g=="], + + "@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.4.0", "", {}, "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw=="], + + "@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.4.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "@vue/babel-helper-vue-transform-on": "1.4.0", "@vue/babel-plugin-resolve-type": "1.4.0", "@vue/shared": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA=="], + + "@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.4.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/parser": "^7.26.9", "@vue/compiler-sfc": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.13", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.13", "", { "dependencies": { "@vue/compiler-core": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.13", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/compiler-core": "3.5.13", "@vue/compiler-dom": "3.5.13", "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA=="], + + "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + + "@vue/devtools-api": ["@vue/devtools-api@7.7.2", "", { "dependencies": { "@vue/devtools-kit": "^7.7.2" } }, "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA=="], + + "@vue/devtools-core": ["@vue/devtools-core@7.7.2", "", { "dependencies": { "@vue/devtools-kit": "^7.7.2", "@vue/devtools-shared": "^7.7.2", "mitt": "^3.0.1", "nanoid": "^5.0.9", "pathe": "^2.0.2", "vite-hot-client": "^0.2.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-lexREWj1lKi91Tblr38ntSsy6CvI8ba7u+jmwh2yruib/ltLUcsIzEjCnrkh1yYGGIKXbAuYV2tOG10fGDB9OQ=="], + + "@vue/devtools-kit": ["@vue/devtools-kit@7.7.2", "", { "dependencies": { "@vue/devtools-shared": "^7.7.2", "birpc": "^0.2.19", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.1" } }, "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ=="], + + "@vue/devtools-shared": ["@vue/devtools-shared@7.7.2", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA=="], + + "@vue/eslint-config-prettier": ["@vue/eslint-config-prettier@10.2.0", "", { "dependencies": { "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw=="], + + "@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.5.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.26.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.26.0", "vue-eslint-parser": "^10.1.1" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-5oPOyuwkw++AP5gHDh5YFmST50dPfWOcm3/W7Nbh42IK5O3H74ytWAw0TrCRTaBoD/02khnWXuZf1Bz1xflavQ=="], + + "@vue/language-core": ["@vue/language-core@2.2.8", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.13", "", { "dependencies": { "@vue/shared": "3.5.13" } }, "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.13", "", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.13", "", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/runtime-core": "3.5.13", "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.13", "", { "dependencies": { "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "vue": "3.5.13" } }, "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA=="], + + "@vue/shared": ["@vue/shared@3.5.13", "", {}, "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="], + + "@vue/test-utils": ["@vue/test-utils@2.4.6", "", { "dependencies": { "js-beautify": "^1.14.9", "vue-component-type-helpers": "^2.0.0" } }, "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow=="], + + "@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="], + + "@vueuse/core": ["@vueuse/core@13.0.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.0.0", "@vueuse/shared": "13.0.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-rkgb4a8/0b234lMGCT29WkCjPfsX0oxrIRR7FDndRoW3FsaC9NBzefXg/9TLhAgwM11f49XnutshM4LzJBrQ5g=="], + + "@vueuse/metadata": ["@vueuse/metadata@13.0.0", "", {}, "sha512-TRNksqmvtvqsuHf7bbgH9OSXEV2b6+M3BSN4LR5oxWKykOFT9gV78+C2/0++Pq9KCp9KQ1OQDPvGlWNQpOb2Mw=="], + + "@vueuse/shared": ["@vueuse/shared@13.0.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-9MiHhAPw+sqCF/RLo8V6HsjRqEdNEWVpDLm2WBRW2G/kSQjb8X901sozXpSCaeLG0f7TEfMrT4XNaA5m1ez7Dg=="], + + "abbrev": ["abbrev@2.0.0", "", {}, "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ=="], + + "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "alien-signals": ["alien-signals@1.0.4", "", {}, "sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "birpc": ["birpc@0.2.19", "", {}, "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ=="], + + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001704", "", {}, "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew=="], + + "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssstyle": ["cssstyle@4.3.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.1.1", "rrweb-cssom": "^0.8.0" } }, "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="], + + "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "editorconfig": ["editorconfig@1.0.4", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.118", "", {}, "sha512-yNDUus0iultYyVoEFLnQeei7LOQkL8wg8GQpkPCRrOlJXlcCwa6eGKZkxQ9ciHsqZyYbj8Jd94X1CTPzGm+uIA=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "error-stack-parser-es": ["error-stack-parser-es@0.1.5", "", {}, "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.25.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.1", "@esbuild/android-arm": "0.25.1", "@esbuild/android-arm64": "0.25.1", "@esbuild/android-x64": "0.25.1", "@esbuild/darwin-arm64": "0.25.1", "@esbuild/darwin-x64": "0.25.1", "@esbuild/freebsd-arm64": "0.25.1", "@esbuild/freebsd-x64": "0.25.1", "@esbuild/linux-arm": "0.25.1", "@esbuild/linux-arm64": "0.25.1", "@esbuild/linux-ia32": "0.25.1", "@esbuild/linux-loong64": "0.25.1", "@esbuild/linux-mips64el": "0.25.1", "@esbuild/linux-ppc64": "0.25.1", "@esbuild/linux-riscv64": "0.25.1", "@esbuild/linux-s390x": "0.25.1", "@esbuild/linux-x64": "0.25.1", "@esbuild/netbsd-arm64": "0.25.1", "@esbuild/netbsd-x64": "0.25.1", "@esbuild/openbsd-arm64": "0.25.1", "@esbuild/openbsd-x64": "0.25.1", "@esbuild/sunos-x64": "0.25.1", "@esbuild/win32-arm64": "0.25.1", "@esbuild/win32-ia32": "0.25.1", "@esbuild/win32-x64": "0.25.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.22.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.1.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.22.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ=="], + + "eslint-config-prettier": ["eslint-config-prettier@10.1.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw=="], + + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="], + + "eslint-plugin-vue": ["eslint-plugin-vue@10.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "vue-eslint-parser": "^10.0.0" } }, "sha512-XKckedtajqwmaX6u1VnECmZ6xJt+YvlmMzBPZd+/sI3ub2lpYZyFnsyWo7c3nMOQKJQudeyk1lw/JxdgeKT64w=="], + + "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "execa": ["execa@9.5.2", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.3", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.0", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.0.0" } }, "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q=="], + + "expect-type": ["expect-type@1.2.0", "", {}, "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="], + + "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "human-signals": ["human-signals@8.0.0", "", {}, "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js", "js-beautify": "js/bin/js-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="], + + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "jsdom": ["jsdom@26.0.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.1", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loupe": ["loupe@3.1.3", "", {}, "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + + "nanoid": ["nanoid@3.3.9", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="], + + "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="], + + "npm-run-all2": ["npm-run-all2@7.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "minimatch": "^9.0.0", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + + "nwsapi": ["nwsapi@2.2.18", "", {}, "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="], + + "open": ["open@10.1.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "pinia": ["pinia@3.0.1", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg=="], + + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], + + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + + "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], + + "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "rollup": ["rollup@4.35.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.35.0", "@rollup/rollup-android-arm64": "4.35.0", "@rollup/rollup-darwin-arm64": "4.35.0", "@rollup/rollup-darwin-x64": "4.35.0", "@rollup/rollup-freebsd-arm64": "4.35.0", "@rollup/rollup-freebsd-x64": "4.35.0", "@rollup/rollup-linux-arm-gnueabihf": "4.35.0", "@rollup/rollup-linux-arm-musleabihf": "4.35.0", "@rollup/rollup-linux-arm64-gnu": "4.35.0", "@rollup/rollup-linux-arm64-musl": "4.35.0", "@rollup/rollup-linux-loongarch64-gnu": "4.35.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.35.0", "@rollup/rollup-linux-riscv64-gnu": "4.35.0", "@rollup/rollup-linux-s390x-gnu": "4.35.0", "@rollup/rollup-linux-x64-gnu": "4.35.0", "@rollup/rollup-linux-x64-musl": "4.35.0", "@rollup/rollup-win32-arm64-msvc": "4.35.0", "@rollup/rollup-win32-ia32-msvc": "4.35.0", "@rollup/rollup-win32-x64-msvc": "4.35.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-kg6oI4g+vc41vePJyO6dHt/yl0Rz3Thv0kJeVQ3D1kS3E5XSuKbPc29G4IpT/Kv1KQwgHVcN+HtyS+HYLNSvQg=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.8.1", "", {}, "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="], + + "tailwindcss": ["tailwindcss@4.0.14", "", {}, "sha512-92YT2dpt671tFiHH/e1ok9D987N9fHD5VWoly1CdPD/Cd1HMglvZwP3nx2yTj2lbXDAHt8QssZkxTLCCTNL+xw=="], + + "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + + "tldts": ["tldts@6.1.84", "", { "dependencies": { "tldts-core": "^6.1.84" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-aRGIbCIF3teodtUFAYSdQONVmDRy21REM3o6JnqWn5ZkQBJJ4gHxhw6OfwQ+WkSAi3ASamrS4N4nyazWx6uTYg=="], + + "tldts-core": ["tldts-core@6.1.84", "", {}, "sha512-NaQa1W76W2aCGjXybvnMYzGSM4x8fvG2AN/pla7qxcg0ZHbooOPhA8kctmOZUDfZyhDL27OGNbwAeig8P4p1vg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g=="], + + "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "typescript-eslint": ["typescript-eslint@8.26.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.26.1", "@typescript-eslint/parser": "8.26.1", "@typescript-eslint/utils": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@6.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ=="], + + "vite-hot-client": ["vite-hot-client@0.2.4", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" } }, "sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA=="], + + "vite-node": ["vite-node@3.0.8", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg=="], + + "vite-plugin-inspect": ["vite-plugin-inspect@0.8.9", "", { "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", "debug": "^4.3.7", "error-stack-parser-es": "^0.1.5", "fs-extra": "^11.2.0", "open": "^10.1.0", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "sirv": "^3.0.0" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" } }, "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A=="], + + "vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@7.7.2", "", { "dependencies": { "@vue/devtools-core": "^7.7.2", "@vue/devtools-kit": "^7.7.2", "@vue/devtools-shared": "^7.7.2", "execa": "^9.5.1", "sirv": "^3.0.0", "vite-plugin-inspect": "0.8.9", "vite-plugin-vue-inspector": "^5.3.1" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" } }, "sha512-5V0UijQWiSBj32blkyPEqIbzc6HO9c1bwnBhx+ay2dzU0FakH+qMdNUT8nF9BvDE+i6I1U8CqCuJiO20vKEdQw=="], + + "vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.1", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" } }, "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A=="], + + "vitest": ["vitest@3.0.8", "", { "dependencies": { "@vitest/expect": "3.0.8", "@vitest/mocker": "3.0.8", "@vitest/pretty-format": "^3.0.8", "@vitest/runner": "3.0.8", "@vitest/snapshot": "3.0.8", "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.8", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.8", "@vitest/ui": "3.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA=="], + + "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vue": ["vue@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ=="], + + "vue-component-type-helpers": ["vue-component-type-helpers@2.2.8", "", {}, "sha512-4bjIsC284coDO9om4HPA62M7wfsTvcmZyzdfR0aUlFXqq4tXxM1APyXpNVxPC8QazKw9OhmZNHBVDA6ODaZsrA=="], + + "vue-eslint-parser": ["vue-eslint-parser@10.1.1", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "lodash": "^4.17.21", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-bh2Z/Au5slro9QJ3neFYLanZtb1jH+W2bKqGHXAoYD4vZgNG3KeotL7JpPv5xzY4UXUXJl7TrIsnzECH63kd3Q=="], + + "vue-router": ["vue-router@4.5.0", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w=="], + + "vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.1.1", "", { "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ=="], + + "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], + + "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "@vue/devtools-core/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="], + + "@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "editorconfig/minimatch": ["minimatch@9.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "jsdom/xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "npm-run-all2/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], + + "w3c-xmlserializer/xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "editorconfig/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "npm-run-all2/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/webapp/embed.go b/webapp/embed.go new file mode 100644 index 000000000..d57aacd76 --- /dev/null +++ b/webapp/embed.go @@ -0,0 +1,11 @@ +package webapp + +import ( + "embed" +) + +//go:embed dist/index.html +var Templates embed.FS + +//go:embed dist/assets dist/*.ico +var Assets embed.FS diff --git a/webapp/env.d.ts b/webapp/env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/webapp/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/webapp/eslint.config.ts b/webapp/eslint.config.ts new file mode 100644 index 000000000..84b2cefa0 --- /dev/null +++ b/webapp/eslint.config.ts @@ -0,0 +1,30 @@ +import pluginVue from 'eslint-plugin-vue' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import pluginVitest from '@vitest/eslint-plugin' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +// To allow more languages other than `ts` in `.vue` files, uncomment the following lines: +// import { configureVueProject } from '@vue/eslint-config-typescript' +// configureVueProject({ scriptLangs: ['ts', 'tsx'] }) +// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup + +export default defineConfigWithVueTs( + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + + { + name: 'app/files-to-ignore', + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], + }, + + pluginVue.configs['flat/essential'], + vueTsConfigs.recommended, + + { + ...pluginVitest.configs.recommended, + files: ['src/**/__tests__/*'], + }, + skipFormatting, +) diff --git a/webapp/index.html b/webapp/index.html new file mode 100644 index 000000000..1c5814f3c --- /dev/null +++ b/webapp/index.html @@ -0,0 +1,14 @@ + + + + + + + Shiori - Simple Bookmark Manager + + + +
+ + + diff --git a/webapp/package.json b/webapp/package.json new file mode 100644 index 000000000..92a3956ef --- /dev/null +++ b/webapp/package.json @@ -0,0 +1,47 @@ +{ + "name": "shiori", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "test:unit": "vitest", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "eslint . --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "@tailwindcss/vite": "^4.0.14", + "@vueuse/core": "^13.0.0", + "pinia": "^3.0.1", + "vue": "^3.5.13", + "vue-router": "4" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.0", + "@types/jsdom": "^21.1.7", + "@types/node": "^22.13.9", + "@types/vue-router": "^2.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "@vitest/eslint-plugin": "^1.1.36", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.5.0", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.7.0", + "eslint": "^9.21.0", + "eslint-plugin-vue": "~10.0.0", + "jiti": "^2.4.2", + "jsdom": "^26.0.0", + "npm-run-all2": "^7.0.2", + "prettier": "3.5.3", + "tailwindcss": "^4.0.14", + "typescript": "~5.8.0", + "vite": "^6.2.1", + "vite-plugin-vue-devtools": "^7.7.2", + "vitest": "^3.0.8", + "vue-tsc": "^2.2.8" + } +} diff --git a/webapp/public/favicon.ico b/webapp/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/webapp/src/App.vue b/webapp/src/App.vue new file mode 100644 index 000000000..8921eec3b --- /dev/null +++ b/webapp/src/App.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/webapp/src/assets/main.css b/webapp/src/assets/main.css new file mode 100644 index 000000000..40cddb39e --- /dev/null +++ b/webapp/src/assets/main.css @@ -0,0 +1,23 @@ +@import "tailwindcss"; + +/* Custom styles */ +:root { + --primary-color: #f44336; + --secondary-color: #ffffff; + --text-color: #333333; + --background-color: #f5f5f5; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: var(--text-color); + background-color: var(--background-color); + min-height: 100vh; + margin: 0; + padding: 0; +} + +#app { + width: 100%; + min-height: 100vh; +} diff --git a/webapp/src/components/HelloWorld.vue b/webapp/src/components/HelloWorld.vue new file mode 100644 index 000000000..d174cf8e1 --- /dev/null +++ b/webapp/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/webapp/src/components/TheWelcome.vue b/webapp/src/components/TheWelcome.vue new file mode 100644 index 000000000..ae6eec3bd --- /dev/null +++ b/webapp/src/components/TheWelcome.vue @@ -0,0 +1,94 @@ + + + diff --git a/webapp/src/components/WelcomeItem.vue b/webapp/src/components/WelcomeItem.vue new file mode 100644 index 000000000..6d7086aea --- /dev/null +++ b/webapp/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/webapp/src/components/__tests__/HelloWorld.spec.ts b/webapp/src/components/__tests__/HelloWorld.spec.ts new file mode 100644 index 000000000..253320200 --- /dev/null +++ b/webapp/src/components/__tests__/HelloWorld.spec.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest' + +import { mount } from '@vue/test-utils' +import HelloWorld from '../HelloWorld.vue' + +describe('HelloWorld', () => { + it('renders properly', () => { + const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) + expect(wrapper.text()).toContain('Hello Vitest') + }) +}) diff --git a/webapp/src/components/icons/IconCommunity.vue b/webapp/src/components/icons/IconCommunity.vue new file mode 100644 index 000000000..2dc8b0552 --- /dev/null +++ b/webapp/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/webapp/src/components/icons/IconDocumentation.vue b/webapp/src/components/icons/IconDocumentation.vue new file mode 100644 index 000000000..6d4791cfb --- /dev/null +++ b/webapp/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/webapp/src/components/icons/IconEcosystem.vue b/webapp/src/components/icons/IconEcosystem.vue new file mode 100644 index 000000000..c3a4f078c --- /dev/null +++ b/webapp/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/webapp/src/components/icons/IconSupport.vue b/webapp/src/components/icons/IconSupport.vue new file mode 100644 index 000000000..7452834d3 --- /dev/null +++ b/webapp/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/webapp/src/components/icons/IconTooling.vue b/webapp/src/components/icons/IconTooling.vue new file mode 100644 index 000000000..660598d7c --- /dev/null +++ b/webapp/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/webapp/src/components/layout/AppLayout.vue b/webapp/src/components/layout/AppLayout.vue new file mode 100644 index 000000000..fd39860d0 --- /dev/null +++ b/webapp/src/components/layout/AppLayout.vue @@ -0,0 +1,48 @@ + + + diff --git a/webapp/src/components/layout/Sidebar.vue b/webapp/src/components/layout/Sidebar.vue new file mode 100644 index 000000000..ced98234f --- /dev/null +++ b/webapp/src/components/layout/Sidebar.vue @@ -0,0 +1,80 @@ + + + diff --git a/webapp/src/components/layout/TopBar.vue b/webapp/src/components/layout/TopBar.vue new file mode 100644 index 000000000..4f2699c26 --- /dev/null +++ b/webapp/src/components/layout/TopBar.vue @@ -0,0 +1,45 @@ + + + diff --git a/webapp/src/main.ts b/webapp/src/main.ts new file mode 100644 index 000000000..5dcad83c3 --- /dev/null +++ b/webapp/src/main.ts @@ -0,0 +1,14 @@ +import './assets/main.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts new file mode 100644 index 000000000..e00478b2c --- /dev/null +++ b/webapp/src/router/index.ts @@ -0,0 +1,71 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw, NavigationGuardNext as NavigationGuard, RouteLocationNormalized } from 'vue-router' +import HomeView from '../views/HomeView.vue' +import LoginView from '../views/LoginView.vue' + +// Simple auth guard +const isAuthenticated = (): boolean => { + // For now, just check if there's a token in localStorage + return !!localStorage.getItem('token') +} + +const routes: Array = [ + { + path: '/', + redirect: '/login' + }, + { + path: '/home', + name: 'home', + component: HomeView, + meta: { requiresAuth: true } + }, + { + path: '/login', + name: 'login', + component: LoginView + }, + { + path: '/tags', + name: 'tags', + component: () => import('../views/TagsView.vue'), + meta: { requiresAuth: true } + }, + { + path: '/archive', + name: 'archive', + component: () => import('../views/ArchiveView.vue'), + meta: { requiresAuth: true } + }, + { + path: '/settings', + name: 'settings', + component: () => import('../views/SettingsView.vue'), + meta: { requiresAuth: true } + }, + // Redirect any unmatched routes to login + { + path: '/:pathMatch(.*)*', + redirect: '/login' + } +] + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes +}) + +// Navigation guard +router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next) => { + if (to.matched.some((record: any) => record.meta.requiresAuth)) { + if (!isAuthenticated()) { + next({ name: 'login' }) + } else { + next() + } + } else { + next() + } +}) + +export default router diff --git a/webapp/src/stores/counter.ts b/webapp/src/stores/counter.ts new file mode 100644 index 000000000..b6757ba57 --- /dev/null +++ b/webapp/src/stores/counter.ts @@ -0,0 +1,12 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/webapp/src/views/AboutView.vue b/webapp/src/views/AboutView.vue new file mode 100644 index 000000000..756ad2a17 --- /dev/null +++ b/webapp/src/views/AboutView.vue @@ -0,0 +1,15 @@ + + + diff --git a/webapp/src/views/ArchiveView.vue b/webapp/src/views/ArchiveView.vue new file mode 100644 index 000000000..76ef8a9ef --- /dev/null +++ b/webapp/src/views/ArchiveView.vue @@ -0,0 +1,17 @@ + + + diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue new file mode 100644 index 000000000..bb03d3938 --- /dev/null +++ b/webapp/src/views/HomeView.vue @@ -0,0 +1,66 @@ + + + diff --git a/webapp/src/views/LoginView.vue b/webapp/src/views/LoginView.vue new file mode 100644 index 000000000..188cdef10 --- /dev/null +++ b/webapp/src/views/LoginView.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/webapp/src/views/SettingsView.vue b/webapp/src/views/SettingsView.vue new file mode 100644 index 000000000..ac961451e --- /dev/null +++ b/webapp/src/views/SettingsView.vue @@ -0,0 +1,17 @@ + + + diff --git a/webapp/src/views/TagsView.vue b/webapp/src/views/TagsView.vue new file mode 100644 index 000000000..a16ae7a43 --- /dev/null +++ b/webapp/src/views/TagsView.vue @@ -0,0 +1,17 @@ + + + diff --git a/webapp/tailwind.config.js b/webapp/tailwind.config.js new file mode 100644 index 000000000..966847fcb --- /dev/null +++ b/webapp/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + primary: '#f44336', + }, + }, + }, + plugins: [], +} diff --git a/webapp/tsconfig.app.json b/webapp/tsconfig.app.json new file mode 100644 index 000000000..913b8f279 --- /dev/null +++ b/webapp/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/webapp/tsconfig.json b/webapp/tsconfig.json new file mode 100644 index 000000000..100cf6a8f --- /dev/null +++ b/webapp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ] +} diff --git a/webapp/tsconfig.node.json b/webapp/tsconfig.node.json new file mode 100644 index 000000000..a83dfc9d4 --- /dev/null +++ b/webapp/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/webapp/tsconfig.vitest.json b/webapp/tsconfig.vitest.json new file mode 100644 index 000000000..7d1d8cef3 --- /dev/null +++ b/webapp/tsconfig.vitest.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "include": ["src/**/__tests__/*", "env.d.ts"], + "exclude": [], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + + "lib": [], + "types": ["node", "jsdom"] + } +} diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts new file mode 100644 index 000000000..7985e9dfe --- /dev/null +++ b/webapp/vite.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + tailwindcss(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, + css: { + devSourcemap: true, + }, +}) diff --git a/webapp/vitest.config.ts b/webapp/vitest.config.ts new file mode 100644 index 000000000..c32871718 --- /dev/null +++ b/webapp/vitest.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' +import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: 'jsdom', + exclude: [...configDefaults.exclude, 'e2e/**'], + root: fileURLToPath(new URL('./', import.meta.url)), + }, + }), +) From 9dd690989c3b52ce964057e30e1f42e4608a5b93 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 10:10:22 +0100 Subject: [PATCH 08/19] chore: updated swagger --- docs/swagger/docs.go | 23 +++++++++++++++++++++++ docs/swagger/swagger.json | 28 ++++++++++++++++++++++++++-- docs/swagger/swagger.yaml | 16 ++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 476d918da..10539de62 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -393,6 +393,15 @@ const docTemplate = `{ "Auth" ], "summary": "Get tags for a bookmark.", + "parameters": [ + { + "type": "integer", + "description": "Bookmark ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -420,6 +429,13 @@ const docTemplate = `{ ], "summary": "Add a tag to a bookmark.", "parameters": [ + { + "type": "integer", + "description": "Bookmark ID", + "name": "id", + "in": "path", + "required": true + }, { "description": "Add Tag Payload", "name": "payload", @@ -451,6 +467,13 @@ const docTemplate = `{ ], "summary": "Remove a tag from a bookmark.", "parameters": [ + { + "type": "integer", + "description": "Bookmark ID", + "name": "id", + "in": "path", + "required": true + }, { "description": "Remove Tag Payload", "name": "payload", diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 27695018f..bf33d3f2d 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -1,7 +1,8 @@ { "swagger": "2.0", "info": { - "contact": {} + "contact": {}, + "title": "Shiori API" }, "paths": { "/api/v1/accounts": { @@ -382,6 +383,15 @@ "Auth" ], "summary": "Get tags for a bookmark.", + "parameters": [ + { + "type": "integer", + "description": "Bookmark ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -409,6 +419,13 @@ ], "summary": "Add a tag to a bookmark.", "parameters": [ + { + "type": "integer", + "description": "Bookmark ID", + "name": "id", + "in": "path", + "required": true + }, { "description": "Add Tag Payload", "name": "payload", @@ -440,6 +457,13 @@ ], "summary": "Remove a tag from a bookmark.", "parameters": [ + { + "type": "integer", + "description": "Bookmark ID", + "name": "id", + "in": "path", + "required": true + }, { "description": "Remove Tag Payload", "name": "payload", @@ -987,4 +1011,4 @@ } } } -} \ No newline at end of file +} diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 0c7d7592e..1d732deac 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -370,6 +370,11 @@ paths: /api/v1/bookmarks/{id}/tags: delete: parameters: + - description: Bookmark ID + in: path + name: id + required: true + type: integer - description: Remove Tag Payload in: body name: payload @@ -389,6 +394,12 @@ paths: tags: - Auth get: + parameters: + - description: Bookmark ID + in: path + name: id + required: true + type: integer produces: - application/json responses: @@ -407,6 +418,11 @@ paths: - Auth post: parameters: + - description: Bookmark ID + in: path + name: id + required: true + type: integer - description: Add Tag Payload in: body name: payload From 6ceb77f8ac197c17630f618a7feb67a4c204f376 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 10:10:29 +0100 Subject: [PATCH 09/19] fix: route params missing --- internal/http/handlers/api/v1/bookmarks.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/http/handlers/api/v1/bookmarks.go b/internal/http/handlers/api/v1/bookmarks.go index 437039f73..11f0674e1 100644 --- a/internal/http/handlers/api/v1/bookmarks.go +++ b/internal/http/handlers/api/v1/bookmarks.go @@ -183,6 +183,7 @@ func (p *bulkUpdateBookmarkTagsPayload) IsValid() error { // @Tags Auth // @securityDefinitions.apikey ApiKeyAuth // @Produce json +// @Param id path int true "Bookmark ID" // @Success 200 {array} model.TagDTO // @Failure 403 {object} nil "Token not provided/invalid" // @Failure 404 {object} nil "Bookmark not found" @@ -239,6 +240,7 @@ func (p *bookmarkTagPayload) IsValid() error { // @Summary Add a tag to a bookmark. // @Tags Auth // @securityDefinitions.apikey ApiKeyAuth +// @Param id path int true "Bookmark ID" // @Param payload body bookmarkTagPayload true "Add Tag Payload" // @Produce json // @Success 200 {object} nil @@ -292,6 +294,7 @@ func HandleAddTagToBookmark(deps model.Dependencies, c model.WebContext) { // @Summary Remove a tag from a bookmark. // @Tags Auth // @securityDefinitions.apikey ApiKeyAuth +// @Param id path int true "Bookmark ID" // @Param payload body bookmarkTagPayload true "Remove Tag Payload" // @Produce json // @Success 200 {object} nil From 265003cb9c3aa3db7fc1085af7b2427cf803c289 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 10:10:44 +0100 Subject: [PATCH 10/19] feat: added cors middleware --- internal/http/middleware/cors.go | 29 +++++++++++++++++++++++++++++ internal/http/server.go | 1 + 2 files changed, 30 insertions(+) create mode 100644 internal/http/middleware/cors.go diff --git a/internal/http/middleware/cors.go b/internal/http/middleware/cors.go new file mode 100644 index 000000000..43de5ba41 --- /dev/null +++ b/internal/http/middleware/cors.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "strings" + + "github.com/go-shiori/shiori/internal/model" +) + +type CORSMiddleware struct { + allowedOrigins []string +} + +func (m *CORSMiddleware) OnRequest(deps model.Dependencies, c model.WebContext) error { + c.ResponseWriter().Header().Set("Access-Control-Allow-Origin", strings.Join(m.allowedOrigins, ", ")) + c.ResponseWriter().Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.ResponseWriter().Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + return nil +} + +func (m *CORSMiddleware) OnResponse(deps model.Dependencies, c model.WebContext) error { + c.ResponseWriter().Header().Set("Access-Control-Allow-Origin", strings.Join(m.allowedOrigins, ", ")) + c.ResponseWriter().Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.ResponseWriter().Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + return nil +} + +func NewCORSMiddleware(allowedOrigins []string) *CORSMiddleware { + return &CORSMiddleware{allowedOrigins: allowedOrigins} +} diff --git a/internal/http/server.go b/internal/http/server.go index 11700fec7..21f95b0a4 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -34,6 +34,7 @@ func (s *HttpServer) Setup(cfg *config.Config, deps *dependencies.Dependencies) globalMiddleware := []model.HttpMiddleware{ middleware.NewAuthMiddleware(deps), middleware.NewRequestIDMiddleware(deps), + middleware.NewCORSMiddleware([]string{"*"}), } if cfg.Http.AccessLog { From e3e3ef5e5aa1dc0794c36fd0993100f9e4e1da8e Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 10:11:04 +0100 Subject: [PATCH 11/19] feat: built api implementation --- webapp/src/client/.openapi-generator-ignore | 23 + webapp/src/client/.openapi-generator/FILES | 23 + webapp/src/client/.openapi-generator/VERSION | 1 + webapp/src/client/apis/AccountsApi.ts | 171 +++++++ webapp/src/client/apis/AuthApi.ts | 437 ++++++++++++++++++ webapp/src/client/apis/SystemApi.ts | 58 +++ webapp/src/client/apis/TagsApi.ts | 245 ++++++++++ webapp/src/client/apis/index.ts | 6 + webapp/src/client/index.ts | 5 + .../client/models/ApiV1BookmarkTagPayload.ts | 66 +++ .../ApiV1BulkUpdateBookmarkTagsPayload.ts | 75 +++ webapp/src/client/models/ApiV1InfoResponse.ts | 89 ++++ .../client/models/ApiV1InfoResponseVersion.ts | 81 ++++ .../client/models/ApiV1LoginRequestPayload.ts | 81 ++++ .../models/ApiV1LoginResponseMessage.ts | 73 +++ .../models/ApiV1ReadableResponseMessage.ts | 73 +++ .../models/ApiV1UpdateAccountPayload.ts | 105 +++++ .../client/models/ApiV1UpdateCachePayload.ts | 98 ++++ webapp/src/client/models/ModelAccount.ts | 105 +++++ webapp/src/client/models/ModelAccountDTO.ts | 105 +++++ webapp/src/client/models/ModelBookmarkDTO.ts | 193 ++++++++ webapp/src/client/models/ModelTagDTO.ts | 89 ++++ webapp/src/client/models/ModelUserConfig.ts | 129 ++++++ webapp/src/client/models/index.ts | 16 + webapp/src/client/runtime.ts | 431 +++++++++++++++++ 25 files changed, 2778 insertions(+) create mode 100644 webapp/src/client/.openapi-generator-ignore create mode 100644 webapp/src/client/.openapi-generator/FILES create mode 100644 webapp/src/client/.openapi-generator/VERSION create mode 100644 webapp/src/client/apis/AccountsApi.ts create mode 100644 webapp/src/client/apis/AuthApi.ts create mode 100644 webapp/src/client/apis/SystemApi.ts create mode 100644 webapp/src/client/apis/TagsApi.ts create mode 100644 webapp/src/client/apis/index.ts create mode 100644 webapp/src/client/index.ts create mode 100644 webapp/src/client/models/ApiV1BookmarkTagPayload.ts create mode 100644 webapp/src/client/models/ApiV1BulkUpdateBookmarkTagsPayload.ts create mode 100644 webapp/src/client/models/ApiV1InfoResponse.ts create mode 100644 webapp/src/client/models/ApiV1InfoResponseVersion.ts create mode 100644 webapp/src/client/models/ApiV1LoginRequestPayload.ts create mode 100644 webapp/src/client/models/ApiV1LoginResponseMessage.ts create mode 100644 webapp/src/client/models/ApiV1ReadableResponseMessage.ts create mode 100644 webapp/src/client/models/ApiV1UpdateAccountPayload.ts create mode 100644 webapp/src/client/models/ApiV1UpdateCachePayload.ts create mode 100644 webapp/src/client/models/ModelAccount.ts create mode 100644 webapp/src/client/models/ModelAccountDTO.ts create mode 100644 webapp/src/client/models/ModelBookmarkDTO.ts create mode 100644 webapp/src/client/models/ModelTagDTO.ts create mode 100644 webapp/src/client/models/ModelUserConfig.ts create mode 100644 webapp/src/client/models/index.ts create mode 100644 webapp/src/client/runtime.ts diff --git a/webapp/src/client/.openapi-generator-ignore b/webapp/src/client/.openapi-generator-ignore new file mode 100644 index 000000000..7484ee590 --- /dev/null +++ b/webapp/src/client/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/webapp/src/client/.openapi-generator/FILES b/webapp/src/client/.openapi-generator/FILES new file mode 100644 index 000000000..63019d02a --- /dev/null +++ b/webapp/src/client/.openapi-generator/FILES @@ -0,0 +1,23 @@ +.openapi-generator-ignore +apis/AccountsApi.ts +apis/AuthApi.ts +apis/SystemApi.ts +apis/TagsApi.ts +apis/index.ts +index.ts +models/ApiV1BookmarkTagPayload.ts +models/ApiV1BulkUpdateBookmarkTagsPayload.ts +models/ApiV1InfoResponse.ts +models/ApiV1InfoResponseVersion.ts +models/ApiV1LoginRequestPayload.ts +models/ApiV1LoginResponseMessage.ts +models/ApiV1ReadableResponseMessage.ts +models/ApiV1UpdateAccountPayload.ts +models/ApiV1UpdateCachePayload.ts +models/ModelAccount.ts +models/ModelAccountDTO.ts +models/ModelBookmarkDTO.ts +models/ModelTagDTO.ts +models/ModelUserConfig.ts +models/index.ts +runtime.ts diff --git a/webapp/src/client/.openapi-generator/VERSION b/webapp/src/client/.openapi-generator/VERSION new file mode 100644 index 000000000..5f84a81db --- /dev/null +++ b/webapp/src/client/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.12.0 diff --git a/webapp/src/client/apis/AccountsApi.ts b/webapp/src/client/apis/AccountsApi.ts new file mode 100644 index 000000000..77fec6a0e --- /dev/null +++ b/webapp/src/client/apis/AccountsApi.ts @@ -0,0 +1,171 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + ApiV1UpdateAccountPayload, + ModelAccountDTO, +} from '../models/index'; +import { + ApiV1UpdateAccountPayloadFromJSON, + ApiV1UpdateAccountPayloadToJSON, + ModelAccountDTOFromJSON, + ModelAccountDTOToJSON, +} from '../models/index'; + +export interface ApiV1AccountsIdDeleteRequest { + id: number; +} + +export interface ApiV1AccountsIdPatchRequest { + id: number; + account: ApiV1UpdateAccountPayload; +} + +/** + * + */ +export class AccountsApi extends runtime.BaseAPI { + + /** + * List accounts + * List accounts + */ + async apiV1AccountsGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/accounts`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(ModelAccountDTOFromJSON)); + } + + /** + * List accounts + * List accounts + */ + async apiV1AccountsGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.apiV1AccountsGetRaw(initOverrides); + return await response.value(); + } + + /** + * Delete an account + */ + async apiV1AccountsIdDeleteRaw(requestParameters: ApiV1AccountsIdDeleteRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1AccountsIdDelete().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/accounts/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Delete an account + */ + async apiV1AccountsIdDelete(requestParameters: ApiV1AccountsIdDeleteRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiV1AccountsIdDeleteRaw(requestParameters, initOverrides); + } + + /** + * Update an account + */ + async apiV1AccountsIdPatchRaw(requestParameters: ApiV1AccountsIdPatchRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1AccountsIdPatch().' + ); + } + + if (requestParameters['account'] == null) { + throw new runtime.RequiredError( + 'account', + 'Required parameter "account" was null or undefined when calling apiV1AccountsIdPatch().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/accounts/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: ApiV1UpdateAccountPayloadToJSON(requestParameters['account']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelAccountDTOFromJSON(jsonValue)); + } + + /** + * Update an account + */ + async apiV1AccountsIdPatch(requestParameters: ApiV1AccountsIdPatchRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1AccountsIdPatchRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Create an account + */ + async apiV1AccountsPostRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/accounts`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelAccountDTOFromJSON(jsonValue)); + } + + /** + * Create an account + */ + async apiV1AccountsPost(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1AccountsPostRaw(initOverrides); + return await response.value(); + } + +} diff --git a/webapp/src/client/apis/AuthApi.ts b/webapp/src/client/apis/AuthApi.ts new file mode 100644 index 000000000..ac956b6b1 --- /dev/null +++ b/webapp/src/client/apis/AuthApi.ts @@ -0,0 +1,437 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + ApiV1BookmarkTagPayload, + ApiV1BulkUpdateBookmarkTagsPayload, + ApiV1LoginRequestPayload, + ApiV1LoginResponseMessage, + ApiV1ReadableResponseMessage, + ApiV1UpdateAccountPayload, + ApiV1UpdateCachePayload, + ModelAccount, + ModelBookmarkDTO, + ModelTagDTO, +} from '../models/index'; +import { + ApiV1BookmarkTagPayloadFromJSON, + ApiV1BookmarkTagPayloadToJSON, + ApiV1BulkUpdateBookmarkTagsPayloadFromJSON, + ApiV1BulkUpdateBookmarkTagsPayloadToJSON, + ApiV1LoginRequestPayloadFromJSON, + ApiV1LoginRequestPayloadToJSON, + ApiV1LoginResponseMessageFromJSON, + ApiV1LoginResponseMessageToJSON, + ApiV1ReadableResponseMessageFromJSON, + ApiV1ReadableResponseMessageToJSON, + ApiV1UpdateAccountPayloadFromJSON, + ApiV1UpdateAccountPayloadToJSON, + ApiV1UpdateCachePayloadFromJSON, + ApiV1UpdateCachePayloadToJSON, + ModelAccountFromJSON, + ModelAccountToJSON, + ModelBookmarkDTOFromJSON, + ModelBookmarkDTOToJSON, + ModelTagDTOFromJSON, + ModelTagDTOToJSON, +} from '../models/index'; + +export interface ApiV1AuthAccountPatchRequest { + payload?: ApiV1UpdateAccountPayload; +} + +export interface ApiV1AuthLoginPostRequest { + payload?: ApiV1LoginRequestPayload; +} + +export interface ApiV1BookmarksBulkTagsPutRequest { + payload: ApiV1BulkUpdateBookmarkTagsPayload; +} + +export interface ApiV1BookmarksCachePutRequest { + payload: ApiV1UpdateCachePayload; +} + +export interface ApiV1BookmarksIdTagsDeleteRequest { + id: number; + payload: ApiV1BookmarkTagPayload; +} + +export interface ApiV1BookmarksIdTagsGetRequest { + id: number; +} + +export interface ApiV1BookmarksIdTagsPostRequest { + id: number; + payload: ApiV1BookmarkTagPayload; +} + +/** + * + */ +export class AuthApi extends runtime.BaseAPI { + + /** + * Update account information + */ + async apiV1AuthAccountPatchRaw(requestParameters: ApiV1AuthAccountPatchRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/auth/account`, + method: 'PATCH', + headers: headerParameters, + query: queryParameters, + body: ApiV1UpdateAccountPayloadToJSON(requestParameters['payload']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelAccountFromJSON(jsonValue)); + } + + /** + * Update account information + */ + async apiV1AuthAccountPatch(requestParameters: ApiV1AuthAccountPatchRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1AuthAccountPatchRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Login to an account using username and password + */ + async apiV1AuthLoginPostRaw(requestParameters: ApiV1AuthLoginPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/auth/login`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: ApiV1LoginRequestPayloadToJSON(requestParameters['payload']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ApiV1LoginResponseMessageFromJSON(jsonValue)); + } + + /** + * Login to an account using username and password + */ + async apiV1AuthLoginPost(requestParameters: ApiV1AuthLoginPostRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1AuthLoginPostRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Logout from the current session + */ + async apiV1AuthLogoutPostRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/auth/logout`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Logout from the current session + */ + async apiV1AuthLogoutPost(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiV1AuthLogoutPostRaw(initOverrides); + } + + /** + * Get information for the current logged in user + */ + async apiV1AuthMeGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/auth/me`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelAccountFromJSON(jsonValue)); + } + + /** + * Get information for the current logged in user + */ + async apiV1AuthMeGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1AuthMeGetRaw(initOverrides); + return await response.value(); + } + + /** + * Refresh a token for an account + */ + async apiV1AuthRefreshPostRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/auth/refresh`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ApiV1LoginResponseMessageFromJSON(jsonValue)); + } + + /** + * Refresh a token for an account + */ + async apiV1AuthRefreshPost(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1AuthRefreshPostRaw(initOverrides); + return await response.value(); + } + + /** + * Bulk update tags for multiple bookmarks. + */ + async apiV1BookmarksBulkTagsPutRaw(requestParameters: ApiV1BookmarksBulkTagsPutRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + if (requestParameters['payload'] == null) { + throw new runtime.RequiredError( + 'payload', + 'Required parameter "payload" was null or undefined when calling apiV1BookmarksBulkTagsPut().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/bookmarks/bulk/tags`, + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: ApiV1BulkUpdateBookmarkTagsPayloadToJSON(requestParameters['payload']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(ModelBookmarkDTOFromJSON)); + } + + /** + * Bulk update tags for multiple bookmarks. + */ + async apiV1BookmarksBulkTagsPut(requestParameters: ApiV1BookmarksBulkTagsPutRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.apiV1BookmarksBulkTagsPutRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Update Cache and Ebook on server. + */ + async apiV1BookmarksCachePutRaw(requestParameters: ApiV1BookmarksCachePutRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['payload'] == null) { + throw new runtime.RequiredError( + 'payload', + 'Required parameter "payload" was null or undefined when calling apiV1BookmarksCachePut().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/bookmarks/cache`, + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: ApiV1UpdateCachePayloadToJSON(requestParameters['payload']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelBookmarkDTOFromJSON(jsonValue)); + } + + /** + * Update Cache and Ebook on server. + */ + async apiV1BookmarksCachePut(requestParameters: ApiV1BookmarksCachePutRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1BookmarksCachePutRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Get readable version of bookmark. + */ + async apiV1BookmarksIdReadableGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/bookmarks/id/readable`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ApiV1ReadableResponseMessageFromJSON(jsonValue)); + } + + /** + * Get readable version of bookmark. + */ + async apiV1BookmarksIdReadableGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1BookmarksIdReadableGetRaw(initOverrides); + return await response.value(); + } + + /** + * Remove a tag from a bookmark. + */ + async apiV1BookmarksIdTagsDeleteRaw(requestParameters: ApiV1BookmarksIdTagsDeleteRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1BookmarksIdTagsDelete().' + ); + } + + if (requestParameters['payload'] == null) { + throw new runtime.RequiredError( + 'payload', + 'Required parameter "payload" was null or undefined when calling apiV1BookmarksIdTagsDelete().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/bookmarks/{id}/tags`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + body: ApiV1BookmarkTagPayloadToJSON(requestParameters['payload']), + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Remove a tag from a bookmark. + */ + async apiV1BookmarksIdTagsDelete(requestParameters: ApiV1BookmarksIdTagsDeleteRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiV1BookmarksIdTagsDeleteRaw(requestParameters, initOverrides); + } + + /** + * Get tags for a bookmark. + */ + async apiV1BookmarksIdTagsGetRaw(requestParameters: ApiV1BookmarksIdTagsGetRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1BookmarksIdTagsGet().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/bookmarks/{id}/tags`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(ModelTagDTOFromJSON)); + } + + /** + * Get tags for a bookmark. + */ + async apiV1BookmarksIdTagsGet(requestParameters: ApiV1BookmarksIdTagsGetRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.apiV1BookmarksIdTagsGetRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Add a tag to a bookmark. + */ + async apiV1BookmarksIdTagsPostRaw(requestParameters: ApiV1BookmarksIdTagsPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1BookmarksIdTagsPost().' + ); + } + + if (requestParameters['payload'] == null) { + throw new runtime.RequiredError( + 'payload', + 'Required parameter "payload" was null or undefined when calling apiV1BookmarksIdTagsPost().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/bookmarks/{id}/tags`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: ApiV1BookmarkTagPayloadToJSON(requestParameters['payload']), + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Add a tag to a bookmark. + */ + async apiV1BookmarksIdTagsPost(requestParameters: ApiV1BookmarksIdTagsPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiV1BookmarksIdTagsPostRaw(requestParameters, initOverrides); + } + +} diff --git a/webapp/src/client/apis/SystemApi.ts b/webapp/src/client/apis/SystemApi.ts new file mode 100644 index 000000000..a727b8d94 --- /dev/null +++ b/webapp/src/client/apis/SystemApi.ts @@ -0,0 +1,58 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + ApiV1InfoResponse, +} from '../models/index'; +import { + ApiV1InfoResponseFromJSON, + ApiV1InfoResponseToJSON, +} from '../models/index'; + +/** + * + */ +export class SystemApi extends runtime.BaseAPI { + + /** + * Get general system information like Shiori version, database, and OS + * Get general system information + */ + async apiV1SystemInfoGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/system/info`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ApiV1InfoResponseFromJSON(jsonValue)); + } + + /** + * Get general system information like Shiori version, database, and OS + * Get general system information + */ + async apiV1SystemInfoGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1SystemInfoGetRaw(initOverrides); + return await response.value(); + } + +} diff --git a/webapp/src/client/apis/TagsApi.ts b/webapp/src/client/apis/TagsApi.ts new file mode 100644 index 000000000..b1ecf541e --- /dev/null +++ b/webapp/src/client/apis/TagsApi.ts @@ -0,0 +1,245 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + ModelTagDTO, +} from '../models/index'; +import { + ModelTagDTOFromJSON, + ModelTagDTOToJSON, +} from '../models/index'; + +export interface ApiV1TagsGetRequest { + withBookmarkCount?: boolean; + bookmarkId?: number; + search?: string; +} + +export interface ApiV1TagsIdDeleteRequest { + id: number; +} + +export interface ApiV1TagsIdGetRequest { + id: number; +} + +export interface ApiV1TagsIdPutRequest { + id: number; + tag: ModelTagDTO; +} + +export interface ApiV1TagsPostRequest { + tag: ModelTagDTO; +} + +/** + * + */ +export class TagsApi extends runtime.BaseAPI { + + /** + * List all tags + * List tags + */ + async apiV1TagsGetRaw(requestParameters: ApiV1TagsGetRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const queryParameters: any = {}; + + if (requestParameters['withBookmarkCount'] != null) { + queryParameters['with_bookmark_count'] = requestParameters['withBookmarkCount']; + } + + if (requestParameters['bookmarkId'] != null) { + queryParameters['bookmark_id'] = requestParameters['bookmarkId']; + } + + if (requestParameters['search'] != null) { + queryParameters['search'] = requestParameters['search']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/tags`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(ModelTagDTOFromJSON)); + } + + /** + * List all tags + * List tags + */ + async apiV1TagsGet(requestParameters: ApiV1TagsGetRequest = {}, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.apiV1TagsGetRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Delete a tag + * Delete tag + */ + async apiV1TagsIdDeleteRaw(requestParameters: ApiV1TagsIdDeleteRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1TagsIdDelete().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/tags/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Delete a tag + * Delete tag + */ + async apiV1TagsIdDelete(requestParameters: ApiV1TagsIdDeleteRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.apiV1TagsIdDeleteRaw(requestParameters, initOverrides); + } + + /** + * Get a tag by ID + * Get tag + */ + async apiV1TagsIdGetRaw(requestParameters: ApiV1TagsIdGetRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1TagsIdGet().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/api/v1/tags/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelTagDTOFromJSON(jsonValue)); + } + + /** + * Get a tag by ID + * Get tag + */ + async apiV1TagsIdGet(requestParameters: ApiV1TagsIdGetRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1TagsIdGetRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Update an existing tag + * Update tag + */ + async apiV1TagsIdPutRaw(requestParameters: ApiV1TagsIdPutRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['id'] == null) { + throw new runtime.RequiredError( + 'id', + 'Required parameter "id" was null or undefined when calling apiV1TagsIdPut().' + ); + } + + if (requestParameters['tag'] == null) { + throw new runtime.RequiredError( + 'tag', + 'Required parameter "tag" was null or undefined when calling apiV1TagsIdPut().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/tags/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters['id']))), + method: 'PUT', + headers: headerParameters, + query: queryParameters, + body: ModelTagDTOToJSON(requestParameters['tag']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelTagDTOFromJSON(jsonValue)); + } + + /** + * Update an existing tag + * Update tag + */ + async apiV1TagsIdPut(requestParameters: ApiV1TagsIdPutRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1TagsIdPutRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Create a new tag + * Create tag + */ + async apiV1TagsPostRaw(requestParameters: ApiV1TagsPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['tag'] == null) { + throw new runtime.RequiredError( + 'tag', + 'Required parameter "tag" was null or undefined when calling apiV1TagsPost().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/api/v1/tags`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: ModelTagDTOToJSON(requestParameters['tag']), + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => ModelTagDTOFromJSON(jsonValue)); + } + + /** + * Create a new tag + * Create tag + */ + async apiV1TagsPost(requestParameters: ApiV1TagsPostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.apiV1TagsPostRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/webapp/src/client/apis/index.ts b/webapp/src/client/apis/index.ts new file mode 100644 index 000000000..9692ddb30 --- /dev/null +++ b/webapp/src/client/apis/index.ts @@ -0,0 +1,6 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './AccountsApi'; +export * from './AuthApi'; +export * from './SystemApi'; +export * from './TagsApi'; diff --git a/webapp/src/client/index.ts b/webapp/src/client/index.ts new file mode 100644 index 000000000..bebe8bbbe --- /dev/null +++ b/webapp/src/client/index.ts @@ -0,0 +1,5 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './runtime'; +export * from './apis/index'; +export * from './models/index'; diff --git a/webapp/src/client/models/ApiV1BookmarkTagPayload.ts b/webapp/src/client/models/ApiV1BookmarkTagPayload.ts new file mode 100644 index 000000000..95454e9b4 --- /dev/null +++ b/webapp/src/client/models/ApiV1BookmarkTagPayload.ts @@ -0,0 +1,66 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1BookmarkTagPayload + */ +export interface ApiV1BookmarkTagPayload { + /** + * + * @type {number} + * @memberof ApiV1BookmarkTagPayload + */ + tagId: number; +} + +/** + * Check if a given object implements the ApiV1BookmarkTagPayload interface. + */ +export function instanceOfApiV1BookmarkTagPayload(value: object): value is ApiV1BookmarkTagPayload { + if (!('tagId' in value) || value['tagId'] === undefined) return false; + return true; +} + +export function ApiV1BookmarkTagPayloadFromJSON(json: any): ApiV1BookmarkTagPayload { + return ApiV1BookmarkTagPayloadFromJSONTyped(json, false); +} + +export function ApiV1BookmarkTagPayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1BookmarkTagPayload { + if (json == null) { + return json; + } + return { + + 'tagId': json['tag_id'], + }; +} + +export function ApiV1BookmarkTagPayloadToJSON(json: any): ApiV1BookmarkTagPayload { + return ApiV1BookmarkTagPayloadToJSONTyped(json, false); +} + +export function ApiV1BookmarkTagPayloadToJSONTyped(value?: ApiV1BookmarkTagPayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'tag_id': value['tagId'], + }; +} + diff --git a/webapp/src/client/models/ApiV1BulkUpdateBookmarkTagsPayload.ts b/webapp/src/client/models/ApiV1BulkUpdateBookmarkTagsPayload.ts new file mode 100644 index 000000000..e182ca473 --- /dev/null +++ b/webapp/src/client/models/ApiV1BulkUpdateBookmarkTagsPayload.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1BulkUpdateBookmarkTagsPayload + */ +export interface ApiV1BulkUpdateBookmarkTagsPayload { + /** + * + * @type {Array} + * @memberof ApiV1BulkUpdateBookmarkTagsPayload + */ + bookmarkIds: Array; + /** + * + * @type {Array} + * @memberof ApiV1BulkUpdateBookmarkTagsPayload + */ + tagIds: Array; +} + +/** + * Check if a given object implements the ApiV1BulkUpdateBookmarkTagsPayload interface. + */ +export function instanceOfApiV1BulkUpdateBookmarkTagsPayload(value: object): value is ApiV1BulkUpdateBookmarkTagsPayload { + if (!('bookmarkIds' in value) || value['bookmarkIds'] === undefined) return false; + if (!('tagIds' in value) || value['tagIds'] === undefined) return false; + return true; +} + +export function ApiV1BulkUpdateBookmarkTagsPayloadFromJSON(json: any): ApiV1BulkUpdateBookmarkTagsPayload { + return ApiV1BulkUpdateBookmarkTagsPayloadFromJSONTyped(json, false); +} + +export function ApiV1BulkUpdateBookmarkTagsPayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1BulkUpdateBookmarkTagsPayload { + if (json == null) { + return json; + } + return { + + 'bookmarkIds': json['bookmark_ids'], + 'tagIds': json['tag_ids'], + }; +} + +export function ApiV1BulkUpdateBookmarkTagsPayloadToJSON(json: any): ApiV1BulkUpdateBookmarkTagsPayload { + return ApiV1BulkUpdateBookmarkTagsPayloadToJSONTyped(json, false); +} + +export function ApiV1BulkUpdateBookmarkTagsPayloadToJSONTyped(value?: ApiV1BulkUpdateBookmarkTagsPayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'bookmark_ids': value['bookmarkIds'], + 'tag_ids': value['tagIds'], + }; +} + diff --git a/webapp/src/client/models/ApiV1InfoResponse.ts b/webapp/src/client/models/ApiV1InfoResponse.ts new file mode 100644 index 000000000..73a1e54a2 --- /dev/null +++ b/webapp/src/client/models/ApiV1InfoResponse.ts @@ -0,0 +1,89 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ApiV1InfoResponseVersion } from './ApiV1InfoResponseVersion'; +import { + ApiV1InfoResponseVersionFromJSON, + ApiV1InfoResponseVersionFromJSONTyped, + ApiV1InfoResponseVersionToJSON, + ApiV1InfoResponseVersionToJSONTyped, +} from './ApiV1InfoResponseVersion'; + +/** + * + * @export + * @interface ApiV1InfoResponse + */ +export interface ApiV1InfoResponse { + /** + * + * @type {string} + * @memberof ApiV1InfoResponse + */ + database?: string; + /** + * + * @type {string} + * @memberof ApiV1InfoResponse + */ + os?: string; + /** + * + * @type {ApiV1InfoResponseVersion} + * @memberof ApiV1InfoResponse + */ + version?: ApiV1InfoResponseVersion; +} + +/** + * Check if a given object implements the ApiV1InfoResponse interface. + */ +export function instanceOfApiV1InfoResponse(value: object): value is ApiV1InfoResponse { + return true; +} + +export function ApiV1InfoResponseFromJSON(json: any): ApiV1InfoResponse { + return ApiV1InfoResponseFromJSONTyped(json, false); +} + +export function ApiV1InfoResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1InfoResponse { + if (json == null) { + return json; + } + return { + + 'database': json['database'] == null ? undefined : json['database'], + 'os': json['os'] == null ? undefined : json['os'], + 'version': json['version'] == null ? undefined : ApiV1InfoResponseVersionFromJSON(json['version']), + }; +} + +export function ApiV1InfoResponseToJSON(json: any): ApiV1InfoResponse { + return ApiV1InfoResponseToJSONTyped(json, false); +} + +export function ApiV1InfoResponseToJSONTyped(value?: ApiV1InfoResponse | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'database': value['database'], + 'os': value['os'], + 'version': ApiV1InfoResponseVersionToJSON(value['version']), + }; +} + diff --git a/webapp/src/client/models/ApiV1InfoResponseVersion.ts b/webapp/src/client/models/ApiV1InfoResponseVersion.ts new file mode 100644 index 000000000..b91ab7633 --- /dev/null +++ b/webapp/src/client/models/ApiV1InfoResponseVersion.ts @@ -0,0 +1,81 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1InfoResponseVersion + */ +export interface ApiV1InfoResponseVersion { + /** + * + * @type {string} + * @memberof ApiV1InfoResponseVersion + */ + commit?: string; + /** + * + * @type {string} + * @memberof ApiV1InfoResponseVersion + */ + date?: string; + /** + * + * @type {string} + * @memberof ApiV1InfoResponseVersion + */ + tag?: string; +} + +/** + * Check if a given object implements the ApiV1InfoResponseVersion interface. + */ +export function instanceOfApiV1InfoResponseVersion(value: object): value is ApiV1InfoResponseVersion { + return true; +} + +export function ApiV1InfoResponseVersionFromJSON(json: any): ApiV1InfoResponseVersion { + return ApiV1InfoResponseVersionFromJSONTyped(json, false); +} + +export function ApiV1InfoResponseVersionFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1InfoResponseVersion { + if (json == null) { + return json; + } + return { + + 'commit': json['commit'] == null ? undefined : json['commit'], + 'date': json['date'] == null ? undefined : json['date'], + 'tag': json['tag'] == null ? undefined : json['tag'], + }; +} + +export function ApiV1InfoResponseVersionToJSON(json: any): ApiV1InfoResponseVersion { + return ApiV1InfoResponseVersionToJSONTyped(json, false); +} + +export function ApiV1InfoResponseVersionToJSONTyped(value?: ApiV1InfoResponseVersion | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'commit': value['commit'], + 'date': value['date'], + 'tag': value['tag'], + }; +} + diff --git a/webapp/src/client/models/ApiV1LoginRequestPayload.ts b/webapp/src/client/models/ApiV1LoginRequestPayload.ts new file mode 100644 index 000000000..91cc321c8 --- /dev/null +++ b/webapp/src/client/models/ApiV1LoginRequestPayload.ts @@ -0,0 +1,81 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1LoginRequestPayload + */ +export interface ApiV1LoginRequestPayload { + /** + * + * @type {string} + * @memberof ApiV1LoginRequestPayload + */ + password?: string; + /** + * + * @type {boolean} + * @memberof ApiV1LoginRequestPayload + */ + rememberMe?: boolean; + /** + * + * @type {string} + * @memberof ApiV1LoginRequestPayload + */ + username?: string; +} + +/** + * Check if a given object implements the ApiV1LoginRequestPayload interface. + */ +export function instanceOfApiV1LoginRequestPayload(value: object): value is ApiV1LoginRequestPayload { + return true; +} + +export function ApiV1LoginRequestPayloadFromJSON(json: any): ApiV1LoginRequestPayload { + return ApiV1LoginRequestPayloadFromJSONTyped(json, false); +} + +export function ApiV1LoginRequestPayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1LoginRequestPayload { + if (json == null) { + return json; + } + return { + + 'password': json['password'] == null ? undefined : json['password'], + 'rememberMe': json['remember_me'] == null ? undefined : json['remember_me'], + 'username': json['username'] == null ? undefined : json['username'], + }; +} + +export function ApiV1LoginRequestPayloadToJSON(json: any): ApiV1LoginRequestPayload { + return ApiV1LoginRequestPayloadToJSONTyped(json, false); +} + +export function ApiV1LoginRequestPayloadToJSONTyped(value?: ApiV1LoginRequestPayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'password': value['password'], + 'remember_me': value['rememberMe'], + 'username': value['username'], + }; +} + diff --git a/webapp/src/client/models/ApiV1LoginResponseMessage.ts b/webapp/src/client/models/ApiV1LoginResponseMessage.ts new file mode 100644 index 000000000..075df7bad --- /dev/null +++ b/webapp/src/client/models/ApiV1LoginResponseMessage.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1LoginResponseMessage + */ +export interface ApiV1LoginResponseMessage { + /** + * + * @type {number} + * @memberof ApiV1LoginResponseMessage + */ + expires?: number; + /** + * + * @type {string} + * @memberof ApiV1LoginResponseMessage + */ + token?: string; +} + +/** + * Check if a given object implements the ApiV1LoginResponseMessage interface. + */ +export function instanceOfApiV1LoginResponseMessage(value: object): value is ApiV1LoginResponseMessage { + return true; +} + +export function ApiV1LoginResponseMessageFromJSON(json: any): ApiV1LoginResponseMessage { + return ApiV1LoginResponseMessageFromJSONTyped(json, false); +} + +export function ApiV1LoginResponseMessageFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1LoginResponseMessage { + if (json == null) { + return json; + } + return { + + 'expires': json['expires'] == null ? undefined : json['expires'], + 'token': json['token'] == null ? undefined : json['token'], + }; +} + +export function ApiV1LoginResponseMessageToJSON(json: any): ApiV1LoginResponseMessage { + return ApiV1LoginResponseMessageToJSONTyped(json, false); +} + +export function ApiV1LoginResponseMessageToJSONTyped(value?: ApiV1LoginResponseMessage | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'expires': value['expires'], + 'token': value['token'], + }; +} + diff --git a/webapp/src/client/models/ApiV1ReadableResponseMessage.ts b/webapp/src/client/models/ApiV1ReadableResponseMessage.ts new file mode 100644 index 000000000..84f0a0e49 --- /dev/null +++ b/webapp/src/client/models/ApiV1ReadableResponseMessage.ts @@ -0,0 +1,73 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1ReadableResponseMessage + */ +export interface ApiV1ReadableResponseMessage { + /** + * + * @type {string} + * @memberof ApiV1ReadableResponseMessage + */ + content?: string; + /** + * + * @type {string} + * @memberof ApiV1ReadableResponseMessage + */ + html?: string; +} + +/** + * Check if a given object implements the ApiV1ReadableResponseMessage interface. + */ +export function instanceOfApiV1ReadableResponseMessage(value: object): value is ApiV1ReadableResponseMessage { + return true; +} + +export function ApiV1ReadableResponseMessageFromJSON(json: any): ApiV1ReadableResponseMessage { + return ApiV1ReadableResponseMessageFromJSONTyped(json, false); +} + +export function ApiV1ReadableResponseMessageFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1ReadableResponseMessage { + if (json == null) { + return json; + } + return { + + 'content': json['content'] == null ? undefined : json['content'], + 'html': json['html'] == null ? undefined : json['html'], + }; +} + +export function ApiV1ReadableResponseMessageToJSON(json: any): ApiV1ReadableResponseMessage { + return ApiV1ReadableResponseMessageToJSONTyped(json, false); +} + +export function ApiV1ReadableResponseMessageToJSONTyped(value?: ApiV1ReadableResponseMessage | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'content': value['content'], + 'html': value['html'], + }; +} + diff --git a/webapp/src/client/models/ApiV1UpdateAccountPayload.ts b/webapp/src/client/models/ApiV1UpdateAccountPayload.ts new file mode 100644 index 000000000..525243ed7 --- /dev/null +++ b/webapp/src/client/models/ApiV1UpdateAccountPayload.ts @@ -0,0 +1,105 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ModelUserConfig } from './ModelUserConfig'; +import { + ModelUserConfigFromJSON, + ModelUserConfigFromJSONTyped, + ModelUserConfigToJSON, + ModelUserConfigToJSONTyped, +} from './ModelUserConfig'; + +/** + * + * @export + * @interface ApiV1UpdateAccountPayload + */ +export interface ApiV1UpdateAccountPayload { + /** + * + * @type {ModelUserConfig} + * @memberof ApiV1UpdateAccountPayload + */ + config?: ModelUserConfig; + /** + * + * @type {string} + * @memberof ApiV1UpdateAccountPayload + */ + newPassword?: string; + /** + * + * @type {string} + * @memberof ApiV1UpdateAccountPayload + */ + oldPassword?: string; + /** + * + * @type {boolean} + * @memberof ApiV1UpdateAccountPayload + */ + owner?: boolean; + /** + * + * @type {string} + * @memberof ApiV1UpdateAccountPayload + */ + username?: string; +} + +/** + * Check if a given object implements the ApiV1UpdateAccountPayload interface. + */ +export function instanceOfApiV1UpdateAccountPayload(value: object): value is ApiV1UpdateAccountPayload { + return true; +} + +export function ApiV1UpdateAccountPayloadFromJSON(json: any): ApiV1UpdateAccountPayload { + return ApiV1UpdateAccountPayloadFromJSONTyped(json, false); +} + +export function ApiV1UpdateAccountPayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1UpdateAccountPayload { + if (json == null) { + return json; + } + return { + + 'config': json['config'] == null ? undefined : ModelUserConfigFromJSON(json['config']), + 'newPassword': json['new_password'] == null ? undefined : json['new_password'], + 'oldPassword': json['old_password'] == null ? undefined : json['old_password'], + 'owner': json['owner'] == null ? undefined : json['owner'], + 'username': json['username'] == null ? undefined : json['username'], + }; +} + +export function ApiV1UpdateAccountPayloadToJSON(json: any): ApiV1UpdateAccountPayload { + return ApiV1UpdateAccountPayloadToJSONTyped(json, false); +} + +export function ApiV1UpdateAccountPayloadToJSONTyped(value?: ApiV1UpdateAccountPayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'config': ModelUserConfigToJSON(value['config']), + 'new_password': value['newPassword'], + 'old_password': value['oldPassword'], + 'owner': value['owner'], + 'username': value['username'], + }; +} + diff --git a/webapp/src/client/models/ApiV1UpdateCachePayload.ts b/webapp/src/client/models/ApiV1UpdateCachePayload.ts new file mode 100644 index 000000000..4caf71ffe --- /dev/null +++ b/webapp/src/client/models/ApiV1UpdateCachePayload.ts @@ -0,0 +1,98 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ApiV1UpdateCachePayload + */ +export interface ApiV1UpdateCachePayload { + /** + * + * @type {boolean} + * @memberof ApiV1UpdateCachePayload + */ + createArchive?: boolean; + /** + * + * @type {boolean} + * @memberof ApiV1UpdateCachePayload + */ + createEbook?: boolean; + /** + * + * @type {Array} + * @memberof ApiV1UpdateCachePayload + */ + ids: Array; + /** + * + * @type {boolean} + * @memberof ApiV1UpdateCachePayload + */ + keepMetadata?: boolean; + /** + * + * @type {boolean} + * @memberof ApiV1UpdateCachePayload + */ + skipExist?: boolean; +} + +/** + * Check if a given object implements the ApiV1UpdateCachePayload interface. + */ +export function instanceOfApiV1UpdateCachePayload(value: object): value is ApiV1UpdateCachePayload { + if (!('ids' in value) || value['ids'] === undefined) return false; + return true; +} + +export function ApiV1UpdateCachePayloadFromJSON(json: any): ApiV1UpdateCachePayload { + return ApiV1UpdateCachePayloadFromJSONTyped(json, false); +} + +export function ApiV1UpdateCachePayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): ApiV1UpdateCachePayload { + if (json == null) { + return json; + } + return { + + 'createArchive': json['create_archive'] == null ? undefined : json['create_archive'], + 'createEbook': json['create_ebook'] == null ? undefined : json['create_ebook'], + 'ids': json['ids'], + 'keepMetadata': json['keep_metadata'] == null ? undefined : json['keep_metadata'], + 'skipExist': json['skip_exist'] == null ? undefined : json['skip_exist'], + }; +} + +export function ApiV1UpdateCachePayloadToJSON(json: any): ApiV1UpdateCachePayload { + return ApiV1UpdateCachePayloadToJSONTyped(json, false); +} + +export function ApiV1UpdateCachePayloadToJSONTyped(value?: ApiV1UpdateCachePayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'create_archive': value['createArchive'], + 'create_ebook': value['createEbook'], + 'ids': value['ids'], + 'keep_metadata': value['keepMetadata'], + 'skip_exist': value['skipExist'], + }; +} + diff --git a/webapp/src/client/models/ModelAccount.ts b/webapp/src/client/models/ModelAccount.ts new file mode 100644 index 000000000..90d704b83 --- /dev/null +++ b/webapp/src/client/models/ModelAccount.ts @@ -0,0 +1,105 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ModelUserConfig } from './ModelUserConfig'; +import { + ModelUserConfigFromJSON, + ModelUserConfigFromJSONTyped, + ModelUserConfigToJSON, + ModelUserConfigToJSONTyped, +} from './ModelUserConfig'; + +/** + * + * @export + * @interface ModelAccount + */ +export interface ModelAccount { + /** + * + * @type {ModelUserConfig} + * @memberof ModelAccount + */ + config?: ModelUserConfig; + /** + * + * @type {number} + * @memberof ModelAccount + */ + id?: number; + /** + * + * @type {boolean} + * @memberof ModelAccount + */ + owner?: boolean; + /** + * + * @type {string} + * @memberof ModelAccount + */ + password?: string; + /** + * + * @type {string} + * @memberof ModelAccount + */ + username?: string; +} + +/** + * Check if a given object implements the ModelAccount interface. + */ +export function instanceOfModelAccount(value: object): value is ModelAccount { + return true; +} + +export function ModelAccountFromJSON(json: any): ModelAccount { + return ModelAccountFromJSONTyped(json, false); +} + +export function ModelAccountFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelAccount { + if (json == null) { + return json; + } + return { + + 'config': json['config'] == null ? undefined : ModelUserConfigFromJSON(json['config']), + 'id': json['id'] == null ? undefined : json['id'], + 'owner': json['owner'] == null ? undefined : json['owner'], + 'password': json['password'] == null ? undefined : json['password'], + 'username': json['username'] == null ? undefined : json['username'], + }; +} + +export function ModelAccountToJSON(json: any): ModelAccount { + return ModelAccountToJSONTyped(json, false); +} + +export function ModelAccountToJSONTyped(value?: ModelAccount | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'config': ModelUserConfigToJSON(value['config']), + 'id': value['id'], + 'owner': value['owner'], + 'password': value['password'], + 'username': value['username'], + }; +} + diff --git a/webapp/src/client/models/ModelAccountDTO.ts b/webapp/src/client/models/ModelAccountDTO.ts new file mode 100644 index 000000000..d630c7b1e --- /dev/null +++ b/webapp/src/client/models/ModelAccountDTO.ts @@ -0,0 +1,105 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ModelUserConfig } from './ModelUserConfig'; +import { + ModelUserConfigFromJSON, + ModelUserConfigFromJSONTyped, + ModelUserConfigToJSON, + ModelUserConfigToJSONTyped, +} from './ModelUserConfig'; + +/** + * + * @export + * @interface ModelAccountDTO + */ +export interface ModelAccountDTO { + /** + * + * @type {ModelUserConfig} + * @memberof ModelAccountDTO + */ + config?: ModelUserConfig; + /** + * + * @type {number} + * @memberof ModelAccountDTO + */ + id?: number; + /** + * + * @type {boolean} + * @memberof ModelAccountDTO + */ + owner?: boolean; + /** + * Used only to store, not to retrieve + * @type {string} + * @memberof ModelAccountDTO + */ + passowrd?: string; + /** + * + * @type {string} + * @memberof ModelAccountDTO + */ + username?: string; +} + +/** + * Check if a given object implements the ModelAccountDTO interface. + */ +export function instanceOfModelAccountDTO(value: object): value is ModelAccountDTO { + return true; +} + +export function ModelAccountDTOFromJSON(json: any): ModelAccountDTO { + return ModelAccountDTOFromJSONTyped(json, false); +} + +export function ModelAccountDTOFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelAccountDTO { + if (json == null) { + return json; + } + return { + + 'config': json['config'] == null ? undefined : ModelUserConfigFromJSON(json['config']), + 'id': json['id'] == null ? undefined : json['id'], + 'owner': json['owner'] == null ? undefined : json['owner'], + 'passowrd': json['passowrd'] == null ? undefined : json['passowrd'], + 'username': json['username'] == null ? undefined : json['username'], + }; +} + +export function ModelAccountDTOToJSON(json: any): ModelAccountDTO { + return ModelAccountDTOToJSONTyped(json, false); +} + +export function ModelAccountDTOToJSONTyped(value?: ModelAccountDTO | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'config': ModelUserConfigToJSON(value['config']), + 'id': value['id'], + 'owner': value['owner'], + 'passowrd': value['passowrd'], + 'username': value['username'], + }; +} + diff --git a/webapp/src/client/models/ModelBookmarkDTO.ts b/webapp/src/client/models/ModelBookmarkDTO.ts new file mode 100644 index 000000000..1cc9ff0a6 --- /dev/null +++ b/webapp/src/client/models/ModelBookmarkDTO.ts @@ -0,0 +1,193 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ModelTagDTO } from './ModelTagDTO'; +import { + ModelTagDTOFromJSON, + ModelTagDTOFromJSONTyped, + ModelTagDTOToJSON, + ModelTagDTOToJSONTyped, +} from './ModelTagDTO'; + +/** + * + * @export + * @interface ModelBookmarkDTO + */ +export interface ModelBookmarkDTO { + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + author?: string; + /** + * TODO: migrate outside the DTO + * @type {boolean} + * @memberof ModelBookmarkDTO + */ + createArchive?: boolean; + /** + * TODO: migrate outside the DTO + * @type {boolean} + * @memberof ModelBookmarkDTO + */ + createEbook?: boolean; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + createdAt?: string; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + excerpt?: string; + /** + * + * @type {boolean} + * @memberof ModelBookmarkDTO + */ + hasArchive?: boolean; + /** + * + * @type {boolean} + * @memberof ModelBookmarkDTO + */ + hasContent?: boolean; + /** + * + * @type {boolean} + * @memberof ModelBookmarkDTO + */ + hasEbook?: boolean; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + html?: string; + /** + * + * @type {number} + * @memberof ModelBookmarkDTO + */ + id?: number; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + imageURL?: string; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + modifiedAt?: string; + /** + * + * @type {number} + * @memberof ModelBookmarkDTO + */ + _public?: number; + /** + * + * @type {Array} + * @memberof ModelBookmarkDTO + */ + tags?: Array; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + title?: string; + /** + * + * @type {string} + * @memberof ModelBookmarkDTO + */ + url?: string; +} + +/** + * Check if a given object implements the ModelBookmarkDTO interface. + */ +export function instanceOfModelBookmarkDTO(value: object): value is ModelBookmarkDTO { + return true; +} + +export function ModelBookmarkDTOFromJSON(json: any): ModelBookmarkDTO { + return ModelBookmarkDTOFromJSONTyped(json, false); +} + +export function ModelBookmarkDTOFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelBookmarkDTO { + if (json == null) { + return json; + } + return { + + 'author': json['author'] == null ? undefined : json['author'], + 'createArchive': json['create_archive'] == null ? undefined : json['create_archive'], + 'createEbook': json['create_ebook'] == null ? undefined : json['create_ebook'], + 'createdAt': json['createdAt'] == null ? undefined : json['createdAt'], + 'excerpt': json['excerpt'] == null ? undefined : json['excerpt'], + 'hasArchive': json['hasArchive'] == null ? undefined : json['hasArchive'], + 'hasContent': json['hasContent'] == null ? undefined : json['hasContent'], + 'hasEbook': json['hasEbook'] == null ? undefined : json['hasEbook'], + 'html': json['html'] == null ? undefined : json['html'], + 'id': json['id'] == null ? undefined : json['id'], + 'imageURL': json['imageURL'] == null ? undefined : json['imageURL'], + 'modifiedAt': json['modifiedAt'] == null ? undefined : json['modifiedAt'], + '_public': json['public'] == null ? undefined : json['public'], + 'tags': json['tags'] == null ? undefined : ((json['tags'] as Array).map(ModelTagDTOFromJSON)), + 'title': json['title'] == null ? undefined : json['title'], + 'url': json['url'] == null ? undefined : json['url'], + }; +} + +export function ModelBookmarkDTOToJSON(json: any): ModelBookmarkDTO { + return ModelBookmarkDTOToJSONTyped(json, false); +} + +export function ModelBookmarkDTOToJSONTyped(value?: ModelBookmarkDTO | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'author': value['author'], + 'create_archive': value['createArchive'], + 'create_ebook': value['createEbook'], + 'createdAt': value['createdAt'], + 'excerpt': value['excerpt'], + 'hasArchive': value['hasArchive'], + 'hasContent': value['hasContent'], + 'hasEbook': value['hasEbook'], + 'html': value['html'], + 'id': value['id'], + 'imageURL': value['imageURL'], + 'modifiedAt': value['modifiedAt'], + 'public': value['_public'], + 'tags': value['tags'] == null ? undefined : ((value['tags'] as Array).map(ModelTagDTOToJSON)), + 'title': value['title'], + 'url': value['url'], + }; +} + diff --git a/webapp/src/client/models/ModelTagDTO.ts b/webapp/src/client/models/ModelTagDTO.ts new file mode 100644 index 000000000..efb088d1e --- /dev/null +++ b/webapp/src/client/models/ModelTagDTO.ts @@ -0,0 +1,89 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ModelTagDTO + */ +export interface ModelTagDTO { + /** + * Number of bookmarks with this tag + * @type {number} + * @memberof ModelTagDTO + */ + bookmarkCount?: number; + /** + * Marks when a tag is deleted from a bookmark + * @type {boolean} + * @memberof ModelTagDTO + */ + deleted?: boolean; + /** + * + * @type {number} + * @memberof ModelTagDTO + */ + id?: number; + /** + * + * @type {string} + * @memberof ModelTagDTO + */ + name?: string; +} + +/** + * Check if a given object implements the ModelTagDTO interface. + */ +export function instanceOfModelTagDTO(value: object): value is ModelTagDTO { + return true; +} + +export function ModelTagDTOFromJSON(json: any): ModelTagDTO { + return ModelTagDTOFromJSONTyped(json, false); +} + +export function ModelTagDTOFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelTagDTO { + if (json == null) { + return json; + } + return { + + 'bookmarkCount': json['bookmark_count'] == null ? undefined : json['bookmark_count'], + 'deleted': json['deleted'] == null ? undefined : json['deleted'], + 'id': json['id'] == null ? undefined : json['id'], + 'name': json['name'] == null ? undefined : json['name'], + }; +} + +export function ModelTagDTOToJSON(json: any): ModelTagDTO { + return ModelTagDTOToJSONTyped(json, false); +} + +export function ModelTagDTOToJSONTyped(value?: ModelTagDTO | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'bookmark_count': value['bookmarkCount'], + 'deleted': value['deleted'], + 'id': value['id'], + 'name': value['name'], + }; +} + diff --git a/webapp/src/client/models/ModelUserConfig.ts b/webapp/src/client/models/ModelUserConfig.ts new file mode 100644 index 000000000..d8c0c9da5 --- /dev/null +++ b/webapp/src/client/models/ModelUserConfig.ts @@ -0,0 +1,129 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ModelUserConfig + */ +export interface ModelUserConfig { + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + createEbook?: boolean; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + hideExcerpt?: boolean; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + hideThumbnail?: boolean; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + keepMetadata?: boolean; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + listMode?: boolean; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + makePublic?: boolean; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + showId?: boolean; + /** + * + * @type {string} + * @memberof ModelUserConfig + */ + theme?: string; + /** + * + * @type {boolean} + * @memberof ModelUserConfig + */ + useArchive?: boolean; +} + +/** + * Check if a given object implements the ModelUserConfig interface. + */ +export function instanceOfModelUserConfig(value: object): value is ModelUserConfig { + return true; +} + +export function ModelUserConfigFromJSON(json: any): ModelUserConfig { + return ModelUserConfigFromJSONTyped(json, false); +} + +export function ModelUserConfigFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelUserConfig { + if (json == null) { + return json; + } + return { + + 'createEbook': json['createEbook'] == null ? undefined : json['createEbook'], + 'hideExcerpt': json['hideExcerpt'] == null ? undefined : json['hideExcerpt'], + 'hideThumbnail': json['hideThumbnail'] == null ? undefined : json['hideThumbnail'], + 'keepMetadata': json['keepMetadata'] == null ? undefined : json['keepMetadata'], + 'listMode': json['listMode'] == null ? undefined : json['listMode'], + 'makePublic': json['makePublic'] == null ? undefined : json['makePublic'], + 'showId': json['showId'] == null ? undefined : json['showId'], + 'theme': json['theme'] == null ? undefined : json['theme'], + 'useArchive': json['useArchive'] == null ? undefined : json['useArchive'], + }; +} + +export function ModelUserConfigToJSON(json: any): ModelUserConfig { + return ModelUserConfigToJSONTyped(json, false); +} + +export function ModelUserConfigToJSONTyped(value?: ModelUserConfig | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'createEbook': value['createEbook'], + 'hideExcerpt': value['hideExcerpt'], + 'hideThumbnail': value['hideThumbnail'], + 'keepMetadata': value['keepMetadata'], + 'listMode': value['listMode'], + 'makePublic': value['makePublic'], + 'showId': value['showId'], + 'theme': value['theme'], + 'useArchive': value['useArchive'], + }; +} + diff --git a/webapp/src/client/models/index.ts b/webapp/src/client/models/index.ts new file mode 100644 index 000000000..b332e9f1f --- /dev/null +++ b/webapp/src/client/models/index.ts @@ -0,0 +1,16 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './ApiV1BookmarkTagPayload'; +export * from './ApiV1BulkUpdateBookmarkTagsPayload'; +export * from './ApiV1InfoResponse'; +export * from './ApiV1InfoResponseVersion'; +export * from './ApiV1LoginRequestPayload'; +export * from './ApiV1LoginResponseMessage'; +export * from './ApiV1ReadableResponseMessage'; +export * from './ApiV1UpdateAccountPayload'; +export * from './ApiV1UpdateCachePayload'; +export * from './ModelAccount'; +export * from './ModelAccountDTO'; +export * from './ModelBookmarkDTO'; +export * from './ModelTagDTO'; +export * from './ModelUserConfig'; diff --git a/webapp/src/client/runtime.ts b/webapp/src/client/runtime.ts new file mode 100644 index 000000000..8bcc03488 --- /dev/null +++ b/webapp/src/client/runtime.ts @@ -0,0 +1,431 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Shiori API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): string | undefined { + return this.configuration.username; + } + + get password(): string | undefined { + return this.configuration.password; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; + + const overriddenInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: any; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +}; + +function isBlob(value: any): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: any): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + } +} + +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class RequiredError extends Error { + override name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } +} + +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = any; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | FormData | URLSearchParams; +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function mapValues(data: any, fn: (item: any) => any) { + return Object.keys(data).reduce( + (acc, key) => ({ ...acc, [key]: fn(data[key]) }), + {} + ); +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: any): T; +} + +export class JSONApiResponse { + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.text(); + }; +} From 922e6fda0a4a63cd5d242b6463bf3511f815eaaf Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 10:11:18 +0100 Subject: [PATCH 12/19] feat: implemented login in webapp --- webapp/src/App.vue | 30 +++- webapp/src/components/layout/Sidebar.vue | 127 ++++++++++--- webapp/src/components/layout/TopBar.vue | 73 ++++++-- webapp/src/router/index.ts | 56 ++++-- webapp/src/stores/auth.ts | 217 +++++++++++++++++++++++ webapp/src/views/FoldersView.vue | 73 ++++++++ webapp/src/views/LoginView.vue | 132 ++++++++++---- 7 files changed, 616 insertions(+), 92 deletions(-) create mode 100644 webapp/src/stores/auth.ts create mode 100644 webapp/src/views/FoldersView.vue diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 8921eec3b..d91f7e4b1 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -1,9 +1,37 @@ diff --git a/webapp/src/components/layout/TopBar.vue b/webapp/src/components/layout/TopBar.vue index 4f2699c26..628e67590 100644 --- a/webapp/src/components/layout/TopBar.vue +++ b/webapp/src/components/layout/TopBar.vue @@ -1,45 +1,88 @@ + + diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index e00478b2c..09ea1510f 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -2,17 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router' import type { RouteRecordRaw, NavigationGuardNext as NavigationGuard, RouteLocationNormalized } from 'vue-router' import HomeView from '../views/HomeView.vue' import LoginView from '../views/LoginView.vue' - -// Simple auth guard -const isAuthenticated = (): boolean => { - // For now, just check if there's a token in localStorage - return !!localStorage.getItem('token') -} +import { useAuthStore } from '@/stores/auth' const routes: Array = [ { path: '/', - redirect: '/login' + redirect: '/home' }, { path: '/home', @@ -23,7 +18,8 @@ const routes: Array = [ { path: '/login', name: 'login', - component: LoginView + component: LoginView, + props: (route) => ({ dst: route.query.dst }) }, { path: '/tags', @@ -31,6 +27,12 @@ const routes: Array = [ component: () => import('../views/TagsView.vue'), meta: { requiresAuth: true } }, + { + path: '/folders', + name: 'folders', + component: () => import('../views/FoldersView.vue'), + meta: { requiresAuth: true } + }, { path: '/archive', name: 'archive', @@ -43,10 +45,10 @@ const routes: Array = [ component: () => import('../views/SettingsView.vue'), meta: { requiresAuth: true } }, - // Redirect any unmatched routes to login + // Redirect any unmatched routes to home (which will redirect to login if not authenticated) { path: '/:pathMatch(.*)*', - redirect: '/login' + redirect: '/home' } ] @@ -56,14 +58,38 @@ const router = createRouter({ }) // Navigation guard -router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next) => { - if (to.matched.some((record: any) => record.meta.requiresAuth)) { - if (!isAuthenticated()) { - next({ name: 'login' }) +router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuard) => { + const authStore = useAuthStore() + + // Check if the route requires authentication + if (to.matched.some((record) => record.meta.requiresAuth)) { + // If we have a token, validate it + if (authStore.token) { + const isValid = await authStore.validateToken() + + if (isValid) { + // Token is valid, proceed to the requested route + next() + } else { + // Token is invalid, redirect to login with destination + const destination = to.fullPath + authStore.setRedirectDestination(destination) + next({ + name: 'login', + query: { dst: destination } + }) + } } else { - next() + // No token, redirect to login with destination + const destination = to.fullPath + authStore.setRedirectDestination(destination) + next({ + name: 'login', + query: { dst: destination } + }) } } else { + // Route doesn't require auth, proceed next() } }) diff --git a/webapp/src/stores/auth.ts b/webapp/src/stores/auth.ts new file mode 100644 index 000000000..8c1614c48 --- /dev/null +++ b/webapp/src/stores/auth.ts @@ -0,0 +1,217 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { AuthApi } from '@/client/apis/AuthApi' +import type { ApiV1LoginRequestPayload } from '@/client/models/ApiV1LoginRequestPayload' +import { Configuration } from '@/client/runtime' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('token')) + const expires = ref(Number(localStorage.getItem('expires')) || null) + const user = ref(null) + const loading = ref(false) + const error = ref(null) + const redirectDestination = ref(null) + + const isAuthenticated = computed(() => { + if (!token.value) return false + if (!expires.value) return false + return expires.value > Date.now() + }) + + // Create API client with auth token + const getApiClient = () => { + const config = new Configuration({ + basePath: 'http://localhost:8080', + accessToken: token.value || undefined, + headers: token.value ? { + 'Authorization': `Bearer ${token.value}` + } : undefined + }) + return new AuthApi(config) + } + + // Validate token by fetching user info + const validateToken = async (): Promise => { + if (!token.value) return false + + loading.value = true + try { + const result = await fetchUserInfo() + loading.value = false + return !!result + } catch (err) { + loading.value = false + return false + } + } + + // Login function + const login = async (username: string, password: string, rememberMe: boolean = false) => { + loading.value = true + error.value = null + + try { + const payload: ApiV1LoginRequestPayload = { + username, + password, + rememberMe, + } + + const api = getApiClient() + const response = await api.apiV1AuthLoginPost({ payload }) + + if (response.token) { + token.value = response.token + expires.value = response.expires || 0 + + // Store in localStorage + localStorage.setItem('token', response.token) + localStorage.setItem('expires', String(response.expires)) + + // Get user info + await fetchUserInfo() + return true + } else { + throw new Error('Invalid response from server') + } + } catch (err: any) { + console.error('Login error:', err) + + // Extract error message from response if available + if (err.response) { + try { + // Try to parse the response body as JSON + const responseBody = await err.response.json() + if (responseBody && responseBody.message) { + error.value = responseBody.message + } else if (responseBody && responseBody.error) { + error.value = responseBody.error + } else if (typeof responseBody === 'string') { + error.value = responseBody + } else { + error.value = `Server error: ${err.response.status}` + } + } catch (jsonError) { + // If response is not JSON, use status text + error.value = err.response.statusText || `Server error: ${err.response.status}` + } + } else { + // If no response object, use the error message + error.value = err.message || 'Failed to login' + } + + return false + } finally { + loading.value = false + } + } + + // Fetch user info + const fetchUserInfo = async () => { + if (!token.value) return null + + try { + // Create a new API client with the current token + const api = getApiClient() + + // Make the API request with the token in the headers + const response = await api.apiV1AuthMeGet() + + if (response) { + user.value = response + return user.value + } else { + throw new Error('Failed to fetch user info') + } + } catch (err: any) { + console.error('Error fetching user info:', err) + + // If we get a 401 Unauthorized, the token is invalid + if (err.response && err.response.status === 401) { + // Clear the invalid token + clearAuth() + } + + return null + } + } + + // Clear authentication data + const clearAuth = () => { + token.value = null + expires.value = null + user.value = null + localStorage.removeItem('token') + localStorage.removeItem('expires') + } + + // Logout function + const logout = async () => { + loading.value = true + + try { + if (token.value) { + const api = getApiClient() + await api.apiV1AuthLogoutPost() + } + } catch (err) { + console.error('Logout error:', err) + } finally { + // Clear state regardless of API success + clearAuth() + loading.value = false + } + } + + // Refresh token + const refreshToken = async () => { + if (!token.value) return false + + try { + const api = getApiClient() + const response = await api.apiV1AuthRefreshPost() + + if (response.token) { + token.value = response.token + expires.value = response.expires || 0 + + localStorage.setItem('token', response.token) + localStorage.setItem('expires', String(response.expires)) + return true + } + return false + } catch (err) { + console.error('Token refresh error:', err) + return false + } + } + + // Set redirect destination + const setRedirectDestination = (destination: string | null) => { + redirectDestination.value = destination + } + + // Get and clear redirect destination + const getAndClearRedirectDestination = () => { + const destination = redirectDestination.value + redirectDestination.value = null + return destination + } + + return { + token, + expires, + user, + loading, + error, + isAuthenticated, + login, + logout, + fetchUserInfo, + refreshToken, + validateToken, + setRedirectDestination, + getAndClearRedirectDestination, + clearAuth + } +}) diff --git a/webapp/src/views/FoldersView.vue b/webapp/src/views/FoldersView.vue new file mode 100644 index 000000000..dc3f35db4 --- /dev/null +++ b/webapp/src/views/FoldersView.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/webapp/src/views/LoginView.vue b/webapp/src/views/LoginView.vue index 188cdef10..6b9861db9 100644 --- a/webapp/src/views/LoginView.vue +++ b/webapp/src/views/LoginView.vue @@ -1,21 +1,76 @@ @@ -30,34 +85,47 @@ const login = async () => {
-
-
-
Username:
- -
- -
-
Password:
- -
+
+ {{ errorMessage }}
-
- - +
+ Verifying your session...
-
- -
+
+
+
+
Username:
+ +
+ +
+
Password:
+ +
+
+ +
+ + +
+ +
+ +
+
From 26677d115e3e93e91a551a6bf62c8b6bbfc62321 Mon Sep 17 00:00:00 2001 From: "Felipe M." Date: Fri, 14 Mar 2025 20:10:31 +0100 Subject: [PATCH 13/19] feat; dark mode in web app --- webapp/src/App.vue | 25 ++++- webapp/src/assets/main.css | 26 ++++- webapp/src/components/layout/AppLayout.vue | 17 ++- webapp/src/components/layout/Sidebar.vue | 119 +++++++++++---------- webapp/src/components/layout/TopBar.vue | 48 +++++---- webapp/src/views/HomeView.vue | 36 +++---- webapp/src/views/LoginView.vue | 23 ++-- webapp/tailwind.config.js | 1 + 8 files changed, 178 insertions(+), 117 deletions(-) diff --git a/webapp/src/App.vue b/webapp/src/App.vue index d91f7e4b1..1a5e08626 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -23,17 +23,32 @@ onMounted(async () => { diff --git a/webapp/src/assets/main.css b/webapp/src/assets/main.css index 40cddb39e..b63a1e9b3 100644 --- a/webapp/src/assets/main.css +++ b/webapp/src/assets/main.css @@ -6,6 +6,27 @@ --secondary-color: #ffffff; --text-color: #333333; --background-color: #f5f5f5; + --card-background: #ffffff; + --border-color: #e0e0e0; +} + +/* Dark mode variables */ +@media (prefers-color-scheme: dark) { + :root { + --primary-color: #f44336; + --secondary-color: #1f1f1f; + --text-color: #f5f5f5; + --background-color: #121212; + --card-background: #1e1e1e; + --border-color: #333333; + } +} + +/* Base styles for full height layout */ +html, body { + height: 100%; + margin: 0; + padding: 0; } body { @@ -13,11 +34,10 @@ body { color: var(--text-color); background-color: var(--background-color); min-height: 100vh; - margin: 0; - padding: 0; } #app { - width: 100%; + display: flex; + flex-direction: column; min-height: 100vh; } diff --git a/webapp/src/components/layout/AppLayout.vue b/webapp/src/components/layout/AppLayout.vue index fd39860d0..fc1db9c74 100644 --- a/webapp/src/components/layout/AppLayout.vue +++ b/webapp/src/components/layout/AppLayout.vue @@ -20,7 +20,7 @@ onUnmounted(() => { + + diff --git a/webapp/src/components/layout/Sidebar.vue b/webapp/src/components/layout/Sidebar.vue index f16218c6c..db2a90098 100644 --- a/webapp/src/components/layout/Sidebar.vue +++ b/webapp/src/components/layout/Sidebar.vue @@ -82,62 +82,71 @@ const icons = { diff --git a/webapp/src/components/layout/TopBar.vue b/webapp/src/components/layout/TopBar.vue index 628e67590..67cb5c8b4 100644 --- a/webapp/src/components/layout/TopBar.vue +++ b/webapp/src/components/layout/TopBar.vue @@ -39,7 +39,8 @@ onUnmounted(() => {