aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/api/controller/category.go104
-rw-r--r--server/api/controller/controller.go21
-rw-r--r--server/api/controller/entry.go224
-rw-r--r--server/api/controller/feed.go179
-rw-r--r--server/api/controller/icon.go44
-rw-r--r--server/api/controller/subscription.go36
-rw-r--r--server/api/controller/user.go186
-rw-r--r--server/api/payload/payload.go109
-rw-r--r--server/cookie/cookie.go44
-rw-r--r--server/core/context.go160
-rw-r--r--server/core/handler.go70
-rw-r--r--server/core/html_response.go65
-rw-r--r--server/core/json_response.go111
-rw-r--r--server/core/request.go125
-rw-r--r--server/core/response.go82
-rw-r--r--server/core/xml_response.go23
-rw-r--r--server/fever/fever.go631
-rw-r--r--server/middleware/basic_auth.go72
-rw-r--r--server/middleware/context_keys.go46
-rw-r--r--server/middleware/fever.go57
-rw-r--r--server/middleware/middleware.go36
-rw-r--r--server/middleware/session.go84
-rw-r--r--server/middleware/user_session.go78
-rw-r--r--server/oauth2/google.go74
-rw-r--r--server/oauth2/manager.go33
-rw-r--r--server/oauth2/profile.go12
-rw-r--r--server/oauth2/provider.go12
-rw-r--r--server/route/route.go38
-rw-r--r--server/routes.go166
-rw-r--r--server/server.go69
-rw-r--r--server/static/bin.go22
-rw-r--r--server/static/bin/favicon.icobin16958 -> 0 bytes
-rw-r--r--server/static/bin/favicon.pngbin847 -> 0 bytes
-rw-r--r--server/static/bin/touch-icon-ipad-retina.pngbin2181 -> 0 bytes
-rw-r--r--server/static/bin/touch-icon-ipad.pngbin1152 -> 0 bytes
-rw-r--r--server/static/bin/touch-icon-iphone-retina.pngbin1801 -> 0 bytes
-rw-r--r--server/static/bin/touch-icon-iphone.pngbin947 -> 0 bytes
-rw-r--r--server/static/css.go14
-rw-r--r--server/static/css/black.css219
-rw-r--r--server/static/css/common.css778
-rw-r--r--server/static/js.go92
-rw-r--r--server/static/js/app.js748
-rw-r--r--server/template/common.go176
-rw-r--r--server/template/html/about.html40
-rw-r--r--server/template/html/add_subscription.html47
-rw-r--r--server/template/html/categories.html56
-rw-r--r--server/template/html/category_entries.html68
-rw-r--r--server/template/html/choose_subscription.html39
-rw-r--r--server/template/html/common/entry_pagination.html19
-rw-r--r--server/template/html/common/layout.html124
-rw-r--r--server/template/html/common/pagination.html19
-rw-r--r--server/template/html/create_category.html27
-rw-r--r--server/template/html/create_user.html44
-rw-r--r--server/template/html/edit_category.html30
-rw-r--r--server/template/html/edit_feed.html77
-rw-r--r--server/template/html/edit_user.html47
-rw-r--r--server/template/html/entry.html109
-rw-r--r--server/template/html/feed_entries.html79
-rw-r--r--server/template/html/feeds.html77
-rw-r--r--server/template/html/history.html68
-rw-r--r--server/template/html/import.html34
-rw-r--r--server/template/html/integrations.html112
-rw-r--r--server/template/html/login.html28
-rw-r--r--server/template/html/sessions.html51
-rw-r--r--server/template/html/settings.html82
-rw-r--r--server/template/html/starred.html61
-rw-r--r--server/template/html/unread.html68
-rw-r--r--server/template/html/users.html60
-rw-r--r--server/template/template.go149
-rw-r--r--server/template/views.go1356
-rw-r--r--server/ui/controller/about.go25
-rw-r--r--server/ui/controller/category.go257
-rw-r--r--server/ui/controller/controller.go66
-rw-r--r--server/ui/controller/entry.go495
-rw-r--r--server/ui/controller/feed.go236
-rw-r--r--server/ui/controller/history.go61
-rw-r--r--server/ui/controller/icon.go33
-rw-r--r--server/ui/controller/integrations.go84
-rw-r--r--server/ui/controller/login.go76
-rw-r--r--server/ui/controller/oauth2.go170
-rw-r--r--server/ui/controller/opml.go71
-rw-r--r--server/ui/controller/pagination.go46
-rw-r--r--server/ui/controller/proxy.go56
-rw-r--r--server/ui/controller/session.go50
-rw-r--r--server/ui/controller/settings.go96
-rw-r--r--server/ui/controller/starred.go68
-rw-r--r--server/ui/controller/static.go97
-rw-r--r--server/ui/controller/subscription.go145
-rw-r--r--server/ui/controller/unread.go49
-rw-r--r--server/ui/controller/user.go238
-rw-r--r--server/ui/filter/image_proxy_filter.go41
-rw-r--r--server/ui/filter/image_proxy_filter_test.go38
-rw-r--r--server/ui/form/auth.go34
-rw-r--r--server/ui/form/category.go38
-rw-r--r--server/ui/form/feed.go64
-rw-r--r--server/ui/form/integration.go73
-rw-r--r--server/ui/form/settings.go70
-rw-r--r--server/ui/form/subscription.go42
-rw-r--r--server/ui/form/user.go87
-rw-r--r--server/ui/payload/payload.go33
100 files changed, 0 insertions, 10950 deletions
diff --git a/server/api/controller/category.go b/server/api/controller/category.go
deleted file mode 100644
index d7b2922..0000000
--- a/server/api/controller/category.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/server/api/payload"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// CreateCategory is the API handler to create a new category.
-func (c *Controller) CreateCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- category, err := payload.DecodeCategoryPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- category.UserID = userID
- if err := category.ValidateCategoryCreation(); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
- response.JSON().BadRequest(errors.New("This category already exists"))
- return
- }
-
- err = c.store.CreateCategory(category)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to create this category"))
- return
- }
-
- response.JSON().Created(category)
-}
-
-// UpdateCategory is the API handler to update a category.
-func (c *Controller) UpdateCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- categoryID, err := request.IntegerParam("categoryID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- category, err := payload.DecodeCategoryPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- category.UserID = ctx.UserID()
- category.ID = categoryID
- if err := category.ValidateCategoryModification(); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- err = c.store.UpdateCategory(category)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to update this category"))
- return
- }
-
- response.JSON().Created(category)
-}
-
-// GetCategories is the API handler to get a list of categories for a given user.
-func (c *Controller) GetCategories(ctx *core.Context, request *core.Request, response *core.Response) {
- categories, err := c.store.Categories(ctx.UserID())
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch categories"))
- return
- }
-
- response.JSON().Standard(categories)
-}
-
-// RemoveCategory is the API handler to remove a category.
-func (c *Controller) RemoveCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- categoryID, err := request.IntegerParam("categoryID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if !c.store.CategoryExists(userID, categoryID) {
- response.JSON().NotFound(errors.New("Category not found"))
- return
- }
-
- if err := c.store.RemoveCategory(userID, categoryID); err != nil {
- response.JSON().ServerError(errors.New("Unable to remove this category"))
- return
- }
-
- response.JSON().NoContent()
-}
diff --git a/server/api/controller/controller.go b/server/api/controller/controller.go
deleted file mode 100644
index c9798e3..0000000
--- a/server/api/controller/controller.go
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "github.com/miniflux/miniflux/reader/feed"
- "github.com/miniflux/miniflux/storage"
-)
-
-// Controller holds all handlers for the API.
-type Controller struct {
- store *storage.Storage
- feedHandler *feed.Handler
-}
-
-// NewController creates a new controller.
-func NewController(store *storage.Storage, feedHandler *feed.Handler) *Controller {
- return &Controller{store: store, feedHandler: feedHandler}
-}
diff --git a/server/api/controller/entry.go b/server/api/controller/entry.go
deleted file mode 100644
index 9c86a7a..0000000
--- a/server/api/controller/entry.go
+++ /dev/null
@@ -1,224 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/api/payload"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// GetFeedEntry is the API handler to get a single feed entry.
-func (c *Controller) GetFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithFeedID(feedID)
- builder.WithEntryID(entryID)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- response.JSON().Standard(entry)
-}
-
-// GetEntry is the API handler to get a single entry.
-func (c *Controller) GetEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithEntryID(entryID)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- response.JSON().Standard(entry)
-}
-
-// GetFeedEntries is the API handler to get all feed entries.
-func (c *Controller) GetFeedEntries(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- status := request.QueryStringParam("status", "")
- if status != "" {
- if err := model.ValidateEntryStatus(status); err != nil {
- response.JSON().BadRequest(err)
- return
- }
- }
-
- order := request.QueryStringParam("order", model.DefaultSortingOrder)
- if err := model.ValidateEntryOrder(order); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
- if err := model.ValidateDirection(direction); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- limit := request.QueryIntegerParam("limit", 100)
- offset := request.QueryIntegerParam("offset", 0)
- if err := model.ValidateRange(offset, limit); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithFeedID(feedID)
- builder.WithStatus(status)
- builder.WithOrder(order)
- builder.WithDirection(direction)
- builder.WithOffset(offset)
- builder.WithLimit(limit)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to count the number of entries"))
- return
- }
-
- response.JSON().Standard(&payload.EntriesResponse{Total: count, Entries: entries})
-}
-
-// GetEntries is the API handler to fetch entries.
-func (c *Controller) GetEntries(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
-
- status := request.QueryStringParam("status", "")
- if status != "" {
- if err := model.ValidateEntryStatus(status); err != nil {
- response.JSON().BadRequest(err)
- return
- }
- }
-
- order := request.QueryStringParam("order", model.DefaultSortingOrder)
- if err := model.ValidateEntryOrder(order); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
- if err := model.ValidateDirection(direction); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- limit := request.QueryIntegerParam("limit", 100)
- offset := request.QueryIntegerParam("offset", 0)
- if err := model.ValidateRange(offset, limit); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithStatus(status)
- builder.WithOrder(order)
- builder.WithDirection(direction)
- builder.WithOffset(offset)
- builder.WithLimit(limit)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to count the number of entries"))
- return
- }
-
- response.JSON().Standard(&payload.EntriesResponse{Total: count, Entries: entries})
-}
-
-// SetEntryStatus is the API handler to change the status of entries.
-func (c *Controller) SetEntryStatus(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
-
- entryIDs, status, err := payload.DecodeEntryStatusPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(errors.New("Invalid JSON payload"))
- return
- }
-
- if err := model.ValidateEntryStatus(status); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil {
- response.JSON().ServerError(errors.New("Unable to change entries status"))
- return
- }
-
- response.JSON().NoContent()
-}
-
-// ToggleBookmark is the API handler to toggle bookmark status.
-func (c *Controller) ToggleBookmark(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if err := c.store.ToggleBookmark(userID, entryID); err != nil {
- response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
- return
- }
-
- response.JSON().NoContent()
-}
diff --git a/server/api/controller/feed.go b/server/api/controller/feed.go
deleted file mode 100644
index fcaeee7..0000000
--- a/server/api/controller/feed.go
+++ /dev/null
@@ -1,179 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/server/api/payload"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// CreateFeed is the API handler to create a new feed.
-func (c *Controller) CreateFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedURL, categoryID, err := payload.DecodeFeedCreationPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if feedURL == "" {
- response.JSON().BadRequest(errors.New("The feed_url is required"))
- return
- }
-
- if categoryID <= 0 {
- response.JSON().BadRequest(errors.New("The category_id is required"))
- return
- }
-
- if c.store.FeedURLExists(userID, feedURL) {
- response.JSON().BadRequest(errors.New("This feed_url already exists"))
- return
- }
-
- if !c.store.CategoryExists(userID, categoryID) {
- response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
- return
- }
-
- feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to create this feed"))
- return
- }
-
- type result struct {
- FeedID int64 `json:"feed_id"`
- }
-
- response.JSON().Created(&result{FeedID: feed.ID})
-}
-
-// RefreshFeed is the API handler to refresh a feed.
-func (c *Controller) RefreshFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if !c.store.FeedExists(userID, feedID) {
- response.JSON().NotFound(errors.New("Unable to find this feed"))
- return
- }
-
- err = c.feedHandler.RefreshFeed(userID, feedID)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to refresh this feed"))
- return
- }
-
- response.JSON().NoContent()
-}
-
-// UpdateFeed is the API handler that is used to update a feed.
-func (c *Controller) UpdateFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- newFeed, err := payload.DecodeFeedModificationPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) {
- response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
- return
- }
-
- originalFeed, err := c.store.FeedByID(userID, feedID)
- if err != nil {
- response.JSON().NotFound(errors.New("Unable to find this feed"))
- return
- }
-
- if originalFeed == nil {
- response.JSON().NotFound(errors.New("Feed not found"))
- return
- }
-
- originalFeed.Merge(newFeed)
- if err := c.store.UpdateFeed(originalFeed); err != nil {
- response.JSON().ServerError(errors.New("Unable to update this feed"))
- return
- }
-
- originalFeed, err = c.store.FeedByID(userID, feedID)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch this feed"))
- return
- }
-
- response.JSON().Created(originalFeed)
-}
-
-// GetFeeds is the API handler that get all feeds that belongs to the given user.
-func (c *Controller) GetFeeds(ctx *core.Context, request *core.Request, response *core.Response) {
- feeds, err := c.store.Feeds(ctx.UserID())
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch feeds from the database"))
- return
- }
-
- response.JSON().Standard(feeds)
-}
-
-// GetFeed is the API handler to get a feed.
-func (c *Controller) GetFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- feed, err := c.store.FeedByID(userID, feedID)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch this feed"))
- return
- }
-
- if feed == nil {
- response.JSON().NotFound(errors.New("Feed not found"))
- return
- }
-
- response.JSON().Standard(feed)
-}
-
-// RemoveFeed is the API handler to remove a feed.
-func (c *Controller) RemoveFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if !c.store.FeedExists(userID, feedID) {
- response.JSON().NotFound(errors.New("Feed not found"))
- return
- }
-
- if err := c.store.RemoveFeed(userID, feedID); err != nil {
- response.JSON().ServerError(errors.New("Unable to remove this feed"))
- return
- }
-
- response.JSON().NoContent()
-}
diff --git a/server/api/controller/icon.go b/server/api/controller/icon.go
deleted file mode 100644
index b8e7a61..0000000
--- a/server/api/controller/icon.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/server/api/payload"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// FeedIcon returns a feed icon.
-func (c *Controller) FeedIcon(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if !c.store.HasIcon(feedID) {
- response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
- return
- }
-
- icon, err := c.store.IconByFeedID(userID, feedID)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch feed icon"))
- return
- }
-
- if icon == nil {
- response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
- return
- }
-
- response.JSON().Standard(&payload.FeedIcon{
- ID: icon.ID,
- MimeType: icon.MimeType,
- Data: icon.DataURL(),
- })
-}
diff --git a/server/api/controller/subscription.go b/server/api/controller/subscription.go
deleted file mode 100644
index aa2a26f..0000000
--- a/server/api/controller/subscription.go
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "errors"
- "fmt"
-
- "github.com/miniflux/miniflux/reader/subscription"
- "github.com/miniflux/miniflux/server/api/payload"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// GetSubscriptions is the API handler to find subscriptions.
-func (c *Controller) GetSubscriptions(ctx *core.Context, request *core.Request, response *core.Response) {
- websiteURL, err := payload.DecodeURLPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- subscriptions, err := subscription.FindSubscriptions(websiteURL)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to discover subscriptions"))
- return
- }
-
- if subscriptions == nil {
- response.JSON().NotFound(fmt.Errorf("No subscription found"))
- return
- }
-
- response.JSON().Standard(subscriptions)
-}
diff --git a/server/api/controller/user.go b/server/api/controller/user.go
deleted file mode 100644
index a925908..0000000
--- a/server/api/controller/user.go
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package api
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/server/api/payload"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// CreateUser is the API handler to create a new user.
-func (c *Controller) CreateUser(ctx *core.Context, request *core.Request, response *core.Response) {
- if !ctx.IsAdminUser() {
- response.JSON().Forbidden()
- return
- }
-
- user, err := payload.DecodeUserPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if err := user.ValidateUserCreation(); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if c.store.UserExists(user.Username) {
- response.JSON().BadRequest(errors.New("This user already exists"))
- return
- }
-
- err = c.store.CreateUser(user)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to create this user"))
- return
- }
-
- user.Password = ""
- response.JSON().Created(user)
-}
-
-// UpdateUser is the API handler to update the given user.
-func (c *Controller) UpdateUser(ctx *core.Context, request *core.Request, response *core.Response) {
- if !ctx.IsAdminUser() {
- response.JSON().Forbidden()
- return
- }
-
- userID, err := request.IntegerParam("userID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- user, err := payload.DecodeUserPayload(request.Body())
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- if err := user.ValidateUserModification(); err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- originalUser, err := c.store.UserByID(userID)
- if err != nil {
- response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
- return
- }
-
- if originalUser == nil {
- response.JSON().NotFound(errors.New("User not found"))
- return
- }
-
- originalUser.Merge(user)
- if err = c.store.UpdateUser(originalUser); err != nil {
- response.JSON().ServerError(errors.New("Unable to update this user"))
- return
- }
-
- response.JSON().Created(originalUser)
-}
-
-// Users is the API handler to get the list of users.
-func (c *Controller) Users(ctx *core.Context, request *core.Request, response *core.Response) {
- if !ctx.IsAdminUser() {
- response.JSON().Forbidden()
- return
- }
-
- users, err := c.store.Users()
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch the list of users"))
- return
- }
-
- response.JSON().Standard(users)
-}
-
-// UserByID is the API handler to fetch the given user by the ID.
-func (c *Controller) UserByID(ctx *core.Context, request *core.Request, response *core.Response) {
- if !ctx.IsAdminUser() {
- response.JSON().Forbidden()
- return
- }
-
- userID, err := request.IntegerParam("userID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- user, err := c.store.UserByID(userID)
- if err != nil {
- response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
- return
- }
-
- if user == nil {
- response.JSON().NotFound(errors.New("User not found"))
- return
- }
-
- response.JSON().Standard(user)
-}
-
-// UserByUsername is the API handler to fetch the given user by the username.
-func (c *Controller) UserByUsername(ctx *core.Context, request *core.Request, response *core.Response) {
- if !ctx.IsAdminUser() {
- response.JSON().Forbidden()
- return
- }
-
- username := request.StringParam("username", "")
- user, err := c.store.UserByUsername(username)
- if err != nil {
- response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
- return
- }
-
- if user == nil {
- response.JSON().NotFound(errors.New("User not found"))
- return
- }
-
- response.JSON().Standard(user)
-}
-
-// RemoveUser is the API handler to remove an existing user.
-func (c *Controller) RemoveUser(ctx *core.Context, request *core.Request, response *core.Response) {
- if !ctx.IsAdminUser() {
- response.JSON().Forbidden()
- return
- }
-
- userID, err := request.IntegerParam("userID")
- if err != nil {
- response.JSON().BadRequest(err)
- return
- }
-
- user, err := c.store.UserByID(userID)
- if err != nil {
- response.JSON().ServerError(errors.New("Unable to fetch this user from the database"))
- return
- }
-
- if user == nil {
- response.JSON().NotFound(errors.New("User not found"))
- return
- }
-
- if err := c.store.RemoveUser(user.ID); err != nil {
- response.JSON().BadRequest(errors.New("Unable to remove this user from the database"))
- return
- }
-
- response.JSON().NoContent()
-}
diff --git a/server/api/payload/payload.go b/server/api/payload/payload.go
deleted file mode 100644
index 25cd657..0000000
--- a/server/api/payload/payload.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package payload
-
-import (
- "encoding/json"
- "fmt"
- "io"
-
- "github.com/miniflux/miniflux/model"
-)
-
-// FeedIcon represents the feed icon response.
-type FeedIcon struct {
- ID int64 `json:"id"`
- MimeType string `json:"mime_type"`
- Data string `json:"data"`
-}
-
-// EntriesResponse represents the response sent when fetching entries.
-type EntriesResponse struct {
- Total int `json:"total"`
- Entries model.Entries `json:"entries"`
-}
-
-// DecodeUserPayload unserialize JSON user object.
-func DecodeUserPayload(data io.Reader) (*model.User, error) {
- var user model.User
-
- decoder := json.NewDecoder(data)
- if err := decoder.Decode(&user); err != nil {
- return nil, fmt.Errorf("Unable to decode user JSON object: %v", err)
- }
-
- return &user, nil
-}
-
-// DecodeURLPayload unserialize JSON subscription object.
-func DecodeURLPayload(data io.Reader) (string, error) {
- type payload struct {
- URL string `json:"url"`
- }
-
- var p payload
- decoder := json.NewDecoder(data)
- if err := decoder.Decode(&p); err != nil {
- return "", fmt.Errorf("invalid JSON payload: %v", err)
- }
-
- return p.URL, nil
-}
-
-// DecodeEntryStatusPayload unserialize JSON entry statuses object.
-func DecodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
- type payload struct {
- EntryIDs []int64 `json:"entry_ids"`
- Status string `json:"status"`
- }
-
- var p payload
- decoder := json.NewDecoder(data)
- if err := decoder.Decode(&p); err != nil {
- return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
- }
-
- return p.EntryIDs, p.Status, nil
-}
-
-// DecodeFeedCreationPayload unserialize JSON feed creation object.
-func DecodeFeedCreationPayload(data io.Reader) (string, int64, error) {
- type payload struct {
- FeedURL string `json:"feed_url"`
- CategoryID int64 `json:"category_id"`
- }
-
- var p payload
- decoder := json.NewDecoder(data)
- if err := decoder.Decode(&p); err != nil {
- return "", 0, fmt.Errorf("invalid JSON payload: %v", err)
- }
-
- return p.FeedURL, p.CategoryID, nil
-}
-
-// DecodeFeedModificationPayload unserialize JSON feed object.
-func DecodeFeedModificationPayload(data io.Reader) (*model.Feed, error) {
- var feed model.Feed
-
- decoder := json.NewDecoder(data)
- if err := decoder.Decode(&feed); err != nil {
- return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err)
- }
-
- return &feed, nil
-}
-
-// DecodeCategoryPayload unserialize JSON category object.
-func DecodeCategoryPayload(data io.Reader) (*model.Category, error) {
- var category model.Category
-
- decoder := json.NewDecoder(data)
- if err := decoder.Decode(&category); err != nil {
- return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
- }
-
- return &category, nil
-}
diff --git a/server/cookie/cookie.go b/server/cookie/cookie.go
deleted file mode 100644
index d1f3e72..0000000
--- a/server/cookie/cookie.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package cookie
-
-import (
- "net/http"
- "time"
-)
-
-// Cookie names.
-const (
- CookieSessionID = "sessionID"
- CookieUserSessionID = "userSessionID"
-
- // Cookie duration in days.
- cookieDuration = 30
-)
-
-// New creates a new cookie.
-func New(name, value string, isHTTPS bool) *http.Cookie {
- return &http.Cookie{
- Name: name,
- Value: value,
- Path: "/",
- Secure: isHTTPS,
- HttpOnly: true,
- Expires: time.Now().Add(cookieDuration * 24 * time.Hour),
- }
-}
-
-// Expired returns an expired cookie.
-func Expired(name string, isHTTPS bool) *http.Cookie {
- return &http.Cookie{
- Name: name,
- Value: "",
- Path: "/",
- Secure: isHTTPS,
- HttpOnly: true,
- MaxAge: -1,
- Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
- }
-}
diff --git a/server/core/context.go b/server/core/context.go
deleted file mode 100644
index 8145b47..0000000
--- a/server/core/context.go
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/crypto"
- "github.com/miniflux/miniflux/locale"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/middleware"
- "github.com/miniflux/miniflux/server/route"
- "github.com/miniflux/miniflux/storage"
-
- "github.com/gorilla/mux"
-)
-
-// Context contains helper functions related to the current request.
-type Context struct {
- writer http.ResponseWriter
- request *http.Request
- store *storage.Storage
- router *mux.Router
- user *model.User
- translator *locale.Translator
-}
-
-// IsAdminUser checks if the logged user is administrator.
-func (c *Context) IsAdminUser() bool {
- if v := c.request.Context().Value(middleware.IsAdminUserContextKey); v != nil {
- return v.(bool)
- }
- return false
-}
-
-// UserTimezone returns the timezone used by the logged user.
-func (c *Context) UserTimezone() string {
- value := c.getContextStringValue(middleware.UserTimezoneContextKey)
- if value == "" {
- value = "UTC"
- }
- return value
-}
-
-// IsAuthenticated returns a boolean if the user is authenticated.
-func (c *Context) IsAuthenticated() bool {
- if v := c.request.Context().Value(middleware.IsAuthenticatedContextKey); v != nil {
- return v.(bool)
- }
- return false
-}
-
-// UserID returns the UserID of the logged user.
-func (c *Context) UserID() int64 {
- if v := c.request.Context().Value(middleware.UserIDContextKey); v != nil {
- return v.(int64)
- }
- return 0
-}
-
-// LoggedUser returns all properties related to the logged user.
-func (c *Context) LoggedUser() *model.User {
- if c.user == nil {
- var err error
- c.user, err = c.store.UserByID(c.UserID())
- if err != nil {
- logger.Fatal("[Context] %v", err)
- }
-
- if c.user == nil {
- logger.Fatal("Unable to find user from context")
- }
- }
-
- return c.user
-}
-
-// UserLanguage get the locale used by the current logged user.
-func (c *Context) UserLanguage() string {
- user := c.LoggedUser()
- return user.Language
-}
-
-// Translate translates a message in the current language.
-func (c *Context) Translate(message string, args ...interface{}) string {
- return c.translator.GetLanguage(c.UserLanguage()).Get(message, args...)
-}
-
-// CSRF returns the current CSRF token.
-func (c *Context) CSRF() string {
- return c.getContextStringValue(middleware.CSRFContextKey)
-}
-
-// SessionID returns the current session ID.
-func (c *Context) SessionID() string {
- return c.getContextStringValue(middleware.SessionIDContextKey)
-}
-
-// UserSessionToken returns the current user session token.
-func (c *Context) UserSessionToken() string {
- return c.getContextStringValue(middleware.UserSessionTokenContextKey)
-}
-
-// OAuth2State returns the current OAuth2 state.
-func (c *Context) OAuth2State() string {
- return c.getContextStringValue(middleware.OAuth2StateContextKey)
-}
-
-// GenerateOAuth2State generate a new OAuth2 state.
-func (c *Context) GenerateOAuth2State() string {
- state := crypto.GenerateRandomString(32)
- c.store.UpdateSessionField(c.SessionID(), "oauth2_state", state)
- return state
-}
-
-// SetFlashMessage defines a new flash message.
-func (c *Context) SetFlashMessage(message string) {
- c.store.UpdateSessionField(c.SessionID(), "flash_message", message)
-}
-
-// FlashMessage returns the flash message and remove it.
-func (c *Context) FlashMessage() string {
- message := c.getContextStringValue(middleware.FlashMessageContextKey)
- c.store.UpdateSessionField(c.SessionID(), "flash_message", "")
- return message
-}
-
-// SetFlashErrorMessage defines a new flash error message.
-func (c *Context) SetFlashErrorMessage(message string) {
- c.store.UpdateSessionField(c.SessionID(), "flash_error_message", message)
-}
-
-// FlashErrorMessage returns the error flash message and remove it.
-func (c *Context) FlashErrorMessage() string {
- message := c.getContextStringValue(middleware.FlashErrorMessageContextKey)
- c.store.UpdateSessionField(c.SessionID(), "flash_error_message", "")
- return message
-}
-
-func (c *Context) getContextStringValue(key *middleware.ContextKey) string {
- if v := c.request.Context().Value(key); v != nil {
- return v.(string)
- }
-
- logger.Error("[Core:Context] Missing key: %s", key)
- return ""
-}
-
-// Route returns the path for the given arguments.
-func (c *Context) Route(name string, args ...interface{}) string {
- return route.Path(c.router, name, args...)
-}
-
-// NewContext creates a new Context.
-func NewContext(w http.ResponseWriter, r *http.Request, store *storage.Storage, router *mux.Router, translator *locale.Translator) *Context {
- return &Context{writer: w, request: r, store: store, router: router, translator: translator}
-}
diff --git a/server/core/handler.go b/server/core/handler.go
deleted file mode 100644
index e6aca98..0000000
--- a/server/core/handler.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "net/http"
- "time"
-
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/locale"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/middleware"
- "github.com/miniflux/miniflux/server/template"
- "github.com/miniflux/miniflux/storage"
- "github.com/miniflux/miniflux/timer"
-
- "github.com/gorilla/mux"
- "github.com/tomasen/realip"
-)
-
-// HandlerFunc is an application HTTP handler.
-type HandlerFunc func(ctx *Context, request *Request, response *Response)
-
-// Handler manages HTTP handlers and middlewares.
-type Handler struct {
- cfg *config.Config
- store *storage.Storage
- translator *locale.Translator
- template *template.Engine
- router *mux.Router
- middleware *middleware.Chain
-}
-
-// Use is a wrapper around an HTTP handler.
-func (h *Handler) Use(f HandlerFunc) http.Handler {
- return h.middleware.WrapFunc(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- defer timer.ExecutionTime(time.Now(), r.URL.Path)
- logger.Debug("[HTTP] %s %s %s", realip.RealIP(r), r.Method, r.URL.Path)
-
- if r.Header.Get("X-Forwarded-Proto") == "https" {
- h.cfg.IsHTTPS = true
- }
-
- ctx := NewContext(w, r, h.store, h.router, h.translator)
- request := NewRequest(w, r)
- response := NewResponse(w, r, h.template)
-
- if ctx.IsAuthenticated() {
- h.template.SetLanguage(ctx.UserLanguage())
- } else {
- h.template.SetLanguage("en_US")
- }
-
- f(ctx, request, response)
- }))
-}
-
-// NewHandler returns a new Handler.
-func NewHandler(cfg *config.Config, store *storage.Storage, router *mux.Router, template *template.Engine, translator *locale.Translator, middleware *middleware.Chain) *Handler {
- return &Handler{
- cfg: cfg,
- store: store,
- translator: translator,
- router: router,
- template: template,
- middleware: middleware,
- }
-}
diff --git a/server/core/html_response.go b/server/core/html_response.go
deleted file mode 100644
index a194163..0000000
--- a/server/core/html_response.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/template"
-)
-
-// HTMLResponse handles HTML responses.
-type HTMLResponse struct {
- writer http.ResponseWriter
- request *http.Request
- template *template.Engine
-}
-
-// Render execute a template and send to the client the generated HTML.
-func (h *HTMLResponse) Render(template string, args map[string]interface{}) {
- h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- h.template.Execute(h.writer, template, args)
-}
-
-// ServerError sends a 500 error to the browser.
-func (h *HTMLResponse) ServerError(err error) {
- h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- h.writer.WriteHeader(http.StatusInternalServerError)
-
- if err != nil {
- logger.Error("[Internal Server Error] %v", err)
- h.writer.Write([]byte("Internal Server Error: " + err.Error()))
- } else {
- h.writer.Write([]byte("Internal Server Error"))
- }
-}
-
-// BadRequest sends a 400 error to the browser.
-func (h *HTMLResponse) BadRequest(err error) {
- h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- h.writer.WriteHeader(http.StatusBadRequest)
-
- if err != nil {
- logger.Error("[Bad Request] %v", err)
- h.writer.Write([]byte("Bad Request: " + err.Error()))
- } else {
- h.writer.Write([]byte("Bad Request"))
- }
-}
-
-// NotFound sends a 404 error to the browser.
-func (h *HTMLResponse) NotFound() {
- h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- h.writer.WriteHeader(http.StatusNotFound)
- h.writer.Write([]byte("Page Not Found"))
-}
-
-// Forbidden sends a 403 error to the browser.
-func (h *HTMLResponse) Forbidden() {
- h.writer.Header().Set("Content-Type", "text/html; charset=utf-8")
- h.writer.WriteHeader(http.StatusForbidden)
- h.writer.Write([]byte("Access Forbidden"))
-}
diff --git a/server/core/json_response.go b/server/core/json_response.go
deleted file mode 100644
index 8ee0b7f..0000000
--- a/server/core/json_response.go
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "encoding/json"
- "errors"
- "net/http"
-
- "github.com/miniflux/miniflux/logger"
-)
-
-// JSONResponse handles JSON responses.
-type JSONResponse struct {
- writer http.ResponseWriter
- request *http.Request
-}
-
-// Standard sends a JSON response with the status code 200.
-func (j *JSONResponse) Standard(v interface{}) {
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusOK)
- j.writer.Write(j.toJSON(v))
-}
-
-// Created sends a JSON response with the status code 201.
-func (j *JSONResponse) Created(v interface{}) {
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusCreated)
- j.writer.Write(j.toJSON(v))
-}
-
-// NoContent sends a JSON response with the status code 204.
-func (j *JSONResponse) NoContent() {
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusNoContent)
-}
-
-// BadRequest sends a JSON response with the status code 400.
-func (j *JSONResponse) BadRequest(err error) {
- logger.Error("[Bad Request] %v", err)
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusBadRequest)
-
- if err != nil {
- j.writer.Write(j.encodeError(err))
- }
-}
-
-// NotFound sends a JSON response with the status code 404.
-func (j *JSONResponse) NotFound(err error) {
- logger.Error("[Not Found] %v", err)
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusNotFound)
- j.writer.Write(j.encodeError(err))
-}
-
-// ServerError sends a JSON response with the status code 500.
-func (j *JSONResponse) ServerError(err error) {
- logger.Error("[Internal Server Error] %v", err)
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusInternalServerError)
-
- if err != nil {
- j.writer.Write(j.encodeError(err))
- }
-}
-
-// Forbidden sends a JSON response with the status code 403.
-func (j *JSONResponse) Forbidden() {
- logger.Info("[API:Forbidden]")
- j.commonHeaders()
- j.writer.WriteHeader(http.StatusForbidden)
- j.writer.Write(j.encodeError(errors.New("Access Forbidden")))
-}
-
-func (j *JSONResponse) commonHeaders() {
- j.writer.Header().Set("Accept", "application/json")
- j.writer.Header().Set("Content-Type", "application/json; charset=utf-8")
-}
-
-func (j *JSONResponse) encodeError(err error) []byte {
- type errorMsg struct {
- ErrorMessage string `json:"error_message"`
- }
-
- tmp := errorMsg{ErrorMessage: err.Error()}
- data, err := json.Marshal(tmp)
- if err != nil {
- logger.Error("encoding error: %v", err)
- }
-
- return data
-}
-
-func (j *JSONResponse) toJSON(v interface{}) []byte {
- b, err := json.Marshal(v)
- if err != nil {
- logger.Error("encoding error: %v", err)
- return []byte("")
- }
-
- return b
-}
-
-// NewJSONResponse returns a new JSONResponse.
-func NewJSONResponse(w http.ResponseWriter, r *http.Request) *JSONResponse {
- return &JSONResponse{request: r, writer: w}
-}
diff --git a/server/core/request.go b/server/core/request.go
deleted file mode 100644
index f3a3652..0000000
--- a/server/core/request.go
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "fmt"
- "io"
- "mime/multipart"
- "net/http"
- "strconv"
-
- "github.com/gorilla/mux"
- "github.com/miniflux/miniflux/logger"
-)
-
-// Request is a thin wrapper around "http.Request".
-type Request struct {
- writer http.ResponseWriter
- request *http.Request
-}
-
-// Request returns the raw Request struct.
-func (r *Request) Request() *http.Request {
- return r.request
-}
-
-// Body returns the request body.
-func (r *Request) Body() io.ReadCloser {
- return r.request.Body
-}
-
-// File returns uploaded file properties.
-func (r *Request) File(name string) (multipart.File, *multipart.FileHeader, error) {
- return r.request.FormFile(name)
-}
-
-// Cookie returns the cookie value.
-func (r *Request) Cookie(name string) string {
- cookie, err := r.request.Cookie(name)
- if err == http.ErrNoCookie {
- return ""
- }
-
- return cookie.Value
-}
-
-// FormValue returns a form value as integer.
-func (r *Request) FormValue(param string) string {
- return r.request.FormValue(param)
-}
-
-// FormIntegerValue returns a form value as integer.
-func (r *Request) FormIntegerValue(param string) int64 {
- value := r.request.FormValue(param)
- integer, _ := strconv.Atoi(value)
- return int64(integer)
-}
-
-// IntegerParam returns an URL parameter as integer.
-func (r *Request) IntegerParam(param string) (int64, error) {
- vars := mux.Vars(r.request)
- value, err := strconv.Atoi(vars[param])
- if err != nil {
- logger.Error("[IntegerParam] %v", err)
- return 0, fmt.Errorf("%s parameter is not an integer", param)
- }
-
- if value < 0 {
- return 0, nil
- }
-
- return int64(value), nil
-}
-
-// StringParam returns an URL parameter as string.
-func (r *Request) StringParam(param, defaultValue string) string {
- vars := mux.Vars(r.request)
- value := vars[param]
- if value == "" {
- value = defaultValue
- }
- return value
-}
-
-// QueryStringParam returns a querystring parameter as string.
-func (r *Request) QueryStringParam(param, defaultValue string) string {
- value := r.request.URL.Query().Get(param)
- if value == "" {
- value = defaultValue
- }
- return value
-}
-
-// QueryIntegerParam returns a querystring parameter as string.
-func (r *Request) QueryIntegerParam(param string, defaultValue int) int {
- value := r.request.URL.Query().Get(param)
- if value == "" {
- return defaultValue
- }
-
- val, err := strconv.Atoi(value)
- if err != nil {
- return defaultValue
- }
-
- if val < 0 {
- return defaultValue
- }
-
- return val
-}
-
-// HasQueryParam checks if the query string contains the given parameter.
-func (r *Request) HasQueryParam(param string) bool {
- values := r.request.URL.Query()
- _, ok := values[param]
- return ok
-}
-
-// NewRequest returns a new Request struct.
-func NewRequest(w http.ResponseWriter, r *http.Request) *Request {
- return &Request{writer: w, request: r}
-}
diff --git a/server/core/response.go b/server/core/response.go
deleted file mode 100644
index f3fc7a1..0000000
--- a/server/core/response.go
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "net/http"
- "time"
-
- "github.com/miniflux/miniflux/server/template"
-)
-
-// Response handles HTTP responses.
-type Response struct {
- writer http.ResponseWriter
- request *http.Request
- template *template.Engine
-}
-
-// SetCookie send a cookie to the client.
-func (r *Response) SetCookie(cookie *http.Cookie) {
- http.SetCookie(r.writer, cookie)
-}
-
-// JSON returns a JSONResponse.
-func (r *Response) JSON() *JSONResponse {
- r.commonHeaders()
- return NewJSONResponse(r.writer, r.request)
-}
-
-// HTML returns a HTMLResponse.
-func (r *Response) HTML() *HTMLResponse {
- r.commonHeaders()
- return &HTMLResponse{writer: r.writer, request: r.request, template: r.template}
-}
-
-// XML returns a XMLResponse.
-func (r *Response) XML() *XMLResponse {
- r.commonHeaders()
- return &XMLResponse{writer: r.writer, request: r.request}
-}
-
-// Redirect redirects the user to another location.
-func (r *Response) Redirect(path string) {
- http.Redirect(r.writer, r.request, path, http.StatusFound)
-}
-
-// NotModified sends a response with a 304 status code.
-func (r *Response) NotModified() {
- r.commonHeaders()
- r.writer.WriteHeader(http.StatusNotModified)
-}
-
-// Cache returns a response with caching headers.
-func (r *Response) Cache(mimeType, etag string, content []byte, duration time.Duration) {
- r.writer.Header().Set("Content-Type", mimeType)
- r.writer.Header().Set("ETag", etag)
- r.writer.Header().Set("Cache-Control", "public")
- r.writer.Header().Set("Expires", time.Now().Add(duration).Format(time.RFC1123))
-
- if etag == r.request.Header.Get("If-None-Match") {
- r.writer.WriteHeader(http.StatusNotModified)
- } else {
- r.writer.Write(content)
- }
-}
-
-func (r *Response) commonHeaders() {
- r.writer.Header().Set("X-XSS-Protection", "1; mode=block")
- r.writer.Header().Set("X-Content-Type-Options", "nosniff")
- r.writer.Header().Set("X-Frame-Options", "DENY")
-
- // Even if the directive "frame-src" has been deprecated in Firefox,
- // we keep it to stay compatible with other browsers.
- r.writer.Header().Set("Content-Security-Policy", "default-src 'self'; img-src *; media-src *; frame-src *; child-src *")
-}
-
-// NewResponse returns a new Response.
-func NewResponse(w http.ResponseWriter, r *http.Request, template *template.Engine) *Response {
- return &Response{writer: w, request: r, template: template}
-}
diff --git a/server/core/xml_response.go b/server/core/xml_response.go
deleted file mode 100644
index e9a2d3f..0000000
--- a/server/core/xml_response.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package core
-
-import (
- "fmt"
- "net/http"
-)
-
-// XMLResponse handles XML responses.
-type XMLResponse struct {
- writer http.ResponseWriter
- request *http.Request
-}
-
-// Download force the download of a XML document.
-func (x *XMLResponse) Download(filename, data string) {
- x.writer.Header().Set("Content-Type", "text/xml")
- x.writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
- x.writer.Write([]byte(data))
-}
diff --git a/server/fever/fever.go b/server/fever/fever.go
deleted file mode 100644
index 6690b7f..0000000
--- a/server/fever/fever.go
+++ /dev/null
@@ -1,631 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package fever
-
-import (
- "strconv"
- "strings"
- "time"
-
- "github.com/miniflux/miniflux/integration"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/storage"
-)
-
-type baseResponse struct {
- Version int `json:"api_version"`
- Authenticated int `json:"auth"`
- LastRefresh int64 `json:"last_refreshed_on_time"`
-}
-
-func (b *baseResponse) SetCommonValues() {
- b.Version = 3
- b.Authenticated = 1
- b.LastRefresh = time.Now().Unix()
-}
-
-/*
-The default response is a JSON object containing two members:
-
- api_version contains the version of the API responding (positive integer)
- auth whether the request was successfully authenticated (boolean integer)
-
-The API can also return XML by passing xml as the optional value of the api argument like so:
-
-http://yourdomain.com/fever/?api=xml
-
-The top level XML element is named response.
-
-The response to each successfully authenticated request will have auth set to 1 and include
-at least one additional member:
-
- last_refreshed_on_time contains the time of the most recently refreshed (not updated)
- feed (Unix timestamp/integer)
-
-*/
-func newBaseResponse() baseResponse {
- r := baseResponse{}
- r.SetCommonValues()
- return r
-}
-
-type groupsResponse struct {
- baseResponse
- Groups []group `json:"groups"`
- FeedsGroups []feedsGroups `json:"feeds_groups"`
-}
-
-type feedsResponse struct {
- baseResponse
- Feeds []feed `json:"feeds"`
- FeedsGroups []feedsGroups `json:"feeds_groups"`
-}
-
-type faviconsResponse struct {
- baseResponse
- Favicons []favicon `json:"favicons"`
-}
-
-type itemsResponse struct {
- baseResponse
- Items []item `json:"items"`
- Total int `json:"total_items"`
-}
-
-type unreadResponse struct {
- baseResponse
- ItemIDs string `json:"unread_item_ids"`
-}
-
-type savedResponse struct {
- baseResponse
- ItemIDs string `json:"saved_item_ids"`
-}
-
-type group struct {
- ID int64 `json:"id"`
- Title string `json:"title"`
-}
-
-type feedsGroups struct {
- GroupID int64 `json:"group_id"`
- FeedIDs string `json:"feed_ids"`
-}
-
-type feed struct {
- ID int64 `json:"id"`
- FaviconID int64 `json:"favicon_id"`
- Title string `json:"title"`
- URL string `json:"url"`
- SiteURL string `json:"site_url"`
- IsSpark int `json:"is_spark"`
- LastUpdated int64 `json:"last_updated_on_time"`
-}
-
-type item struct {
- ID int64 `json:"id"`
- FeedID int64 `json:"feed_id"`
- Title string `json:"title"`
- Author string `json:"author"`
- HTML string `json:"html"`
- URL string `json:"url"`
- IsSaved int `json:"is_saved"`
- IsRead int `json:"is_read"`
- CreatedAt int64 `json:"created_on_time"`
-}
-
-type favicon struct {
- ID int64 `json:"id"`
- Data string `json:"data"`
-}
-
-// Controller implements the Fever API.
-type Controller struct {
- store *storage.Storage
-}
-
-// Handler handles Fever API calls
-func (c *Controller) Handler(ctx *core.Context, request *core.Request, response *core.Response) {
- switch {
- case request.HasQueryParam("groups"):
- c.handleGroups(ctx, request, response)
- case request.HasQueryParam("feeds"):
- c.handleFeeds(ctx, request, response)
- case request.HasQueryParam("favicons"):
- c.handleFavicons(ctx, request, response)
- case request.HasQueryParam("unread_item_ids"):
- c.handleUnreadItems(ctx, request, response)
- case request.HasQueryParam("saved_item_ids"):
- c.handleSavedItems(ctx, request, response)
- case request.HasQueryParam("items"):
- c.handleItems(ctx, request, response)
- case request.FormValue("mark") == "item":
- c.handleWriteItems(ctx, request, response)
- case request.FormValue("mark") == "feed":
- c.handleWriteFeeds(ctx, request, response)
- case request.FormValue("mark") == "group":
- c.handleWriteGroups(ctx, request, response)
- default:
- response.JSON().Standard(newBaseResponse())
- }
-}
-
-/*
-A request with the groups argument will return two additional members:
-
- groups contains an array of group objects
- feeds_groups contains an array of feeds_group objects
-
-A group object has the following members:
-
- id (positive integer)
- title (utf-8 string)
-
-The feeds_group object is documented under “Feeds/Groups Relationships.”
-
-The “Kindling” super group is not included in this response and is composed of all feeds with
-an is_spark equal to 0.
-
-The “Sparks” super group is not included in this response and is composed of all feeds with an
-is_spark equal to 1.
-
-*/
-func (c *Controller) handleGroups(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Fetching groups for userID=%d", userID)
-
- categories, err := c.store.Categories(userID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- feeds, err := c.store.Feeds(userID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- var result groupsResponse
- for _, category := range categories {
- result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
- }
-
- result.FeedsGroups = c.buildFeedGroups(feeds)
- result.SetCommonValues()
- response.JSON().Standard(result)
-}
-
-/*
-A request with the feeds argument will return two additional members:
-
- feeds contains an array of group objects
- feeds_groups contains an array of feeds_group objects
-
-A feed object has the following members:
-
- id (positive integer)
- favicon_id (positive integer)
- title (utf-8 string)
- url (utf-8 string)
- site_url (utf-8 string)
- is_spark (boolean integer)
- last_updated_on_time (Unix timestamp/integer)
-
-The feeds_group object is documented under “Feeds/Groups Relationships.”
-
-The “All Items” super feed is not included in this response and is composed of all items from all feeds
-that belong to a given group. For the “Kindling” super group and all user created groups the items
-should be limited to feeds with an is_spark equal to 0.
-
-For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
-*/
-func (c *Controller) handleFeeds(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
-
- feeds, err := c.store.Feeds(userID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- var result feedsResponse
- result.Feeds = make([]feed, 0)
- for _, f := range feeds {
- subscripion := feed{
- ID: f.ID,
- Title: f.Title,
- URL: f.FeedURL,
- SiteURL: f.SiteURL,
- IsSpark: 0,
- LastUpdated: f.CheckedAt.Unix(),
- }
-
- if f.Icon != nil {
- subscripion.FaviconID = f.Icon.IconID
- }
-
- result.Feeds = append(result.Feeds, subscripion)
- }
-
- result.FeedsGroups = c.buildFeedGroups(feeds)
- result.SetCommonValues()
- response.JSON().Standard(result)
-}
-
-/*
-A request with the favicons argument will return one additional member:
-
- favicons contains an array of favicon objects
-
-A favicon object has the following members:
-
- id (positive integer)
- data (base64 encoded image data; prefixed by image type)
-
-An example data value:
-
- image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
-
-The data member of a favicon object can be used with the data: protocol to embed an image in CSS or HTML.
-A PHP/HTML example:
-
- echo '<img src="data:'.$favicon['data'].'">';
-*/
-func (c *Controller) handleFavicons(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
-
- icons, err := c.store.Icons(userID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- var result faviconsResponse
- for _, i := range icons {
- result.Favicons = append(result.Favicons, favicon{
- ID: i.ID,
- Data: i.DataURL(),
- })
- }
-
- result.SetCommonValues()
- response.JSON().Standard(result)
-}
-
-/*
-A request with the items argument will return two additional members:
-
- items contains an array of item objects
- total_items contains the total number of items stored in the database (added in API version 2)
-
-An item object has the following members:
-
- id (positive integer)
- feed_id (positive integer)
- title (utf-8 string)
- author (utf-8 string)
- html (utf-8 string)
- url (utf-8 string)
- is_saved (boolean integer)
- is_read (boolean integer)
- created_on_time (Unix timestamp/integer)
-
-Most servers won’t have enough memory allocated to PHP to dump all items at once.
-Three optional arguments control determine the items included in the response.
-
- Use the since_id argument with the highest id of locally cached items to request 50 additional items.
- Repeat until the items array in the response is empty.
-
- Use the max_id argument with the lowest id of locally cached items (or 0 initially) to request 50 previous items.
- Repeat until the items array in the response is empty. (added in API version 2)
-
- Use the with_ids argument with a comma-separated list of item ids to request (a maximum of 50) specific items.
- (added in API version 2)
-
-*/
-func (c *Controller) handleItems(ctx *core.Context, request *core.Request, response *core.Response) {
- var result itemsResponse
-
- userID := ctx.UserID()
- logger.Debug("[Fever] Fetching items for userID=%d", userID)
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithLimit(50)
- builder.WithOrder("id")
- builder.WithDirection(model.DefaultSortingDirection)
-
- sinceID := request.QueryIntegerParam("since_id", 0)
- if sinceID > 0 {
- builder.WithGreaterThanEntryID(int64(sinceID))
- }
-
- maxID := request.QueryIntegerParam("max_id", 0)
- if maxID > 0 {
- builder.WithOffset(maxID)
- }
-
- csvItemIDs := request.QueryStringParam("with_ids", "")
- if csvItemIDs != "" {
- var itemIDs []int64
-
- for _, strItemID := range strings.Split(csvItemIDs, ",") {
- strItemID = strings.TrimSpace(strItemID)
- itemID, _ := strconv.Atoi(strItemID)
- itemIDs = append(itemIDs, int64(itemID))
- }
-
- builder.WithEntryIDs(itemIDs)
- }
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(userID)
- builder.WithoutStatus(model.EntryStatusRemoved)
- result.Total, err = builder.CountEntries()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- for _, entry := range entries {
- isRead := 0
- if entry.Status == model.EntryStatusRead {
- isRead = 1
- }
-
- isSaved := 0
- if entry.Starred {
- isSaved = 1
- }
-
- result.Items = append(result.Items, item{
- ID: entry.ID,
- FeedID: entry.FeedID,
- Title: entry.Title,
- Author: entry.Author,
- HTML: entry.Content,
- URL: entry.URL,
- IsSaved: isSaved,
- IsRead: isRead,
- CreatedAt: entry.Date.Unix(),
- })
- }
-
- result.SetCommonValues()
- response.JSON().Standard(result)
-}
-
-/*
-The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
-with the remote Fever installation.
-
-A request with the unread_item_ids argument will return one additional member:
- unread_item_ids (string/comma-separated list of positive integers)
-*/
-func (c *Controller) handleUnreadItems(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithStatus(model.EntryStatusUnread)
- entries, err := builder.GetEntries()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- var itemIDs []string
- for _, entry := range entries {
- itemIDs = append(itemIDs, strconv.FormatInt(entry.ID, 10))
- }
-
- var result unreadResponse
- result.ItemIDs = strings.Join(itemIDs, ",")
- result.SetCommonValues()
- response.JSON().Standard(result)
-}
-
-/*
-The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
-with the remote Fever installation.
-
- A request with the saved_item_ids argument will return one additional member:
-
- saved_item_ids (string/comma-separated list of positive integers)
-*/
-func (c *Controller) handleSavedItems(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithStarred()
-
- entryIDs, err := builder.GetEntryIDs()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- var itemsIDs []string
- for _, entryID := range entryIDs {
- itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
- }
-
- result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
- result.SetCommonValues()
- response.JSON().Standard(result)
-}
-
-/*
- mark=item
- as=? where ? is replaced with read, saved or unsaved
- id=? where ? is replaced with the id of the item to modify
-*/
-func (c *Controller) handleWriteItems(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
-
- entryID := request.FormIntegerValue("id")
- if entryID <= 0 {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- if entry == nil {
- return
- }
-
- switch request.FormValue("as") {
- case "read":
- c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
- case "unread":
- c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
- case "saved":
- if err := c.store.ToggleBookmark(userID, entryID); err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- settings, err := c.store.Integration(userID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- go func() {
- integration.SendEntry(entry, settings)
- }()
- }
-
- response.JSON().Standard(newBaseResponse())
-}
-
-/*
- mark=? where ? is replaced with feed or group
- as=read
- id=? where ? is replaced with the id of the feed or group to modify
- before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
-*/
-func (c *Controller) handleWriteFeeds(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
-
- feedID := request.FormIntegerValue("id")
- if feedID <= 0 {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithStatus(model.EntryStatusUnread)
- builder.WithFeedID(feedID)
-
- before := request.FormIntegerValue("before")
- if before > 0 {
- t := time.Unix(before, 0)
- builder.Before(&t)
- }
-
- entryIDs, err := builder.GetEntryIDs()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- response.JSON().Standard(newBaseResponse())
-}
-
-/*
- mark=? where ? is replaced with feed or group
- as=read
- id=? where ? is replaced with the id of the feed or group to modify
- before=? where ? is replaced with the Unix timestamp of the the local client’s most recent items API request
-*/
-func (c *Controller) handleWriteGroups(ctx *core.Context, request *core.Request, response *core.Response) {
- userID := ctx.UserID()
- logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
-
- groupID := request.FormIntegerValue("id")
- if groupID < 0 {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(userID)
- builder.WithStatus(model.EntryStatusUnread)
- builder.WithCategoryID(groupID)
-
- before := request.FormIntegerValue("before")
- if before > 0 {
- t := time.Unix(before, 0)
- builder.Before(&t)
- }
-
- entryIDs, err := builder.GetEntryIDs()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- response.JSON().Standard(newBaseResponse())
-}
-
-/*
-A feeds_group object has the following members:
-
- group_id (positive integer)
- feed_ids (string/comma-separated list of positive integers)
-
-*/
-func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
- feedsGroupedByCategory := make(map[int64][]string)
- for _, feed := range feeds {
- feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
- }
-
- result := make([]feedsGroups, 0)
- for categoryID, feedIDs := range feedsGroupedByCategory {
- result = append(result, feedsGroups{
- GroupID: categoryID,
- FeedIDs: strings.Join(feedIDs, ","),
- })
- }
-
- return result
-}
-
-// NewController returns a new Fever API.
-func NewController(store *storage.Storage) *Controller {
- return &Controller{store: store}
-}
diff --git a/server/middleware/basic_auth.go b/server/middleware/basic_auth.go
deleted file mode 100644
index 35a9f81..0000000
--- a/server/middleware/basic_auth.go
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
- "context"
- "net/http"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/storage"
-)
-
-// BasicAuthMiddleware is the middleware for HTTP Basic authentication.
-type BasicAuthMiddleware struct {
- store *storage.Storage
-}
-
-// Handler executes the middleware.
-func (b *BasicAuthMiddleware) Handler(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
- errorResponse := `{"error_message": "Not Authorized"}`
-
- username, password, authOK := r.BasicAuth()
- if !authOK {
- logger.Debug("[Middleware:BasicAuth] No authentication headers sent")
- w.WriteHeader(http.StatusUnauthorized)
- w.Write([]byte(errorResponse))
- return
- }
-
- if err := b.store.CheckPassword(username, password); err != nil {
- logger.Info("[Middleware:BasicAuth] Invalid username or password: %s", username)
- w.WriteHeader(http.StatusUnauthorized)
- w.Write([]byte(errorResponse))
- return
- }
-
- user, err := b.store.UserByUsername(username)
- if err != nil {
- logger.Error("[Middleware:BasicAuth] %v", err)
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(errorResponse))
- return
- }
-
- if user == nil {
- logger.Info("[Middleware:BasicAuth] User not found: %s", username)
- w.WriteHeader(http.StatusUnauthorized)
- w.Write([]byte(errorResponse))
- return
- }
-
- logger.Info("[Middleware:BasicAuth] User authenticated: %s", username)
- b.store.SetLastLogin(user.ID)
-
- ctx := r.Context()
- ctx = context.WithValue(ctx, UserIDContextKey, user.ID)
- ctx = context.WithValue(ctx, UserTimezoneContextKey, user.Timezone)
- ctx = context.WithValue(ctx, IsAdminUserContextKey, user.IsAdmin)
- ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
-
- next.ServeHTTP(w, r.WithContext(ctx))
- })
-}
-
-// NewBasicAuthMiddleware returns a new BasicAuthMiddleware.
-func NewBasicAuthMiddleware(s *storage.Storage) *BasicAuthMiddleware {
- return &BasicAuthMiddleware{store: s}
-}
diff --git a/server/middleware/context_keys.go b/server/middleware/context_keys.go
deleted file mode 100644
index 31ad286..0000000
--- a/server/middleware/context_keys.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-// ContextKey represents a context key.
-type ContextKey struct {
- name string
-}
-
-func (c ContextKey) String() string {
- return c.name
-}
-
-var (
- // UserIDContextKey is the context key used to store the user ID.
- UserIDContextKey = &ContextKey{"UserID"}
-
- // UserTimezoneContextKey is the context key used to store the user timezone.
- UserTimezoneContextKey = &ContextKey{"UserTimezone"}
-
- // IsAdminUserContextKey is the context key used to store the user role.
- IsAdminUserContextKey = &ContextKey{"IsAdminUser"}
-
- // IsAuthenticatedContextKey is the context key used to store the authentication flag.
- IsAuthenticatedContextKey = &ContextKey{"IsAuthenticated"}
-
- // UserSessionTokenContextKey is the context key used to store the user session ID.
- UserSessionTokenContextKey = &ContextKey{"UserSessionToken"}
-
- // SessionIDContextKey is the context key used to store the session ID.
- SessionIDContextKey = &ContextKey{"SessionID"}
-
- // CSRFContextKey is the context key used to store CSRF token.
- CSRFContextKey = &ContextKey{"CSRF"}
-
- // OAuth2StateContextKey is the context key used to store OAuth2 state.
- OAuth2StateContextKey = &ContextKey{"OAuth2State"}
-
- // FlashMessageContextKey is the context key used to store a flash message.
- FlashMessageContextKey = &ContextKey{"FlashMessage"}
-
- // FlashErrorMessageContextKey is the context key used to store a flash error message.
- FlashErrorMessageContextKey = &ContextKey{"FlashErrorMessage"}
-)
diff --git a/server/middleware/fever.go b/server/middleware/fever.go
deleted file mode 100644
index 54eb0ca..0000000
--- a/server/middleware/fever.go
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
- "context"
- "net/http"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/storage"
-)
-
-// FeverMiddleware is the middleware that handles Fever API.
-type FeverMiddleware struct {
- store *storage.Storage
-}
-
-// Handler executes the middleware.
-func (f *FeverMiddleware) Handler(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- logger.Debug("[Middleware:Fever]")
-
- apiKey := r.FormValue("api_key")
- user, err := f.store.UserByFeverToken(apiKey)
- if err != nil {
- logger.Error("[Fever] %v", err)
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(`{"api_version": 3, "auth": 0}`))
- return
- }
-
- if user == nil {
- logger.Info("[Middleware:Fever] Fever authentication failure")
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(`{"api_version": 3, "auth": 0}`))
- return
- }
-
- logger.Info("[Middleware:Fever] User #%d is authenticated", user.ID)
- f.store.SetLastLogin(user.ID)
-
- ctx := r.Context()
- ctx = context.WithValue(ctx, UserIDContextKey, user.ID)
- ctx = context.WithValue(ctx, UserTimezoneContextKey, user.Timezone)
- ctx = context.WithValue(ctx, IsAdminUserContextKey, user.IsAdmin)
- ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
-
- next.ServeHTTP(w, r.WithContext(ctx))
- })
-}
-
-// NewFeverMiddleware returns a new FeverMiddleware.
-func NewFeverMiddleware(s *storage.Storage) *FeverMiddleware {
- return &FeverMiddleware{store: s}
-}
diff --git a/server/middleware/middleware.go b/server/middleware/middleware.go
deleted file mode 100644
index 9853bc3..0000000
--- a/server/middleware/middleware.go
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
- "net/http"
-)
-
-// Middleware represents a HTTP middleware.
-type Middleware func(http.Handler) http.Handler
-
-// Chain handles a list of middlewares.
-type Chain struct {
- middlewares []Middleware
-}
-
-// Wrap adds a HTTP handler into the chain.
-func (m *Chain) Wrap(h http.Handler) http.Handler {
- for i := range m.middlewares {
- h = m.middlewares[len(m.middlewares)-1-i](h)
- }
-
- return h
-}
-
-// WrapFunc adds a HTTP handler function into the chain.
-func (m *Chain) WrapFunc(fn http.HandlerFunc) http.Handler {
- return m.Wrap(fn)
-}
-
-// NewChain returns a new Chain.
-func NewChain(middlewares ...Middleware) *Chain {
- return &Chain{append(([]Middleware)(nil), middlewares...)}
-}
diff --git a/server/middleware/session.go b/server/middleware/session.go
deleted file mode 100644
index ad02bb2..0000000
--- a/server/middleware/session.go
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
- "context"
- "net/http"
-
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/cookie"
- "github.com/miniflux/miniflux/storage"
-)
-
-// SessionMiddleware represents a session middleware.
-type SessionMiddleware struct {
- cfg *config.Config
- store *storage.Storage
-}
-
-// Handler execute the middleware.
-func (s *SessionMiddleware) Handler(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- var err error
- session := s.getSessionValueFromCookie(r)
-
- if session == nil {
- logger.Debug("[Middleware:Session] Session not found")
- session, err = s.store.CreateSession()
- if err != nil {
- logger.Error("[Middleware:Session] %v", err)
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, s.cfg.IsHTTPS))
- } else {
- logger.Debug("[Middleware:Session] %s", session)
- }
-
- if r.Method == "POST" {
- formValue := r.FormValue("csrf")
- headerValue := r.Header.Get("X-Csrf-Token")
-
- if session.Data.CSRF != formValue && session.Data.CSRF != headerValue {
- logger.Error(`[Middleware:Session] Invalid or missing CSRF token: Form="%s", Header="%s"`, formValue, headerValue)
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("Invalid or missing CSRF session!"))
- return
- }
- }
-
- ctx := r.Context()
- ctx = context.WithValue(ctx, SessionIDContextKey, session.ID)
- ctx = context.WithValue(ctx, CSRFContextKey, session.Data.CSRF)
- ctx = context.WithValue(ctx, OAuth2StateContextKey, session.Data.OAuth2State)
- ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage)
- ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
- next.ServeHTTP(w, r.WithContext(ctx))
- })
-}
-
-func (s *SessionMiddleware) getSessionValueFromCookie(r *http.Request) *model.Session {
- sessionCookie, err := r.Cookie(cookie.CookieSessionID)
- if err == http.ErrNoCookie {
- return nil
- }
-
- session, err := s.store.Session(sessionCookie.Value)
- if err != nil {
- logger.Error("[Middleware:Session] %v", err)
- return nil
- }
-
- return session
-}
-
-// NewSessionMiddleware returns a new SessionMiddleware.
-func NewSessionMiddleware(cfg *config.Config, store *storage.Storage) *SessionMiddleware {
- return &SessionMiddleware{cfg, store}
-}
diff --git a/server/middleware/user_session.go b/server/middleware/user_session.go
deleted file mode 100644
index 3d1dae6..0000000
--- a/server/middleware/user_session.go
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package middleware
-
-import (
- "context"
- "net/http"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/cookie"
- "github.com/miniflux/miniflux/server/route"
- "github.com/miniflux/miniflux/storage"
-
- "github.com/gorilla/mux"
-)
-
-// UserSessionMiddleware represents a user session middleware.
-type UserSessionMiddleware struct {
- store *storage.Storage
- router *mux.Router
-}
-
-// Handler execute the middleware.
-func (s *UserSessionMiddleware) Handler(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- session := s.getSessionFromCookie(r)
-
- if session == nil {
- logger.Debug("[Middleware:UserSession] Session not found")
- if s.isPublicRoute(r) {
- next.ServeHTTP(w, r)
- } else {
- http.Redirect(w, r, route.Path(s.router, "login"), http.StatusFound)
- }
- } else {
- logger.Debug("[Middleware:UserSession] %s", session)
- ctx := r.Context()
- ctx = context.WithValue(ctx, UserIDContextKey, session.UserID)
- ctx = context.WithValue(ctx, IsAuthenticatedContextKey, true)
- ctx = context.WithValue(ctx, UserSessionTokenContextKey, session.Token)
-
- next.ServeHTTP(w, r.WithContext(ctx))
- }
- })
-}
-
-func (s *UserSessionMiddleware) isPublicRoute(r *http.Request) bool {
- route := mux.CurrentRoute(r)
- switch route.GetName() {
- case "login", "checkLogin", "stylesheet", "javascript", "oauth2Redirect", "oauth2Callback", "appIcon", "favicon", "webManifest":
- return true
- default:
- return false
- }
-}
-
-func (s *UserSessionMiddleware) getSessionFromCookie(r *http.Request) *model.UserSession {
- sessionCookie, err := r.Cookie(cookie.CookieUserSessionID)
- if err == http.ErrNoCookie {
- return nil
- }
-
- session, err := s.store.UserSessionByToken(sessionCookie.Value)
- if err != nil {
- logger.Error("[Middleware:UserSession] %v", err)
- return nil
- }
-
- return session
-}
-
-// NewUserSessionMiddleware returns a new UserSessionMiddleware.
-func NewUserSessionMiddleware(s *storage.Storage, r *mux.Router) *UserSessionMiddleware {
- return &UserSessionMiddleware{store: s, router: r}
-}
diff --git a/server/oauth2/google.go b/server/oauth2/google.go
deleted file mode 100644
index e57e027..0000000
--- a/server/oauth2/google.go
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package oauth2
-
-import (
- "context"
- "encoding/json"
- "fmt"
-
- "golang.org/x/oauth2"
-)
-
-type googleProfile struct {
- Sub string `json:"sub"`
- Email string `json:"email"`
-}
-
-type googleProvider struct {
- clientID string
- clientSecret string
- redirectURL string
-}
-
-func (g googleProvider) GetUserExtraKey() string {
- return "google_id"
-}
-
-func (g googleProvider) GetRedirectURL(state string) string {
- return g.config().AuthCodeURL(state)
-}
-
-func (g googleProvider) GetProfile(code string) (*Profile, error) {
- conf := g.config()
- ctx := context.Background()
- token, err := conf.Exchange(ctx, code)
- if err != nil {
- return nil, err
- }
-
- client := conf.Client(ctx, token)
- resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var user googleProfile
- decoder := json.NewDecoder(resp.Body)
- if err := decoder.Decode(&user); err != nil {
- return nil, fmt.Errorf("unable to unserialize google profile: %v", err)
- }
-
- profile := &Profile{Key: g.GetUserExtraKey(), ID: user.Sub, Username: user.Email}
- return profile, nil
-}
-
-func (g googleProvider) config() *oauth2.Config {
- return &oauth2.Config{
- RedirectURL: g.redirectURL,
- ClientID: g.clientID,
- ClientSecret: g.clientSecret,
- Scopes: []string{"email"},
- Endpoint: oauth2.Endpoint{
- AuthURL: "https://accounts.google.com/o/oauth2/auth",
- TokenURL: "https://accounts.google.com/o/oauth2/token",
- },
- }
-}
-
-func newGoogleProvider(clientID, clientSecret, redirectURL string) *googleProvider {
- return &googleProvider{clientID: clientID, clientSecret: clientSecret, redirectURL: redirectURL}
-}
diff --git a/server/oauth2/manager.go b/server/oauth2/manager.go
deleted file mode 100644
index 08360a9..0000000
--- a/server/oauth2/manager.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package oauth2
-
-import "errors"
-
-// Manager handles OAuth2 providers.
-type Manager struct {
- providers map[string]Provider
-}
-
-// Provider returns the given provider.
-func (m *Manager) Provider(name string) (Provider, error) {
- if provider, found := m.providers[name]; found {
- return provider, nil
- }
-
- return nil, errors.New("oauth2 provider not found")
-}
-
-// AddProvider add a new OAuth2 provider.
-func (m *Manager) AddProvider(name string, provider Provider) {
- m.providers[name] = provider
-}
-
-// NewManager returns a new Manager.
-func NewManager(clientID, clientSecret, redirectURL string) *Manager {
- m := &Manager{providers: make(map[string]Provider)}
- m.AddProvider("google", newGoogleProvider(clientID, clientSecret, redirectURL))
- return m
-}
diff --git a/server/oauth2/profile.go b/server/oauth2/profile.go
deleted file mode 100644
index 488ffb2..0000000
--- a/server/oauth2/profile.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package oauth2
-
-// Profile is the OAuth2 user profile.
-type Profile struct {
- Key string
- ID string
- Username string
-}
diff --git a/server/oauth2/provider.go b/server/oauth2/provider.go
deleted file mode 100644
index c43931c..0000000
--- a/server/oauth2/provider.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package oauth2
-
-// Provider is an interface for OAuth2 providers.
-type Provider interface {
- GetUserExtraKey() string
- GetRedirectURL(state string) string
- GetProfile(code string) (*Profile, error)
-}
diff --git a/server/route/route.go b/server/route/route.go
deleted file mode 100644
index ee574a5..0000000
--- a/server/route/route.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package route
-
-import (
- "strconv"
-
- "github.com/gorilla/mux"
- "github.com/miniflux/miniflux/logger"
-)
-
-// Path returns the defined route based on given arguments.
-func Path(router *mux.Router, name string, args ...interface{}) string {
- route := router.Get(name)
- if route == nil {
- logger.Fatal("[Route] Route not found: %s", name)
- }
-
- var pairs []string
- for _, param := range args {
- switch param.(type) {
- case string:
- pairs = append(pairs, param.(string))
- case int64:
- val := param.(int64)
- pairs = append(pairs, strconv.FormatInt(val, 10))
- }
- }
-
- result, err := route.URLPath(pairs...)
- if err != nil {
- logger.Fatal("[Route] %v", err)
- }
-
- return result.String()
-}
diff --git a/server/routes.go b/server/routes.go
deleted file mode 100644
index e56b8cc..0000000
--- a/server/routes.go
+++ /dev/null
@@ -1,166 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package server
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/scheduler"
-
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/locale"
- "github.com/miniflux/miniflux/reader/feed"
- "github.com/miniflux/miniflux/reader/opml"
- api_controller "github.com/miniflux/miniflux/server/api/controller"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/fever"
- "github.com/miniflux/miniflux/server/middleware"
- "github.com/miniflux/miniflux/server/template"
- ui_controller "github.com/miniflux/miniflux/server/ui/controller"
- "github.com/miniflux/miniflux/storage"
-
- "github.com/gorilla/mux"
-)
-
-func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool) *mux.Router {
- router := mux.NewRouter()
- translator := locale.Load()
- templateEngine := template.NewEngine(cfg, router, translator)
-
- apiController := api_controller.NewController(store, feedHandler)
- feverController := fever.NewController(store)
- uiController := ui_controller.NewController(cfg, store, pool, feedHandler, opml.NewHandler(store))
-
- apiHandler := core.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
- middleware.NewBasicAuthMiddleware(store).Handler,
- ))
-
- feverHandler := core.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
- middleware.NewFeverMiddleware(store).Handler,
- ))
-
- uiHandler := core.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
- middleware.NewUserSessionMiddleware(store, router).Handler,
- middleware.NewSessionMiddleware(cfg, store).Handler,
- ))
-
- router.Handle("/fever/", feverHandler.Use(feverController.Handler))
-
- router.Handle("/v1/users", apiHandler.Use(apiController.CreateUser)).Methods("POST")
- router.Handle("/v1/users", apiHandler.Use(apiController.Users)).Methods("GET")
- router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UserByID)).Methods("GET")
- router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UpdateUser)).Methods("PUT")
- router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.RemoveUser)).Methods("DELETE")
- router.Handle("/v1/users/{username}", apiHandler.Use(apiController.UserByUsername)).Methods("GET")
-
- router.Handle("/v1/categories", apiHandler.Use(apiController.CreateCategory)).Methods("POST")
- router.Handle("/v1/categories", apiHandler.Use(apiController.GetCategories)).Methods("GET")
- router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.UpdateCategory)).Methods("PUT")
- router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.RemoveCategory)).Methods("DELETE")
-
- router.Handle("/v1/discover", apiHandler.Use(apiController.GetSubscriptions)).Methods("POST")
-
- router.Handle("/v1/feeds", apiHandler.Use(apiController.CreateFeed)).Methods("POST")
- router.Handle("/v1/feeds", apiHandler.Use(apiController.GetFeeds)).Methods("Get")
- router.Handle("/v1/feeds/{feedID}/refresh", apiHandler.Use(apiController.RefreshFeed)).Methods("PUT")
- router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.GetFeed)).Methods("GET")
- router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.UpdateFeed)).Methods("PUT")
- router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.RemoveFeed)).Methods("DELETE")
- router.Handle("/v1/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET")
-
- router.Handle("/v1/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET")
- router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET")
- router.Handle("/v1/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET")
- router.Handle("/v1/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT")
- router.Handle("/v1/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
- router.Handle("/v1/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT")
-
- router.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET")
- router.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET")
- router.Handle("/favicon.ico", uiHandler.Use(uiController.Favicon)).Name("favicon").Methods("GET")
- router.Handle("/icon/{filename}", uiHandler.Use(uiController.AppIcon)).Name("appIcon").Methods("GET")
- router.Handle("/manifest.json", uiHandler.Use(uiController.WebManifest)).Name("webManifest").Methods("GET")
-
- router.Handle("/subscribe", uiHandler.Use(uiController.AddSubscription)).Name("addSubscription").Methods("GET")
- router.Handle("/subscribe", uiHandler.Use(uiController.SubmitSubscription)).Name("submitSubscription").Methods("POST")
- router.Handle("/subscriptions", uiHandler.Use(uiController.ChooseSubscription)).Name("chooseSubscription").Methods("POST")
-
- router.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET")
- router.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET")
- router.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET")
-
- router.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET")
- router.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET")
- router.Handle("/feed/{feedID}/remove", uiHandler.Use(uiController.RemoveFeed)).Name("removeFeed").Methods("POST")
- router.Handle("/feed/{feedID}/update", uiHandler.Use(uiController.UpdateFeed)).Name("updateFeed").Methods("POST")
- router.Handle("/feed/{feedID}/entries", uiHandler.Use(uiController.ShowFeedEntries)).Name("feedEntries").Methods("GET")
- router.Handle("/feeds", uiHandler.Use(uiController.ShowFeedsPage)).Name("feeds").Methods("GET")
- router.Handle("/feeds/refresh", uiHandler.Use(uiController.RefreshAllFeeds)).Name("refreshAllFeeds").Methods("GET")
-
- router.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET")
- router.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET")
- router.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET")
- router.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET")
- router.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET")
- router.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET")
-
- router.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
- router.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
- router.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
- router.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST")
-
- router.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
- router.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
- router.Handle("/category/save", uiHandler.Use(uiController.SaveCategory)).Name("saveCategory").Methods("POST")
- router.Handle("/category/{categoryID}/entries", uiHandler.Use(uiController.ShowCategoryEntries)).Name("categoryEntries").Methods("GET")
- router.Handle("/category/{categoryID}/edit", uiHandler.Use(uiController.EditCategory)).Name("editCategory").Methods("GET")
- router.Handle("/category/{categoryID}/update", uiHandler.Use(uiController.UpdateCategory)).Name("updateCategory").Methods("POST")
- router.Handle("/category/{categoryID}/remove", uiHandler.Use(uiController.RemoveCategory)).Name("removeCategory").Methods("POST")
-
- router.Handle("/feed/icon/{iconID}", uiHandler.Use(uiController.ShowIcon)).Name("icon").Methods("GET")
- router.Handle("/proxy/{encodedURL}", uiHandler.Use(uiController.ImageProxy)).Name("proxy").Methods("GET")
-
- router.Handle("/users", uiHandler.Use(uiController.ShowUsers)).Name("users").Methods("GET")
- router.Handle("/user/create", uiHandler.Use(uiController.CreateUser)).Name("createUser").Methods("GET")
- router.Handle("/user/save", uiHandler.Use(uiController.SaveUser)).Name("saveUser").Methods("POST")
- router.Handle("/users/{userID}/edit", uiHandler.Use(uiController.EditUser)).Name("editUser").Methods("GET")
- router.Handle("/users/{userID}/update", uiHandler.Use(uiController.UpdateUser)).Name("updateUser").Methods("POST")
- router.Handle("/users/{userID}/remove", uiHandler.Use(uiController.RemoveUser)).Name("removeUser").Methods("POST")
-
- router.Handle("/about", uiHandler.Use(uiController.AboutPage)).Name("about").Methods("GET")
-
- router.Handle("/settings", uiHandler.Use(uiController.ShowSettings)).Name("settings").Methods("GET")
- router.Handle("/settings", uiHandler.Use(uiController.UpdateSettings)).Name("updateSettings").Methods("POST")
-
- router.Handle("/bookmarklet", uiHandler.Use(uiController.Bookmarklet)).Name("bookmarklet").Methods("GET")
- router.Handle("/integrations", uiHandler.Use(uiController.ShowIntegrations)).Name("integrations").Methods("GET")
- router.Handle("/integration", uiHandler.Use(uiController.UpdateIntegration)).Name("updateIntegration").Methods("POST")
-
- router.Handle("/sessions", uiHandler.Use(uiController.ShowSessions)).Name("sessions").Methods("GET")
- router.Handle("/sessions/{sessionID}/remove", uiHandler.Use(uiController.RemoveSession)).Name("removeSession").Methods("POST")
-
- router.Handle("/export", uiHandler.Use(uiController.Export)).Name("export").Methods("GET")
- router.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET")
- router.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST")
-
- router.Handle("/oauth2/{provider}/unlink", uiHandler.Use(uiController.OAuth2Unlink)).Name("oauth2Unlink").Methods("GET")
- router.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET")
- router.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET")
-
- router.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST")
- router.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET")
- router.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET")
-
- router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("OK"))
- })
-
- router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/plain")
- w.Write([]byte("User-agent: *\nDisallow: /"))
- })
-
- return router
-}
diff --git a/server/server.go b/server/server.go
deleted file mode 100644
index 4627b83..0000000
--- a/server/server.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package server
-
-import (
- "crypto/tls"
- "net/http"
- "time"
-
- "github.com/gorilla/mux"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/scheduler"
- "golang.org/x/crypto/acme/autocert"
-
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/reader/feed"
- "github.com/miniflux/miniflux/storage"
-)
-
-// NewServer returns a new HTTP server.
-func NewServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *http.Server {
- return startServer(cfg, getRoutes(cfg, store, feedHandler, pool))
-}
-
-func startServer(cfg *config.Config, handler *mux.Router) *http.Server {
- certFile := cfg.Get("CERT_FILE", config.DefaultCertFile)
- keyFile := cfg.Get("KEY_FILE", config.DefaultKeyFile)
- certDomain := cfg.Get("CERT_DOMAIN", config.DefaultCertDomain)
- certCache := cfg.Get("CERT_CACHE", config.DefaultCertCache)
- server := &http.Server{
- ReadTimeout: 5 * time.Second,
- WriteTimeout: 10 * time.Second,
- IdleTimeout: 60 * time.Second,
- Addr: cfg.Get("LISTEN_ADDR", config.DefaultListenAddr),
- Handler: handler,
- }
-
- if certDomain != "" && certCache != "" {
- cfg.IsHTTPS = true
- server.Addr = ":https"
- certManager := autocert.Manager{
- Cache: autocert.DirCache(certCache),
- Prompt: autocert.AcceptTOS,
- HostPolicy: autocert.HostWhitelist(certDomain),
- }
-
- go func() {
- logger.Info(`Listening on "%s" by using auto-configured certificate for "%s"`, server.Addr, certDomain)
- logger.Fatal(server.Serve(certManager.Listener()).Error())
- }()
- } else if certFile != "" && keyFile != "" {
- server.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
- cfg.IsHTTPS = true
-
- go func() {
- logger.Info(`Listening on "%s" by using certificate "%s" and key "%s"`, server.Addr, certFile, keyFile)
- logger.Fatal(server.ListenAndServeTLS(certFile, keyFile).Error())
- }()
- } else {
- go func() {
- logger.Info(`Listening on "%s" without TLS`, server.Addr)
- logger.Fatal(server.ListenAndServe().Error())
- }()
- }
-
- return server
-}
diff --git a/server/static/bin.go b/server/static/bin.go
deleted file mode 100644
index b464f30..0000000
--- a/server/static/bin.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Code generated by go generate; DO NOT EDIT.
-// 2017-12-22 11:25:01.957187237 -0800 PST m=+0.022154999
-
-package static
-
-var Binaries = map[string]string{
- "favicon.ico": `AAABAAEAQEAAAAEAIAAoQgAAFgAAACgAAABAAAAAgAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAADoAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAFf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAAYAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAADf///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAADYAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAGf///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAYQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADe////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADZAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAGEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA3v///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAVv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAABhAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAN7///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAYQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADe////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADZAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAGEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA3v///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAVv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAABhAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAN7///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAYQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADe////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADZAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAGEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA3v///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAVv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAABhAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAN7///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAYQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADe////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADZAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAGEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA3v///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAVv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAABhAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAN7///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAYQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADe////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADZAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAGEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA3v///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAVv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAABhAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAN7///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAYQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADe////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADZAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAGEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA3v///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA2QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAXf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAABhAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAOX///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGv///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAaAAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADz////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADVAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGb///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAB/////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAHYAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/gAAAAn///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAzQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABm////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAApv///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAACDAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAAy////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAANMAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZv///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAANf///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAkAAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAZ////wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAF////8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAKf///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAJ8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAALf///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAACAAAA/QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABR////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAJP///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADVAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD7AAAAI////wD///8A////AP///wD///8A////AP///wD///8AAAAAJwAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAQ////wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD4AAAAJ////wD///8A////AP///wD///8A////AP///wAAAAAdAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAKr///8A////AP///wD///8A////AP///wD///8A////AAAAAIQAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAC7///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAANoAAAAa////AP///wD///8A////AP///wD///8AAAAAgQAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAfP///wD///8A////AP///wD///8A////AAAAABoAAADlAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPcAAAAF////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA6wAAAGUAAAAN////AP///wAAAAAdAAAAhwAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAACyAAAAOf///wD///8AAAAACQAAAEAAAADgAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAADI////AP///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/QAAAOYAAADtAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD0AAAA4AAAAPwAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAiP///wD///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/gAAACX///8A////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA8AAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD+AAAAUQAAAOEAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAALz///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAGcAAADYAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAmP///wAAAAAxAAAA9gAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPoAAAAu////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAA6QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAABWAAAAIwAAAOsAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA4gAAAA////8A////AAAAAFoAAAD9AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAB9////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAAOkAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAVv///wAAAAAxAAAA3QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA0QAAAB3///8A////AP///wD///8AAAAARgAAAOsAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAACQAAAAAf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAADpAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAFb///8A////AAAAAA8AAACsAAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAswAAAA3///8A////AP///wD///8A////AP///wAAAAAbAAAAwgAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPMAAABy////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAArwAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAADAAAAAwAAAAMAAAABA////AP///wD///8A////AAAAAEUAAACrAAAA+gAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAPsAAACpAAAAOf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAA9AAAApwAAAPoAAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA5wAAAJEAAAAW////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AAAAABcAAABLAAAAagAAAIkAAACGAAAAZQAAAEQAAAAY////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAAUAAAAQgAAAGMAAACFAAAAjwAAAHsAAABnAAAAMwAAAAH///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AB/+AD/4AP8AH/4AP/gA/wAf/gA/+AD/AB/+AD/4AP8AH/4AP/gA/wAf/gA/+AD/AB/+AD/4AP8AH/4AP/gA/wAf/gA/+AD/AB/+AD/4AP8AH/4AP/gA/wAf/gA/+AD/AB/+AD/4AP8AH/4AP/gA/wAf/gA/+AD/AB/+AD/4AP8AH/4AP/gA/wAf/gA/+AD/AB/+AD/4AP8AH/4AP/gA/wAP/AA/+AD/AA/8AD/4AP8AD/wAH/gA/wAH/AAf+AD/AAf8AA/wAP8AA/gAD/AA/wAB8AAD4AD/AAAAAAAAAP8AAAAAAAAB/wAAAACAAAH/ABAAAMAAA/8AGAAB4AAH/wAcAAPwAAf/AB4AB/gAH/8AH4Af/gA/////+f//5///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8=`,
- "favicon.png": `iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAGJwAABicBTVTYxwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAALMSURBVHic7Zo7a1RRFIW/I8YXaBBEJRJEU8RqQBBBQRBEWxHBwlZUsLRWUFBsA4L4G4IY0TaF2PhEEQwmhuADJIkRUUOMr2RZ3Em8mcxkzrkPtjhnwS7msveadT/Ofc44SbSyllkHsFYEYB3AWhGAdQBrRQDWAawVAVgHsFYEYB3AWhGAdQBrLS/L2Dm3CdgFbK3WDPC6Wi8kjWX03QBUgG3AdmAN8LFaT4CnCnjEdbW9zrk+YL3n/AVJd2vmDwKngMNAW4O538BNoEfSfa+gzu0DzgBHl/AFGAN6gcuSPjQ1lrSggHFAnnUsNdcO3AiYnas7wNraHCnfLcC9DL6TwNlGvvP+RQAAdgIjGULO1XOgs06WQ8BEDl8BPVRXeikAgK4CQgp4B7SnchwnOW/k9RVwviwAp4HBgkIKuJ5aUd8K9P0JVMoA8LnAkAJmgSPA24J9BfTXA1DvKjAObOT/k4BuScPpjWXcCM0Co8CnErynSFbHTIZZB5xYtDXnIZCuCeAkqUsa0AlcyeiXrtvAnpTvamA/8CbQ50HR54C5egV0LHEtv5hj588t4dsBvA/wmgbaigbwneTYanyzkayELDvf2/RGBi4FelaKBnC1Wciq70Cg7y+gy8O3O9D3QHq+iJPgNc++R4G+/ZJGPPqGSU68vlqX/pAXwKCkl569XwK9b/k0SZoleRL0VaEAngX0TgZ6Pw7obf7U91cr0x/yAhgK6A0BIMB3ZUFyq5tJeQGELL2vAb1TkqYD+lcF9C5QXgAhO/WjJF/I8WYrL4CQnfoXfBep5V+KRgDWAawVAVgHsFYEYB3AWhGAdQBrRQDWAawVAVgHsFYEYB3AWi0PoN6Po3uBFZ7zA5ImvL7Iuc3ADk/faUkPPXtxzu0m+a+Qj4Ykjc7P1gJoNbX8IRABWAewVgRgHcBaEYB1AGtFANYBrBUBWAewVssD+AMBy6wzsaDiAwAAAABJRU5ErkJggg==`,
- "touch-icon-ipad-retina.png": `iVBORw0KGgoAAAANSUhEUgAAAJAAAACQCAYAAADnRuK4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAgCSURBVHic7d1NSFRdHMfx/2iOzeSMQ5SaqZk5RWaO2btZiQVFRW9EYWSRhRZtWrRqWbRoq4twYS0CwcJeFtkqIiQqa1EkGgwSglGaNmOZM5r1fxbPM2D26Nzxf8+9d+L3gbO7nTn38J3rdV7MxsxMADOUYPYCIL4hIBBBQCCCgEAEAYEIAgIRBAQiCAhEEBCIICAQQUAggoBABAGBCAICEQQEIggIRBAQiCAgEEFAIIKAQAQBgQgCAhEEBCIICEQQEIggIBBBQCCCgEAEAYEIAgIRBAQis8xegN7Gxsaoq6uL3rx5Qx8+fKBgMEjBYJBGRkbI5XKRx+Mhj8dDGRkZtGrVKiooKKDExESzl/2HQCBAr1+/pq6uLvry5QsFAgEKBoM0OjpKTqeTnE4nuVwuys3Npby8PPJ6vZSVlWX4Ov+KgDo6OqilpYXu379PHR0d9OPHD83/1uFwUHFxMW3fvp0qKytp+fLlClc6tXA4TK2trXT79m168eIFvX//PuY5MjMzqbS0lLZu3UoHDx6kzMxMBSv9nU3LXyg7deoUdXd3K11IdXU1HT9+XPPx4XCYbty4QXV1dfTu3Tvd1uHz+aiqqorOnDlDc+bM0W3eqbx8+ZLq6+vp3r179O3bN93mtdlsVFZWRjU1NVRZWanuKssarFy5kolI6bh06ZKWpXAoFOKrV69yRkaG0vWkp6dzfX09j46OalpXrNrb23nXrl3K95WIOC8vjxsbG/nXr1+6n0dcBfT06VNetmyZIZseGYsXL+a2tjbxRkf09fXxoUOHDD2HyCgtLeWOjg7dzoU5TgIaGxvj8+fPc0JCgikbn5iYyFeuXBE/g2/dusXz5s0z5Rwiw263c0NDg+g8JrJ8QIODg1xeXm7qpkfGjh07eHh4OOZNDofDfOzYMdPXP3HU1tby+Ph4zOcymaUD8vv9vHTpUtM3e+LYsmVLTBENDg5yWVmZ6ev+v1FVVSW+qlo2IL/fr/xGWXVE3d3dht+zxTrOnj07s3L+Y8mAenp6OCcnx/TNnW4cOHBg2j3r7e21/DlERmNj498TUF9fH+fn55u+qVpGXV3d/+5XIBDgwsJC09endTgcjhn/dmapgMbHx7miosL0DdU67HY7v3r16re9CoVClr3nmW6sX79+RvdDlgro4sWLpm9krGP16tW/bfzp06dNX9NMx/Xr1+M3oNLSUrbZbKZv4kxGc3MzMzM3NTWZvhbJSE9P55GRkfgMKJ6H1+vlzs5Odrlcpq9FOq5du4aAzBipqammr0GP4fV6+efPn5oDwgfKdDI0NGT2EnTh9/vp8ePHmo9HQPCH5uZmzcciIPjDnTt3NH8oLy4/keh2u6m8vJxycnIoKyuLFixYQMPDw/T582fy+/304MEDCgaDZi9Tk9zcXNqwYQNlZWVRdnY2eTweGhgYoP7+fmpra6Nnz54RG/y/kg4ODtLz589p8+bN0Q+Op5voiooKvnnzJn///n3a9Y6NjfHdu3ct+1bC7Nmzubq6mp88eRL1xbve3l4+d+6c4S9xXL58WdNNdFwElJqayk1NTZpOaKKvX7/y3r17TQ9m4igqKuK3b9/GfC6PHj3ilJQUw9a5bds2TeuyfEDr1q3jnp6emDc8Ynh4mIuKikwPh+jfd77D4fCMz6W1tZUTExMNWavL5dL01oalA1qyZAkPDAzMeMMj2tvbTY/n8OHD4vNgZr5w4YJha9byxLVsQG63mzs7O8UbHuHz+UyLZ82aNTG/RTCVvr4+djgchqy7tbU16nos+2t8Q0ODrt/ROnnypG5zxSI5OZlaWlrI4XDoMl9aWhodPXpUl7mi0fJ1KUsGVFJSQkeOHNF1zk2bNuk6n1a1tbWUk5Oj65y7d+/Wdb6pfPz4MfpBWi6bRv8Ie/jwofhSP9no6Cjb7XZDz8PpdPKnT590P5ehoSFOSkpSvv4TJ05EXYvlrkAbN26knTt36j6v3W6nwsJC3eedTk1NDaWnp+s+r9vtNuQr2P39/VGPsVxAlZWVyuY2+nvvKs8lPz9f2dwRgUAg6jGWC2jfvn3K5lZxNZjKwoULae3atcrm93q9yuaOCIVCUY+xVEAlJSW633BONH/+fGVzT7Z//36y2WzK5le5TxHhcDjqMZYKSPVvF2lpaUrnn2jPnj1K509JSVE6P1EcBuTz+ZTOb2RAxcXFSud3Op1K5yciTR/psFRABQUFSud3u91K54+YO3cuZWRkKH0MI/52kRaWCSgpKUn5jaHdblc6f4QRLxfMmmWNj3JZJiCv16t8U5KTk5XOH7FixQrlj6Hl/sQIlgkoOztb+WMYFdCiRYuUPwYCmsTlcil/DKN+hBlxr6XlNRojWCYgI34tNeoKZMSTAVegSYwIyKi/B23EFWh8fFz5Y2hhmYCMeNYaxaiXC6zAMgHp9YErKzDiRT6rsExAfxOV74FZDQICEQQEIggIRBAQiCAgEEFAIIKAQAQBgQgCAhEEBCIICEQQEIggIBBBQCCCgEAEAYEIAgIRBAQiCAhEEBCIICAQQUAggoBABAGBCAICEQQEIggIRBAQiCAgEEFAIIKAQAQBgQgCAhEEBCIICEQQEIggIBCxMTObvQiIX7gCgQgCAhEEBCIICEQQEIggIBBBQCCCgEAEAYEIAgIRBAQiCAhEEBCIICAQQUAggoBABAGBCAICEQQEIggIRBAQiCAgEEFAIIKAQAQBgQgCAhEEBCIICEQQEIggIBD5B/3K+BzBV8ffAAAAAElFTkSuQmCC`,
- "touch-icon-ipad.png": `iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAG7AAABuwBHnU4NQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAP9SURBVHic7Zo/SHJvFMePmaH90Uot0UH6A5aBEuHgIoQXXAKXBhvaHWoOanAMmqOWhsClaKuhWoqWIIoIKiGoMKGGltQCK7qe3+TLq/Z27nPfe3vjx/nAszye53wfPt5/4jUgIgLzRxr+9QZ+OiyIgAURsCACFkTAgghYEAELImBBBCyIgAURsCACFkTAgghYEEGj3gGyLMP29jacnp5CLpeDXC4H+XwebDYbeDweCAQCIEkSBAIB1RmFQgG2trbg9vYWstksZLNZKJVK4HA4wOl0Qm9vL8RiMRgZGYGGBsFjAmtIp9Po9XqFxtHRUW0bvL+/x1QqhR6PBwGAHMFgEDc2Nur6fMXFxQUmk0lsaWlRlOFwOHB2dhYLhYLijDpBS0tLisJ+HwcHB1U91tfX0Wq1CvcBAJQkCZ+enr7c9MvLC05OTqrqXxG1urr6/YJKpRImk0nVG6+MgYEBvLm5+XTDmUwG/X7/X2cAAM7Pz3+fIFmWcWxsTJONV77ly8vLqr3t7u4qPp2UjoWFhe8RND09renGAQDD4TDKsoyIiFdXV2iz2TTPMJlMeHZ2pq+g8fFxzTdeGYuLi5jP59Hn8+mWEQwGf30RugjSc1itVhwdHdU9Z2dn51NBP/5BsVgswv7+vu45Kysrn87r9qBoMpkgEAiA2+2Gh4cHOD8/h/f3d81zenp6oL+/HxARTk5OIJ/Pq+qzubkJxWIRrFZr9Qdan2KNjY2YSqXw8fGxqu/r6ysuLy+j0WjU5JSIRqN4fHxclVEul/Hw8BDdbreqnnt7e/peg8xmc91DYy1ra2t/LWdqauqPF1VExLu7O3S5XMJ9P7vlayoonU5/KQcRUZZltNvtqjMkSfpSToW5uTnh3olEQj9B4XCY3HSFRCKhKsNoNGImk1GUcX19jQaDQah/JBKp66PZXWxmZkZxbTQaVZURj8dhcHBQUW1fXx+4XC6h/oVCoW5OE0Gtra0Qi8UU13d3d6vKicfjQvVdXV1C9boJikQiYDabFdfX3UoVIkmSUL2ooOfn57o5TQQNDQ0J1be1tQlndHZ2gtvtFlrjdDqF6svlct2cJoJ8Pp9QvZojyO/3C69BDV6e00RQR0eHUL2aI8hutwuveXt7E15Ti2YXaRGampqEM9rb24XXfHx8CK+pRRNBIhdotTQ3N+ue8Rk//tf8v4YFEbAgAhZEwIIIWBABCyJgQQQsiIAFEbAgAhZEwIIIWBABCyJgQQQsiIAFEdS9/hIKhSCVSgk18Xq9QvUWi0U4IxQKCdUDAExMTMDw8LDieovFUjdnQC3+G/kfw6cYAQsiYEEELIiABRGwIAIWRMCCCFgQAQsiYEEELIiABRGwIIL/ABedeOtpRvUuAAAAAElFTkSuQmCC`,
- "touch-icon-iphone-retina.png": `iVBORw0KGgoAAAANSUhEUgAAAHIAAAByCAYAAACP3YV9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK9QAACvUBbxZRbgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAaGSURBVHic7Z1fSFNvGMe/+6eujWbLkmXoGlbSwMiigowiggIjjKI/EF50oYEYRH+uggiMIrsxSqgo6iKEILzpMqSC9GY30UVZwRYF0tQgFV1z8/ldxPhpupnu2Tnz6fnAe3UO3/d73g/zeLZzNgsREZRFj9XsAgoPKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIKlIIdrML/C2/fv3Cx48f8ePHDwwPDyMej8Pj8cDj8WDlypWoqKgwrRsRIRwOY2BgAD9//sTw8DAcDgecTieWLVsGv9+PFStW5LRD3opMJBLo7u7Gs2fP0NPTgw8fPiCRSKTd3+v1YvPmzdi5cyeOHTuGdevW5bRfKBTC06dP0dvbi7dv32JkZCTj/m63G1u3bsWOHTtQV1eHbdu2sfaxZHpkoLGxEa9fv2ab7Pbt29i7d2/GfaLRKNrb23H37l0MDQ0teK6amhq0tLSgoaEBVivPGWR0dBQdHR24f/8+Pn/+nFVWRUUFGhsb0dzcDI/Hk305ysD+/fsJANvo6upKO9fY2BhdvHiRnE4n65zBYJCeP3+e6TDnJBaL0fXr16mkpIS1GwBaunQp3bhxgyYmJrLqmBciX716RZWVleyLNHU0NTXR+Pj4vBcoFApRMBjMaTcAVF1dTX19ffPul8J0kR0dHWSz2XK+UKnF+vr1618vzrVr18hutxvSDQAVFxfTixcv5m+RTBZ57tw5wxYpNQKBAH358iXjosTjcTp16pTh3QBQUVERdXd3Lx6RFy5cMGWhAJDf76f+/v5ZjzkWi9G+fftM6waAXC4XvXv3Lv9Ftra2mrpQAGjPnj2UTCanHW8ymaQjR46Y3g0AVVVV0ejoaP6K7OrqMn2RUuPKlSvTjre5udn0TlPH2bNn81NkW1sbeTwe0xcoNQoKCigcDhMR0ePHj03v8+dwOBz0/v37/BNZWFho+uL8ORoaGqivr4/cbrfpXWYbJ06cyD+R+TisViutXbvW9B7pht1up0gkMqfIf/7Tj8nJSXz69MnsGmlJJBK4d+/enPv98yIXA52dnXPuk3effjgcDpSVlWHVqlXweDwYGBhAJBLB4OCg2dUAADabDT6fD2VlZfB6vRgaGkIkEkE0Gs3ZnOFwGKFQCFu2bEm/U76cI2tra+nhw4c0MjIyo0cymaQ3b96Yeo23ceNGam9vp2g0OqPf5OQk9fb20tGjR3M2/82bNzOeI00X6Xa76dGjRxlLTuXBgweGCrTb7XT16tUZbx6k48mTJ2S1Wtl71NfX56/I0tLSBb3jf/78eUMkFhUV0cuXL+fdr62tjb1LeXl5xjlNE1lYWEg9PT3zXiQiosHBQUOuSTs7OxfUb2JiggKBAGsXi8Uy62knhWki79y5s6BFSnH8+PGcSjxz5kxW/W7dusXeKRQKpZ3PlMuP9evXo6mpKauM2tpapjYzcbvduHTpUlYZdXV1TG3+59u3b2m3mSKytbUVNpstq4yamhqmNjNpaWnJ+q63QCCANWvWMDX6TX9/f9pthov0+Xw4fPhw1jnV1dUMbWbn9OnTLDkbNmxgyUnx/fv3tNsMF3nw4EFYLJasc1wuF4qLixkaTWfTpk0oLy9nyaqsrGTJSTE+Pp52m+Ei6+vr2bJKS0vZslIcOnSILWv16tVsWQAQi8XSbjNUpNVqxa5du9jyciFy9+7dbFkul4stC/h9t306DBXp9/vhdDrZ8pYvX86WlSIYDLJlLVmyhC0LACjD188bKpL75O9wOFjzfD4fvF4vW15BQQFb1lwYKrKqqoo1j3uhOF+NQOY/hdwYKrKkpIQ1j/sVyX3OFSuS++TPLZLlYZopjI2NseZlwlCRbrebNY/rKasU3Nel8XicNS8Ti1okN9yvSCMxVKTdnnd3lkzDyP8yudGbr4SgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWgIoWQ8fGokydPsv48Hvej5wcOHGB9ynj79u1sWcDvr1m7fPkyW16mL97N+LODyuJB/7QKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUKQUUK4T96hT9reYw+rgAAAABJRU5ErkJggg==`,
- "touch-icon-iphone.png": `iVBORw0KGgoAAAANSUhEUgAAADkAAAA5CAYAAACMGIOFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAFewAABXsBE7im1wAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAMwSURBVGiB7Zk7SOtgFMf/bYLFZ7VoW6EEikPAF1iwONS6iOBWF50EdRJE7aCLi5urODoUBBcndxfFV3EVB3EoIlIE3XxBtMm5k96bW9oe09zXd78/fEPSc345vyS0TeshIoLg8f7pAX5HpKQokZKiREqKEikpSqSkKFF/BdSyLNzd3cEwDGiaBlWt7jDv7++4ubmBqqoIBoOoq6v7Ur/t6JeXl9jb26vYpOs6RkdHbfvy+TwymQx2dnaQy+Xw9vYGAFAUBZqmIZlMYmFhAbFYjDXY0dERNjc3kc1mcXt7C9M0P18LhUIYGRlBKpXC2NgYPB5PeRj9kK2tLQJQcU1MTHz2GIZBi4uLpCgKqzeZTNL19TWVyvHxMXV1dbFYAKivr4+y2WxJHhFRVZK5XI5isRh7oI8VDAbp7OysaJj19XVSVfXLPJ/PR7u7u+5LPjw8UDQa/fJAH6u2tpbOz88/j51Opx2zAJCiKHRycuKeZCqVokQiUdVQACgej5NpmpTJZKpmAaCOjg56eXlxR9Lr9boyFACanp6mmpoa13gbGxvuSP7Nq6enp0jSlc9JXdeRSCTQ39+PfD6P/f19nJ6eOuZFo1EMDg5iYGAA9/f3ODg4wOHhIav34uICV1dX0HX9+85qr+Tq6mrRmbMsi+bn5x1dibm5ObIsq4i5vLzMZmxvb7t3u66trRUN85HX11fy+Xxf4i0tLZXkmaZJkUiExUmn0+5IappGhUKh5FBERMPDw2ye3++n5+fnsryVlRUWa3x83Nbn+Av67OwsFEUpWxOPx9m8yclJ1NfXl63p7u5msR4fH23bjiVnZmYq1rS0tLB5U1NTFWtaW1tZrKenJ9u2I0m/349QKMSq48b2blgigUCAxSoUCrZtR5Lt7e2sOq5kc3MzGhoaKtY5fWRzJBkOh1l1jY2NrLpIJMKqMwyDVfdzHEm2tbXx4F4ennvSyOF/U44kucNzU+ldutr8F7/xSElRIiVFiZQUJVJSlEhJUSIlRYntKTQcDmNoaKhiU2dnJwseCARYvN7eXhavqanJ0XwecvqQ9g/lv7hdpaQokZKiREqKEikpSqSkKPkGbzop0HEztyoAAAAASUVORK5CYII=`,
-}
-
-var BinariesChecksums = map[string]string{
- "favicon.ico": "abb2a2675b0696252719f51dbfc1efc50affb2f17ec82166e27f9529eec896fb",
- "favicon.png": "86465aec3a1bcd2c9b19ce8b5ccbc41b1209e0246100371ca2ac2a0222d0b67c",
- "touch-icon-ipad-retina.png": "7c2495ef638c56c2479cacce66b676ead381e6606415523bf8c9a814ff48cc0d",
- "touch-icon-ipad.png": "0044edc3bd2f7a240abbe6ddb5a54bb4c5eb8d45692f79b1683a803841cfdf5d",
- "touch-icon-iphone-retina.png": "cfdea4cef7d5c04cc5375ffd919d9a94da739cd0730bc3fb807bb187d8290255",
- "touch-icon-iphone.png": "9a18a70f4389e83d5fbee0c6f6286c3fa2f6db4cdd9b53fc3f2232fde938fd5b",
-}
diff --git a/server/static/bin/favicon.ico b/server/static/bin/favicon.ico
deleted file mode 100644
index 77af6f9..0000000
--- a/server/static/bin/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/server/static/bin/favicon.png b/server/static/bin/favicon.png
deleted file mode 100644
index 7f96f55..0000000
--- a/server/static/bin/favicon.png
+++ /dev/null
Binary files differ
diff --git a/server/static/bin/touch-icon-ipad-retina.png b/server/static/bin/touch-icon-ipad-retina.png
deleted file mode 100644
index 92da239..0000000
--- a/server/static/bin/touch-icon-ipad-retina.png
+++ /dev/null
Binary files differ
diff --git a/server/static/bin/touch-icon-ipad.png b/server/static/bin/touch-icon-ipad.png
deleted file mode 100644
index 4de6120..0000000
--- a/server/static/bin/touch-icon-ipad.png
+++ /dev/null
Binary files differ
diff --git a/server/static/bin/touch-icon-iphone-retina.png b/server/static/bin/touch-icon-iphone-retina.png
deleted file mode 100644
index 71de36e..0000000
--- a/server/static/bin/touch-icon-iphone-retina.png
+++ /dev/null
Binary files differ
diff --git a/server/static/bin/touch-icon-iphone.png b/server/static/bin/touch-icon-iphone.png
deleted file mode 100644
index 1a46c5e..0000000
--- a/server/static/bin/touch-icon-iphone.png
+++ /dev/null
Binary files differ
diff --git a/server/static/css.go b/server/static/css.go
deleted file mode 100644
index e7f678f..0000000
--- a/server/static/css.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Code generated by go generate; DO NOT EDIT.
-// 2017-12-28 18:55:07.393604426 -0800 PST m=+0.020325012
-
-package static
-
-var Stylesheets = map[string]string{
- "black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555;color:#9b9b9b}#modal-left{background:#333;color:#efefef;box-shadow:0 0 10px rgba(82,168,236,.6)}.keyboard-shortcuts li{color:#9b9b9b}.unread-counter-wrapper{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}article.feed-parsing-error{background-color:#343434}.parsing-error{color:#eee}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-enclosure{border-color:#333}`,
- "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input[type=checkbox]{margin-bottom:15px}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}.form-section{border-left:2px dotted #ddd;padding-left:20px;margin-left:10px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#fcfcfc;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}#modal-left{position:fixed;top:0;left:0;bottom:0;width:350px;overflow:auto;background:#f0f0f0;box-shadow:2px 0 5px 0 #ccc;padding:5px;padding-top:30px}#modal-left h3{font-weight:400}.btn-close-modal{position:absolute;top:0;right:0;font-size:1.7em;color:#ccc;padding:0 .2em;margin:10px;text-decoration:none}.btn-close-modal:hover{color:#999}.keyboard-shortcuts li{margin-left:25px;list-style-type:square;color:#333;font-size:.95em;line-height:1.45em}.keyboard-shortcuts p{line-height:1.9em}.login-form{margin:50px auto 0;max-width:280px}.unread-counter-wrapper{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}article.feed-parsing-error{background-color:#fcf8e3;border-color:#aaa}.parsing-error{font-size:.85em;margin-top:2px;color:#333}.parsing-error-count{cursor:pointer}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-actions{margin-bottom:20px}.entry-actions li{display:inline}.entry-actions li:not(:last-child):after{content:"|"}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure{margin-top:15px;margin-bottom:15px}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em;overflow-wrap:break-word}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
-}
-
-var StylesheetsChecksums = map[string]string{
- "black": "832d07879dbb5e91a55055d66797f87003adbb09e5e54234a1ff4d722a33168e",
- "common": "c978d4bfc06bdeb345d55ce7e14ccdf6bd7608d607df2d2eeec37ea74eaeaa67",
-}
diff --git a/server/static/css/black.css b/server/static/css/black.css
deleted file mode 100644
index f97ed2f..0000000
--- a/server/static/css/black.css
+++ /dev/null
@@ -1,219 +0,0 @@
-/* Layout */
-body {
- background: #222;
- color: #efefef;
-}
-
-h1, h2, h3 {
- color: #aaa;
-}
-
-a {
- color: #aaa;
-}
-
-a:focus,
-a:hover {
- color: #ddd;
-}
-
-.header li {
- border-color: #333;
-}
-
-.header a {
- color: #ddd;
- font-weight: 400;
-}
-
-.header .active a {
- font-weight: 400;
- color: #9b9494;
-}
-
-.header a:focus,
-.header a:hover {
- color: rgba(82, 168, 236, 0.85);
-}
-
-.page-header h1 {
- border-color: #333;
-}
-
-.logo a:hover span {
- color: #555;
-}
-
-/* Tables */
-table, th, td {
- border: 1px solid #555;
-}
-
-th {
- background: #333;
- color: #aaa;
- font-weight: 400;
-}
-
-tr:hover {
- background-color: #333;
- color: #aaa;
-}
-
-/* Forms */
-input[type="url"],
-input[type="password"],
-input[type="text"] {
- border: 1px solid #555;
- background: #333;
- color: #ccc;
-}
-
-input[type="url"]:focus,
-input[type="password"]:focus,
-input[type="text"]:focus {
- color: #efefef;
- border-color: rgba(82, 168, 236, 0.8);
- box-shadow: 0 0 8px rgba(82, 168, 236, 0.6);
-}
-
-/* Buttons */
-.button-primary {
- border-color: #444;
- background: #333;
- color: #efefef;
-}
-
-.button-primary:hover,
-.button-primary:focus {
- border-color: #888;
- background: #555;
-}
-
-/* Alerts */
-.alert,
-.alert-success,
-.alert-error,
-.alert-info,
-.alert-normal {
- color: #efefef;
- background-color: #333;
- border-color: #444;
-}
-
-/* Panel */
-.panel {
- background: #333;
- border-color: #555;
- color: #9b9b9b;
-}
-
-/* Modals */
-#modal-left {
- background: #333;
- color: #efefef;
- box-shadow: 0 0 10px rgba(82, 168, 236, 0.6);
-}
-
-/* Keyboard Shortcuts */
-.keyboard-shortcuts li {
- color: #9b9b9b;
-}
-
-/* Counter */
-.unread-counter-wrapper {
- color: #bbb;
-}
-
-/* Category label */
-.category {
- color: #efefef;
- background-color: #333;
- border-color: #444;
-}
-
-.category a {
- color: #999;
-}
-
-.category a:hover,
-.category a:focus {
- color: #aaa;
-}
-
-/* Pagination */
-.pagination a {
- color: #aaa;
-}
-
-.pagination-bottom {
- border-color: #333;
-}
-
-/* List view */
-.item {
- border-color: #666;
- padding: 4px;
-}
-
-.item.current-item {
- border-width: 2px;
- border-color: rgba(82, 168, 236, 0.8);
- box-shadow: 0 0 8px rgba(82, 168, 236, 0.6);
-}
-
-.item-title a {
- font-weight: 400;
-}
-
-.item-status-read .item-title a {
- color: #666;
-}
-
-.item-status-read .item-title a:focus,
-.item-status-read .item-title a:hover {
- color: rgba(82, 168, 236, 0.6);
-}
-
-.item-meta a:hover,
-.item-meta a:focus {
- color: #aaa;
-}
-
-.item-meta li:after {
- color: #ddd;
-}
-
-/* Feeds list */
-article.feed-parsing-error {
- background-color: #343434;
-}
-
-.parsing-error {
- color: #eee;
-}
-
-/* Entry view */
-.entry header {
- border-color: #333;
-}
-
-.entry header h1 a {
- color: #bbb;
-}
-
-.entry-content,
-.entry-content p, ul {
- color: #999;
-}
-
-.entry-content pre,
-.entry-content code {
- color: #fff;
- background: #555;
- border-color: #888;
-}
-
-.entry-enclosure {
- border-color: #333;
-}
diff --git a/server/static/css/common.css b/server/static/css/common.css
deleted file mode 100644
index accefb0..0000000
--- a/server/static/css/common.css
+++ /dev/null
@@ -1,778 +0,0 @@
-/* Layout */
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- text-rendering: optimizeLegibility;
-}
-
-main {
- padding-left: 5px;
- padding-right: 5px;
-}
-
-a {
- color: #3366CC;
-}
-
-a:focus {
- outline: 0;
- color: red;
- text-decoration: none;
- border: 1px dotted #aaa;
-}
-
-a:hover {
- color: #333;
- text-decoration: none;
-}
-
-.header {
- margin-top: 10px;
- margin-bottom: 20px;
-}
-
-.header nav ul {
- display: none;
-}
-
-.header li {
- cursor: pointer;
- padding-left: 10px;
- line-height: 2.1em;
- font-size: 1.2em;
- border-bottom: 1px dotted #ddd;
-}
-
-.header li:hover a {
- color: #888;
-}
-
-.header a {
- font-size: 0.9em;
- color: #444;
- text-decoration: none;
- border: none;
-}
-
-.header .active a {
- font-weight: 600;
-}
-
-.header a:hover,
-.header a:focus {
- color: #888;
-}
-
-.page-header {
- margin-bottom: 25px;
-}
-
-.page-header h1 {
- font-weight: 500;
- border-bottom: 1px dotted #ddd;
-}
-
-.page-header ul {
- margin-left: 25px;
-}
-
-.page-header li {
- list-style-type: circle;
- line-height: 1.8em;
-}
-
-.logo {
- cursor: pointer;
- text-align: center;
-}
-
-.logo a {
- color: #000;
- letter-spacing: 1px;
-}
-
-.logo a:hover {
- color: #339966;
-}
-
-.logo a span {
- color: #339966;
-}
-
-.logo a:hover span {
- color: #000;
-}
-
-@media (min-width: 600px) {
- body {
- margin: auto;
- max-width: 750px;
- }
-
- .logo {
- text-align: left;
- float: left;
- margin-right: 15px;
- }
-
- .header nav ul {
- display: block;
- }
-
- .header li {
- display: inline;
- padding: 0;
- padding-right: 15px;
- line-height: normal;
- border: none;
- font-size: 1.0em;
- }
-
- .page-header ul {
- margin-left: 0;
- }
-
- .page-header li {
- display: inline;
- padding-right: 15px;
- }
-}
-
-/* Tables */
-table {
- width: 100%;
- border-collapse: collapse;
-}
-
-table, th, td {
- border: 1px solid #ddd;
-}
-
-th, td {
- padding: 5px;
- text-align: left;
-}
-
-td {
- vertical-align: top;
-}
-
-th {
- background: #fcfcfc;
-}
-
-tr:hover {
- background-color: #f9f9f9;
-}
-
-.column-40 {
- width: 40%;
-}
-
-.column-25 {
- width: 25%;
-}
-
-.column-20 {
- width: 20%;
-}
-
-/* Forms */
-label {
- cursor: pointer;
- display: block;
-}
-
-.radio-group {
- line-height: 1.9em;
-}
-
-div.radio-group label {
- display: inline-block;
-}
-
-select {
- margin-bottom: 15px;
-}
-
-input[type="url"],
-input[type="password"],
-input[type="text"] {
- border: 1px solid #ccc;
- padding: 3px;
- line-height: 20px;
- width: 250px;
- font-size: 99%;
- margin-bottom: 10px;
- margin-top: 5px;
- -webkit-appearance: none;
-}
-
-input[type="url"]:focus,
-input[type="password"]:focus,
-input[type="text"]:focus {
- color: #000;
- border-color: rgba(82, 168, 236, 0.8);
- outline: 0;
- box-shadow: 0 0 8px rgba(82, 168, 236, 0.6);
-}
-
-input[type="checkbox"] {
- margin-bottom: 15px;
-}
-
-::-moz-placeholder,
-::-ms-input-placeholder,
-::-webkit-input-placeholder {
- color: #ddd;
- padding-top: 2px;
-}
-
-.form-help {
- font-size: 0.9em;
- color: brown;
- margin-bottom: 15px;
-}
-
-.form-section {
- border-left: 2px dotted #ddd;
- padding-left: 20px;
- margin-left: 10px;
-}
-
-/* Buttons */
-a.button {
- text-decoration: none;
-}
-
-.button {
- display: inline-block;
- -webkit-appearance: none;
- -moz-appearance: none;
- font-size: 1.1em;
- cursor: pointer;
- padding: 3px 10px;
- border: 1px solid;
- border-radius: unset;
-}
-
-.button-primary {
- border-color: #3079ed;
- background: #4d90fe;
- color: #fff;
-}
-
-.button-primary:hover,
-.button-primary:focus {
- border-color: #2f5bb7;
- background: #357ae8;
-}
-
-.button-danger {
- border-color: #b0281a;
- background: #d14836;
- color: #fff;
-}
-
-.button-danger:hover,
-.button-danger:focus {
- color: #fff;
- background: #c53727;
-}
-
-.button:disabled {
- color: #ccc;
- background: #f7f7f7;
- border-color: #ccc;
-}
-
-.buttons {
- margin-top: 10px;
- margin-bottom: 20px;
-}
-
-/* Alerts */
-.alert {
- padding: 8px 35px 8px 14px;
- margin-bottom: 20px;
- color: #c09853;
- background-color: #fcf8e3;
- border: 1px solid #fbeed5;
- border-radius: 4px;
- overflow: auto;
-}
-
-.alert h3 {
- margin-top: 0;
- margin-bottom: 15px;
-}
-
-.alert-success {
- color: #468847;
- background-color: #dff0d8;
- border-color: #d6e9c6;
-}
-
-.alert-error {
- color: #b94a48;
- background-color: #f2dede;
- border-color: #eed3d7;
-}
-
-.alert-error a {
- color: #b94a48;
-}
-
-.alert-info {
- color: #3a87ad;
- background-color: #d9edf7;
- border-color: #bce8f1;
-}
-
-/* Panel */
-.panel {
- color: #333;
- background-color: #fcfcfc;
- border: 1px solid #ddd;
- border-radius: 5px;
- padding: 10px;
- margin-bottom: 15px;
-}
-
-.panel h3 {
- font-weight: 500;
- margin-top: 0;
- margin-bottom: 20px;
-}
-
-.panel ul {
- margin-left: 30px;
-}
-
-/* Modals */
-#modal-left {
- position: fixed;
- top: 0;
- left: 0;
- bottom: 0;
- width: 350px;
- overflow: auto;
- background: #f0f0f0;
- box-shadow: 2px 0 5px 0 #ccc;
- padding: 5px;
- padding-top: 30px;
-}
-
-#modal-left h3 {
- font-weight: 400;
-}
-
-.btn-close-modal {
- position: absolute;
- top: 0;
- right: 0;
- font-size: 1.7em;
- color: #ccc;
- padding:0 .2em;
- margin: 10px;
- text-decoration: none;
-}
-
-.btn-close-modal:hover {
- color: #999;
-}
-
-/* Keyboard Shortcuts */
-.keyboard-shortcuts li {
- margin-left: 25px;
- list-style-type: square;
- color: #333;
- font-size: 0.95em;
- line-height: 1.45em;
-}
-
-.keyboard-shortcuts p {
- line-height: 1.9em;
-}
-
-/* Login form */
-.login-form {
- margin: 50px auto 0;
- max-width: 280px;
-}
-
-/* Counter */
-.unread-counter-wrapper {
- font-size: 0.8em;
- font-weight: 300;
- color: #666;
-}
-
-/* Category label */
-.category {
- font-size: 0.75em;
- background-color: #fffcd7;
- border: 1px solid #d5d458;
- border-radius: 5px;
- margin-left: 0.25em;
- padding: 1px 0.4em 1px 0.4em;
- white-space: nowrap;
-}
-
-.category a {
- color: #555;
- text-decoration: none;
-}
-
-.category a:hover,
-.category a:focus {
- color: #000;
-}
-
-/* Pagination */
-.pagination {
- font-size: 1.1em;
- display: flex;
- align-items: center;
- padding-top: 8px;
-}
-
-.pagination-bottom {
- border-top: 1px dotted #ddd;
- margin-bottom: 15px;
- margin-top: 50px;
-}
-
-.pagination > div {
- flex: 1;
-}
-
-.pagination-next {
- text-align: right;
-}
-
-.pagination-prev:before {
- content: "« ";
-}
-
-.pagination-next:after {
- content: " »";
-}
-
-.pagination a {
- color: #333;
-}
-
-.pagination a:hover,
-.pagination a:focus {
- text-decoration: none;
-}
-
-/* List view */
-.item {
- border: 1px dotted #ddd;
- margin-bottom: 20px;
- padding: 5px;
- overflow: hidden;
-}
-
-.item.current-item {
- border: 3px solid #bce;
- padding: 3px;
-}
-
-.item-title a {
- text-decoration: none;
- font-weight: 600;
-}
-
-.item-status-read .item-title a {
- color: #777;
-}
-
-.item-meta {
- color: #777;
- font-size: 0.8em;
-}
-
-.item-meta a {
- color: #777;
- text-decoration: none;
-}
-
-.item-meta a:hover,
-.item-meta a:focus {
- color: #333;
-}
-
-.item-meta ul {
- margin-top: 5px;
-}
-
-.item-meta li {
- display: inline;
-}
-
-.item-meta li:after {
- content: "|";
- color: #aaa;
-}
-
-.item-meta li:last-child:after {
- content: "";
-}
-
-.hide-read-items .item-status-read {
- display: none;
-}
-
-/* Feeds list */
-article.feed-parsing-error {
- background-color: #fcf8e3;
- border-color: #aaa;
-}
-
-.parsing-error {
- font-size: 0.85em;
- margin-top: 2px;
- color: #333;
-}
-
-.parsing-error-count {
- cursor: pointer;
-}
-
-/* Entry view */
-.entry header {
- padding-bottom: 5px;
- border-bottom: 1px dotted #ddd;
-}
-
-.entry header h1 {
- font-size: 2.0em;
- line-height: 1.25em;
- margin: 30px 0;
-}
-
-.entry header h1 a {
- text-decoration: none;
- color: #333;
-}
-
-.entry header h1 a:hover,
-.entry header h1 a:focus {
- color: #666;
-}
-
-.entry-actions {
- margin-bottom: 20px;
-}
-
-.entry-actions li {
- display: inline;
-}
-
-.entry-actions li:not(:last-child):after {
- content: "|";
-}
-
-.entry-meta {
- font-size: 0.95em;
- margin: 0 0 20px;
- color: #666;
- overflow-wrap: break-word;
-}
-
-.entry-website img {
- vertical-align: top;
-}
-
-.entry-website a {
- color: #666;
- vertical-align: top;
- text-decoration: none;
-}
-
-.entry-website a:hover,
-.entry-website a:focus {
- text-decoration: underline;
-}
-
-.entry-date {
- font-size: 0.65em;
- font-style: italic;
- color: #555;
-}
-
-.entry-content {
- padding-top: 15px;
- font-size: 1.2em;
- font-weight: 300;
- font-family: Georgia, 'Times New Roman', Times, serif;
- color: #555;
- line-height: 1.4em;
- overflow-wrap: break-word;
-}
-
-.entry-content h1, h2, h3, h4, h5, h6 {
- margin-top: 15px;
- margin-bottom: 10px;
-}
-
-.entry-content iframe,
-.entry-content video,
-.entry-content img {
- max-width: 100%;
-}
-
-.entry-content figure {
- margin-top: 15px;
- margin-bottom: 15px;
-}
-
-.entry-content figure img {
- border: 1px solid #000;
-}
-
-.entry-content figcaption {
- font-size: 0.75em;
- text-transform: uppercase;
- color: #777;
-}
-
-.entry-content p {
- margin-top: 10px;
- margin-bottom: 15px;
-}
-
-.entry-content a {
- overflow-wrap: break-word;
-}
-
-.entry-content a:visited {
- color: purple;
-}
-
-.entry-content dt {
- font-weight: 500;
- margin-top: 15px;
- color: #555;
-}
-
-.entry-content dd {
- margin-left: 15px;
- margin-top: 5px;
- padding-left: 20px;
- border-left: 3px solid #ddd;
- color: #777;
- font-weight: 300;
- line-height: 1.4em;
-}
-
-.entry-content blockquote {
- border-left: 4px solid #ddd;
- padding-left: 25px;
- margin-left: 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- color: #888;
- line-height: 1.4em;
- font-family: Georgia, serif;
-}
-
-.entry-content blockquote + p {
- color: #555;
- font-style: italic;
- font-weight: 200;
-}
-
-.entry-content q {
- color: purple;
- font-family: Georgia, serif;
- font-style: italic;
-}
-
-.entry-content q:before {
- content: "“";
-}
-
-.entry-content q:after {
- content: "”";
-}
-
-.entry-content pre {
- padding: 5px;
- background: #f0f0f0;
- border: 1px solid #ddd;
- overflow: scroll;
- overflow-wrap: initial;
-}
-
-.entry-content table {
- table-layout: fixed;
- max-width: 100%;
-}
-
-.entry-content ul,
-.entry-content ol {
- margin-left: 30px;
-}
-
-.entry-content ul {
- list-style-type: square;
-}
-
-.entry-enclosures h3 {
- font-weight: 500;
-}
-
-.entry-enclosure {
- border: 1px dotted #ddd;
- padding: 5px;
- margin-top: 10px;
- max-width: 100%;
-}
-
-.entry-enclosure-download {
- font-size: 0.85em;
- overflow-wrap: break-word;
-}
-
-.enclosure-video video,
-.enclosure-image img {
- max-width: 100%;
-}
-
-/* Confirmation */
-.confirm {
- font-weight: 500;
- color: #ed2d04;
-}
-
-.confirm a {
- color: #ed2d04;
-}
-
-.loading {
- font-style: italic;
-}
-
-/* Bookmarlet */
-.bookmarklet {
- border: 1px dashed #ccc;
- border-radius: 5px;
- padding: 15px;
- margin: 15px;
- text-align: center;
-}
-
-.bookmarklet a {
- font-weight: 600;
- text-decoration: none;
- font-size: 1.2em;
-}
diff --git a/server/static/js.go b/server/static/js.go
deleted file mode 100644
index ce9d070..0000000
--- a/server/static/js.go
+++ /dev/null
@@ -1,92 +0,0 @@
-// Code generated by go generate; DO NOT EDIT.
-// 2017-12-28 18:55:07.395760341 -0800 PST m=+0.022480927
-
-package static
-
-var Javascript = map[string]string{
- "app": `(function(){'use strict';class DomHelper{static isVisible(element){return element.offsetParent!==null;}
-static openNewTab(url){let win=window.open(url,"_blank");win.focus();}
-static scrollPageTo(element){let windowScrollPosition=window.pageYOffset;let windowHeight=document.documentElement.clientHeight;let viewportPosition=windowScrollPosition+windowHeight;let itemBottomPosition=element.offsetTop+element.offsetHeight;if(viewportPosition-itemBottomPosition<0||viewportPosition-element.offsetTop>windowHeight){window.scrollTo(0,element.offsetTop-10);}}
-static getVisibleElements(selector){let elements=document.querySelectorAll(selector);let result=[];for(let i=0;i<elements.length;i++){if(this.isVisible(elements[i])){result.push(elements[i]);}}
-return result;}}
-class TouchHandler{constructor(){this.reset();}
-reset(){this.touch={start:{x:-1,y:-1},move:{x:-1,y:-1},element:null};}
-calculateDistance(){if(this.touch.start.x>=-1&&this.touch.move.x>=-1){let horizontalDistance=Math.abs(this.touch.move.x-this.touch.start.x);let verticalDistance=Math.abs(this.touch.move.y-this.touch.start.y);if(horizontalDistance>30&&verticalDistance<70){return this.touch.move.x-this.touch.start.x;}}
-return 0;}
-findElement(element){if(element.classList.contains("touch-item")){return element;}
-for(;element&&element!==document;element=element.parentNode){if(element.classList.contains("touch-item")){return element;}}
-return null;}
-onTouchStart(event){if(event.touches===undefined||event.touches.length!==1){return;}
-this.reset();this.touch.start.x=event.touches[0].clientX;this.touch.start.y=event.touches[0].clientY;this.touch.element=this.findElement(event.touches[0].target);}
-onTouchMove(event){if(event.touches===undefined||event.touches.length!==1||this.element===null){return;}
-this.touch.move.x=event.touches[0].clientX;this.touch.move.y=event.touches[0].clientY;let distance=this.calculateDistance();let absDistance=Math.abs(distance);if(absDistance>0){let opacity=1-(absDistance>75?0.9:absDistance/75*0.9);let tx=distance>75?75:(distance<-75?-75:distance);this.touch.element.style.opacity=opacity;this.touch.element.style.transform="translateX("+tx+"px)";}}
-onTouchEnd(event){if(event.touches===undefined){return;}
-if(this.touch.element!==null){let distance=Math.abs(this.calculateDistance());if(distance>75){EntryHandler.toggleEntryStatus(this.touch.element);this.touch.element.style.opacity=1;this.touch.element.style.transform="none";}}
-this.reset();}
-listen(){let elements=document.querySelectorAll(".touch-item");elements.forEach((element)=>{element.addEventListener("touchstart",(e)=>this.onTouchStart(e),false);element.addEventListener("touchmove",(e)=>this.onTouchMove(e),false);element.addEventListener("touchend",(e)=>this.onTouchEnd(e),false);element.addEventListener("touchcancel",()=>this.reset(),false);});}}
-class KeyboardHandler{constructor(){this.queue=[];this.shortcuts={};}
-on(combination,callback){this.shortcuts[combination]=callback;}
-listen(){document.onkeydown=(event)=>{if(this.isEventIgnored(event)){return;}
-let key=this.getKey(event);this.queue.push(key);for(let combination in this.shortcuts){let keys=combination.split(" ");if(keys.every((value,index)=>value===this.queue[index])){this.queue=[];this.shortcuts[combination]();return;}
-if(keys.length===1&&key===keys[0]){this.queue=[];this.shortcuts[combination]();return;}}
-if(this.queue.length>=2){this.queue=[];}};}
-isEventIgnored(event){return event.target.tagName==="INPUT"||event.target.tagName==="TEXTAREA";}
-getKey(event){const mapping={'Esc':'Escape','Up':'ArrowUp','Down':'ArrowDown','Left':'ArrowLeft','Right':'ArrowRight'};for(let key in mapping){if(mapping.hasOwnProperty(key)&&key===event.key){return mapping[key];}}
-return event.key;}}
-class FormHandler{static handleSubmitButtons(){let elements=document.querySelectorAll("form");elements.forEach((element)=>{element.onsubmit=()=>{let button=document.querySelector("button");if(button){button.innerHTML=button.dataset.labelLoading;button.disabled=true;}};});}}
-class MouseHandler{onClick(selector,callback){let elements=document.querySelectorAll(selector);elements.forEach((element)=>{element.onclick=(event)=>{event.preventDefault();callback(event);};});}}
-class RequestBuilder{constructor(url){this.callback=null;this.url=url;this.options={method:"POST",cache:"no-cache",credentials:"include",body:null,headers:new Headers({"Content-Type":"application/json","X-Csrf-Token":this.getCsrfToken()})};}
-withBody(body){this.options.body=JSON.stringify(body);return this;}
-withCallback(callback){this.callback=callback;return this;}
-getCsrfToken(){let element=document.querySelector("meta[name=X-CSRF-Token]");if(element!==null){return element.getAttribute("value");}
-return "";}
-execute(){fetch(new Request(this.url,this.options)).then((response)=>{if(this.callback){this.callback(response);}});}}
-class UnreadCounterHandler{static decrement(n){this.updateValue((current)=>{return current-n;});}
-static increment(n){this.updateValue((current)=>{return current+n;});}
-static updateValue(callback){let counterElements=document.querySelectorAll("span.unread-counter");counterElements.forEach((element)=>{let oldValue=parseInt(element.textContent,10);element.innerHTML=callback(oldValue);});}}
-class EntryHandler{static updateEntriesStatus(entryIDs,status,callback){let url=document.body.dataset.entriesStatusUrl;let request=new RequestBuilder(url);request.withBody({entry_ids:entryIDs,status:status});request.withCallback(callback);request.execute();}
-static toggleEntryStatus(element){let entryID=parseInt(element.dataset.id,10);let statuses={read:"unread",unread:"read"};for(let currentStatus in statuses){let newStatus=statuses[currentStatus];if(element.classList.contains("item-status-"+currentStatus)){element.classList.remove("item-status-"+currentStatus);element.classList.add("item-status-"+newStatus);this.updateEntriesStatus([entryID],newStatus);if(newStatus==="read"){UnreadCounterHandler.decrement(1);}else{UnreadCounterHandler.increment(1);}
-break;}}}
-static toggleBookmark(element){element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.bookmarkUrl);request.withCallback(()=>{if(element.dataset.value==="star"){element.innerHTML=element.dataset.labelStar;element.dataset.value="unstar";}else{element.innerHTML=element.dataset.labelUnstar;element.dataset.value="star";}});request.execute();}
-static markEntryAsRead(element){if(element.classList.contains("item-status-unread")){element.classList.remove("item-status-unread");element.classList.add("item-status-read");let entryID=parseInt(element.dataset.id,10);this.updateEntriesStatus([entryID],"read");}}
-static saveEntry(element){if(element.dataset.completed){return;}
-element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.saveUrl);request.withCallback(()=>{element.innerHTML=element.dataset.labelDone;element.dataset.completed=true;});request.execute();}
-static fetchOriginalContent(element){if(element.dataset.completed){return;}
-element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.fetchContentUrl);request.withCallback((response)=>{element.innerHTML=element.dataset.labelDone;element.dataset.completed=true;response.json().then((data)=>{if(data.hasOwnProperty("content")){document.querySelector(".entry-content").innerHTML=data.content;}});});request.execute();}}
-class ConfirmHandler{remove(url){let request=new RequestBuilder(url);request.withCallback(()=>window.location.reload());request.execute();}
-handle(event){let questionElement=document.createElement("span");let linkElement=event.target;let containerElement=linkElement.parentNode;linkElement.style.display="none";let yesElement=document.createElement("a");yesElement.href="#";yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));yesElement.onclick=(event)=>{event.preventDefault();let loadingElement=document.createElement("span");loadingElement.className="loading";loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));questionElement.remove();containerElement.appendChild(loadingElement);this.remove(linkElement.dataset.url);};let noElement=document.createElement("a");noElement.href="#";noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));noElement.onclick=(event)=>{event.preventDefault();linkElement.style.display="inline";questionElement.remove();};questionElement.className="confirm";questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion+" "));questionElement.appendChild(yesElement);questionElement.appendChild(document.createTextNode(", "));questionElement.appendChild(noElement);containerElement.appendChild(questionElement);}}
-class MenuHandler{clickMenuListItem(event){let element=event.target;if(element.tagName==="A"){window.location.href=element.getAttribute("href");}else{window.location.href=element.querySelector("a").getAttribute("href");}}
-toggleMainMenu(){let menu=document.querySelector(".header nav ul");if(DomHelper.isVisible(menu)){menu.style.display="none";}else{menu.style.display="block";}}}
-class ModalHandler{static exists(){return document.getElementById("modal-container")!==null;}
-static open(fragment){if(ModalHandler.exists()){return;}
-let container=document.createElement("div");container.id="modal-container";container.appendChild(document.importNode(fragment,true));document.body.appendChild(container);let closeButton=document.querySelector("a.btn-close-modal");if(closeButton!==null){closeButton.onclick=(event)=>{event.preventDefault();ModalHandler.close();};}}
-static close(){let container=document.getElementById("modal-container");if(container!==null){container.parentNode.removeChild(container);}}}
-class NavHandler{showKeyboardShortcuts(){let template=document.getElementById("keyboard-shortcuts");if(template!==null){ModalHandler.open(template.content);}}
-markPageAsRead(){let items=DomHelper.getVisibleElements(".items .item");let entryIDs=[];items.forEach((element)=>{element.classList.add("item-status-read");entryIDs.push(parseInt(element.dataset.id,10));});if(entryIDs.length>0){EntryHandler.updateEntriesStatus(entryIDs,"read",()=>{this.goToPage("next",true);});}}
-saveEntry(){if(this.isListView()){let currentItem=document.querySelector(".current-item");if(currentItem!==null){let saveLink=currentItem.querySelector("a[data-save-entry]");if(saveLink){EntryHandler.saveEntry(saveLink);}}}else{let saveLink=document.querySelector("a[data-save-entry]");if(saveLink){EntryHandler.saveEntry(saveLink);}}}
-fetchOriginalContent(){if(!this.isListView()){let link=document.querySelector("a[data-fetch-content-entry]");if(link){EntryHandler.fetchOriginalContent(link);}}}
-toggleEntryStatus(){let currentItem=document.querySelector(".current-item");if(currentItem!==null){this.goToNextListItem();EntryHandler.toggleEntryStatus(currentItem);}}
-toggleBookmark(){if(!this.isListView()){this.toggleBookmarkLink(document.querySelector(".entry"));return;}
-let currentItem=document.querySelector(".current-item");if(currentItem!==null){this.toggleBookmarkLink(currentItem);}}
-toggleBookmarkLink(parent){let bookmarkLink=parent.querySelector("a[data-toggle-bookmark]");if(bookmarkLink){EntryHandler.toggleBookmark(bookmarkLink);}}
-openOriginalLink(){let entryLink=document.querySelector(".entry h1 a");if(entryLink!==null){DomHelper.openNewTab(entryLink.getAttribute("href"));return;}
-let currentItemOriginalLink=document.querySelector(".current-item a[data-original-link]");if(currentItemOriginalLink!==null){DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));let currentItem=document.querySelector(".current-item");this.goToNextListItem();EntryHandler.markEntryAsRead(currentItem);}}
-openSelectedItem(){let currentItemLink=document.querySelector(".current-item .item-title a");if(currentItemLink!==null){window.location.href=currentItemLink.getAttribute("href");}}
-goToPage(page,fallbackSelf){let element=document.querySelector("a[data-page="+page+"]");if(element){document.location.href=element.href;}else if(fallbackSelf){window.location.reload();}}
-goToPrevious(){if(this.isListView()){this.goToPreviousListItem();}else{this.goToPage("previous");}}
-goToNext(){if(this.isListView()){this.goToNextListItem();}else{this.goToPage("next");}}
-goToPreviousListItem(){let items=DomHelper.getVisibleElements(".items .item");if(items.length===0){return;}
-if(document.querySelector(".current-item")===null){items[0].classList.add("current-item");return;}
-for(let i=0;i<items.length;i++){if(items[i].classList.contains("current-item")){items[i].classList.remove("current-item");if(i-1>=0){items[i-1].classList.add("current-item");DomHelper.scrollPageTo(items[i-1]);}
-break;}}}
-goToNextListItem(){let currentItem=document.querySelector(".current-item");let items=DomHelper.getVisibleElements(".items .item");if(items.length===0){return;}
-if(currentItem===null){items[0].classList.add("current-item");return;}
-for(let i=0;i<items.length;i++){if(items[i].classList.contains("current-item")){items[i].classList.remove("current-item");if(i+1<items.length){items[i+1].classList.add("current-item");DomHelper.scrollPageTo(items[i+1]);}
-break;}}}
-isListView(){return document.querySelector(".items")!==null;}}
-document.addEventListener("DOMContentLoaded",function(){FormHandler.handleSubmitButtons();let touchHandler=new TouchHandler();touchHandler.listen();let navHandler=new NavHandler();let keyboardHandler=new KeyboardHandler();keyboardHandler.on("g u",()=>navHandler.goToPage("unread"));keyboardHandler.on("g b",()=>navHandler.goToPage("starred"));keyboardHandler.on("g h",()=>navHandler.goToPage("history"));keyboardHandler.on("g f",()=>navHandler.goToPage("feeds"));keyboardHandler.on("g c",()=>navHandler.goToPage("categories"));keyboardHandler.on("g s",()=>navHandler.goToPage("settings"));keyboardHandler.on("ArrowLeft",()=>navHandler.goToPrevious());keyboardHandler.on("ArrowRight",()=>navHandler.goToNext());keyboardHandler.on("j",()=>navHandler.goToPrevious());keyboardHandler.on("p",()=>navHandler.goToPrevious());keyboardHandler.on("k",()=>navHandler.goToNext());keyboardHandler.on("n",()=>navHandler.goToNext());keyboardHandler.on("h",()=>navHandler.goToPage("previous"));keyboardHandler.on("l",()=>navHandler.goToPage("next"));keyboardHandler.on("o",()=>navHandler.openSelectedItem());keyboardHandler.on("v",()=>navHandler.openOriginalLink());keyboardHandler.on("m",()=>navHandler.toggleEntryStatus());keyboardHandler.on("A",()=>navHandler.markPageAsRead());keyboardHandler.on("s",()=>navHandler.saveEntry());keyboardHandler.on("d",()=>navHandler.fetchOriginalContent());keyboardHandler.on("f",()=>navHandler.toggleBookmark());keyboardHandler.on("?",()=>navHandler.showKeyboardShortcuts());keyboardHandler.on("Escape",()=>ModalHandler.close());keyboardHandler.listen();let mouseHandler=new MouseHandler();mouseHandler.onClick("a[data-save-entry]",(event)=>{event.preventDefault();EntryHandler.saveEntry(event.target);});mouseHandler.onClick("a[data-toggle-bookmark]",(event)=>{event.preventDefault();EntryHandler.toggleBookmark(event.target);});mouseHandler.onClick("a[data-fetch-content-entry]",(event)=>{event.preventDefault();EntryHandler.fetchOriginalContent(event.target);});mouseHandler.onClick("a[data-on-click=markPageAsRead]",()=>navHandler.markPageAsRead());mouseHandler.onClick("a[data-confirm]",(event)=>{(new ConfirmHandler()).handle(event);});if(document.documentElement.clientWidth<600){let menuHandler=new MenuHandler();mouseHandler.onClick(".logo",()=>menuHandler.toggleMainMenu());mouseHandler.onClick(".header nav li",(event)=>menuHandler.clickMenuListItem(event));}});})();`,
-}
-
-var JavascriptChecksums = map[string]string{
- "app": "36dfcfb33ddc3f75f701fc4353873e2ce6da813dbfdd3b37100a4475a32b0545",
-}
diff --git a/server/static/js/app.js b/server/static/js/app.js
deleted file mode 100644
index 4ec82e4..0000000
--- a/server/static/js/app.js
+++ /dev/null
@@ -1,748 +0,0 @@
-/*jshint esversion: 6 */
-(function() {
-'use strict';
-
-class DomHelper {
- static isVisible(element) {
- return element.offsetParent !== null;
- }
-
- static openNewTab(url) {
- let win = window.open(url, "_blank");
- win.focus();
- }
-
- static scrollPageTo(element) {
- let windowScrollPosition = window.pageYOffset;
- let windowHeight = document.documentElement.clientHeight;
- let viewportPosition = windowScrollPosition + windowHeight;
- let itemBottomPosition = element.offsetTop + element.offsetHeight;
-
- if (viewportPosition - itemBottomPosition < 0 || viewportPosition - element.offsetTop > windowHeight) {
- window.scrollTo(0, element.offsetTop - 10);
- }
- }
-
- static getVisibleElements(selector) {
- let elements = document.querySelectorAll(selector);
- let result = [];
-
- for (let i = 0; i < elements.length; i++) {
- if (this.isVisible(elements[i])) {
- result.push(elements[i]);
- }
- }
-
- return result;
- }
-}
-
-class TouchHandler {
- constructor() {
- this.reset();
- }
-
- reset() {
- this.touch = {
- start: {x: -1, y: -1},
- move: {x: -1, y: -1},
- element: null
- };
- }
-
- calculateDistance() {
- if (this.touch.start.x >= -1 && this.touch.move.x >= -1) {
- let horizontalDistance = Math.abs(this.touch.move.x - this.touch.start.x);
- let verticalDistance = Math.abs(this.touch.move.y - this.touch.start.y);
-
- if (horizontalDistance > 30 && verticalDistance < 70) {
- return this.touch.move.x - this.touch.start.x;
- }
- }
-
- return 0;
- }
-
- findElement(element) {
- if (element.classList.contains("touch-item")) {
- return element;
- }
-
- for (; element && element !== document; element = element.parentNode) {
- if (element.classList.contains("touch-item")) {
- return element;
- }
- }
-
- return null;
- }
-
- onTouchStart(event) {
- if (event.touches === undefined || event.touches.length !== 1) {
- return;
- }
-
- this.reset();
- this.touch.start.x = event.touches[0].clientX;
- this.touch.start.y = event.touches[0].clientY;
- this.touch.element = this.findElement(event.touches[0].target);
- }
-
- onTouchMove(event) {
- if (event.touches === undefined || event.touches.length !== 1 || this.element === null) {
- return;
- }
-
- this.touch.move.x = event.touches[0].clientX;
- this.touch.move.y = event.touches[0].clientY;
-
- let distance = this.calculateDistance();
- let absDistance = Math.abs(distance);
-
- if (absDistance > 0) {
- let opacity = 1 - (absDistance > 75 ? 0.9 : absDistance / 75 * 0.9);
- let tx = distance > 75 ? 75 : (distance < -75 ? -75 : distance);
-
- this.touch.element.style.opacity = opacity;
- this.touch.element.style.transform = "translateX(" + tx + "px)";
- }
- }
-
- onTouchEnd(event) {
- if (event.touches === undefined) {
- return;
- }
-
- if (this.touch.element !== null) {
- let distance = Math.abs(this.calculateDistance());
-
- if (distance > 75) {
- EntryHandler.toggleEntryStatus(this.touch.element);
- this.touch.element.style.opacity = 1;
- this.touch.element.style.transform = "none";
- }
- }
-
- this.reset();
- }
-
- listen() {
- let elements = document.querySelectorAll(".touch-item");
-
- elements.forEach((element) => {
- element.addEventListener("touchstart", (e) => this.onTouchStart(e), false);
- element.addEventListener("touchmove", (e) => this.onTouchMove(e), false);
- element.addEventListener("touchend", (e) => this.onTouchEnd(e), false);
- element.addEventListener("touchcancel", () => this.reset(), false);
- });
- }
-}
-
-class KeyboardHandler {
- constructor() {
- this.queue = [];
- this.shortcuts = {};
- }
-
- on(combination, callback) {
- this.shortcuts[combination] = callback;
- }
-
- listen() {
- document.onkeydown = (event) => {
- if (this.isEventIgnored(event)) {
- return;
- }
-
- let key = this.getKey(event);
- this.queue.push(key);
-
- for (let combination in this.shortcuts) {
- let keys = combination.split(" ");
-
- if (keys.every((value, index) => value === this.queue[index])) {
- this.queue = [];
- this.shortcuts[combination]();
- return;
- }
-
- if (keys.length === 1 && key === keys[0]) {
- this.queue = [];
- this.shortcuts[combination]();
- return;
- }
- }
-
- if (this.queue.length >= 2) {
- this.queue = [];
- }
- };
- }
-
- isEventIgnored(event) {
- return event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA";
- }
-
- getKey(event) {
- const mapping = {
- 'Esc': 'Escape',
- 'Up': 'ArrowUp',
- 'Down': 'ArrowDown',
- 'Left': 'ArrowLeft',
- 'Right': 'ArrowRight'
- };
-
- for (let key in mapping) {
- if (mapping.hasOwnProperty(key) && key === event.key) {
- return mapping[key];
- }
- }
-
- return event.key;
- }
-}
-
-class FormHandler {
- static handleSubmitButtons() {
- let elements = document.querySelectorAll("form");
- elements.forEach((element) => {
- element.onsubmit = () => {
- let button = document.querySelector("button");
-
- if (button) {
- button.innerHTML = button.dataset.labelLoading;
- button.disabled = true;
- }
- };
- });
- }
-}
-
-class MouseHandler {
- onClick(selector, callback) {
- let elements = document.querySelectorAll(selector);
- elements.forEach((element) => {
- element.onclick = (event) => {
- event.preventDefault();
- callback(event);
- };
- });
- }
-}
-
-class RequestBuilder {
- constructor(url) {
- this.callback = null;
- this.url = url;
- this.options = {
- method: "POST",
- cache: "no-cache",
- credentials: "include",
- body: null,
- headers: new Headers({
- "Content-Type": "application/json",
- "X-Csrf-Token": this.getCsrfToken()
- })
- };
- }
-
- withBody(body) {
- this.options.body = JSON.stringify(body);
- return this;
- }
-
- withCallback(callback) {
- this.callback = callback;
- return this;
- }
-
- getCsrfToken() {
- let element = document.querySelector("meta[name=X-CSRF-Token]");
- if (element !== null) {
- return element.getAttribute("value");
- }
-
- return "";
- }
-
- execute() {
- fetch(new Request(this.url, this.options)).then((response) => {
- if (this.callback) {
- this.callback(response);
- }
- });
- }
-}
-
-class UnreadCounterHandler {
- static decrement(n) {
- this.updateValue((current) => {
- return current - n;
- });
- }
-
- static increment(n) {
- this.updateValue((current) => {
- return current + n;
- });
- }
-
- static updateValue(callback) {
- let counterElements = document.querySelectorAll("span.unread-counter");
- counterElements.forEach((element) => {
- let oldValue = parseInt(element.textContent, 10);
- element.innerHTML = callback(oldValue);
- });
- }
-}
-
-class EntryHandler {
- static updateEntriesStatus(entryIDs, status, callback) {
- let url = document.body.dataset.entriesStatusUrl;
- let request = new RequestBuilder(url);
- request.withBody({entry_ids: entryIDs, status: status});
- request.withCallback(callback);
- request.execute();
- }
-
- static toggleEntryStatus(element) {
- let entryID = parseInt(element.dataset.id, 10);
- let statuses = {read: "unread", unread: "read"};
-
- for (let currentStatus in statuses) {
- let newStatus = statuses[currentStatus];
-
- if (element.classList.contains("item-status-" + currentStatus)) {
- element.classList.remove("item-status-" + currentStatus);
- element.classList.add("item-status-" + newStatus);
-
- this.updateEntriesStatus([entryID], newStatus);
-
- if (newStatus === "read") {
- UnreadCounterHandler.decrement(1);
- } else {
- UnreadCounterHandler.increment(1);
- }
-
- break;
- }
- }
- }
-
- static toggleBookmark(element) {
- element.innerHTML = element.dataset.labelLoading;
-
- let request = new RequestBuilder(element.dataset.bookmarkUrl);
- request.withCallback(() => {
- if (element.dataset.value === "star") {
- element.innerHTML = element.dataset.labelStar;
- element.dataset.value = "unstar";
- } else {
- element.innerHTML = element.dataset.labelUnstar;
- element.dataset.value = "star";
- }
- });
- request.execute();
- }
-
- static markEntryAsRead(element) {
- if (element.classList.contains("item-status-unread")) {
- element.classList.remove("item-status-unread");
- element.classList.add("item-status-read");
-
- let entryID = parseInt(element.dataset.id, 10);
- this.updateEntriesStatus([entryID], "read");
- }
- }
-
- static saveEntry(element) {
- if (element.dataset.completed) {
- return;
- }
-
- element.innerHTML = element.dataset.labelLoading;
-
- let request = new RequestBuilder(element.dataset.saveUrl);
- request.withCallback(() => {
- element.innerHTML = element.dataset.labelDone;
- element.dataset.completed = true;
- });
- request.execute();
- }
-
- static fetchOriginalContent(element) {
- if (element.dataset.completed) {
- return;
- }
-
- element.innerHTML = element.dataset.labelLoading;
-
- let request = new RequestBuilder(element.dataset.fetchContentUrl);
- request.withCallback((response) => {
- element.innerHTML = element.dataset.labelDone;
- element.dataset.completed = true;
-
- response.json().then((data) => {
- if (data.hasOwnProperty("content")) {
- document.querySelector(".entry-content").innerHTML = data.content;
- }
- });
- });
- request.execute();
- }
-}
-
-class ConfirmHandler {
- remove(url) {
- let request = new RequestBuilder(url);
- request.withCallback(() => window.location.reload());
- request.execute();
- }
-
- handle(event) {
- let questionElement = document.createElement("span");
- let linkElement = event.target;
- let containerElement = linkElement.parentNode;
- linkElement.style.display = "none";
-
- let yesElement = document.createElement("a");
- yesElement.href = "#";
- yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));
- yesElement.onclick = (event) => {
- event.preventDefault();
-
- let loadingElement = document.createElement("span");
- loadingElement.className = "loading";
- loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));
-
- questionElement.remove();
- containerElement.appendChild(loadingElement);
-
- this.remove(linkElement.dataset.url);
- };
-
- let noElement = document.createElement("a");
- noElement.href = "#";
- noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));
- noElement.onclick = (event) => {
- event.preventDefault();
- linkElement.style.display = "inline";
- questionElement.remove();
- };
-
- questionElement.className = "confirm";
- questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion + " "));
- questionElement.appendChild(yesElement);
- questionElement.appendChild(document.createTextNode(", "));
- questionElement.appendChild(noElement);
-
- containerElement.appendChild(questionElement);
- }
-}
-
-class MenuHandler {
- clickMenuListItem(event) {
- let element = event.target;
-
- if (element.tagName === "A") {
- window.location.href = element.getAttribute("href");
- } else {
- window.location.href = element.querySelector("a").getAttribute("href");
- }
- }
-
- toggleMainMenu() {
- let menu = document.querySelector(".header nav ul");
- if (DomHelper.isVisible(menu)) {
- menu.style.display = "none";
- } else {
- menu.style.display = "block";
- }
- }
-}
-
-class ModalHandler {
- static exists() {
- return document.getElementById("modal-container") !== null;
- }
-
- static open(fragment) {
- if (ModalHandler.exists()) {
- return;
- }
-
- let container = document.createElement("div");
- container.id = "modal-container";
- container.appendChild(document.importNode(fragment, true));
- document.body.appendChild(container);
-
- let closeButton = document.querySelector("a.btn-close-modal");
- if (closeButton !== null) {
- closeButton.onclick = (event) => {
- event.preventDefault();
- ModalHandler.close();
- };
- }
- }
-
- static close() {
- let container = document.getElementById("modal-container");
- if (container !== null) {
- container.parentNode.removeChild(container);
- }
- }
-}
-
-class NavHandler {
- showKeyboardShortcuts() {
- let template = document.getElementById("keyboard-shortcuts");
- if (template !== null) {
- ModalHandler.open(template.content);
- }
- }
-
- markPageAsRead() {
- let items = DomHelper.getVisibleElements(".items .item");
- let entryIDs = [];
-
- items.forEach((element) => {
- element.classList.add("item-status-read");
- entryIDs.push(parseInt(element.dataset.id, 10));
- });
-
- if (entryIDs.length > 0) {
- EntryHandler.updateEntriesStatus(entryIDs, "read", () => {
- // This callback make sure the Ajax request reach the server before we reload the page.
- this.goToPage("next", true);
- });
- }
- }
-
- saveEntry() {
- if (this.isListView()) {
- let currentItem = document.querySelector(".current-item");
- if (currentItem !== null) {
- let saveLink = currentItem.querySelector("a[data-save-entry]");
- if (saveLink) {
- EntryHandler.saveEntry(saveLink);
- }
- }
- } else {
- let saveLink = document.querySelector("a[data-save-entry]");
- if (saveLink) {
- EntryHandler.saveEntry(saveLink);
- }
- }
- }
-
- fetchOriginalContent() {
- if (! this.isListView()){
- let link = document.querySelector("a[data-fetch-content-entry]");
- if (link) {
- EntryHandler.fetchOriginalContent(link);
- }
- }
- }
-
- toggleEntryStatus() {
- let currentItem = document.querySelector(".current-item");
- if (currentItem !== null) {
- // The order is important here,
- // On the unread page, the read item will be hidden.
- this.goToNextListItem();
- EntryHandler.toggleEntryStatus(currentItem);
- }
- }
-
- toggleBookmark() {
- if (! this.isListView()) {
- this.toggleBookmarkLink(document.querySelector(".entry"));
- return;
- }
-
- let currentItem = document.querySelector(".current-item");
- if (currentItem !== null) {
- this.toggleBookmarkLink(currentItem);
- }
- }
-
- toggleBookmarkLink(parent) {
- let bookmarkLink = parent.querySelector("a[data-toggle-bookmark]");
- if (bookmarkLink) {
- EntryHandler.toggleBookmark(bookmarkLink);
- }
- }
-
- openOriginalLink() {
- let entryLink = document.querySelector(".entry h1 a");
- if (entryLink !== null) {
- DomHelper.openNewTab(entryLink.getAttribute("href"));
- return;
- }
-
- let currentItemOriginalLink = document.querySelector(".current-item a[data-original-link]");
- if (currentItemOriginalLink !== null) {
- DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));
-
- // Move to the next item and if we are on the unread page mark this item as read.
- let currentItem = document.querySelector(".current-item");
- this.goToNextListItem();
- EntryHandler.markEntryAsRead(currentItem);
- }
- }
-
- openSelectedItem() {
- let currentItemLink = document.querySelector(".current-item .item-title a");
- if (currentItemLink !== null) {
- window.location.href = currentItemLink.getAttribute("href");
- }
- }
-
- /**
- * @param {string} page Page to redirect to.
- * @param {boolean} fallbackSelf Refresh actual page if the page is not found.
- */
- goToPage(page, fallbackSelf) {
- let element = document.querySelector("a[data-page=" + page + "]");
-
- if (element) {
- document.location.href = element.href;
- } else if (fallbackSelf) {
- window.location.reload();
- }
- }
-
- goToPrevious() {
- if (this.isListView()) {
- this.goToPreviousListItem();
- } else {
- this.goToPage("previous");
- }
- }
-
- goToNext() {
- if (this.isListView()) {
- this.goToNextListItem();
- } else {
- this.goToPage("next");
- }
- }
-
- goToPreviousListItem() {
- let items = DomHelper.getVisibleElements(".items .item");
- if (items.length === 0) {
- return;
- }
-
- if (document.querySelector(".current-item") === null) {
- items[0].classList.add("current-item");
- return;
- }
-
- for (let i = 0; i < items.length; i++) {
- if (items[i].classList.contains("current-item")) {
- items[i].classList.remove("current-item");
-
- if (i - 1 >= 0) {
- items[i - 1].classList.add("current-item");
- DomHelper.scrollPageTo(items[i - 1]);
- }
-
- break;
- }
- }
- }
-
- goToNextListItem() {
- let currentItem = document.querySelector(".current-item");
- let items = DomHelper.getVisibleElements(".items .item");
- if (items.length === 0) {
- return;
- }
-
- if (currentItem === null) {
- items[0].classList.add("current-item");
- return;
- }
-
- for (let i = 0; i < items.length; i++) {
- if (items[i].classList.contains("current-item")) {
- items[i].classList.remove("current-item");
-
- if (i + 1 < items.length) {
- items[i + 1].classList.add("current-item");
- DomHelper.scrollPageTo(items[i + 1]);
- }
-
- break;
- }
- }
- }
-
- isListView() {
- return document.querySelector(".items") !== null;
- }
-}
-
-document.addEventListener("DOMContentLoaded", function() {
- FormHandler.handleSubmitButtons();
-
- let touchHandler = new TouchHandler();
- touchHandler.listen();
-
- let navHandler = new NavHandler();
- let keyboardHandler = new KeyboardHandler();
- keyboardHandler.on("g u", () => navHandler.goToPage("unread"));
- keyboardHandler.on("g b", () => navHandler.goToPage("starred"));
- keyboardHandler.on("g h", () => navHandler.goToPage("history"));
- keyboardHandler.on("g f", () => navHandler.goToPage("feeds"));
- keyboardHandler.on("g c", () => navHandler.goToPage("categories"));
- keyboardHandler.on("g s", () => navHandler.goToPage("settings"));
- keyboardHandler.on("ArrowLeft", () => navHandler.goToPrevious());
- keyboardHandler.on("ArrowRight", () => navHandler.goToNext());
- keyboardHandler.on("j", () => navHandler.goToPrevious());
- keyboardHandler.on("p", () => navHandler.goToPrevious());
- keyboardHandler.on("k", () => navHandler.goToNext());
- keyboardHandler.on("n", () => navHandler.goToNext());
- keyboardHandler.on("h", () => navHandler.goToPage("previous"));
- keyboardHandler.on("l", () => navHandler.goToPage("next"));
- keyboardHandler.on("o", () => navHandler.openSelectedItem());
- keyboardHandler.on("v", () => navHandler.openOriginalLink());
- keyboardHandler.on("m", () => navHandler.toggleEntryStatus());
- keyboardHandler.on("A", () => navHandler.markPageAsRead());
- keyboardHandler.on("s", () => navHandler.saveEntry());
- keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
- keyboardHandler.on("f", () => navHandler.toggleBookmark());
- keyboardHandler.on("?", () => navHandler.showKeyboardShortcuts());
- keyboardHandler.on("Escape", () => ModalHandler.close());
- keyboardHandler.listen();
-
- let mouseHandler = new MouseHandler();
- mouseHandler.onClick("a[data-save-entry]", (event) => {
- event.preventDefault();
- EntryHandler.saveEntry(event.target);
- });
-
- mouseHandler.onClick("a[data-toggle-bookmark]", (event) => {
- event.preventDefault();
- EntryHandler.toggleBookmark(event.target);
- });
-
- mouseHandler.onClick("a[data-fetch-content-entry]", (event) => {
- event.preventDefault();
- EntryHandler.fetchOriginalContent(event.target);
- });
-
- mouseHandler.onClick("a[data-on-click=markPageAsRead]", () => navHandler.markPageAsRead());
- mouseHandler.onClick("a[data-confirm]", (event) => {
- (new ConfirmHandler()).handle(event);
- });
-
- if (document.documentElement.clientWidth < 600) {
- let menuHandler = new MenuHandler();
- mouseHandler.onClick(".logo", () => menuHandler.toggleMainMenu());
- mouseHandler.onClick(".header nav li", (event) => menuHandler.clickMenuListItem(event));
- }
-});
-
-})();
diff --git a/server/template/common.go b/server/template/common.go
deleted file mode 100644
index f0a481d..0000000
--- a/server/template/common.go
+++ /dev/null
@@ -1,176 +0,0 @@
-// Code generated by go generate; DO NOT EDIT.
-// 2017-12-31 18:38:42.07097409 -0800 PST m=+0.051805248
-
-package template
-
-var templateCommonMap = map[string]string{
- "entry_pagination": `{{ define "entry_pagination" }}
-<div class="pagination">
- <div class="pagination-prev">
- {{ if .prevEntry }}
- <a href="{{ .prevEntryRoute }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "Previous" }}</a>
- {{ else }}
- {{ t "Previous" }}
- {{ end }}
- </div>
-
- <div class="pagination-next">
- {{ if .nextEntry }}
- <a href="{{ .nextEntryRoute }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "Next" }}</a>
- {{ else }}
- {{ t "Next" }}
- {{ end }}
- </div>
-</div>
-{{ end }}`,
- "layout": `{{ define "base" }}
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
-
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="apple-mobile-web-app-title" content="Miniflux">
- <link rel="manifest" href="{{ route "webManifest" }}">
-
- <meta name="robots" content="noindex,nofollow">
- <meta name="referrer" content="no-referrer">
-
- <link rel="icon" type="image/png" href="{{ route "appIcon" "filename" "favicon.png" }}">
- <link rel="apple-touch-icon" href="{{ route "appIcon" "filename" "touch-icon-iphone.png" }}">
- <link rel="apple-touch-icon" sizes="72x72" href="{{ route "appIcon" "filename" "touch-icon-ipad.png" }}">
- <link rel="apple-touch-icon" sizes="114x114" href="{{ route "appIcon" "filename" "touch-icon-iphone-retina.png" }}">
- <link rel="apple-touch-icon" sizes="144x144" href="{{ route "appIcon" "filename" "touch-icon-ipad-retina.png" }}">
- <link rel="shortcut icon" type="image/x-icon" href="{{ route "favicon" }}">
-
- {{ if .csrf }}
- <meta name="X-CSRF-Token" value="{{ .csrf }}">
- {{ end }}
- <title>{{template "title" .}} - Miniflux</title>
- {{ if .user }}
- <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .user.Theme }}">
- {{ else }}
- <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" "white" }}">
- {{ end }}
- <script type="text/javascript" src="{{ route "javascript" }}" defer></script>
-</head>
-<body data-entries-status-url="{{ route "updateEntriesStatus" }}">
- {{ if .user }}
- <header class="header">
- <nav>
- <div class="logo">
- <a href="{{ route "unread" }}">Mini<span>flux</span></a>
- </div>
- <ul>
- <li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g u" }}">
- <a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}</a>
- {{ if gt .countUnread 0 }}
- <span class="unread-counter-wrapper">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
- {{ end }}
- </li>
- <li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g b" }}">
- <a href="{{ route "starred" }}" data-page="starred">{{ t "Starred" }}</a>
- </li>
- <li {{ if eq .menu "history" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g h" }}">
- <a href="{{ route "history" }}" data-page="history">{{ t "History" }}</a>
- </li>
- <li {{ if eq .menu "feeds" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g f" }}">
- <a href="{{ route "feeds" }}" data-page="feeds">{{ t "Feeds" }}</a>
- </li>
- <li {{ if eq .menu "categories" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g c" }}">
- <a href="{{ route "categories" }}" data-page="categories">{{ t "Categories" }}</a>
- </li>
- <li {{ if eq .menu "settings" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g s" }}">
- <a href="{{ route "settings" }}" data-page="settings">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "logout" }}" title="Logged as {{ .user.Username }}">{{ t "Logout" }}</a>
- </li>
- </ul>
- </nav>
- </header>
- {{ end }}
- {{ if .flashMessage }}
- <div class="flash-message alert alert-success">{{ .flashMessage }}</div>
- {{ end }}
- {{ if .flashErrorMessage }}
- <div class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
- {{ end }}
- <main>
- {{template "content" .}}
- </main>
- <template id="keyboard-shortcuts">
- <div id="modal-left">
- <a href="#" class="btn-close-modal">x</a>
- <h3>{{ t "Keyboard Shortcuts" }}</h3>
-
- <div class="keyboard-shortcuts">
- <p>{{ t "Sections Navigation" }}</p>
- <ul>
- <li>{{ t "Go to unread" }} = <strong>g + u</strong></li>
- <li>{{ t "Go to bookmarks" }} = <strong>g + b</strong></li>
- <li>{{ t "Go to history" }} = <strong>g + h</strong></li>
- <li>{{ t "Go to feeds" }} = <strong>g + f</strong></li>
- <li>{{ t "Go to categories" }} = <strong>g + c</strong></li>
- <li>{{ t "Go to settings" }} = <strong>g + s</strong></li>
- <li>{{ t "Show keyboard shortcuts" }} = <strong>?</strong></li>
- </ul>
-
- <p>{{ t "Items Navigation" }}</p>
- <ul>
- <li>{{ t "Go to previous item" }} = <strong>p or j or ◄</strong></li>
- <li>{{ t "Go to next item" }} = <strong>n or k or ►</strong></li>
- </ul>
-
- <p>{{ t "Pages Navigation" }}</p>
- <ul>
- <li>{{ t "Go to previous page" }} = <strong>h</strong></li>
- <li>{{ t "Go to next page" }} = <strong>l</strong></li>
- </ul>
-
- <p>{{ t "Actions" }}</p>
- <ul>
- <li>{{ t "Open selected item" }} = <strong>o</strong></li>
- <li>{{ t "Open original link" }} = <strong>v</strong></li>
- <li>{{ t "Toggle read/unread" }} = <strong>m</strong></li>
- <li>{{ t "Mark current page as read" }} = <strong>A</strong></li>
- <li>{{ t "Download original content" }} = <strong>d</strong></li>
- <li>{{ t "Toggle bookmark" }} = <strong>f</strong></li>
- <li>{{ t "Save article" }} = <strong>s</strong></li>
- <li>{{ t "Close modal dialog" }} = <strong>Esc</strong></li>
- </ul>
- </div>
- </div>
- </template>
-</body>
-</html>
-{{ end }}`,
- "pagination": `{{ define "pagination" }}
-<div class="pagination">
- <div class="pagination-prev">
- {{ if .ShowPrev }}
- <a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ end }}" data-page="previous">{{ t "Previous" }}</a>
- {{ else }}
- {{ t "Previous" }}
- {{ end }}
- </div>
-
- <div class="pagination-next">
- {{ if .ShowNext }}
- <a href="{{ .Route }}?offset={{ .NextOffset }}" data-page="next">{{ t "Next" }}</a>
- {{ else }}
- {{ t "Next" }}
- {{ end }}
- </div>
-</div>
-{{ end }}
-`,
-}
-
-var templateCommonMapChecksums = map[string]string{
- "entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
- "layout": "83786d9e657a17cb531007b5639dc021b7cb2bff1a19162769b3a961a22e5087",
- "pagination": "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
-}
diff --git a/server/template/html/about.html b/server/template/html/about.html
deleted file mode 100644
index 24c0f2c..0000000
--- a/server/template/html/about.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{{ define "title"}}{{ t "About" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "About" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- {{ if .user.IsAdmin }}
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- {{ end }}
- </ul>
-</section>
-
-<div class="panel">
- <h3>{{ t "Version" }}</h3>
- <ul>
- <li><strong>{{ t "Version:" }}</strong> {{ .version }}</li>
- <li><strong>{{ t "Build Date:" }}</strong> {{ .build_date }}</li>
- </ul>
-</div>
-
-<div class="panel">
- <h3>{{ t "Authors" }}</h3>
- <ul>
- <li><strong>{{ t "Author:" }}</strong> Frédéric Guillot</li>
- <li><strong>{{ t "License:" }}</strong> Apache 2.0</li>
- </ul>
-</div>
-
-{{ end }}
diff --git a/server/template/html/add_subscription.html b/server/template/html/add_subscription.html
deleted file mode 100644
index b65dabb..0000000
--- a/server/template/html/add_subscription.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{{ define "title"}}{{ t "New Subscription" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New Subscription" }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .categories }}
- <p class="alert alert-error">{{ t "There is no category. You must have at least one category." }}</p>
-{{ else }}
- <form action="{{ route "submitSubscription" }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-url">{{ t "URL" }}</label>
- <input type="url" name="url" id="form-url" placeholder="https://domain.tld/" value="{{ .form.URL }}" required autofocus>
-
- <label for="form-category">{{ t "Category" }}</label>
- <select id="form-category" name="category_id">
- {{ range .categories }}
- <option value="{{ .ID }}">{{ .Title }}</option>
- {{ end }}
- </select>
-
- <label><input type="checkbox" name="crawler" value="1" {{ if .form.Crawler }}checked{{ end }}> {{ t "Fetch original content" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Find a subscription" }}</button>
- </div>
- </form>
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/categories.html b/server/template/html/categories.html
deleted file mode 100644
index c2d7850..0000000
--- a/server/template/html/categories.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{{ define "title"}}{{ t "Categories" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Categories" }} ({{ .total }})</h1>
- <ul>
- <li>
- <a href="{{ route "createCategory" }}">{{ t "Create a category" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .categories }}
- <p class="alert alert-error">{{ t "There is no category." }}</p>
-{{ else }}
- <div class="items">
- {{ range .categories }}
- <article class="item">
- <div class="item-header">
- <span class="item-title">
- <a href="{{ route "categoryEntries" "categoryID" .ID }}">{{ .Title }}</a>
- </span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- {{ if eq .FeedCount 0 }}
- {{ t "No feed." }}
- {{ else }}
- {{ plural "plural.categories.feed_count" .FeedCount .FeedCount }}
- {{ end }}
- </li>
- </ul>
- <ul>
- <li>
- <a href="{{ route "editCategory" "categoryID" .ID }}">{{ t "Edit" }}</a>
- </li>
- {{ if eq .FeedCount 0 }}
- <li>
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeCategory" "categoryID" .ID }}">{{ t "Remove" }}</a>
- </li>
- {{ end }}
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/category_entries.html b/server/template/html/category_entries.html
deleted file mode 100644
index ff73a16..0000000
--- a/server/template/html/category_entries.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{{ define "title"}}{{ .category.Title }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ .category.Title }} ({{ .total }})</h1>
- {{ if .entries }}
- <ul>
- <li>
- <a href="#" data-on-click="markPageAsRead">{{ t "Mark this page as read" }}</a>
- </li>
- </ul>
- {{ end }}
-</section>
-
-{{ if not .entries }}
- <p class="alert">{{ t "There is no article in this category." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "categoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/choose_subscription.html b/server/template/html/choose_subscription.html
deleted file mode 100644
index a1a8e68..0000000
--- a/server/template/html/choose_subscription.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{{ define "title"}}{{ t "Choose a Subscription" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New Subscription" }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "chooseSubscription" }}" method="POST">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
- <input type="hidden" name="category_id" value="{{ .categoryID }}">
-
- <h3>{{ t "Choose a Subscription" }}</h3>
-
- {{ range .subscriptions }}
- <div class="radio-group">
- <label title="{{ .URL }}"><input type="radio" name="url" value="{{ .URL }}"> {{ .Title }}</label> ({{ .Type }})
- <small title="Type = {{ .Type }}"><a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL }}</a></small>
- </div>
- {{ end }}
-
- <br>
- <label><input type="checkbox" name="crawler" value="1" {{ if .form.Crawler }}checked{{ end }}> {{ t "Fetch original content" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Subscribe" }}</button>
- </div>
-</form>
-{{ end }}
diff --git a/server/template/html/common/entry_pagination.html b/server/template/html/common/entry_pagination.html
deleted file mode 100644
index 6c9f29c..0000000
--- a/server/template/html/common/entry_pagination.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{{ define "entry_pagination" }}
-<div class="pagination">
- <div class="pagination-prev">
- {{ if .prevEntry }}
- <a href="{{ .prevEntryRoute }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "Previous" }}</a>
- {{ else }}
- {{ t "Previous" }}
- {{ end }}
- </div>
-
- <div class="pagination-next">
- {{ if .nextEntry }}
- <a href="{{ .nextEntryRoute }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "Next" }}</a>
- {{ else }}
- {{ t "Next" }}
- {{ end }}
- </div>
-</div>
-{{ end }} \ No newline at end of file
diff --git a/server/template/html/common/layout.html b/server/template/html/common/layout.html
deleted file mode 100644
index 85f1df4..0000000
--- a/server/template/html/common/layout.html
+++ /dev/null
@@ -1,124 +0,0 @@
-{{ define "base" }}
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
-
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
- <meta name="mobile-web-app-capable" content="yes">
- <meta name="apple-mobile-web-app-title" content="Miniflux">
- <link rel="manifest" href="{{ route "webManifest" }}">
-
- <meta name="robots" content="noindex,nofollow">
- <meta name="referrer" content="no-referrer">
-
- <link rel="icon" type="image/png" href="{{ route "appIcon" "filename" "favicon.png" }}">
- <link rel="apple-touch-icon" href="{{ route "appIcon" "filename" "touch-icon-iphone.png" }}">
- <link rel="apple-touch-icon" sizes="72x72" href="{{ route "appIcon" "filename" "touch-icon-ipad.png" }}">
- <link rel="apple-touch-icon" sizes="114x114" href="{{ route "appIcon" "filename" "touch-icon-iphone-retina.png" }}">
- <link rel="apple-touch-icon" sizes="144x144" href="{{ route "appIcon" "filename" "touch-icon-ipad-retina.png" }}">
- <link rel="shortcut icon" type="image/x-icon" href="{{ route "favicon" }}">
-
- {{ if .csrf }}
- <meta name="X-CSRF-Token" value="{{ .csrf }}">
- {{ end }}
- <title>{{template "title" .}} - Miniflux</title>
- {{ if .user }}
- <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" .user.Theme }}">
- {{ else }}
- <link rel="stylesheet" type="text/css" href="{{ route "stylesheet" "name" "white" }}">
- {{ end }}
- <script type="text/javascript" src="{{ route "javascript" }}" defer></script>
-</head>
-<body data-entries-status-url="{{ route "updateEntriesStatus" }}">
- {{ if .user }}
- <header class="header">
- <nav>
- <div class="logo">
- <a href="{{ route "unread" }}">Mini<span>flux</span></a>
- </div>
- <ul>
- <li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g u" }}">
- <a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}</a>
- {{ if gt .countUnread 0 }}
- <span class="unread-counter-wrapper">(<span class="unread-counter">{{ .countUnread }}</span>)</span>
- {{ end }}
- </li>
- <li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g b" }}">
- <a href="{{ route "starred" }}" data-page="starred">{{ t "Starred" }}</a>
- </li>
- <li {{ if eq .menu "history" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g h" }}">
- <a href="{{ route "history" }}" data-page="history">{{ t "History" }}</a>
- </li>
- <li {{ if eq .menu "feeds" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g f" }}">
- <a href="{{ route "feeds" }}" data-page="feeds">{{ t "Feeds" }}</a>
- </li>
- <li {{ if eq .menu "categories" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g c" }}">
- <a href="{{ route "categories" }}" data-page="categories">{{ t "Categories" }}</a>
- </li>
- <li {{ if eq .menu "settings" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g s" }}">
- <a href="{{ route "settings" }}" data-page="settings">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "logout" }}" title="Logged as {{ .user.Username }}">{{ t "Logout" }}</a>
- </li>
- </ul>
- </nav>
- </header>
- {{ end }}
- {{ if .flashMessage }}
- <div class="flash-message alert alert-success">{{ .flashMessage }}</div>
- {{ end }}
- {{ if .flashErrorMessage }}
- <div class="flash-error-message alert alert-error">{{ .flashErrorMessage }}</div>
- {{ end }}
- <main>
- {{template "content" .}}
- </main>
- <template id="keyboard-shortcuts">
- <div id="modal-left">
- <a href="#" class="btn-close-modal">x</a>
- <h3>{{ t "Keyboard Shortcuts" }}</h3>
-
- <div class="keyboard-shortcuts">
- <p>{{ t "Sections Navigation" }}</p>
- <ul>
- <li>{{ t "Go to unread" }} = <strong>g + u</strong></li>
- <li>{{ t "Go to bookmarks" }} = <strong>g + b</strong></li>
- <li>{{ t "Go to history" }} = <strong>g + h</strong></li>
- <li>{{ t "Go to feeds" }} = <strong>g + f</strong></li>
- <li>{{ t "Go to categories" }} = <strong>g + c</strong></li>
- <li>{{ t "Go to settings" }} = <strong>g + s</strong></li>
- <li>{{ t "Show keyboard shortcuts" }} = <strong>?</strong></li>
- </ul>
-
- <p>{{ t "Items Navigation" }}</p>
- <ul>
- <li>{{ t "Go to previous item" }} = <strong>p or j or ◄</strong></li>
- <li>{{ t "Go to next item" }} = <strong>n or k or ►</strong></li>
- </ul>
-
- <p>{{ t "Pages Navigation" }}</p>
- <ul>
- <li>{{ t "Go to previous page" }} = <strong>h</strong></li>
- <li>{{ t "Go to next page" }} = <strong>l</strong></li>
- </ul>
-
- <p>{{ t "Actions" }}</p>
- <ul>
- <li>{{ t "Open selected item" }} = <strong>o</strong></li>
- <li>{{ t "Open original link" }} = <strong>v</strong></li>
- <li>{{ t "Toggle read/unread" }} = <strong>m</strong></li>
- <li>{{ t "Mark current page as read" }} = <strong>A</strong></li>
- <li>{{ t "Download original content" }} = <strong>d</strong></li>
- <li>{{ t "Toggle bookmark" }} = <strong>f</strong></li>
- <li>{{ t "Save article" }} = <strong>s</strong></li>
- <li>{{ t "Close modal dialog" }} = <strong>Esc</strong></li>
- </ul>
- </div>
- </div>
- </template>
-</body>
-</html>
-{{ end }} \ No newline at end of file
diff --git a/server/template/html/common/pagination.html b/server/template/html/common/pagination.html
deleted file mode 100644
index 4c6766a..0000000
--- a/server/template/html/common/pagination.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{{ define "pagination" }}
-<div class="pagination">
- <div class="pagination-prev">
- {{ if .ShowPrev }}
- <a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ end }}" data-page="previous">{{ t "Previous" }}</a>
- {{ else }}
- {{ t "Previous" }}
- {{ end }}
- </div>
-
- <div class="pagination-next">
- {{ if .ShowNext }}
- <a href="{{ .Route }}?offset={{ .NextOffset }}" data-page="next">{{ t "Next" }}</a>
- {{ else }}
- {{ t "Next" }}
- {{ end }}
- </div>
-</div>
-{{ end }}
diff --git a/server/template/html/create_category.html b/server/template/html/create_category.html
deleted file mode 100644
index 7c4c93f..0000000
--- a/server/template/html/create_category.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{ define "title"}}{{ t "New Category" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New Category" }}</h1>
- <ul>
- <li>
- <a href="{{ route "categories" }}">{{ t "Categories" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "saveCategory" }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-title">{{ t "Title" }}</label>
- <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Save" }}</button> {{ t "or" }} <a href="{{ route "categories" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
diff --git a/server/template/html/create_user.html b/server/template/html/create_user.html
deleted file mode 100644
index 8faab49..0000000
--- a/server/template/html/create_user.html
+++ /dev/null
@@ -1,44 +0,0 @@
-{{ define "title"}}{{ t "New User" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New User" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "saveUser" }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required autofocus>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}" required>
-
- <label for="form-confirmation">{{ t "Confirmation" }}</label>
- <input type="password" name="confirmation" id="form-confirmation" value="{{ .form.Confirmation }}" required>
-
- <label><input type="checkbox" name="is_admin" value="1" {{ if .form.IsAdmin }}checked{{ end }}> {{ t "Administrator" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Save" }}</button> {{ t "or" }} <a href="{{ route "users" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
diff --git a/server/template/html/edit_category.html b/server/template/html/edit_category.html
deleted file mode 100644
index 2981fa4..0000000
--- a/server/template/html/edit_category.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{ define "title"}}{{ t "Edit Category: %s" .category.Title }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Edit Category: %s" .category.Title }}</h1>
- <ul>
- <li>
- <a href="{{ route "categories" }}">{{ t "Categories" }}</a>
- </li>
- <li>
- <a href="{{ route "createCategory" }}">{{ t "Create a category" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "updateCategory" "categoryID" .category.ID }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-title">{{ t "Title" }}</label>
- <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button> {{ t "or" }} <a href="{{ route "categories" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
diff --git a/server/template/html/edit_feed.html b/server/template/html/edit_feed.html
deleted file mode 100644
index ebc9a02..0000000
--- a/server/template/html/edit_feed.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{{ define "title"}}{{ t "Edit Feed: %s" .feed.Title }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ .feed.Title }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "addSubscription" }}">{{ t "Add subscription" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .categories }}
- <p class="alert alert-error">{{ t "There is no category!" }}</p>
-{{ else }}
- {{ if ne .feed.ParsingErrorCount 0 }}
- <div class="alert alert-error">
- <h3>{{ t "Last Parsing Error" }}</h3>
- {{ .feed.ParsingErrorMsg }}
- </div>
- {{ end }}
-
- <form action="{{ route "updateFeed" "feedID" .feed.ID }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-title">{{ t "Title" }}</label>
- <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
-
- <label for="form-site-url">{{ t "Site URL" }}</label>
- <input type="url" name="site_url" id="form-site-url" placeholder="https://domain.tld/" value="{{ .form.SiteURL }}" required>
-
- <label for="form-feed-url">{{ t "Feed URL" }}</label>
- <input type="url" name="feed_url" id="form-feed-url" placeholder="https://domain.tld/" value="{{ .form.FeedURL }}" required>
-
- <label for="form-scraper-rules">{{ t "Scraper Rules" }}</label>
- <input type="text" name="scraper_rules" id="form-scraper-rules" value="{{ .form.ScraperRules }}">
-
- <label for="form-rewrite-rules">{{ t "Rewrite Rules" }}</label>
- <input type="text" name="rewrite_rules" id="form-rewrite-rules" value="{{ .form.RewriteRules }}">
-
- <label for="form-category">{{ t "Category" }}</label>
- <select id="form-category" name="category_id">
- {{ range .categories }}
- <option value="{{ .ID }}" {{ if eq .ID $.form.CategoryID }}selected="selected"{{ end }}>{{ .Title }}</option>
- {{ end }}
- </select>
-
- <label><input type="checkbox" name="crawler" value="1" {{ if .form.Crawler }}checked{{ end }}> {{ t "Fetch original content" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button> {{ t "or" }} <a href="{{ route "feeds" }}">{{ t "cancel" }}</a>
- </div>
- </form>
-
- <div class="panel">
- <ul>
- <li><strong>{{ t "Last checked:" }} </strong><time datetime="{{ isodate .feed.CheckedAt }}" title="{{ isodate .feed.CheckedAt }}">{{ elapsed .feed.CheckedAt }}</time></li>
- <li><strong>{{ t "ETag header:" }} </strong>{{ if .feed.EtagHeader }}{{ .feed.EtagHeader }}{{ else }}{{ t "None" }}{{ end }}</li>
- <li><strong>{{ t "LastModified header:" }} </strong>{{ if .feed.LastModifiedHeader }}{{ .feed.LastModifiedHeader }}{{ else }}{{ t "None" }}{{ end }}</li>
- </ul>
- </div>
-{{ end }}
-
-{{ end }} \ No newline at end of file
diff --git a/server/template/html/edit_user.html b/server/template/html/edit_user.html
deleted file mode 100644
index 6611943..0000000
--- a/server/template/html/edit_user.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{{ define "title"}}{{ t "Edit user: %s" .selected_user.Username }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Edit user %s" .selected_user.Username }}"</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- <li>
- <a href="{{ route "createUser" }}">{{ t "Add user" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "updateUser" "userID" .selected_user.ID }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required autofocus>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}">
-
- <label for="form-confirmation">{{ t "Confirmation" }}</label>
- <input type="password" name="confirmation" id="form-confirmation" value="{{ .form.Confirmation }}">
-
- <label><input type="checkbox" name="is_admin" value="1" {{ if .form.IsAdmin }}checked{{ end }}> {{ t "Administrator" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button> {{ t "or" }} <a href="{{ route "users" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
diff --git a/server/template/html/entry.html b/server/template/html/entry.html
deleted file mode 100644
index 66d08fb..0000000
--- a/server/template/html/entry.html
+++ /dev/null
@@ -1,109 +0,0 @@
-{{ define "title"}}{{ .entry.Title }}{{ end }}
-
-{{ define "content"}}
-<section class="entry">
- <header class="entry-header">
- <h1>
- <a href="{{ .entry.URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
- </h1>
- <div class="entry-actions">
- <ul>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .entry.Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="#"
- title="{{ t "Fetch original content" }}"
- data-fetch-content-entry="true"
- data-fetch-content-url="{{ route "fetchContent" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Loading..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Fetch original content" }}</a>
- </li>
- </ul>
- </div>
- <div class="entry-meta">
- <span class="entry-website">
- {{ if ne .entry.Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
- </span>
- {{ if .entry.Author }}
- <span class="entry-author">
- {{ if isEmail .entry.Author }}
- - <a href="mailto:{{ .entry.Author }}">{{ .entry.Author }}</a>
- {{ else }}
- – <em>{{ .entry.Author }}</em>
- {{ end }}
- </span>
- {{ end }}
- <span class="category">
- <a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
- </span>
- </div>
- <div class="entry-date">
- <time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed .entry.Date }}</time>
- </div>
- </header>
- {{ if gt (len .entry.Content) 120 }}
- <div class="pagination-top">
- {{ template "entry_pagination" . }}
- </div>
- {{ end }}
- <article class="entry-content">
- {{ noescape (proxyFilter .entry.Content) }}
- </article>
- {{ if .entry.Enclosures }}
- <aside class="entry-enclosures">
- <h3>{{ t "Attachments" }}</h3>
- {{ range .entry.Enclosures }}
- <div class="entry-enclosure">
- {{ if hasPrefix .MimeType "audio/" }}
- <div class="enclosure-audio">
- <audio controls preload="metadata">
- <source src="{{ .URL }}" type="{{ .MimeType }}">
- </audio>
- </div>
- {{ else if hasPrefix .MimeType "video/" }}
- <div class="enclosure-video">
- <video controls preload="metadata">
- <source src="{{ .URL }}" type="{{ .MimeType }}">
- </video>
- </div>
- {{ else if hasPrefix .MimeType "image/" }}
- <div class="enclosure-image">
- <img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" alt="{{ .URL }} ({{ .MimeType }})">
- </div>
- {{ end }}
-
- <div class="entry-enclosure-download">
- <a href="{{ .URL }}" title="{{ .URL }} ({{ .MimeType }})" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ t "Download" }}</a>
- <small>({{ .URL }})</small>
- </div>
- </div>
- {{ end }}
- </aside>
- {{ end }}
-</section>
-
-<div class="pagination-bottom">
- {{ template "entry_pagination" . }}
-</div>
-{{ end }}
diff --git a/server/template/html/feed_entries.html b/server/template/html/feed_entries.html
deleted file mode 100644
index 4317f88..0000000
--- a/server/template/html/feed_entries.html
+++ /dev/null
@@ -1,79 +0,0 @@
-{{ define "title"}}{{ .feed.Title }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ .feed.Title }} ({{ .total }})</h1>
- <ul>
- <li>
- <a href="{{ route "refreshFeed" "feedID" .feed.ID }}">{{ t "Refresh" }}</a>
- </li>
- <li>
- <a href="{{ route "editFeed" "feedID" .feed.ID }}">{{ t "Edit" }}</a>
- </li>
- {{ if .entries }}
- <li>
- <a href="#" data-on-click="markPageAsRead">{{ t "Mark this page as read" }}</a>
- </li>
- {{ end }}
- </ul>
-</section>
-
-{{ if ne .feed.ParsingErrorCount 0 }}
-<div class="alert alert-error">
- <h3>{{ t "There is a problem with this feed" }}</h3>
- {{ .feed.ParsingErrorMsg }}
-</div>
-{{ else if not .entries }}
- <p class="alert">{{ t "There is no article for this feed." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "feedEntry" "feedID" .Feed.ID "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/feeds.html b/server/template/html/feeds.html
deleted file mode 100644
index 5500c92..0000000
--- a/server/template/html/feeds.html
+++ /dev/null
@@ -1,77 +0,0 @@
-{{ define "title"}}{{ t "Feeds" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Feeds" }} ({{ .total }})</h1>
- <ul>
- <li>
- <a href="{{ route "addSubscription" }}">{{ t "Add subscription" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- <li>
- <a href="{{ route "refreshAllFeeds" }}">{{ t "Refresh all feeds in background" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .feeds }}
- <p class="alert">{{ t "You don't have any subscription." }}</p>
-{{ else }}
- <div class="items">
- {{ range .feeds }}
- <article class="item {{ if ne .ParsingErrorCount 0 }}feed-parsing-error{{ end }}">
- <div class="item-header">
- <span class="item-title">
- {{ if .Icon }}
- <img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "feedEntries" "feedID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category">
- <a href="{{ route "categoryEntries" "categoryID" .Category.ID }}">{{ .Category.Title }}</a>
- </span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ .SiteURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ domain .SiteURL }}</a>
- </li>
- <li>
- {{ t "Last check:" }} <time datetime="{{ isodate .CheckedAt }}" title="{{ isodate .CheckedAt }}">{{ elapsed .CheckedAt }}</time>
- </li>
- </ul>
- <ul>
- <li>
- <a href="{{ route "refreshFeed" "feedID" .ID }}">{{ t "Refresh" }}</a>
- </li>
- <li>
- <a href="{{ route "editFeed" "feedID" .ID }}">{{ t "Edit" }}</a>
- </li>
- <li>
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeFeed" "feedID" .ID }}">{{ t "Remove" }}</a>
- </li>
- </ul>
- </div>
- {{ if ne .ParsingErrorCount 0 }}
- <div class="parsing-error">
- <strong title="{{ .ParsingErrorMsg }}" class="parsing-error-count">{{ plural "plural.feed.error_count" .ParsingErrorCount .ParsingErrorCount }}</strong>
- <small class="parsing-error-message">({{ .ParsingErrorMsg }})</small>
- </div>
- {{ end }}
- </article>
- {{ end }}
- </div>
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/history.html b/server/template/html/history.html
deleted file mode 100644
index 5baa0df..0000000
--- a/server/template/html/history.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{{ define "title"}}{{ t "History" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "History" }} ({{ .total }})</h1>
- {{ if .entries }}
- <ul>
- <li>
- <a href="{{ route "flushHistory" }}">{{ t "Flush history" }}</a>
- </li>
- </ul>
- {{ end }}
-</section>
-
-{{ if not .entries }}
- <p class="alert alert-info">{{ t "There is no history at the moment." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "readEntry" "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/import.html b/server/template/html/import.html
deleted file mode 100644
index dbdb9b0..0000000
--- a/server/template/html/import.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{{ define "title"}}{{ t "Import" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Import" }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "addSubscription" }}">{{ t "Add subscription" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "uploadOPML" }}" method="post" enctype="multipart/form-data">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-file">{{ t "OPML file" }}</label>
- <input type="file" name="file" id="form-file">
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Import" }}</button>
- </div>
-</form>
-
-{{ end }}
diff --git a/server/template/html/integrations.html b/server/template/html/integrations.html
deleted file mode 100644
index 5005d68..0000000
--- a/server/template/html/integrations.html
+++ /dev/null
@@ -1,112 +0,0 @@
-{{ define "title"}}{{ t "Integrations" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Integrations" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- {{ if .user.IsAdmin }}
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- {{ end }}
- <li>
- <a href="{{ route "about" }}">{{ t "About" }}</a>
- </li>
- </ul>
-</section>
-
-<form method="post" autocomplete="off" action="{{ route "updateIntegration" }}">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <h3>Fever</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="fever_enabled" value="1" {{ if .form.FeverEnabled }}checked{{ end }}> {{ t "Activate Fever API" }}
- </label>
-
- <label for="form-fever-username">{{ t "Fever Username" }}</label>
- <input type="text" name="fever_username" id="form-fever-username" value="{{ .form.FeverUsername }}">
-
- <label for="form-fever-password">{{ t "Fever Password" }}</label>
- <input type="password" name="fever_password" id="form-fever-password" value="{{ .form.FeverPassword }}">
- </div>
-
- <h3>Pinboard</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="pinboard_enabled" value="1" {{ if .form.PinboardEnabled }}checked{{ end }}> {{ t "Save articles to Pinboard" }}
- </label>
-
- <label for="form-pinboard-token">{{ t "Pinboard API Token" }}</label>
- <input type="password" name="pinboard_token" id="form-pinboard-token" value="{{ .form.PinboardToken }}">
-
- <label for="form-pinboard-tags">{{ t "Pinboard Tags" }}</label>
- <input type="text" name="pinboard_tags" id="form-pinboard-tags" value="{{ .form.PinboardTags }}">
-
- <label>
- <input type="checkbox" name="pinboard_mark_as_unread" value="1" {{ if .form.PinboardMarkAsUnread }}checked{{ end }}> {{ t "Mark bookmark as unread" }}
- </label>
- </div>
-
- <h3>Instapaper</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="instapaper_enabled" value="1" {{ if .form.InstapaperEnabled }}checked{{ end }}> {{ t "Save articles to Instapaper" }}
- </label>
-
- <label for="form-instapaper-username">{{ t "Instapaper Username" }}</label>
- <input type="text" name="instapaper_username" id="form-instapaper-username" value="{{ .form.InstapaperUsername }}">
-
- <label for="form-instapaper-password">{{ t "Instapaper Password" }}</label>
- <input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}">
- </div>
-
- <h3>Wallabag</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "Save articles to Wallabag" }}
- </label>
-
- <label for="form-wallabag-url">{{ t "Wallabag API Endpoint" }}</label>
- <input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/">
-
- <label for="form-wallabag-client-id">{{ t "Wallabag Client ID" }}</label>
- <input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}">
-
- <label for="form-wallabag-client-secret">{{ t "Wallabag Client Secret" }}</label>
- <input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}">
-
- <label for="form-wallabag-username">{{ t "Wallabag Username" }}</label>
- <input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}">
-
- <label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
- <input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
- </div>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
- </div>
-</form>
-
-<div class="panel">
- <h3>{{ t "Bookmarklet" }}</h3>
- <p>{{ t "This special link allows you to subscribe to a website directly by using a bookmark in your web browser." }}</p>
-
- <div class="bookmarklet">
- <a href="javascript:location.href='{{ baseURL }}{{ route "bookmarklet" }}?uri='+encodeURIComponent(window.location.href)">{{ t "Add to Miniflux" }}</a>
- </div>
-
- <p>{{ t "Drag and drop this link to your bookmarks." }}</p>
-</div>
-
-{{ end }}
diff --git a/server/template/html/login.html b/server/template/html/login.html
deleted file mode 100644
index 906458a..0000000
--- a/server/template/html/login.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{{ define "title"}}{{ t "Sign In" }}{{ end }}
-
-{{ define "content"}}
-<section class="login-form">
- <form action="{{ route "checkLogin" }}" method="post">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required autofocus>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}" required>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Sign in" }}</button>
- </div>
- </form>
- {{ if hasOAuth2Provider "google" }}
- <div class="oauth2">
- <a href="{{ route "oauth2Redirect" "provider" "google" }}">{{ t "Sign in with Google" }}</a>
- </div>
- {{ end }}
-</section>
-{{ end }}
diff --git a/server/template/html/sessions.html b/server/template/html/sessions.html
deleted file mode 100644
index 6c76867..0000000
--- a/server/template/html/sessions.html
+++ /dev/null
@@ -1,51 +0,0 @@
-{{ define "title"}}{{ t "Sessions" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Sessions" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- <li>
- <a href="{{ route "createUser" }}">{{ t "Add user" }}</a>
- </li>
- </ul>
-</section>
-
-<table>
- <tr>
- <th>{{ t "Date" }}</th>
- <th>{{ t "IP Address" }}</th>
- <th>{{ t "User Agent" }}</th>
- <th>{{ t "Actions" }}</th>
- </tr>
- {{ range .sessions }}
- <tr {{ if eq .Token $.currentSessionToken }}class="row-highlighted"{{ end }}>
- <td class="column-20" title="{{ isodate .CreatedAt }}">{{ elapsed .CreatedAt }}</td>
- <td class="column-20" title="{{ .IP }}">{{ .IP }}</td>
- <td title="{{ .UserAgent }}">{{ .UserAgent }}</td>
- <td class="column-20">
- {{ if eq .Token $.currentSessionToken }}
- {{ t "Current session" }}
- {{ else }}
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeSession" "sessionID" .ID }}">{{ t "Remove" }}</a>
- {{ end }}
- </td>
- </tr>
- {{ end }}
-</table>
-
-{{ end }}
diff --git a/server/template/html/settings.html b/server/template/html/settings.html
deleted file mode 100644
index 8e66a10..0000000
--- a/server/template/html/settings.html
+++ /dev/null
@@ -1,82 +0,0 @@
-{{ define "title"}}{{ t "Settings" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Settings" }}</h1>
- <ul>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- {{ if .user.IsAdmin }}
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- {{ end }}
- <li>
- <a href="{{ route "about" }}">{{ t "About" }}</a>
- </li>
- </ul>
-</section>
-
-<form method="post" autocomplete="off" action="{{ route "updateSettings" }}">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}" autocomplete="off">
-
- <label for="form-confirmation">{{ t "Confirmation" }}</label>
- <input type="password" name="confirmation" id="form-confirmation" value="{{ .form.Confirmation }}" autocomplete="off">
-
- <label for="form-language">{{ t "Language" }}</label>
- <select id="form-language" name="language">
- {{ range $key, $value := .languages }}
- <option value="{{ $key }}" {{ if eq $key $.form.Language }}selected="selected"{{ end }}>{{ $value }}</option>
- {{ end }}
- </select>
-
- <label for="form-timezone">{{ t "Timezone" }}</label>
- <select id="form-timezone" name="timezone">
- {{ range $key, $value := .timezones }}
- <option value="{{ $key }}" {{ if eq $key $.form.Timezone }}selected="selected"{{ end }}>{{ $value }}</option>
- {{ end }}
- </select>
-
- <label for="form-theme">{{ t "Theme" }}</label>
- <select id="form-theme" name="theme">
- {{ range $key, $value := .themes }}
- <option value="{{ $key }}" {{ if eq $key $.form.Theme }}selected="selected"{{ end }}>{{ $value }}</option>
- {{ end }}
- </select>
-
- <label for="form-entry-direction">{{ t "Entry Sorting" }}</label>
- <select id="form-entry-direction" name="entry_direction">
- <option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Older entries first" }}</option>
- <option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Recent entries first" }}</option>
- </select>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
- </div>
-</form>
-
-{{ if hasOAuth2Provider "google" }}
-<div class="panel">
- {{ if hasKey .user.Extra "google_id" }}
- <a href="{{ route "oauth2Unlink" "provider" "google" }}">{{ t "Unlink my Google account" }}</a>
- {{ else }}
- <a href="{{ route "oauth2Redirect" "provider" "google" }}">{{ t "Link my Google account" }}</a>
- {{ end }}
-</div>
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/starred.html b/server/template/html/starred.html
deleted file mode 100644
index 1ed1b13..0000000
--- a/server/template/html/starred.html
+++ /dev/null
@@ -1,61 +0,0 @@
-{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Favorites" }} ({{ .total }})</h1>
-</section>
-
-{{ if not .entries }}
- <p class="alert alert-info">{{ t "There is no bookmark at the moment." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
diff --git a/server/template/html/unread.html b/server/template/html/unread.html
deleted file mode 100644
index feb5beb..0000000
--- a/server/template/html/unread.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{{ define "title"}}{{ t "Unread Items" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Unread" }} (<span class="unread-counter">{{ .countUnread }}</span>)</h1>
- {{ if .entries }}
- <ul>
- <li>
- <a href="#" data-on-click="markPageAsRead">{{ t "Mark this page as read" }}</a>
- </li>
- </ul>
- {{ end }}
-</section>
-
-{{ if not .entries }}
- <p class="alert">{{ t "There is no unread article." }}</p>
-{{ else }}
- <div class="items hide-read-items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "unreadEntry" "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }} \ No newline at end of file
diff --git a/server/template/html/users.html b/server/template/html/users.html
deleted file mode 100644
index 40ca5e9..0000000
--- a/server/template/html/users.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{{ define "title"}}{{ t "Users" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Users" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- <li>
- <a href="{{ route "createUser" }}">{{ t "Add user" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if eq (len .users) 1 }}
- <p class="alert">{{ t "You are the only user." }}</p>
-{{ else }}
- <table>
- <tr>
- <th class="column-20">{{ t "Username" }}</th>
- <th>{{ t "Administrator" }}</th>
- <th>{{ t "Last Login" }}</th>
- <th>{{ t "Actions" }}</th>
- </tr>
- {{ range .users }}
- {{ if ne .ID $.user.ID }}
- <tr>
- <td>{{ .Username }}</td>
- <td>{{ if eq .IsAdmin true }}{{ t "Yes" }}{{ else }}{{ t "No" }}{{ end }}</td>
- <td>
- {{ if .LastLoginAt }}
- <time datetime="{{ isodate .LastLoginAt }}" title="{{ isodate .LastLoginAt }}">{{ elapsed .LastLoginAt }}</time>
- {{ else }}
- {{ t "Never" }}
- {{ end }}
- </td>
- <td>
- <a href="{{ route "editUser" "userID" .ID }}">{{ t "Edit" }}</a>,
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeUser" "userID" .ID }}">{{ t "Remove" }}</a>
- </td>
- </tr>
- {{ end }}
- {{ end }}
- </table>
-{{ end }}
-
-{{ end }}
diff --git a/server/template/template.go b/server/template/template.go
deleted file mode 100644
index a87d097..0000000
--- a/server/template/template.go
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright 2017 Frédéric Guilloe. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package template
-
-import (
- "bytes"
- "html/template"
- "io"
- "net/mail"
- "strings"
- "time"
-
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/duration"
- "github.com/miniflux/miniflux/errors"
- "github.com/miniflux/miniflux/locale"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/route"
- "github.com/miniflux/miniflux/server/ui/filter"
- "github.com/miniflux/miniflux/url"
-
- "github.com/gorilla/mux"
-)
-
-// Engine handles the templating system.
-type Engine struct {
- templates map[string]*template.Template
- router *mux.Router
- translator *locale.Translator
- currentLocale *locale.Language
- cfg *config.Config
-}
-
-func (e *Engine) parseAll() {
- funcMap := template.FuncMap{
- "baseURL": func() string {
- return e.cfg.Get("BASE_URL", config.DefaultBaseURL)
- },
- "hasOAuth2Provider": func(provider string) bool {
- return e.cfg.Get("OAUTH2_PROVIDER", "") == provider
- },
- "hasKey": func(dict map[string]string, key string) bool {
- if value, found := dict[key]; found {
- return value != ""
- }
- return false
- },
- "route": func(name string, args ...interface{}) string {
- return route.Path(e.router, name, args...)
- },
- "noescape": func(str string) template.HTML {
- return template.HTML(str)
- },
- "proxyFilter": func(data string) string {
- return filter.ImageProxyFilter(e.router, data)
- },
- "proxyURL": func(link string) string {
- if url.IsHTTPS(link) {
- return link
- }
-
- return filter.Proxify(e.router, link)
- },
- "domain": func(websiteURL string) string {
- return url.Domain(websiteURL)
- },
- "isEmail": func(str string) bool {
- _, err := mail.ParseAddress(str)
- if err != nil {
- return false
- }
- return true
- },
- "hasPrefix": func(str, prefix string) bool {
- return strings.HasPrefix(str, prefix)
- },
- "contains": func(str, substr string) bool {
- return strings.Contains(str, substr)
- },
- "isodate": func(ts time.Time) string {
- return ts.Format("2006-01-02 15:04:05")
- },
- "elapsed": func(ts time.Time) string {
- return duration.ElapsedTime(e.currentLocale, ts)
- },
- "t": func(key interface{}, args ...interface{}) string {
- switch key.(type) {
- case string:
- return e.currentLocale.Get(key.(string), args...)
- case errors.LocalizedError:
- err := key.(errors.LocalizedError)
- return err.Localize(e.currentLocale)
- case error:
- return key.(error).Error()
- default:
- return ""
- }
- },
- "plural": func(key string, n int, args ...interface{}) string {
- return e.currentLocale.Plural(key, n, args...)
- },
- }
-
- commonTemplates := ""
- for _, content := range templateCommonMap {
- commonTemplates += content
- }
-
- for name, content := range templateViewsMap {
- logger.Debug("[Template] Parsing: %s", name)
- e.templates[name] = template.Must(template.New("main").Funcs(funcMap).Parse(commonTemplates + content))
- }
-}
-
-// SetLanguage change the language for template processing.
-func (e *Engine) SetLanguage(language string) {
- e.currentLocale = e.translator.GetLanguage(language)
-}
-
-// Execute process a template.
-func (e *Engine) Execute(w io.Writer, name string, data interface{}) {
- tpl, ok := e.templates[name]
- if !ok {
- logger.Fatal("[Template] The template %s does not exists", name)
- }
-
- var b bytes.Buffer
- err := tpl.ExecuteTemplate(&b, "base", data)
- if err != nil {
- logger.Fatal("[Template] Unable to render template: %v", err)
- }
-
- b.WriteTo(w)
-}
-
-// NewEngine returns a new template Engine.
-func NewEngine(cfg *config.Config, router *mux.Router, translator *locale.Translator) *Engine {
- tpl := &Engine{
- templates: make(map[string]*template.Template),
- router: router,
- translator: translator,
- cfg: cfg,
- }
-
- tpl.parseAll()
- return tpl
-}
diff --git a/server/template/views.go b/server/template/views.go
deleted file mode 100644
index 4eac6e4..0000000
--- a/server/template/views.go
+++ /dev/null
@@ -1,1356 +0,0 @@
-// Code generated by go generate; DO NOT EDIT.
-// 2017-12-31 18:38:42.048775793 -0800 PST m=+0.029606951
-
-package template
-
-var templateViewsMap = map[string]string{
- "about": `{{ define "title"}}{{ t "About" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "About" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- {{ if .user.IsAdmin }}
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- {{ end }}
- </ul>
-</section>
-
-<div class="panel">
- <h3>{{ t "Version" }}</h3>
- <ul>
- <li><strong>{{ t "Version:" }}</strong> {{ .version }}</li>
- <li><strong>{{ t "Build Date:" }}</strong> {{ .build_date }}</li>
- </ul>
-</div>
-
-<div class="panel">
- <h3>{{ t "Authors" }}</h3>
- <ul>
- <li><strong>{{ t "Author:" }}</strong> Frédéric Guillot</li>
- <li><strong>{{ t "License:" }}</strong> Apache 2.0</li>
- </ul>
-</div>
-
-{{ end }}
-`,
- "add_subscription": `{{ define "title"}}{{ t "New Subscription" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New Subscription" }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .categories }}
- <p class="alert alert-error">{{ t "There is no category. You must have at least one category." }}</p>
-{{ else }}
- <form action="{{ route "submitSubscription" }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-url">{{ t "URL" }}</label>
- <input type="url" name="url" id="form-url" placeholder="https://domain.tld/" value="{{ .form.URL }}" required autofocus>
-
- <label for="form-category">{{ t "Category" }}</label>
- <select id="form-category" name="category_id">
- {{ range .categories }}
- <option value="{{ .ID }}">{{ .Title }}</option>
- {{ end }}
- </select>
-
- <label><input type="checkbox" name="crawler" value="1" {{ if .form.Crawler }}checked{{ end }}> {{ t "Fetch original content" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Find a subscription" }}</button>
- </div>
- </form>
-{{ end }}
-
-{{ end }}
-`,
- "categories": `{{ define "title"}}{{ t "Categories" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Categories" }} ({{ .total }})</h1>
- <ul>
- <li>
- <a href="{{ route "createCategory" }}">{{ t "Create a category" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .categories }}
- <p class="alert alert-error">{{ t "There is no category." }}</p>
-{{ else }}
- <div class="items">
- {{ range .categories }}
- <article class="item">
- <div class="item-header">
- <span class="item-title">
- <a href="{{ route "categoryEntries" "categoryID" .ID }}">{{ .Title }}</a>
- </span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- {{ if eq .FeedCount 0 }}
- {{ t "No feed." }}
- {{ else }}
- {{ plural "plural.categories.feed_count" .FeedCount .FeedCount }}
- {{ end }}
- </li>
- </ul>
- <ul>
- <li>
- <a href="{{ route "editCategory" "categoryID" .ID }}">{{ t "Edit" }}</a>
- </li>
- {{ if eq .FeedCount 0 }}
- <li>
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeCategory" "categoryID" .ID }}">{{ t "Remove" }}</a>
- </li>
- {{ end }}
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
-{{ end }}
-
-{{ end }}
-`,
- "category_entries": `{{ define "title"}}{{ .category.Title }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ .category.Title }} ({{ .total }})</h1>
- {{ if .entries }}
- <ul>
- <li>
- <a href="#" data-on-click="markPageAsRead">{{ t "Mark this page as read" }}</a>
- </li>
- </ul>
- {{ end }}
-</section>
-
-{{ if not .entries }}
- <p class="alert">{{ t "There is no article in this category." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "categoryEntry" "categoryID" .Feed.Category.ID "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
-`,
- "choose_subscription": `{{ define "title"}}{{ t "Choose a Subscription" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New Subscription" }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "chooseSubscription" }}" method="POST">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
- <input type="hidden" name="category_id" value="{{ .categoryID }}">
-
- <h3>{{ t "Choose a Subscription" }}</h3>
-
- {{ range .subscriptions }}
- <div class="radio-group">
- <label title="{{ .URL }}"><input type="radio" name="url" value="{{ .URL }}"> {{ .Title }}</label> ({{ .Type }})
- <small title="Type = {{ .Type }}"><a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .URL }}</a></small>
- </div>
- {{ end }}
-
- <br>
- <label><input type="checkbox" name="crawler" value="1" {{ if .form.Crawler }}checked{{ end }}> {{ t "Fetch original content" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Subscribe" }}</button>
- </div>
-</form>
-{{ end }}
-`,
- "create_category": `{{ define "title"}}{{ t "New Category" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New Category" }}</h1>
- <ul>
- <li>
- <a href="{{ route "categories" }}">{{ t "Categories" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "saveCategory" }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-title">{{ t "Title" }}</label>
- <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Save" }}</button> {{ t "or" }} <a href="{{ route "categories" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
-`,
- "create_user": `{{ define "title"}}{{ t "New User" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "New User" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "saveUser" }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required autofocus>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}" required>
-
- <label for="form-confirmation">{{ t "Confirmation" }}</label>
- <input type="password" name="confirmation" id="form-confirmation" value="{{ .form.Confirmation }}" required>
-
- <label><input type="checkbox" name="is_admin" value="1" {{ if .form.IsAdmin }}checked{{ end }}> {{ t "Administrator" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Save" }}</button> {{ t "or" }} <a href="{{ route "users" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
-`,
- "edit_category": `{{ define "title"}}{{ t "Edit Category: %s" .category.Title }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Edit Category: %s" .category.Title }}</h1>
- <ul>
- <li>
- <a href="{{ route "categories" }}">{{ t "Categories" }}</a>
- </li>
- <li>
- <a href="{{ route "createCategory" }}">{{ t "Create a category" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "updateCategory" "categoryID" .category.ID }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-title">{{ t "Title" }}</label>
- <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button> {{ t "or" }} <a href="{{ route "categories" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
-`,
- "edit_feed": `{{ define "title"}}{{ t "Edit Feed: %s" .feed.Title }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ .feed.Title }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "addSubscription" }}">{{ t "Add subscription" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .categories }}
- <p class="alert alert-error">{{ t "There is no category!" }}</p>
-{{ else }}
- {{ if ne .feed.ParsingErrorCount 0 }}
- <div class="alert alert-error">
- <h3>{{ t "Last Parsing Error" }}</h3>
- {{ .feed.ParsingErrorMsg }}
- </div>
- {{ end }}
-
- <form action="{{ route "updateFeed" "feedID" .feed.ID }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-title">{{ t "Title" }}</label>
- <input type="text" name="title" id="form-title" value="{{ .form.Title }}" required autofocus>
-
- <label for="form-site-url">{{ t "Site URL" }}</label>
- <input type="url" name="site_url" id="form-site-url" placeholder="https://domain.tld/" value="{{ .form.SiteURL }}" required>
-
- <label for="form-feed-url">{{ t "Feed URL" }}</label>
- <input type="url" name="feed_url" id="form-feed-url" placeholder="https://domain.tld/" value="{{ .form.FeedURL }}" required>
-
- <label for="form-scraper-rules">{{ t "Scraper Rules" }}</label>
- <input type="text" name="scraper_rules" id="form-scraper-rules" value="{{ .form.ScraperRules }}">
-
- <label for="form-rewrite-rules">{{ t "Rewrite Rules" }}</label>
- <input type="text" name="rewrite_rules" id="form-rewrite-rules" value="{{ .form.RewriteRules }}">
-
- <label for="form-category">{{ t "Category" }}</label>
- <select id="form-category" name="category_id">
- {{ range .categories }}
- <option value="{{ .ID }}" {{ if eq .ID $.form.CategoryID }}selected="selected"{{ end }}>{{ .Title }}</option>
- {{ end }}
- </select>
-
- <label><input type="checkbox" name="crawler" value="1" {{ if .form.Crawler }}checked{{ end }}> {{ t "Fetch original content" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button> {{ t "or" }} <a href="{{ route "feeds" }}">{{ t "cancel" }}</a>
- </div>
- </form>
-
- <div class="panel">
- <ul>
- <li><strong>{{ t "Last checked:" }} </strong><time datetime="{{ isodate .feed.CheckedAt }}" title="{{ isodate .feed.CheckedAt }}">{{ elapsed .feed.CheckedAt }}</time></li>
- <li><strong>{{ t "ETag header:" }} </strong>{{ if .feed.EtagHeader }}{{ .feed.EtagHeader }}{{ else }}{{ t "None" }}{{ end }}</li>
- <li><strong>{{ t "LastModified header:" }} </strong>{{ if .feed.LastModifiedHeader }}{{ .feed.LastModifiedHeader }}{{ else }}{{ t "None" }}{{ end }}</li>
- </ul>
- </div>
-{{ end }}
-
-{{ end }}`,
- "edit_user": `{{ define "title"}}{{ t "Edit user: %s" .selected_user.Username }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Edit user %s" .selected_user.Username }}"</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- <li>
- <a href="{{ route "createUser" }}">{{ t "Add user" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "updateUser" "userID" .selected_user.ID }}" method="post" autocomplete="off">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required autofocus>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}">
-
- <label for="form-confirmation">{{ t "Confirmation" }}</label>
- <input type="password" name="confirmation" id="form-confirmation" value="{{ .form.Confirmation }}">
-
- <label><input type="checkbox" name="is_admin" value="1" {{ if .form.IsAdmin }}checked{{ end }}> {{ t "Administrator" }}</label>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button> {{ t "or" }} <a href="{{ route "users" }}">{{ t "cancel" }}</a>
- </div>
-</form>
-{{ end }}
-`,
- "entry": `{{ define "title"}}{{ .entry.Title }}{{ end }}
-
-{{ define "content"}}
-<section class="entry">
- <header class="entry-header">
- <h1>
- <a href="{{ .entry.URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ .entry.Title }}</a>
- </h1>
- <div class="entry-actions">
- <ul>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .entry.Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="#"
- title="{{ t "Fetch original content" }}"
- data-fetch-content-entry="true"
- data-fetch-content-url="{{ route "fetchContent" "entryID" .entry.ID }}"
- data-label-loading="{{ t "Loading..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Fetch original content" }}</a>
- </li>
- </ul>
- </div>
- <div class="entry-meta">
- <span class="entry-website">
- {{ if ne .entry.Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .entry.Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "feedEntries" "feedID" .entry.Feed.ID }}">{{ .entry.Feed.Title }}</a>
- </span>
- {{ if .entry.Author }}
- <span class="entry-author">
- {{ if isEmail .entry.Author }}
- - <a href="mailto:{{ .entry.Author }}">{{ .entry.Author }}</a>
- {{ else }}
- – <em>{{ .entry.Author }}</em>
- {{ end }}
- </span>
- {{ end }}
- <span class="category">
- <a href="{{ route "categoryEntries" "categoryID" .entry.Feed.Category.ID }}">{{ .entry.Feed.Category.Title }}</a>
- </span>
- </div>
- <div class="entry-date">
- <time datetime="{{ isodate .entry.Date }}" title="{{ isodate .entry.Date }}">{{ elapsed .entry.Date }}</time>
- </div>
- </header>
- {{ if gt (len .entry.Content) 120 }}
- <div class="pagination-top">
- {{ template "entry_pagination" . }}
- </div>
- {{ end }}
- <article class="entry-content">
- {{ noescape (proxyFilter .entry.Content) }}
- </article>
- {{ if .entry.Enclosures }}
- <aside class="entry-enclosures">
- <h3>{{ t "Attachments" }}</h3>
- {{ range .entry.Enclosures }}
- <div class="entry-enclosure">
- {{ if hasPrefix .MimeType "audio/" }}
- <div class="enclosure-audio">
- <audio controls preload="metadata">
- <source src="{{ .URL }}" type="{{ .MimeType }}">
- </audio>
- </div>
- {{ else if hasPrefix .MimeType "video/" }}
- <div class="enclosure-video">
- <video controls preload="metadata">
- <source src="{{ .URL }}" type="{{ .MimeType }}">
- </video>
- </div>
- {{ else if hasPrefix .MimeType "image/" }}
- <div class="enclosure-image">
- <img src="{{ proxyURL .URL }}" title="{{ .URL }} ({{ .MimeType }})" alt="{{ .URL }} ({{ .MimeType }})">
- </div>
- {{ end }}
-
- <div class="entry-enclosure-download">
- <a href="{{ .URL }}" title="{{ .URL }} ({{ .MimeType }})" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">{{ t "Download" }}</a>
- <small>({{ .URL }})</small>
- </div>
- </div>
- {{ end }}
- </aside>
- {{ end }}
-</section>
-
-<div class="pagination-bottom">
- {{ template "entry_pagination" . }}
-</div>
-{{ end }}
-`,
- "feed_entries": `{{ define "title"}}{{ .feed.Title }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ .feed.Title }} ({{ .total }})</h1>
- <ul>
- <li>
- <a href="{{ route "refreshFeed" "feedID" .feed.ID }}">{{ t "Refresh" }}</a>
- </li>
- <li>
- <a href="{{ route "editFeed" "feedID" .feed.ID }}">{{ t "Edit" }}</a>
- </li>
- {{ if .entries }}
- <li>
- <a href="#" data-on-click="markPageAsRead">{{ t "Mark this page as read" }}</a>
- </li>
- {{ end }}
- </ul>
-</section>
-
-{{ if ne .feed.ParsingErrorCount 0 }}
-<div class="alert alert-error">
- <h3>{{ t "There is a problem with this feed" }}</h3>
- {{ .feed.ParsingErrorMsg }}
-</div>
-{{ else if not .entries }}
- <p class="alert">{{ t "There is no article for this feed." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "feedEntry" "feedID" .Feed.ID "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
-`,
- "feeds": `{{ define "title"}}{{ t "Feeds" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Feeds" }} ({{ .total }})</h1>
- <ul>
- <li>
- <a href="{{ route "addSubscription" }}">{{ t "Add subscription" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- <li>
- <a href="{{ route "import" }}">{{ t "Import" }}</a>
- </li>
- <li>
- <a href="{{ route "refreshAllFeeds" }}">{{ t "Refresh all feeds in background" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if not .feeds }}
- <p class="alert">{{ t "You don't have any subscription." }}</p>
-{{ else }}
- <div class="items">
- {{ range .feeds }}
- <article class="item {{ if ne .ParsingErrorCount 0 }}feed-parsing-error{{ end }}">
- <div class="item-header">
- <span class="item-title">
- {{ if .Icon }}
- <img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "feedEntries" "feedID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category">
- <a href="{{ route "categoryEntries" "categoryID" .Category.ID }}">{{ .Category.Title }}</a>
- </span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ .SiteURL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ domain .SiteURL }}</a>
- </li>
- <li>
- {{ t "Last check:" }} <time datetime="{{ isodate .CheckedAt }}" title="{{ isodate .CheckedAt }}">{{ elapsed .CheckedAt }}</time>
- </li>
- </ul>
- <ul>
- <li>
- <a href="{{ route "refreshFeed" "feedID" .ID }}">{{ t "Refresh" }}</a>
- </li>
- <li>
- <a href="{{ route "editFeed" "feedID" .ID }}">{{ t "Edit" }}</a>
- </li>
- <li>
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeFeed" "feedID" .ID }}">{{ t "Remove" }}</a>
- </li>
- </ul>
- </div>
- {{ if ne .ParsingErrorCount 0 }}
- <div class="parsing-error">
- <strong title="{{ .ParsingErrorMsg }}" class="parsing-error-count">{{ plural "plural.feed.error_count" .ParsingErrorCount .ParsingErrorCount }}</strong>
- <small class="parsing-error-message">({{ .ParsingErrorMsg }})</small>
- </div>
- {{ end }}
- </article>
- {{ end }}
- </div>
-{{ end }}
-
-{{ end }}
-`,
- "history": `{{ define "title"}}{{ t "History" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "History" }} ({{ .total }})</h1>
- {{ if .entries }}
- <ul>
- <li>
- <a href="{{ route "flushHistory" }}">{{ t "Flush history" }}</a>
- </li>
- </ul>
- {{ end }}
-</section>
-
-{{ if not .entries }}
- <p class="alert alert-info">{{ t "There is no history at the moment." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "readEntry" "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
-`,
- "import": `{{ define "title"}}{{ t "Import" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Import" }}</h1>
- <ul>
- <li>
- <a href="{{ route "feeds" }}">{{ t "Feeds" }}</a>
- </li>
- <li>
- <a href="{{ route "addSubscription" }}">{{ t "Add subscription" }}</a>
- </li>
- <li>
- <a href="{{ route "export" }}">{{ t "Export" }}</a>
- </li>
- </ul>
-</section>
-
-<form action="{{ route "uploadOPML" }}" method="post" enctype="multipart/form-data">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-file">{{ t "OPML file" }}</label>
- <input type="file" name="file" id="form-file">
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Import" }}</button>
- </div>
-</form>
-
-{{ end }}
-`,
- "integrations": `{{ define "title"}}{{ t "Integrations" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Integrations" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- {{ if .user.IsAdmin }}
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- {{ end }}
- <li>
- <a href="{{ route "about" }}">{{ t "About" }}</a>
- </li>
- </ul>
-</section>
-
-<form method="post" autocomplete="off" action="{{ route "updateIntegration" }}">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <h3>Fever</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="fever_enabled" value="1" {{ if .form.FeverEnabled }}checked{{ end }}> {{ t "Activate Fever API" }}
- </label>
-
- <label for="form-fever-username">{{ t "Fever Username" }}</label>
- <input type="text" name="fever_username" id="form-fever-username" value="{{ .form.FeverUsername }}">
-
- <label for="form-fever-password">{{ t "Fever Password" }}</label>
- <input type="password" name="fever_password" id="form-fever-password" value="{{ .form.FeverPassword }}">
- </div>
-
- <h3>Pinboard</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="pinboard_enabled" value="1" {{ if .form.PinboardEnabled }}checked{{ end }}> {{ t "Save articles to Pinboard" }}
- </label>
-
- <label for="form-pinboard-token">{{ t "Pinboard API Token" }}</label>
- <input type="password" name="pinboard_token" id="form-pinboard-token" value="{{ .form.PinboardToken }}">
-
- <label for="form-pinboard-tags">{{ t "Pinboard Tags" }}</label>
- <input type="text" name="pinboard_tags" id="form-pinboard-tags" value="{{ .form.PinboardTags }}">
-
- <label>
- <input type="checkbox" name="pinboard_mark_as_unread" value="1" {{ if .form.PinboardMarkAsUnread }}checked{{ end }}> {{ t "Mark bookmark as unread" }}
- </label>
- </div>
-
- <h3>Instapaper</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="instapaper_enabled" value="1" {{ if .form.InstapaperEnabled }}checked{{ end }}> {{ t "Save articles to Instapaper" }}
- </label>
-
- <label for="form-instapaper-username">{{ t "Instapaper Username" }}</label>
- <input type="text" name="instapaper_username" id="form-instapaper-username" value="{{ .form.InstapaperUsername }}">
-
- <label for="form-instapaper-password">{{ t "Instapaper Password" }}</label>
- <input type="password" name="instapaper_password" id="form-instapaper-password" value="{{ .form.InstapaperPassword }}">
- </div>
-
- <h3>Wallabag</h3>
- <div class="form-section">
- <label>
- <input type="checkbox" name="wallabag_enabled" value="1" {{ if .form.WallabagEnabled }}checked{{ end }}> {{ t "Save articles to Wallabag" }}
- </label>
-
- <label for="form-wallabag-url">{{ t "Wallabag API Endpoint" }}</label>
- <input type="url" name="wallabag_url" id="form-wallabag-url" value="{{ .form.WallabagURL }}" placeholder="http://v2.wallabag.org/">
-
- <label for="form-wallabag-client-id">{{ t "Wallabag Client ID" }}</label>
- <input type="text" name="wallabag_client_id" id="form-wallabag-client-id" value="{{ .form.WallabagClientID }}">
-
- <label for="form-wallabag-client-secret">{{ t "Wallabag Client Secret" }}</label>
- <input type="password" name="wallabag_client_secret" id="form-wallabag-client-secret" value="{{ .form.WallabagClientSecret }}">
-
- <label for="form-wallabag-username">{{ t "Wallabag Username" }}</label>
- <input type="text" name="wallabag_username" id="form-wallabag-username" value="{{ .form.WallabagUsername }}">
-
- <label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
- <input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
- </div>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
- </div>
-</form>
-
-<div class="panel">
- <h3>{{ t "Bookmarklet" }}</h3>
- <p>{{ t "This special link allows you to subscribe to a website directly by using a bookmark in your web browser." }}</p>
-
- <div class="bookmarklet">
- <a href="javascript:location.href='{{ baseURL }}{{ route "bookmarklet" }}?uri='+encodeURIComponent(window.location.href)">{{ t "Add to Miniflux" }}</a>
- </div>
-
- <p>{{ t "Drag and drop this link to your bookmarks." }}</p>
-</div>
-
-{{ end }}
-`,
- "login": `{{ define "title"}}{{ t "Sign In" }}{{ end }}
-
-{{ define "content"}}
-<section class="login-form">
- <form action="{{ route "checkLogin" }}" method="post">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required autofocus>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}" required>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Sign in" }}</button>
- </div>
- </form>
- {{ if hasOAuth2Provider "google" }}
- <div class="oauth2">
- <a href="{{ route "oauth2Redirect" "provider" "google" }}">{{ t "Sign in with Google" }}</a>
- </div>
- {{ end }}
-</section>
-{{ end }}
-`,
- "sessions": `{{ define "title"}}{{ t "Sessions" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Sessions" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- <li>
- <a href="{{ route "createUser" }}">{{ t "Add user" }}</a>
- </li>
- </ul>
-</section>
-
-<table>
- <tr>
- <th>{{ t "Date" }}</th>
- <th>{{ t "IP Address" }}</th>
- <th>{{ t "User Agent" }}</th>
- <th>{{ t "Actions" }}</th>
- </tr>
- {{ range .sessions }}
- <tr {{ if eq .Token $.currentSessionToken }}class="row-highlighted"{{ end }}>
- <td class="column-20" title="{{ isodate .CreatedAt }}">{{ elapsed .CreatedAt }}</td>
- <td class="column-20" title="{{ .IP }}">{{ .IP }}</td>
- <td title="{{ .UserAgent }}">{{ .UserAgent }}</td>
- <td class="column-20">
- {{ if eq .Token $.currentSessionToken }}
- {{ t "Current session" }}
- {{ else }}
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeSession" "sessionID" .ID }}">{{ t "Remove" }}</a>
- {{ end }}
- </td>
- </tr>
- {{ end }}
-</table>
-
-{{ end }}
-`,
- "settings": `{{ define "title"}}{{ t "Settings" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Settings" }}</h1>
- <ul>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- {{ if .user.IsAdmin }}
- <li>
- <a href="{{ route "users" }}">{{ t "Users" }}</a>
- </li>
- {{ end }}
- <li>
- <a href="{{ route "about" }}">{{ t "About" }}</a>
- </li>
- </ul>
-</section>
-
-<form method="post" autocomplete="off" action="{{ route "updateSettings" }}">
- <input type="hidden" name="csrf" value="{{ .csrf }}">
-
- {{ if .errorMessage }}
- <div class="alert alert-error">{{ t .errorMessage }}</div>
- {{ end }}
-
- <label for="form-username">{{ t "Username" }}</label>
- <input type="text" name="username" id="form-username" value="{{ .form.Username }}" required>
-
- <label for="form-password">{{ t "Password" }}</label>
- <input type="password" name="password" id="form-password" value="{{ .form.Password }}" autocomplete="off">
-
- <label for="form-confirmation">{{ t "Confirmation" }}</label>
- <input type="password" name="confirmation" id="form-confirmation" value="{{ .form.Confirmation }}" autocomplete="off">
-
- <label for="form-language">{{ t "Language" }}</label>
- <select id="form-language" name="language">
- {{ range $key, $value := .languages }}
- <option value="{{ $key }}" {{ if eq $key $.form.Language }}selected="selected"{{ end }}>{{ $value }}</option>
- {{ end }}
- </select>
-
- <label for="form-timezone">{{ t "Timezone" }}</label>
- <select id="form-timezone" name="timezone">
- {{ range $key, $value := .timezones }}
- <option value="{{ $key }}" {{ if eq $key $.form.Timezone }}selected="selected"{{ end }}>{{ $value }}</option>
- {{ end }}
- </select>
-
- <label for="form-theme">{{ t "Theme" }}</label>
- <select id="form-theme" name="theme">
- {{ range $key, $value := .themes }}
- <option value="{{ $key }}" {{ if eq $key $.form.Theme }}selected="selected"{{ end }}>{{ $value }}</option>
- {{ end }}
- </select>
-
- <label for="form-entry-direction">{{ t "Entry Sorting" }}</label>
- <select id="form-entry-direction" name="entry_direction">
- <option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Older entries first" }}</option>
- <option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Recent entries first" }}</option>
- </select>
-
- <div class="buttons">
- <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
- </div>
-</form>
-
-{{ if hasOAuth2Provider "google" }}
-<div class="panel">
- {{ if hasKey .user.Extra "google_id" }}
- <a href="{{ route "oauth2Unlink" "provider" "google" }}">{{ t "Unlink my Google account" }}</a>
- {{ else }}
- <a href="{{ route "oauth2Redirect" "provider" "google" }}">{{ t "Link my Google account" }}</a>
- {{ end }}
-</div>
-{{ end }}
-
-{{ end }}
-`,
- "starred": `{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Favorites" }} ({{ .total }})</h1>
-</section>
-
-{{ if not .entries }}
- <p class="alert alert-info">{{ t "There is no bookmark at the moment." }}</p>
-{{ else }}
- <div class="items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}
-`,
- "unread": `{{ define "title"}}{{ t "Unread Items" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Unread" }} (<span class="unread-counter">{{ .countUnread }}</span>)</h1>
- {{ if .entries }}
- <ul>
- <li>
- <a href="#" data-on-click="markPageAsRead">{{ t "Mark this page as read" }}</a>
- </li>
- </ul>
- {{ end }}
-</section>
-
-{{ if not .entries }}
- <p class="alert">{{ t "There is no unread article." }}</p>
-{{ else }}
- <div class="items hide-read-items">
- {{ range .entries }}
- <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
- <div class="item-header">
- <span class="item-title">
- {{ if ne .Feed.Icon.IconID 0 }}
- <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
- {{ end }}
- <a href="{{ route "unreadEntry" "entryID" .ID }}">{{ .Title }}</a>
- </span>
- <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
- </div>
- <div class="item-meta">
- <ul>
- <li>
- <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
- </li>
- <li>
- <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
- </li>
- <li>
- <a href="#"
- title="{{ t "Save this article" }}"
- data-save-entry="true"
- data-save-url="{{ route "saveEntry" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-done="{{ t "Done!" }}"
- >{{ t "Save" }}</a>
- </li>
- <li>
- <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
- </li>
- <li>
- <a href="#"
- data-toggle-bookmark="true"
- data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
- data-label-loading="{{ t "Saving..." }}"
- data-label-star="☆ {{ t "Star" }}"
- data-label-unstar="★ {{ t "Unstar" }}"
- data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
- >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
- </li>
- </ul>
- </div>
- </article>
- {{ end }}
- </div>
- {{ template "pagination" .pagination }}
-{{ end }}
-
-{{ end }}`,
- "users": `{{ define "title"}}{{ t "Users" }}{{ end }}
-
-{{ define "content"}}
-<section class="page-header">
- <h1>{{ t "Users" }}</h1>
- <ul>
- <li>
- <a href="{{ route "settings" }}">{{ t "Settings" }}</a>
- </li>
- <li>
- <a href="{{ route "integrations" }}">{{ t "Integrations" }}</a>
- </li>
- <li>
- <a href="{{ route "sessions" }}">{{ t "Sessions" }}</a>
- </li>
- <li>
- <a href="{{ route "createUser" }}">{{ t "Add user" }}</a>
- </li>
- </ul>
-</section>
-
-{{ if eq (len .users) 1 }}
- <p class="alert">{{ t "You are the only user." }}</p>
-{{ else }}
- <table>
- <tr>
- <th class="column-20">{{ t "Username" }}</th>
- <th>{{ t "Administrator" }}</th>
- <th>{{ t "Last Login" }}</th>
- <th>{{ t "Actions" }}</th>
- </tr>
- {{ range .users }}
- {{ if ne .ID $.user.ID }}
- <tr>
- <td>{{ .Username }}</td>
- <td>{{ if eq .IsAdmin true }}{{ t "Yes" }}{{ else }}{{ t "No" }}{{ end }}</td>
- <td>
- {{ if .LastLoginAt }}
- <time datetime="{{ isodate .LastLoginAt }}" title="{{ isodate .LastLoginAt }}">{{ elapsed .LastLoginAt }}</time>
- {{ else }}
- {{ t "Never" }}
- {{ end }}
- </td>
- <td>
- <a href="{{ route "editUser" "userID" .ID }}">{{ t "Edit" }}</a>,
- <a href="#"
- data-confirm="true"
- data-label-question="{{ t "Are you sure?" }}"
- data-label-yes="{{ t "yes" }}"
- data-label-no="{{ t "no" }}"
- data-label-loading="{{ t "Work in progress..." }}"
- data-url="{{ route "removeUser" "userID" .ID }}">{{ t "Remove" }}</a>
- </td>
- </tr>
- {{ end }}
- {{ end }}
- </table>
-{{ end }}
-
-{{ end }}
-`,
-}
-
-var templateViewsMapChecksums = map[string]string{
- "about": "ad2fb778fc73c39b733b3f81b13e5c7d689b041fadd24ee2d4577f545aa788ad",
- "add_subscription": "053c920b0d7e109ea19dce6a448e304ce720db8633588ea04db16677f7209a7b",
- "categories": "ca1280cd157bb527d4fc907da67b05a8347378f6dce965b9389d4bcdf3600a11",
- "category_entries": "ce59529666520b8363c9588ce2c437de5a3f6d91941e5c46be25ca08f6900364",
- "choose_subscription": "a325f9c976ca2b2dc148e25c8fef0cf6ccab0e04e86e604e7812bb18dc4cdde1",
- "create_category": "2b82af5d2dcd67898dc5daa57a6461e6ff8121a6089b2a2a1be909f35e4a2275",
- "create_user": "45e226df757126d5fe7c464e295e9a34f07952cfdb71e31e49839850d35af139",
- "edit_category": "cee720faadcec58289b707ad30af623d2ee66c1ce23a732965463250d7ff41c5",
- "edit_feed": "15f19ab44057fca1630c6860d5951d6073f82f83ad682a176c475591c6f26377",
- "edit_user": "82d9749d76ddbd2352816d813c4b1f6d92f2222de678b4afe5821090246735c7",
- "entry": "6b4405e0c8e4a7d31874659f8835f4e43e01dc3c20686091517ac750196dd70f",
- "feed_entries": "ac93cb9a90f93ddd9dd8a67d7e160592ecb9f5e465ee9679bb14eecd8d4caf20",
- "feeds": "65b0a47c4438810b9d51c60f3f3b2519690e56ff74029e6296c68626b83a470b",
- "history": "abc7ea29f7d54f28f73fe14979bbd03dbc41fa6a7c86f95f56d6e94f7b09b9ba",
- "import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
- "integrations": "3c14d7de904911aad7f3ebec6d1a20b50843287f58125c526e167f429f3d455d",
- "login": "7d83c3067c02f1f6aafdd8816c7f97a4eb5a5a4bdaaaa4cc1e2fbb9c17ea65e8",
- "sessions": "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf",
- "settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
- "starred": "33dd40d1a24739e9d05f9cc4b66497cfdb8c86a7abb209a66ca65c2fbafc7d87",
- "unread": "f4eb7410925e174918f1b55414c9b0b81632f7e13ce649579c8593097bb0f1d7",
- "users": "44677e28bb5347799ed0020c90ec785aadec4b1454446d92411cfdaf6e32110b",
-}
diff --git a/server/ui/controller/about.go b/server/ui/controller/about.go
deleted file mode 100644
index d6bfc27..0000000
--- a/server/ui/controller/about.go
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/version"
-)
-
-// AboutPage shows the about page.
-func (c *Controller) AboutPage(ctx *core.Context, request *core.Request, response *core.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("about", args.Merge(tplParams{
- "version": version.Version,
- "build_date": version.BuildDate,
- "menu": "settings",
- }))
-}
diff --git a/server/ui/controller/category.go b/server/ui/controller/category.go
deleted file mode 100644
index cf378c6..0000000
--- a/server/ui/controller/category.go
+++ /dev/null
@@ -1,257 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-)
-
-// ShowCategories shows the page with all categories.
-func (c *Controller) ShowCategories(ctx *core.Context, request *core.Request, response *core.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- user := ctx.LoggedUser()
- categories, err := c.store.CategoriesWithFeedCount(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("categories", args.Merge(tplParams{
- "categories": categories,
- "total": len(categories),
- "menu": "categories",
- }))
-}
-
-// ShowCategoryEntries shows all entries for the given category.
-func (c *Controller) ShowCategoryEntries(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithCategoryID(category.ID)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("category_entries", args.Merge(tplParams{
- "category": category,
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("categoryEntries", "categoryID", category.ID), count, offset),
- "menu": "categories",
- }))
-}
-
-// CreateCategory shows the form to create a new category.
-func (c *Controller) CreateCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("create_category", args.Merge(tplParams{
- "menu": "categories",
- }))
-}
-
-// SaveCategory validate and save the new category into the database.
-func (c *Controller) SaveCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- categoryForm := form.NewCategoryForm(request.Request())
- if err := categoryForm.Validate(); err != nil {
- response.HTML().Render("create_category", args.Merge(tplParams{
- "errorMessage": err.Error(),
- }))
- return
- }
-
- duplicateCategory, err := c.store.CategoryByTitle(user.ID, categoryForm.Title)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if duplicateCategory != nil {
- response.HTML().Render("create_category", args.Merge(tplParams{
- "errorMessage": "This category already exists.",
- }))
- return
- }
-
- category := model.Category{Title: categoryForm.Title, UserID: user.ID}
- err = c.store.CreateCategory(&category)
- if err != nil {
- logger.Info("[Controller:CreateCategory] %v", err)
- response.HTML().Render("create_category", args.Merge(tplParams{
- "errorMessage": "Unable to create this category.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("categories"))
-}
-
-// EditCategory shows the form to modify a category.
-func (c *Controller) EditCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- logger.Error("[Controller:EditCategory] %v", err)
- return
- }
-
- args, err := c.getCategoryFormTemplateArgs(ctx, user, category, nil)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("edit_category", args)
-}
-
-// UpdateCategory validate and update a category.
-func (c *Controller) UpdateCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- logger.Error("[Controller:UpdateCategory] %v", err)
- return
- }
-
- categoryForm := form.NewCategoryForm(request.Request())
- args, err := c.getCategoryFormTemplateArgs(ctx, user, category, categoryForm)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if err := categoryForm.Validate(); err != nil {
- response.HTML().Render("edit_category", args.Merge(tplParams{
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.AnotherCategoryExists(user.ID, category.ID, categoryForm.Title) {
- response.HTML().Render("edit_category", args.Merge(tplParams{
- "errorMessage": "This category already exists.",
- }))
- return
- }
-
- err = c.store.UpdateCategory(categoryForm.Merge(category))
- if err != nil {
- logger.Error("[Controller:UpdateCategory] %v", err)
- response.HTML().Render("edit_category", args.Merge(tplParams{
- "errorMessage": "Unable to update this category.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("categories"))
-}
-
-// RemoveCategory delete a category from the database.
-func (c *Controller) RemoveCategory(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- category, err := c.getCategoryFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- if err := c.store.RemoveCategory(user.ID, category.ID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("categories"))
-}
-
-func (c *Controller) getCategoryFromURL(ctx *core.Context, request *core.Request, response *core.Response) (*model.Category, error) {
- categoryID, err := request.IntegerParam("categoryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return nil, err
- }
-
- user := ctx.LoggedUser()
- category, err := c.store.Category(user.ID, categoryID)
- if err != nil {
- response.HTML().ServerError(err)
- return nil, err
- }
-
- if category == nil {
- response.HTML().NotFound()
- return nil, errors.New("Category not found")
- }
-
- return category, nil
-}
-
-func (c *Controller) getCategoryFormTemplateArgs(ctx *core.Context, user *model.User, category *model.Category, categoryForm *form.CategoryForm) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return nil, err
- }
-
- if categoryForm == nil {
- args["form"] = form.CategoryForm{
- Title: category.Title,
- }
- } else {
- args["form"] = categoryForm
- }
-
- args["category"] = category
- args["menu"] = "categories"
- return args, nil
-}
diff --git a/server/ui/controller/controller.go b/server/ui/controller/controller.go
deleted file mode 100644
index 8555c7b..0000000
--- a/server/ui/controller/controller.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/reader/feed"
- "github.com/miniflux/miniflux/reader/opml"
- "github.com/miniflux/miniflux/scheduler"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/storage"
-)
-
-type tplParams map[string]interface{}
-
-func (t tplParams) Merge(d tplParams) tplParams {
- for k, v := range d {
- t[k] = v
- }
-
- return t
-}
-
-// Controller contains all HTTP handlers for the user interface.
-type Controller struct {
- cfg *config.Config
- store *storage.Storage
- pool *scheduler.WorkerPool
- feedHandler *feed.Handler
- opmlHandler *opml.Handler
-}
-
-func (c *Controller) getCommonTemplateArgs(ctx *core.Context) (tplParams, error) {
- user := ctx.LoggedUser()
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
-
- countUnread, err := builder.CountEntries()
- if err != nil {
- return nil, err
- }
-
- params := tplParams{
- "menu": "",
- "user": user,
- "countUnread": countUnread,
- "csrf": ctx.CSRF(),
- "flashMessage": ctx.FlashMessage(),
- "flashErrorMessage": ctx.FlashErrorMessage(),
- }
- return params, nil
-}
-
-// NewController returns a new Controller.
-func NewController(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, opmlHandler *opml.Handler) *Controller {
- return &Controller{
- cfg: cfg,
- store: store,
- pool: pool,
- feedHandler: feedHandler,
- opmlHandler: opmlHandler,
- }
-}
diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go
deleted file mode 100644
index ca9f44a..0000000
--- a/server/ui/controller/entry.go
+++ /dev/null
@@ -1,495 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/reader/sanitizer"
-
- "github.com/miniflux/miniflux/integration"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/reader/scraper"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/payload"
- "github.com/miniflux/miniflux/storage"
-)
-
-// FetchContent downloads the original HTML page and returns relevant contents.
-func (c *Controller) FetchContent(ctx *core.Context, request *core.Request, response *core.Response) {
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- entry.Content = sanitizer.Sanitize(entry.URL, content)
- c.store.UpdateEntryContent(entry)
-
- response.JSON().Created(map[string]string{"content": entry.Content})
-}
-
-// SaveEntry send the link to external services.
-func (c *Controller) SaveEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- if entry == nil {
- response.JSON().NotFound(errors.New("Entry not found"))
- return
- }
-
- settings, err := c.store.Integration(user.ID)
- if err != nil {
- response.JSON().ServerError(err)
- return
- }
-
- go func() {
- integration.SendEntry(entry, settings)
- }()
-
- response.JSON().Created(map[string]string{"message": "saved"})
-}
-
-// ShowFeedEntry shows a single feed entry in "feed" mode.
-func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithFeedID(feedID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowFeedEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithFeedID(feedID)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "feeds",
- }))
-}
-
-// ShowCategoryEntry shows a single feed entry in "category" mode.
-func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- categoryID, err := request.IntegerParam("categoryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithCategoryID(categoryID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowCategoryEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithCategoryID(categoryID)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "categories",
- }))
-}
-
-// ShowUnreadEntry shows a single feed entry in "unread" mode.
-func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("unreadEntry", "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
- }
-
- // We change the status here, otherwise we cannot get the pagination for unread items.
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowUnreadEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- response.HTML().Render("entry", args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "unread",
- }))
-}
-
-// ShowReadEntry shows a single feed entry in "history" mode.
-func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusRead)
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("readEntry", "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("readEntry", "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "history",
- }))
-}
-
-// ShowStarredEntry shows a single feed entry in "starred" mode.
-func (c *Controller) ShowStarredEntry(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithEntryID(entryID)
- builder.WithoutStatus(model.EntryStatusRemoved)
-
- entry, err := builder.GetEntry()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if entry == nil {
- response.HTML().NotFound()
- return
- }
-
- if entry.Status == model.EntryStatusUnread {
- err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
- if err != nil {
- logger.Error("[Controller:ShowReadEntry] %v", err)
- response.HTML().ServerError(nil)
- return
- }
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStarred()
-
- prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- nextEntryRoute := ""
- if nextEntry != nil {
- nextEntryRoute = ctx.Route("starredEntry", "entryID", nextEntry.ID)
- }
-
- prevEntryRoute := ""
- if prevEntry != nil {
- prevEntryRoute = ctx.Route("starredEntry", "entryID", prevEntry.ID)
- }
-
- response.HTML().Render("entry", args.Merge(tplParams{
- "entry": entry,
- "prevEntry": prevEntry,
- "nextEntry": nextEntry,
- "nextEntryRoute": nextEntryRoute,
- "prevEntryRoute": prevEntryRoute,
- "menu": "starred",
- }))
-}
-
-// UpdateEntriesStatus handles Ajax request to update the status for a list of entries.
-func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- entryIDs, status, err := payload.DecodeEntryStatusPayload(request.Body())
- if err != nil {
- logger.Error("[Controller:UpdateEntryStatus] %v", err)
- response.JSON().BadRequest(nil)
- return
- }
-
- if len(entryIDs) == 0 {
- response.JSON().BadRequest(errors.New("The list of entryID is empty"))
- return
- }
-
- err = c.store.SetEntriesStatus(user.ID, entryIDs, status)
- if err != nil {
- logger.Error("[Controller:UpdateEntryStatus] %v", err)
- response.JSON().ServerError(nil)
- return
- }
-
- response.JSON().Standard("OK")
-}
-
-func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
-
- entries, err := builder.GetEntries()
- if err != nil {
- return nil, nil, err
- }
-
- n := len(entries)
- for i := 0; i < n; i++ {
- if entries[i].ID == entryID {
- if i-1 >= 0 {
- prev = entries[i-1]
- }
-
- if i+1 < n {
- next = entries[i+1]
- }
- }
- }
-
- return prev, next, nil
-}
diff --git a/server/ui/controller/feed.go b/server/ui/controller/feed.go
deleted file mode 100644
index 7dfc56e..0000000
--- a/server/ui/controller/feed.go
+++ /dev/null
@@ -1,236 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-)
-
-// RefreshAllFeeds refresh all feeds in the background for the current user.
-func (c *Controller) RefreshAllFeeds(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- jobs, err := c.store.NewUserBatch(user.ID, c.store.CountFeeds(user.ID))
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- go func() {
- c.pool.Push(jobs)
- }()
-
- response.Redirect(ctx.Route("feeds"))
-}
-
-// ShowFeedsPage shows the page with all subscriptions.
-func (c *Controller) ShowFeedsPage(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- feeds, err := c.store.Feeds(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("feeds", args.Merge(tplParams{
- "feeds": feeds,
- "total": len(feeds),
- "menu": "feeds",
- }))
-}
-
-// ShowFeedEntries shows all entries for the given feed.
-func (c *Controller) ShowFeedEntries(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- feed, err := c.getFeedFromURL(request, response, user)
- if err != nil {
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithFeedID(feed.ID)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("feed_entries", args.Merge(tplParams{
- "feed": feed,
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("feedEntries", "feedID", feed.ID), count, offset),
- "menu": "feeds",
- }))
-}
-
-// EditFeed shows the form to modify a subscription.
-func (c *Controller) EditFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- feed, err := c.getFeedFromURL(request, response, user)
- if err != nil {
- return
- }
-
- args, err := c.getFeedFormTemplateArgs(ctx, user, feed, nil)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("edit_feed", args)
-}
-
-// UpdateFeed update a subscription and redirect to the feed entries page.
-func (c *Controller) UpdateFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- feed, err := c.getFeedFromURL(request, response, user)
- if err != nil {
- return
- }
-
- feedForm := form.NewFeedForm(request.Request())
- args, err := c.getFeedFormTemplateArgs(ctx, user, feed, feedForm)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if err := feedForm.ValidateModification(); err != nil {
- response.HTML().Render("edit_feed", args.Merge(tplParams{
- "errorMessage": err.Error(),
- }))
- return
- }
-
- err = c.store.UpdateFeed(feedForm.Merge(feed))
- if err != nil {
- logger.Error("[Controller:EditFeed] %v", err)
- response.HTML().Render("edit_feed", args.Merge(tplParams{
- "errorMessage": "Unable to update this feed.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
-}
-
-// RemoveFeed delete a subscription from the database and redirect to the list of feeds page.
-func (c *Controller) RemoveFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- user := ctx.LoggedUser()
- if err := c.store.RemoveFeed(user.ID, feedID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("feeds"))
-}
-
-// RefreshFeed refresh a subscription and redirect to the feed entries page.
-func (c *Controller) RefreshFeed(ctx *core.Context, request *core.Request, response *core.Response) {
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- user := ctx.LoggedUser()
- if err := c.feedHandler.RefreshFeed(user.ID, feedID); err != nil {
- logger.Error("[Controller:RefreshFeed] %v", err)
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feedID))
-}
-
-func (c *Controller) getFeedFromURL(request *core.Request, response *core.Response, user *model.User) (*model.Feed, error) {
- feedID, err := request.IntegerParam("feedID")
- if err != nil {
- response.HTML().BadRequest(err)
- return nil, err
- }
-
- feed, err := c.store.FeedByID(user.ID, feedID)
- if err != nil {
- response.HTML().ServerError(err)
- return nil, err
- }
-
- if feed == nil {
- response.HTML().NotFound()
- return nil, errors.New("Feed not found")
- }
-
- return feed, nil
-}
-
-func (c *Controller) getFeedFormTemplateArgs(ctx *core.Context, user *model.User, feed *model.Feed, feedForm *form.FeedForm) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return nil, err
- }
-
- categories, err := c.store.Categories(user.ID)
- if err != nil {
- return nil, err
- }
-
- if feedForm == nil {
- args["form"] = form.FeedForm{
- SiteURL: feed.SiteURL,
- FeedURL: feed.FeedURL,
- Title: feed.Title,
- ScraperRules: feed.ScraperRules,
- RewriteRules: feed.RewriteRules,
- Crawler: feed.Crawler,
- CategoryID: feed.Category.ID,
- }
- } else {
- args["form"] = feedForm
- }
-
- args["categories"] = categories
- args["feed"] = feed
- args["menu"] = "feeds"
- return args, nil
-}
diff --git a/server/ui/controller/history.go b/server/ui/controller/history.go
deleted file mode 100644
index 7347bac..0000000
--- a/server/ui/controller/history.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// ShowHistoryPage renders the page with all read entries.
-func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusRead)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("history", args.Merge(tplParams{
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("history"), count, offset),
- "menu": "history",
- }))
-}
-
-// FlushHistory changes all "read" items to "removed".
-func (c *Controller) FlushHistory(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- err := c.store.FlushHistory(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("history"))
-}
diff --git a/server/ui/controller/icon.go b/server/ui/controller/icon.go
deleted file mode 100644
index f5ff1db..0000000
--- a/server/ui/controller/icon.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "time"
-
- "github.com/miniflux/miniflux/server/core"
-)
-
-// ShowIcon shows the feed icon.
-func (c *Controller) ShowIcon(ctx *core.Context, request *core.Request, response *core.Response) {
- iconID, err := request.IntegerParam("iconID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- icon, err := c.store.IconByID(iconID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if icon == nil {
- response.HTML().NotFound()
- return
- }
-
- response.Cache(icon.MimeType, icon.Hash, icon.Content, 72*time.Hour)
-}
diff --git a/server/ui/controller/integrations.go b/server/ui/controller/integrations.go
deleted file mode 100644
index 9ff4baa..0000000
--- a/server/ui/controller/integrations.go
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "crypto/md5"
- "fmt"
-
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-)
-
-// ShowIntegrations renders the page with all external integrations.
-func (c *Controller) ShowIntegrations(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- integration, err := c.store.Integration(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("integrations", args.Merge(tplParams{
- "menu": "settings",
- "form": form.IntegrationForm{
- PinboardEnabled: integration.PinboardEnabled,
- PinboardToken: integration.PinboardToken,
- PinboardTags: integration.PinboardTags,
- PinboardMarkAsUnread: integration.PinboardMarkAsUnread,
- InstapaperEnabled: integration.InstapaperEnabled,
- InstapaperUsername: integration.InstapaperUsername,
- InstapaperPassword: integration.InstapaperPassword,
- FeverEnabled: integration.FeverEnabled,
- FeverUsername: integration.FeverUsername,
- FeverPassword: integration.FeverPassword,
- WallabagEnabled: integration.WallabagEnabled,
- WallabagURL: integration.WallabagURL,
- WallabagClientID: integration.WallabagClientID,
- WallabagClientSecret: integration.WallabagClientSecret,
- WallabagUsername: integration.WallabagUsername,
- WallabagPassword: integration.WallabagPassword,
- },
- }))
-}
-
-// UpdateIntegration updates integration settings.
-func (c *Controller) UpdateIntegration(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- integration, err := c.store.Integration(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- integrationForm := form.NewIntegrationForm(request.Request())
- integrationForm.Merge(integration)
-
- if integration.FeverUsername != "" && c.store.HasDuplicateFeverUsername(user.ID, integration.FeverUsername) {
- ctx.SetFlashErrorMessage(ctx.Translate("There is already someone else with the same Fever username!"))
- response.Redirect(ctx.Route("integrations"))
- return
- }
-
- if integration.FeverEnabled {
- integration.FeverToken = fmt.Sprintf("%x", md5.Sum([]byte(integration.FeverUsername+":"+integration.FeverPassword)))
- } else {
- integration.FeverToken = ""
- }
-
- err = c.store.UpdateIntegration(integration)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("integrations"))
-}
diff --git a/server/ui/controller/login.go b/server/ui/controller/login.go
deleted file mode 100644
index ef99c82..0000000
--- a/server/ui/controller/login.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/cookie"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-
- "github.com/tomasen/realip"
-)
-
-// ShowLoginPage shows the login form.
-func (c *Controller) ShowLoginPage(ctx *core.Context, request *core.Request, response *core.Response) {
- if ctx.IsAuthenticated() {
- response.Redirect(ctx.Route("unread"))
- return
- }
-
- response.HTML().Render("login", tplParams{
- "csrf": ctx.CSRF(),
- })
-}
-
-// CheckLogin validates the username/password and redirects the user to the unread page.
-func (c *Controller) CheckLogin(ctx *core.Context, request *core.Request, response *core.Response) {
- authForm := form.NewAuthForm(request.Request())
- tplParams := tplParams{
- "errorMessage": "Invalid username or password.",
- "csrf": ctx.CSRF(),
- "form": authForm,
- }
-
- if err := authForm.Validate(); err != nil {
- logger.Error("[Controller:CheckLogin] %v", err)
- response.HTML().Render("login", tplParams)
- return
- }
-
- if err := c.store.CheckPassword(authForm.Username, authForm.Password); err != nil {
- logger.Error("[Controller:CheckLogin] %v", err)
- response.HTML().Render("login", tplParams)
- return
- }
-
- sessionToken, err := c.store.CreateUserSession(
- authForm.Username,
- request.Request().UserAgent(),
- realip.RealIP(request.Request()),
- )
-
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username)
-
- response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS))
- response.Redirect(ctx.Route("unread"))
-}
-
-// Logout destroy the session and redirects the user to the login page.
-func (c *Controller) Logout(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil {
- logger.Error("[Controller:Logout] %v", err)
- }
-
- response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, c.cfg.IsHTTPS))
- response.Redirect(ctx.Route("login"))
-}
diff --git a/server/ui/controller/oauth2.go b/server/ui/controller/oauth2.go
deleted file mode 100644
index 2aaa5d7..0000000
--- a/server/ui/controller/oauth2.go
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/config"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/cookie"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/oauth2"
- "github.com/tomasen/realip"
-)
-
-// OAuth2Redirect redirects the user to the consent page to ask for permission.
-func (c *Controller) OAuth2Redirect(ctx *core.Context, request *core.Request, response *core.Response) {
- provider := request.StringParam("provider", "")
- if provider == "" {
- logger.Error("[OAuth2] Invalid or missing provider: %s", provider)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- response.Redirect(authProvider.GetRedirectURL(ctx.GenerateOAuth2State()))
-}
-
-// OAuth2Callback receives the authorization code and create a new session.
-func (c *Controller) OAuth2Callback(ctx *core.Context, request *core.Request, response *core.Response) {
- provider := request.StringParam("provider", "")
- if provider == "" {
- logger.Error("[OAuth2] Invalid or missing provider")
- response.Redirect(ctx.Route("login"))
- return
- }
-
- code := request.QueryStringParam("code", "")
- if code == "" {
- logger.Error("[OAuth2] No code received on callback")
- response.Redirect(ctx.Route("login"))
- return
- }
-
- state := request.QueryStringParam("state", "")
- if state == "" || state != ctx.OAuth2State() {
- logger.Error(`[OAuth2] Invalid state value: got "%s" instead of "%s"`, state, ctx.OAuth2State())
- response.Redirect(ctx.Route("login"))
- return
- }
-
- authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- profile, err := authProvider.GetProfile(code)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("login"))
- return
- }
-
- if ctx.IsAuthenticated() {
- user, err := c.store.UserByExtraField(profile.Key, profile.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if user != nil {
- logger.Error("[OAuth2] User #%d cannot be associated because %s is already associated", ctx.UserID(), user.Username)
- ctx.SetFlashErrorMessage(ctx.Translate("There is already someone associated with this provider!"))
- response.Redirect(ctx.Route("settings"))
- return
- }
-
- user = ctx.LoggedUser()
- if err := c.store.UpdateExtraField(user.ID, profile.Key, profile.ID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- ctx.SetFlashMessage(ctx.Translate("Your external account is now linked !"))
- response.Redirect(ctx.Route("settings"))
- return
- }
-
- user, err := c.store.UserByExtraField(profile.Key, profile.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if user == nil {
- if c.cfg.GetInt("OAUTH2_USER_CREATION", 0) == 0 {
- response.HTML().Forbidden()
- return
- }
-
- user = model.NewUser()
- user.Username = profile.Username
- user.IsAdmin = false
- user.Extra[profile.Key] = profile.ID
-
- if err := c.store.CreateUser(user); err != nil {
- response.HTML().ServerError(err)
- return
- }
- }
-
- sessionToken, err := c.store.CreateUserSession(
- user.Username,
- request.Request().UserAgent(),
- realip.RealIP(request.Request()),
- )
-
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username)
-
- response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS))
- response.Redirect(ctx.Route("unread"))
-}
-
-// OAuth2Unlink unlink an account from the external provider.
-func (c *Controller) OAuth2Unlink(ctx *core.Context, request *core.Request, response *core.Response) {
- provider := request.StringParam("provider", "")
- if provider == "" {
- logger.Info("[OAuth2] Invalid or missing provider")
- response.Redirect(ctx.Route("login"))
- return
- }
-
- authProvider, err := getOAuth2Manager(c.cfg).Provider(provider)
- if err != nil {
- logger.Error("[OAuth2] %v", err)
- response.Redirect(ctx.Route("settings"))
- return
- }
-
- user := ctx.LoggedUser()
- if err := c.store.RemoveExtraField(user.ID, authProvider.GetUserExtraKey()); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("settings"))
- return
-}
-
-func getOAuth2Manager(cfg *config.Config) *oauth2.Manager {
- return oauth2.NewManager(
- cfg.Get("OAUTH2_CLIENT_ID", ""),
- cfg.Get("OAUTH2_CLIENT_SECRET", ""),
- cfg.Get("OAUTH2_REDIRECT_URL", ""),
- )
-}
diff --git a/server/ui/controller/opml.go b/server/ui/controller/opml.go
deleted file mode 100644
index d801677..0000000
--- a/server/ui/controller/opml.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// Export generates the OPML file.
-func (c *Controller) Export(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- opml, err := c.opmlHandler.Export(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.XML().Download("feeds.opml", opml)
-}
-
-// Import shows the import form.
-func (c *Controller) Import(ctx *core.Context, request *core.Request, response *core.Response) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("import", args.Merge(tplParams{
- "menu": "feeds",
- }))
-}
-
-// UploadOPML handles OPML file importation.
-func (c *Controller) UploadOPML(ctx *core.Context, request *core.Request, response *core.Response) {
- file, fileHeader, err := request.File("file")
- if err != nil {
- logger.Error("[Controller:UploadOPML] %v", err)
- response.Redirect(ctx.Route("import"))
- return
- }
- defer file.Close()
-
- user := ctx.LoggedUser()
- logger.Info(
- "[Controller:UploadOPML] User #%d uploaded this file: %s (%d bytes)",
- user.ID,
- fileHeader.Filename,
- fileHeader.Size,
- )
-
- if impErr := c.opmlHandler.Import(user.ID, file); impErr != nil {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("import", args.Merge(tplParams{
- "errorMessage": impErr,
- "menu": "feeds",
- }))
-
- return
- }
-
- response.Redirect(ctx.Route("feeds"))
-}
diff --git a/server/ui/controller/pagination.go b/server/ui/controller/pagination.go
deleted file mode 100644
index 1d61f74..0000000
--- a/server/ui/controller/pagination.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-const (
- nbItemsPerPage = 100
-)
-
-type pagination struct {
- Route string
- Total int
- Offset int
- ItemsPerPage int
- ShowNext bool
- ShowPrev bool
- NextOffset int
- PrevOffset int
-}
-
-func (c *Controller) getPagination(route string, total, offset int) pagination {
- nextOffset := 0
- prevOffset := 0
- showNext := (total - offset) > nbItemsPerPage
- showPrev := offset > 0
-
- if showNext {
- nextOffset = offset + nbItemsPerPage
- }
-
- if showPrev {
- prevOffset = offset - nbItemsPerPage
- }
-
- return pagination{
- Route: route,
- Total: total,
- Offset: offset,
- ItemsPerPage: nbItemsPerPage,
- ShowNext: showNext,
- NextOffset: nextOffset,
- ShowPrev: showPrev,
- PrevOffset: prevOffset,
- }
-}
diff --git a/server/ui/controller/proxy.go b/server/ui/controller/proxy.go
deleted file mode 100644
index 6ee52b8..0000000
--- a/server/ui/controller/proxy.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "encoding/base64"
- "errors"
- "io/ioutil"
- "time"
-
- "github.com/miniflux/miniflux/crypto"
- "github.com/miniflux/miniflux/http"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// ImageProxy fetch an image from a remote server and sent it back to the browser.
-func (c *Controller) ImageProxy(ctx *core.Context, request *core.Request, response *core.Response) {
- // If we receive a "If-None-Match" header we assume the image in stored in browser cache
- if request.Request().Header.Get("If-None-Match") != "" {
- response.NotModified()
- return
- }
-
- encodedURL := request.StringParam("encodedURL", "")
- if encodedURL == "" {
- response.HTML().BadRequest(errors.New("No URL provided"))
- return
- }
-
- decodedURL, err := base64.URLEncoding.DecodeString(encodedURL)
- if err != nil {
- response.HTML().BadRequest(errors.New("Unable to decode this URL"))
- return
- }
-
- client := http.NewClient(string(decodedURL))
- resp, err := client.Get()
- if err != nil {
- logger.Error("[Controller:ImageProxy] %v", err)
- response.HTML().NotFound()
- return
- }
-
- if resp.HasServerFailure() {
- response.HTML().NotFound()
- return
- }
-
- body, _ := ioutil.ReadAll(resp.Body)
- etag := crypto.HashFromBytes(body)
-
- response.Cache(resp.ContentType, etag, body, 72*time.Hour)
-}
diff --git a/server/ui/controller/session.go b/server/ui/controller/session.go
deleted file mode 100644
index 05cb29e..0000000
--- a/server/ui/controller/session.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// ShowSessions shows the list of active user sessions.
-func (c *Controller) ShowSessions(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- sessions, err := c.store.UserSessions(user.ID)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("sessions", args.Merge(tplParams{
- "sessions": sessions,
- "currentSessionToken": ctx.UserSessionToken(),
- "menu": "settings",
- }))
-}
-
-// RemoveSession remove a user session.
-func (c *Controller) RemoveSession(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- sessionID, err := request.IntegerParam("sessionID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- err = c.store.RemoveUserSessionByID(user.ID, sessionID)
- if err != nil {
- logger.Error("[Controller:RemoveSession] %v", err)
- }
-
- response.Redirect(ctx.Route("sessions"))
-}
diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go
deleted file mode 100644
index feba893..0000000
--- a/server/ui/controller/settings.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/locale"
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-)
-
-// ShowSettings shows the settings page.
-func (c *Controller) ShowSettings(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSettingsFormTemplateArgs(ctx, user, nil)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("settings", args)
-}
-
-// UpdateSettings update the settings.
-func (c *Controller) UpdateSettings(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- settingsForm := form.NewSettingsForm(request.Request())
- args, err := c.getSettingsFormTemplateArgs(ctx, user, settingsForm)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if err := settingsForm.Validate(); err != nil {
- response.HTML().Render("settings", args.Merge(tplParams{
- "form": settingsForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.AnotherUserExists(user.ID, settingsForm.Username) {
- response.HTML().Render("settings", args.Merge(tplParams{
- "form": settingsForm,
- "errorMessage": "This user already exists.",
- }))
- return
- }
-
- err = c.store.UpdateUser(settingsForm.Merge(user))
- if err != nil {
- logger.Error("[Controller:UpdateSettings] %v", err)
- response.HTML().Render("settings", args.Merge(tplParams{
- "form": settingsForm,
- "errorMessage": "Unable to update this user.",
- }))
- return
- }
-
- ctx.SetFlashMessage(ctx.Translate("Preferences saved!"))
- response.Redirect(ctx.Route("settings"))
-}
-
-func (c *Controller) getSettingsFormTemplateArgs(ctx *core.Context, user *model.User, settingsForm *form.SettingsForm) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return args, err
- }
-
- if settingsForm == nil {
- args["form"] = form.SettingsForm{
- Username: user.Username,
- Theme: user.Theme,
- Language: user.Language,
- Timezone: user.Timezone,
- EntryDirection: user.EntryDirection,
- }
- } else {
- args["form"] = settingsForm
- }
-
- args["menu"] = "settings"
- args["themes"] = model.Themes()
- args["languages"] = locale.AvailableLanguages()
- args["timezones"], err = c.store.Timezones()
- if err != nil {
- return args, err
- }
-
- return args, nil
-}
diff --git a/server/ui/controller/starred.go b/server/ui/controller/starred.go
deleted file mode 100644
index e9da241..0000000
--- a/server/ui/controller/starred.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// ShowStarredPage renders the page with all starred entries.
-func (c *Controller) ShowStarredPage(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithoutStatus(model.EntryStatusRemoved)
- builder.WithStarred()
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
-
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- count, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("starred", args.Merge(tplParams{
- "entries": entries,
- "total": count,
- "pagination": c.getPagination(ctx.Route("starred"), count, offset),
- "menu": "starred",
- }))
-}
-
-// ToggleBookmark handles Ajax request to toggle bookmark value.
-func (c *Controller) ToggleBookmark(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- entryID, err := request.IntegerParam("entryID")
- if err != nil {
- response.HTML().BadRequest(err)
- return
- }
-
- if err := c.store.ToggleBookmark(user.ID, entryID); err != nil {
- logger.Error("[Controller:UpdateEntryStatus] %v", err)
- response.JSON().ServerError(nil)
- return
- }
-
- response.JSON().Standard("OK")
-}
diff --git a/server/ui/controller/static.go b/server/ui/controller/static.go
deleted file mode 100644
index 7cf7a35..0000000
--- a/server/ui/controller/static.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "encoding/base64"
- "time"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/static"
-)
-
-// Stylesheet renders the CSS.
-func (c *Controller) Stylesheet(ctx *core.Context, request *core.Request, response *core.Response) {
- stylesheet := request.StringParam("name", "white")
- body := static.Stylesheets["common"]
- etag := static.StylesheetsChecksums["common"]
-
- if theme, found := static.Stylesheets[stylesheet]; found {
- body += theme
- etag += static.StylesheetsChecksums[stylesheet]
- }
-
- response.Cache("text/css; charset=utf-8", etag, []byte(body), 48*time.Hour)
-}
-
-// Javascript renders application client side code.
-func (c *Controller) Javascript(ctx *core.Context, request *core.Request, response *core.Response) {
- response.Cache("text/javascript; charset=utf-8", static.JavascriptChecksums["app"], []byte(static.Javascript["app"]), 48*time.Hour)
-}
-
-// Favicon renders the application favicon.
-func (c *Controller) Favicon(ctx *core.Context, request *core.Request, response *core.Response) {
- blob, err := base64.StdEncoding.DecodeString(static.Binaries["favicon.ico"])
- if err != nil {
- logger.Error("[Controller:Favicon] %v", err)
- response.HTML().NotFound()
- return
- }
-
- response.Cache("image/x-icon", static.BinariesChecksums["favicon.ico"], blob, 48*time.Hour)
-}
-
-// AppIcon returns application icons.
-func (c *Controller) AppIcon(ctx *core.Context, request *core.Request, response *core.Response) {
- filename := request.StringParam("filename", "favicon.png")
- encodedBlob, found := static.Binaries[filename]
- if !found {
- logger.Info("[Controller:AppIcon] This icon doesn't exists: %s", filename)
- response.HTML().NotFound()
- return
- }
-
- blob, err := base64.StdEncoding.DecodeString(encodedBlob)
- if err != nil {
- logger.Error("[Controller:AppIcon] %v", err)
- response.HTML().NotFound()
- return
- }
-
- response.Cache("image/png", static.BinariesChecksums[filename], blob, 48*time.Hour)
-}
-
-// WebManifest renders web manifest file.
-func (c *Controller) WebManifest(ctx *core.Context, request *core.Request, response *core.Response) {
- type webManifestIcon struct {
- Source string `json:"src"`
- Sizes string `json:"sizes"`
- Type string `json:"type"`
- }
-
- type webManifest struct {
- Name string `json:"name"`
- Description string `json:"description"`
- ShortName string `json:"short_name"`
- StartURL string `json:"start_url"`
- Icons []webManifestIcon `json:"icons"`
- Display string `json:"display"`
- }
-
- manifest := &webManifest{
- Name: "Miniflux",
- ShortName: "Miniflux",
- Description: "Minimalist Feed Reader",
- Display: "minimal-ui",
- StartURL: ctx.Route("unread"),
- Icons: []webManifestIcon{
- webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-ipad-retina.png"), Sizes: "144x144", Type: "image/png"},
- webManifestIcon{Source: ctx.Route("appIcon", "filename", "touch-icon-iphone-retina.png"), Sizes: "114x114", Type: "image/png"},
- },
- }
-
- response.JSON().Standard(manifest)
-}
diff --git a/server/ui/controller/subscription.go b/server/ui/controller/subscription.go
deleted file mode 100644
index d243f9a..0000000
--- a/server/ui/controller/subscription.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/reader/subscription"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-)
-
-// Bookmarklet prefill the form to add a subscription from the URL provided by the bookmarklet.
-func (c *Controller) Bookmarklet(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- bookmarkletURL := request.QueryStringParam("uri", "")
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": &form.SubscriptionForm{URL: bookmarkletURL},
- }))
-}
-
-// AddSubscription shows the form to add a new feed.
-func (c *Controller) AddSubscription(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("add_subscription", args)
-}
-
-// SubmitSubscription try to find a feed from the URL provided by the user.
-func (c *Controller) SubmitSubscription(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- subscriptionForm := form.NewSubscriptionForm(request.Request())
- if err := subscriptionForm.Validate(); err != nil {
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- subscriptions, err := subscription.FindSubscriptions(subscriptionForm.URL)
- if err != nil {
- logger.Error("[Controller:SubmitSubscription] %v", err)
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err,
- }))
- return
- }
-
- logger.Info("[UI:SubmitSubscription] %s", subscriptions)
-
- n := len(subscriptions)
- switch {
- case n == 0:
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": "Unable to find any subscription.",
- }))
- case n == 1:
- feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptions[0].URL, subscriptionForm.Crawler)
- if err != nil {
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err,
- }))
- return
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
- case n > 1:
- response.HTML().Render("choose_subscription", args.Merge(tplParams{
- "categoryID": subscriptionForm.CategoryID,
- "subscriptions": subscriptions,
- }))
- }
-}
-
-// ChooseSubscription shows a page to choose a subscription.
-func (c *Controller) ChooseSubscription(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- args, err := c.getSubscriptionFormTemplateArgs(ctx, user)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- subscriptionForm := form.NewSubscriptionForm(request.Request())
- if err := subscriptionForm.Validate(); err != nil {
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- feed, err := c.feedHandler.CreateFeed(user.ID, subscriptionForm.CategoryID, subscriptionForm.URL, subscriptionForm.Crawler)
- if err != nil {
- response.HTML().Render("add_subscription", args.Merge(tplParams{
- "form": subscriptionForm,
- "errorMessage": err,
- }))
- return
- }
-
- response.Redirect(ctx.Route("feedEntries", "feedID", feed.ID))
-}
-
-func (c *Controller) getSubscriptionFormTemplateArgs(ctx *core.Context, user *model.User) (tplParams, error) {
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- return nil, err
- }
-
- categories, err := c.store.Categories(user.ID)
- if err != nil {
- return nil, err
- }
-
- args["categories"] = categories
- args["menu"] = "feeds"
- return args, nil
-}
diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go
deleted file mode 100644
index 1dd7b07..0000000
--- a/server/ui/controller/unread.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
-)
-
-// ShowUnreadPage render the page with all unread entries.
-func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- offset := request.QueryIntegerParam("offset", 0)
-
- builder := c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
- countUnread, err := builder.CountEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- if offset >= countUnread {
- offset = 0
- }
-
- builder = c.store.NewEntryQueryBuilder(user.ID)
- builder.WithStatus(model.EntryStatusUnread)
- builder.WithOrder(model.DefaultSortingOrder)
- builder.WithDirection(user.EntryDirection)
- builder.WithOffset(offset)
- builder.WithLimit(nbItemsPerPage)
- entries, err := builder.GetEntries()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("unread", tplParams{
- "user": user,
- "countUnread": countUnread,
- "entries": entries,
- "pagination": c.getPagination(ctx.Route("unread"), countUnread, offset),
- "menu": "unread",
- "csrf": ctx.CSRF(),
- })
-}
diff --git a/server/ui/controller/user.go b/server/ui/controller/user.go
deleted file mode 100644
index c5d4dba..0000000
--- a/server/ui/controller/user.go
+++ /dev/null
@@ -1,238 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package controller
-
-import (
- "errors"
-
- "github.com/miniflux/miniflux/logger"
- "github.com/miniflux/miniflux/model"
- "github.com/miniflux/miniflux/server/core"
- "github.com/miniflux/miniflux/server/ui/form"
-)
-
-// ShowUsers shows the list of users.
-func (c *Controller) ShowUsers(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- users, err := c.store.Users()
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("users", args.Merge(tplParams{
- "users": users,
- "menu": "settings",
- }))
-}
-
-// CreateUser shows the user creation form.
-func (c *Controller) CreateUser(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.HTML().Render("create_user", args.Merge(tplParams{
- "menu": "settings",
- "form": &form.UserForm{},
- }))
-}
-
-// SaveUser validate and save the new user into the database.
-func (c *Controller) SaveUser(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- userForm := form.NewUserForm(request.Request())
- if err := userForm.ValidateCreation(); err != nil {
- response.HTML().Render("create_user", args.Merge(tplParams{
- "menu": "settings",
- "form": userForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.UserExists(userForm.Username) {
- response.HTML().Render("create_user", args.Merge(tplParams{
- "menu": "settings",
- "form": userForm,
- "errorMessage": "This user already exists.",
- }))
- return
- }
-
- newUser := userForm.ToUser()
- if err := c.store.CreateUser(newUser); err != nil {
- logger.Error("[Controller:SaveUser] %v", err)
- response.HTML().Render("edit_user", args.Merge(tplParams{
- "menu": "settings",
- "form": userForm,
- "errorMessage": "Unable to create this user.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("users"))
-}
-
-// EditUser shows the form to edit a user.
-func (c *Controller) EditUser(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- selectedUser, err := c.getUserFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- response.HTML().Render("edit_user", args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": &form.UserForm{
- Username: selectedUser.Username,
- IsAdmin: selectedUser.IsAdmin,
- },
- }))
-}
-
-// UpdateUser validate and update a user.
-func (c *Controller) UpdateUser(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
-
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- args, err := c.getCommonTemplateArgs(ctx)
- if err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- selectedUser, err := c.getUserFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- userForm := form.NewUserForm(request.Request())
- if err := userForm.ValidateModification(); err != nil {
- response.HTML().Render("edit_user", args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": userForm,
- "errorMessage": err.Error(),
- }))
- return
- }
-
- if c.store.AnotherUserExists(selectedUser.ID, userForm.Username) {
- response.HTML().Render("edit_user", args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": userForm,
- "errorMessage": "This user already exists.",
- }))
- return
- }
-
- userForm.Merge(selectedUser)
- if err := c.store.UpdateUser(selectedUser); err != nil {
- logger.Error("[Controller:UpdateUser] %v", err)
- response.HTML().Render("edit_user", args.Merge(tplParams{
- "menu": "settings",
- "selected_user": selectedUser,
- "form": userForm,
- "errorMessage": "Unable to update this user.",
- }))
- return
- }
-
- response.Redirect(ctx.Route("users"))
-}
-
-// RemoveUser deletes a user from the database.
-func (c *Controller) RemoveUser(ctx *core.Context, request *core.Request, response *core.Response) {
- user := ctx.LoggedUser()
- if !user.IsAdmin {
- response.HTML().Forbidden()
- return
- }
-
- selectedUser, err := c.getUserFromURL(ctx, request, response)
- if err != nil {
- return
- }
-
- if err := c.store.RemoveUser(selectedUser.ID); err != nil {
- response.HTML().ServerError(err)
- return
- }
-
- response.Redirect(ctx.Route("users"))
-}
-
-func (c *Controller) getUserFromURL(ctx *core.Context, request *core.Request, response *core.Response) (*model.User, error) {
- userID, err := request.IntegerParam("userID")
- if err != nil {
- response.HTML().BadRequest(err)
- return nil, err
- }
-
- user, err := c.store.UserByID(userID)
- if err != nil {
- response.HTML().ServerError(err)
- return nil, err
- }
-
- if user == nil {
- response.HTML().NotFound()
- return nil, errors.New("User not found")
- }
-
- return user, nil
-}
diff --git a/server/ui/filter/image_proxy_filter.go b/server/ui/filter/image_proxy_filter.go
deleted file mode 100644
index 12c9da6..0000000
--- a/server/ui/filter/image_proxy_filter.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package filter
-
-import (
- "encoding/base64"
- "strings"
-
- "github.com/miniflux/miniflux/server/route"
- "github.com/miniflux/miniflux/url"
-
- "github.com/PuerkitoBio/goquery"
- "github.com/gorilla/mux"
-)
-
-// ImageProxyFilter rewrites image tag URLs without HTTPS to local proxy URL
-func ImageProxyFilter(router *mux.Router, data string) string {
- doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
- if err != nil {
- return data
- }
-
- doc.Find("img").Each(func(i int, img *goquery.Selection) {
- if srcAttr, ok := img.Attr("src"); ok {
- if !url.IsHTTPS(srcAttr) {
- img.SetAttr("src", Proxify(router, srcAttr))
- }
- }
- })
-
- output, _ := doc.Find("body").First().Html()
- return output
-}
-
-// Proxify returns a proxified link.
-func Proxify(router *mux.Router, link string) string {
- // We use base64 url encoding to avoid slash in the URL.
- return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
-}
diff --git a/server/ui/filter/image_proxy_filter_test.go b/server/ui/filter/image_proxy_filter_test.go
deleted file mode 100644
index 992516e..0000000
--- a/server/ui/filter/image_proxy_filter_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package filter
-
-import (
- "net/http"
- "testing"
-
- "github.com/gorilla/mux"
-)
-
-func TestProxyFilterWithHttp(t *testing.T) {
- r := mux.NewRouter()
- r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
-
- input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
- output := ImageProxyFilter(r, input)
- expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
-
- if expected != output {
- t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
- }
-}
-
-func TestProxyFilterWithHttps(t *testing.T) {
- r := mux.NewRouter()
- r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
-
- input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
- output := ImageProxyFilter(r, input)
- expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
-
- if expected != output {
- t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
- }
-}
diff --git a/server/ui/form/auth.go b/server/ui/form/auth.go
deleted file mode 100644
index c18a0be..0000000
--- a/server/ui/form/auth.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/errors"
-)
-
-// AuthForm represents the authentication form.
-type AuthForm struct {
- Username string
- Password string
-}
-
-// Validate makes sure the form values are valid.
-func (a AuthForm) Validate() error {
- if a.Username == "" || a.Password == "" {
- return errors.NewLocalizedError("All fields are mandatory.")
- }
-
- return nil
-}
-
-// NewAuthForm returns a new AuthForm.
-func NewAuthForm(r *http.Request) *AuthForm {
- return &AuthForm{
- Username: r.FormValue("username"),
- Password: r.FormValue("password"),
- }
-}
diff --git a/server/ui/form/category.go b/server/ui/form/category.go
deleted file mode 100644
index 31b7196..0000000
--- a/server/ui/form/category.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/errors"
- "github.com/miniflux/miniflux/model"
-)
-
-// CategoryForm represents a feed form in the UI
-type CategoryForm struct {
- Title string
-}
-
-// Validate makes sure the form values are valid.
-func (c CategoryForm) Validate() error {
- if c.Title == "" {
- return errors.NewLocalizedError("The title is mandatory.")
- }
- return nil
-}
-
-// Merge update the given category fields.
-func (c CategoryForm) Merge(category *model.Category) *model.Category {
- category.Title = c.Title
- return category
-}
-
-// NewCategoryForm returns a new CategoryForm.
-func NewCategoryForm(r *http.Request) *CategoryForm {
- return &CategoryForm{
- Title: r.FormValue("title"),
- }
-}
diff --git a/server/ui/form/feed.go b/server/ui/form/feed.go
deleted file mode 100644
index 896a6d7..0000000
--- a/server/ui/form/feed.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
- "strconv"
-
- "github.com/miniflux/miniflux/errors"
- "github.com/miniflux/miniflux/model"
-)
-
-// FeedForm represents a feed form in the UI
-type FeedForm struct {
- FeedURL string
- SiteURL string
- Title string
- ScraperRules string
- RewriteRules string
- Crawler bool
- CategoryID int64
-}
-
-// ValidateModification validates FeedForm fields
-func (f FeedForm) ValidateModification() error {
- if f.FeedURL == "" || f.SiteURL == "" || f.Title == "" || f.CategoryID == 0 {
- return errors.NewLocalizedError("All fields are mandatory.")
- }
- return nil
-}
-
-// Merge updates the fields of the given feed.
-func (f FeedForm) Merge(feed *model.Feed) *model.Feed {
- feed.Category.ID = f.CategoryID
- feed.Title = f.Title
- feed.SiteURL = f.SiteURL
- feed.FeedURL = f.FeedURL
- feed.ScraperRules = f.ScraperRules
- feed.RewriteRules = f.RewriteRules
- feed.Crawler = f.Crawler
- feed.ParsingErrorCount = 0
- feed.ParsingErrorMsg = ""
- return feed
-}
-
-// NewFeedForm parses the HTTP request and returns a FeedForm
-func NewFeedForm(r *http.Request) *FeedForm {
- categoryID, err := strconv.Atoi(r.FormValue("category_id"))
- if err != nil {
- categoryID = 0
- }
-
- return &FeedForm{
- FeedURL: r.FormValue("feed_url"),
- SiteURL: r.FormValue("site_url"),
- Title: r.FormValue("title"),
- ScraperRules: r.FormValue("scraper_rules"),
- RewriteRules: r.FormValue("rewrite_rules"),
- Crawler: r.FormValue("crawler") == "1",
- CategoryID: int64(categoryID),
- }
-}
diff --git a/server/ui/form/integration.go b/server/ui/form/integration.go
deleted file mode 100644
index 8cc6d35..0000000
--- a/server/ui/form/integration.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/model"
-)
-
-// IntegrationForm represents user integration settings form.
-type IntegrationForm struct {
- PinboardEnabled bool
- PinboardToken string
- PinboardTags string
- PinboardMarkAsUnread bool
- InstapaperEnabled bool
- InstapaperUsername string
- InstapaperPassword string
- FeverEnabled bool
- FeverUsername string
- FeverPassword string
- WallabagEnabled bool
- WallabagURL string
- WallabagClientID string
- WallabagClientSecret string
- WallabagUsername string
- WallabagPassword string
-}
-
-// Merge copy form values to the model.
-func (i IntegrationForm) Merge(integration *model.Integration) {
- integration.PinboardEnabled = i.PinboardEnabled
- integration.PinboardToken = i.PinboardToken
- integration.PinboardTags = i.PinboardTags
- integration.PinboardMarkAsUnread = i.PinboardMarkAsUnread
- integration.InstapaperEnabled = i.InstapaperEnabled
- integration.InstapaperUsername = i.InstapaperUsername
- integration.InstapaperPassword = i.InstapaperPassword
- integration.FeverEnabled = i.FeverEnabled
- integration.FeverUsername = i.FeverUsername
- integration.FeverPassword = i.FeverPassword
- integration.WallabagEnabled = i.WallabagEnabled
- integration.WallabagURL = i.WallabagURL
- integration.WallabagClientID = i.WallabagClientID
- integration.WallabagClientSecret = i.WallabagClientSecret
- integration.WallabagUsername = i.WallabagUsername
- integration.WallabagPassword = i.WallabagPassword
-}
-
-// NewIntegrationForm returns a new AuthForm.
-func NewIntegrationForm(r *http.Request) *IntegrationForm {
- return &IntegrationForm{
- PinboardEnabled: r.FormValue("pinboard_enabled") == "1",
- PinboardToken: r.FormValue("pinboard_token"),
- PinboardTags: r.FormValue("pinboard_tags"),
- PinboardMarkAsUnread: r.FormValue("pinboard_mark_as_unread") == "1",
- InstapaperEnabled: r.FormValue("instapaper_enabled") == "1",
- InstapaperUsername: r.FormValue("instapaper_username"),
- InstapaperPassword: r.FormValue("instapaper_password"),
- FeverEnabled: r.FormValue("fever_enabled") == "1",
- FeverUsername: r.FormValue("fever_username"),
- FeverPassword: r.FormValue("fever_password"),
- WallabagEnabled: r.FormValue("wallabag_enabled") == "1",
- WallabagURL: r.FormValue("wallabag_url"),
- WallabagClientID: r.FormValue("wallabag_client_id"),
- WallabagClientSecret: r.FormValue("wallabag_client_secret"),
- WallabagUsername: r.FormValue("wallabag_username"),
- WallabagPassword: r.FormValue("wallabag_password"),
- }
-}
diff --git a/server/ui/form/settings.go b/server/ui/form/settings.go
deleted file mode 100644
index e5f6939..0000000
--- a/server/ui/form/settings.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/errors"
- "github.com/miniflux/miniflux/model"
-)
-
-// SettingsForm represents the settings form.
-type SettingsForm struct {
- Username string
- Password string
- Confirmation string
- Theme string
- Language string
- Timezone string
- EntryDirection string
-}
-
-// Merge updates the fields of the given user.
-func (s *SettingsForm) Merge(user *model.User) *model.User {
- user.Username = s.Username
- user.Theme = s.Theme
- user.Language = s.Language
- user.Timezone = s.Timezone
- user.EntryDirection = s.EntryDirection
-
- if s.Password != "" {
- user.Password = s.Password
- }
-
- return user
-}
-
-// Validate makes sure the form values are valid.
-func (s *SettingsForm) Validate() error {
- if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" {
- return errors.NewLocalizedError("The username, theme, language and timezone fields are mandatory.")
- }
-
- if s.Password != "" {
- if s.Password != s.Confirmation {
- return errors.NewLocalizedError("Passwords are not the same.")
- }
-
- if len(s.Password) < 6 {
- return errors.NewLocalizedError("You must use at least 6 characters")
- }
- }
-
- return nil
-}
-
-// NewSettingsForm returns a new SettingsForm.
-func NewSettingsForm(r *http.Request) *SettingsForm {
- return &SettingsForm{
- Username: r.FormValue("username"),
- Password: r.FormValue("password"),
- Confirmation: r.FormValue("confirmation"),
- Theme: r.FormValue("theme"),
- Language: r.FormValue("language"),
- Timezone: r.FormValue("timezone"),
- EntryDirection: r.FormValue("entry_direction"),
- }
-}
diff --git a/server/ui/form/subscription.go b/server/ui/form/subscription.go
deleted file mode 100644
index 7d2caaf..0000000
--- a/server/ui/form/subscription.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
- "strconv"
-
- "github.com/miniflux/miniflux/errors"
-)
-
-// SubscriptionForm represents the subscription form.
-type SubscriptionForm struct {
- URL string
- CategoryID int64
- Crawler bool
-}
-
-// Validate makes sure the form values are valid.
-func (s *SubscriptionForm) Validate() error {
- if s.URL == "" || s.CategoryID == 0 {
- return errors.NewLocalizedError("The URL and the category are mandatory.")
- }
-
- return nil
-}
-
-// NewSubscriptionForm returns a new SubscriptionForm.
-func NewSubscriptionForm(r *http.Request) *SubscriptionForm {
- categoryID, err := strconv.Atoi(r.FormValue("category_id"))
- if err != nil {
- categoryID = 0
- }
-
- return &SubscriptionForm{
- URL: r.FormValue("url"),
- Crawler: r.FormValue("crawler") == "1",
- CategoryID: int64(categoryID),
- }
-}
diff --git a/server/ui/form/user.go b/server/ui/form/user.go
deleted file mode 100644
index 8b8346e..0000000
--- a/server/ui/form/user.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package form
-
-import (
- "net/http"
-
- "github.com/miniflux/miniflux/errors"
- "github.com/miniflux/miniflux/model"
-)
-
-// UserForm represents the user form.
-type UserForm struct {
- Username string
- Password string
- Confirmation string
- IsAdmin bool
-}
-
-// ValidateCreation validates user creation.
-func (u UserForm) ValidateCreation() error {
- if u.Username == "" || u.Password == "" || u.Confirmation == "" {
- return errors.NewLocalizedError("All fields are mandatory.")
- }
-
- if u.Password != u.Confirmation {
- return errors.NewLocalizedError("Passwords are not the same.")
- }
-
- if len(u.Password) < 6 {
- return errors.NewLocalizedError("You must use at least 6 characters.")
- }
-
- return nil
-}
-
-// ValidateModification validates user modification.
-func (u UserForm) ValidateModification() error {
- if u.Username == "" {
- return errors.NewLocalizedError("The username is mandatory.")
- }
-
- if u.Password != "" {
- if u.Password != u.Confirmation {
- return errors.NewLocalizedError("Passwords are not the same.")
- }
-
- if len(u.Password) < 6 {
- return errors.NewLocalizedError("You must use at least 6 characters.")
- }
- }
-
- return nil
-}
-
-// ToUser returns a User from the form values.
-func (u UserForm) ToUser() *model.User {
- return &model.User{
- Username: u.Username,
- Password: u.Password,
- IsAdmin: u.IsAdmin,
- }
-}
-
-// Merge updates the fields of the given user.
-func (u UserForm) Merge(user *model.User) *model.User {
- user.Username = u.Username
- user.IsAdmin = u.IsAdmin
-
- if u.Password != "" {
- user.Password = u.Password
- }
-
- return user
-}
-
-// NewUserForm returns a new UserForm.
-func NewUserForm(r *http.Request) *UserForm {
- return &UserForm{
- Username: r.FormValue("username"),
- Password: r.FormValue("password"),
- Confirmation: r.FormValue("confirmation"),
- IsAdmin: r.FormValue("is_admin") == "1",
- }
-}
diff --git a/server/ui/payload/payload.go b/server/ui/payload/payload.go
deleted file mode 100644
index d91e34a..0000000
--- a/server/ui/payload/payload.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
-// Use of this source code is governed by the Apache 2.0
-// license that can be found in the LICENSE file.
-
-package payload
-
-import (
- "encoding/json"
- "fmt"
- "io"
-
- "github.com/miniflux/miniflux/model"
-)
-
-// DecodeEntryStatusPayload unserialize JSON request to update entry statuses.
-func DecodeEntryStatusPayload(data io.Reader) (entryIDs []int64, status string, err error) {
- type payload struct {
- EntryIDs []int64 `json:"entry_ids"`
- Status string `json:"status"`
- }
-
- var p payload
- decoder := json.NewDecoder(data)
- if err = decoder.Decode(&p); err != nil {
- return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
- }
-
- if err := model.ValidateEntryStatus(p.Status); err != nil {
- return nil, "", err
- }
-
- return p.EntryIDs, p.Status, nil
-}