Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd68181a83 | |||
| a8be61cdbb | |||
| 609c57332e | |||
| 5185bf0c7e | |||
| cbd273da2b | |||
| 0c7039de0e | |||
| f110384f11 | |||
| 953d0a2dc0 | |||
| 9694861cb6 | |||
| 34ce114231 | |||
| 3030145b30 | |||
| a9f0fdaf22 |
@@ -1,3 +1,17 @@
|
||||
Version 2.0.4 (Mar 5, 2018)
|
||||
---------------------------
|
||||
|
||||
* Add Simplified Chinese translation
|
||||
* Add Nunux Keeper integration
|
||||
* Filter the list of timezones
|
||||
* Add timezone to entries dates for REST and Fever API
|
||||
* Show last login and session creation date in current timezone
|
||||
* Fix typo in edit user template
|
||||
* Improve parser error messages
|
||||
* Remove parentheses around feed error messages
|
||||
* Support localized feed errors generated by background workers
|
||||
* Print info message if DATABASE_URL is not set
|
||||
|
||||
Version 2.0.3 (Feb 19, 2018)
|
||||
----------------------------
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ func (c *Controller) Users(ctx *handler.Context, request *handler.Request, respo
|
||||
return
|
||||
}
|
||||
|
||||
users.UseTimezone(ctx.UserTimezone())
|
||||
response.JSON().Standard(users)
|
||||
}
|
||||
|
||||
@@ -127,6 +128,7 @@ func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, re
|
||||
return
|
||||
}
|
||||
|
||||
user.UseTimezone(ctx.UserTimezone())
|
||||
response.JSON().Standard(user)
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -28,6 +28,11 @@ func Parse() {
|
||||
flag.Parse()
|
||||
|
||||
cfg := config.NewConfig()
|
||||
|
||||
if *flagDebugMode || cfg.HasDebugMode() {
|
||||
logger.EnableDebug()
|
||||
}
|
||||
|
||||
store := storage.NewStorage(
|
||||
cfg.DatabaseURL(),
|
||||
cfg.DatabaseMaxConnections(),
|
||||
@@ -63,9 +68,5 @@ func Parse() {
|
||||
return
|
||||
}
|
||||
|
||||
if *flagDebugMode || cfg.HasDebugMode() {
|
||||
logger.EnableDebug()
|
||||
}
|
||||
|
||||
daemon.Run(cfg, store)
|
||||
}
|
||||
|
||||
+12
-1
@@ -8,6 +8,8 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -89,7 +91,16 @@ func (c *Config) BasePath() string {
|
||||
|
||||
// DatabaseURL returns the database URL.
|
||||
func (c *Config) DatabaseURL() string {
|
||||
return c.get("DATABASE_URL", defaultDatabaseURL)
|
||||
value, exists := os.LookupEnv("DATABASE_URL")
|
||||
if !exists {
|
||||
logger.Info("The environment variable DATABASE_URL is not configured (the default value is used instead)")
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
value = defaultDatabaseURL
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// DatabaseMaxConnections returns the number of maximum database connections.
|
||||
|
||||
+4
-2
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/scheduler"
|
||||
@@ -26,9 +27,10 @@ func Run(cfg *config.Config, store *storage.Storage) {
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
signal.Notify(stop, syscall.SIGTERM)
|
||||
|
||||
feedHandler := feed.NewFeedHandler(store)
|
||||
translator := locale.Load()
|
||||
feedHandler := feed.NewFeedHandler(store, translator)
|
||||
pool := scheduler.NewWorkerPool(feedHandler, cfg.WorkerPoolSize())
|
||||
server := newServer(cfg, store, pool, feedHandler)
|
||||
server := newServer(cfg, store, pool, feedHandler, translator)
|
||||
|
||||
scheduler.NewFeedScheduler(
|
||||
store,
|
||||
|
||||
+1
-2
@@ -23,9 +23,8 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool) *mux.Router {
|
||||
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
translator := locale.Load()
|
||||
templateEngine := template.NewEngine(cfg, router, translator)
|
||||
|
||||
apiController := api.NewController(store, feedHandler)
|
||||
|
||||
+3
-2
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/config"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/reader/feed"
|
||||
"github.com/miniflux/miniflux/scheduler"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *http.Server {
|
||||
func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, translator *locale.Translator) *http.Server {
|
||||
certFile := cfg.CertFile()
|
||||
keyFile := cfg.KeyFile()
|
||||
certDomain := cfg.CertDomain()
|
||||
@@ -28,7 +29,7 @@ func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.Worke
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
Addr: cfg.ListenAddr(),
|
||||
Handler: routes(cfg, store, feedHandler, pool),
|
||||
Handler: routes(cfg, store, feedHandler, pool, translator),
|
||||
}
|
||||
|
||||
if certDomain != "" && certCache != "" {
|
||||
|
||||
+2
-2
@@ -27,6 +27,6 @@ func (l LocalizedError) Localize(translation *locale.Language) string {
|
||||
}
|
||||
|
||||
// NewLocalizedError returns a new LocalizedError.
|
||||
func NewLocalizedError(message string, args ...interface{}) LocalizedError {
|
||||
return LocalizedError{message: message, args: args}
|
||||
func NewLocalizedError(message string, args ...interface{}) *LocalizedError {
|
||||
return &LocalizedError{message: message, args: args}
|
||||
}
|
||||
|
||||
+2
-1
@@ -149,11 +149,12 @@ func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header = c.buildHeaders()
|
||||
|
||||
if c.username != "" && c.password != "" {
|
||||
request.SetBasicAuth(c.username, c.password)
|
||||
}
|
||||
|
||||
request.Header = c.buildHeaders()
|
||||
return request, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package integration
|
||||
|
||||
import (
|
||||
"github.com/miniflux/miniflux/integration/instapaper"
|
||||
"github.com/miniflux/miniflux/integration/nunuxkeeper"
|
||||
"github.com/miniflux/miniflux/integration/pinboard"
|
||||
"github.com/miniflux/miniflux/integration/wallabag"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
@@ -48,4 +49,15 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
|
||||
logger.Error("[Integration] %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if integration.NunuxKeeperEnabled {
|
||||
client := nunuxkeeper.NewClient(
|
||||
integration.NunuxKeeperURL,
|
||||
integration.NunuxKeeperAPIKey,
|
||||
)
|
||||
|
||||
if err := client.AddEntry(entry.URL, entry.Title, entry.Content); err != nil {
|
||||
logger.Error("[Integration] %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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 nunuxkeeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/miniflux/miniflux/http"
|
||||
)
|
||||
|
||||
// Document structure of a Nununx Keeper document
|
||||
type Document struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Origin string `json:"origin,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ContentType string `json:"contentType,omitempty"`
|
||||
}
|
||||
|
||||
// Client represents an Nunux Keeper client.
|
||||
type Client struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// AddEntry sends an entry to Nunux Keeper.
|
||||
func (c *Client) AddEntry(link, title, content string) error {
|
||||
doc := &Document{
|
||||
Title: title,
|
||||
Origin: link,
|
||||
Content: content,
|
||||
ContentType: "text/html",
|
||||
}
|
||||
|
||||
apiURL, err := getAPIEndpoint(c.baseURL, "/v2/documents")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := http.NewClientWithCredentials(apiURL, "api", c.apiKey)
|
||||
response, err := client.PostJSON(doc)
|
||||
if response.HasServerFailure() {
|
||||
return fmt.Errorf("nunux-keeper: unable to send entry, status=%d", response.StatusCode)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NewClient returns a new Nunux Keeepr client.
|
||||
func NewClient(baseURL, apiKey string) *Client {
|
||||
return &Client{baseURL: baseURL, apiKey: apiKey}
|
||||
}
|
||||
|
||||
func getAPIEndpoint(baseURL, pathURL string) (string, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("nunux-keeper: invalid API endpoint: %v", err)
|
||||
}
|
||||
u.Path = path.Join(u.Path, pathURL)
|
||||
return u.String(), nil
|
||||
}
|
||||
@@ -31,5 +31,6 @@ func AvailableLanguages() map[string]string {
|
||||
"fr_FR": "Français",
|
||||
"de_DE": "Deutsch",
|
||||
"pl_PL": "Polski",
|
||||
"zh_CN": "简体中文",
|
||||
}
|
||||
}
|
||||
|
||||
+255
-23
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-02-19 22:49:00.3105786 +0100 STD m=+0.012978001
|
||||
// 2018-02-27 21:15:00.586846241 -0800 PST m=+0.029084447
|
||||
|
||||
package locale
|
||||
|
||||
@@ -116,12 +116,12 @@ var translations = map[string]string{
|
||||
"Unable to execute request: %v": "Diese Anfrage konnte nicht ausgeführt werden: %v",
|
||||
"Last Parsing Error": "Letzter Analysefehler",
|
||||
"There is a problem with this feed": "Es gibt ein Problem mit diesem Abonnement",
|
||||
"Unable to parse OPML file: %v.": "OPML Datei konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse RSS feed: %v.": "RSS Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse Atom feed: %v.": "Atom Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse JSON feed: %v.": "JSON Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse RDF feed: %v.": "RDF Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to normalize encoding: %v": "Zeichenkodierung konnte nicht normalisiert werden: %v",
|
||||
"Unable to parse OPML file: %q": "OPML Datei konnte nicht gelesen werden: %q",
|
||||
"Unable to parse RSS feed: %q": "RSS Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to parse Atom feed: %q": "Atom Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to parse JSON feed: %q": "JSON Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to parse RDF feed: %q": "RDF Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to normalize encoding: %q": "Zeichenkodierung konnte nicht normalisiert werden: %q",
|
||||
"Unable to create this category.": "Diese Kategorie konnte nicht angelegt werden.",
|
||||
"yes": "ja",
|
||||
"no": "nein",
|
||||
@@ -168,6 +168,9 @@ var translations = map[string]string{
|
||||
"Wallabag Client Secret": "Wallabag Client-Secret",
|
||||
"Wallabag Username": "Wallabag Benutzername",
|
||||
"Wallabag Password": "Wallabag Passwort",
|
||||
"Save articles to Nunux Keeper": "Artikel in Nunux Keeper speichern",
|
||||
"Nunux Keeper API Endpoint": "Nunux Keeper API-Endpunkt",
|
||||
"Nunux Keeper API key": "Nunux Keeper API-Schlüssel",
|
||||
"Keyboard Shortcut: %s": "Tastenkürzel: %s",
|
||||
"Favorites": "Lesezeichen",
|
||||
"Star": "Lesezeichen hinzufügen",
|
||||
@@ -345,12 +348,12 @@ var translations = map[string]string{
|
||||
"Unable to execute request: %v": "Impossible d'exécuter cette requête: %v",
|
||||
"Last Parsing Error": "Dernière erreur d'analyse",
|
||||
"There is a problem with this feed": "Il y a un problème avec cet abonnement",
|
||||
"Unable to parse OPML file: %v.": "Impossible de lire ce fichier OPML : %v.",
|
||||
"Unable to parse RSS feed: %v.": "Impossible de lire ce flux RSS: %v.",
|
||||
"Unable to parse Atom feed: %v.": "Impossible de lire ce flux Atom: %v.",
|
||||
"Unable to parse JSON feed: %v.": "Impossible de lire ce flux JSON: %v.",
|
||||
"Unable to parse RDF feed: %v.": "Impossible de lire ce flux RDF: %v.",
|
||||
"Unable to normalize encoding: %v": "Impossible de normaliser l'encodage : %v",
|
||||
"Unable to parse OPML file: %q": "Impossible de lire ce fichier OPML : %q",
|
||||
"Unable to parse RSS feed: %q": "Impossible de lire ce flux RSS : %q",
|
||||
"Unable to parse Atom feed: %q": "Impossible de lire ce flux Atom : %q",
|
||||
"Unable to parse JSON feed: %q": "Impossible de lire ce flux JSON : %q",
|
||||
"Unable to parse RDF feed: %q": "Impossible de lire ce flux RDF : %q",
|
||||
"Unable to normalize encoding: %q": "Impossible de normaliser l'encodage : %q",
|
||||
"Unable to create this category.": "Impossible de créer cette catégorie.",
|
||||
"yes": "oui",
|
||||
"no": "non",
|
||||
@@ -397,6 +400,9 @@ var translations = map[string]string{
|
||||
"Wallabag Client Secret": "Clé secrète du client Wallabag",
|
||||
"Wallabag Username": "Nom d'utilisateur de Wallabag",
|
||||
"Wallabag Password": "Mot de passe de Wallabag",
|
||||
"Save articles to Nunux Keeper": "Sauvegarder les articles vers Nunux Keeper",
|
||||
"Nunux Keeper API Endpoint": "URL de l'API de Nunux Keeper",
|
||||
"Nunux Keeper API key": "Clé d'API de Nunux Keeper",
|
||||
"Keyboard Shortcut: %s": "Raccourci clavier : %s",
|
||||
"Favorites": "Favoris",
|
||||
"Star": "Favoris",
|
||||
@@ -460,7 +466,7 @@ var translations = map[string]string{
|
||||
],
|
||||
"plural.categories.feed_count": [
|
||||
"Jest %d kanał.",
|
||||
"Są %d kanały.",
|
||||
"Są %d kanały.",
|
||||
"Jest %d kanałów."
|
||||
],
|
||||
"Username": "Nazwa użytkownika",
|
||||
@@ -566,12 +572,12 @@ var translations = map[string]string{
|
||||
"Unable to execute request: %v": "To polecenie nie mogło zostać wykonane: %v",
|
||||
"Last Parsing Error": "Ostatni błąd analizy",
|
||||
"There is a problem with this feed": "Z tym kanałem jest problem",
|
||||
"Unable to parse OPML file: %v.": "Plik OPML nie mógł zostać odczytany: %v.",
|
||||
"Unable to parse RSS feed: %v.": "Nie można było odczytać kanału RSS: %v.",
|
||||
"Unable to parse Atom feed: %v.": "Nie można było odczytać kanału Atom: %v.",
|
||||
"Unable to parse JSON feed: %v.": "Nie można było odczytać kanału JSON: %v.",
|
||||
"Unable to parse RDF feed: %v.": "Nie można było odczytać kanału RDF: %v.",
|
||||
"Unable to normalize encoding: %v": "Kodowanie znaków nie mogło zostać znormalizowane: %v",
|
||||
"Unable to parse OPML file: %q": "Plik OPML nie mógł zostać odczytany: %q",
|
||||
"Unable to parse RSS feed: %q": "Nie można było odczytać kanału RSS: %q",
|
||||
"Unable to parse Atom feed: %q": "Nie można było odczytać kanału Atom: %q",
|
||||
"Unable to parse JSON feed: %q": "Nie można było odczytać kanału JSON: %q",
|
||||
"Unable to parse RDF feed: %q": "Nie można było odczytać kanału RDF: %q",
|
||||
"Unable to normalize encoding: %q": "Kodowanie znaków nie mogło zostać znormalizowane: %q",
|
||||
"Unable to create this category.": "Ta kategoria nie mogła zostać utworzona.",
|
||||
"yes": "tak",
|
||||
"no": "nie",
|
||||
@@ -618,6 +624,9 @@ var translations = map[string]string{
|
||||
"Wallabag Client Secret": "Wallabag Client Secret",
|
||||
"Wallabag Username": "Login do Wallabag",
|
||||
"Wallabag Password": "Hasło do Wallabag",
|
||||
"Save articles to Nunux Keeper": "Zapisz artykuly do Nunux Keeper",
|
||||
"Nunux Keeper API Endpoint": "Nunux Keeper URL",
|
||||
"Nunux Keeper API key": "Nunux Keeper API key",
|
||||
"Keyboard Shortcut: %s": "Skróty klawiszowe: %s",
|
||||
"Favorites": "Ulubione",
|
||||
"Star": "Oznacz gwiazdką",
|
||||
@@ -672,12 +681,235 @@ var translations = map[string]string{
|
||||
"This website is permanently unreachable (original error: %q)": "Ta strona jest niedostępna (błąd: %q)",
|
||||
"Website unreachable, the request timed out after %d seconds": "Strona internetowa nieosiągalna, żądanie wygasło po %d sekundach"
|
||||
}
|
||||
`,
|
||||
"zh_CN": `{
|
||||
"plural.feed.error_count": [
|
||||
"%d 错误",
|
||||
"%d 错误"
|
||||
],
|
||||
"plural.categories.feed_count": [
|
||||
"有 %d 个源.",
|
||||
"有 %d 个源."
|
||||
],
|
||||
"Username": "用户名",
|
||||
"Password": "密码",
|
||||
"Unread": "未读",
|
||||
"History": "历史",
|
||||
"Feeds": "源",
|
||||
"Categories": "分类",
|
||||
"Settings": "设置",
|
||||
"Logout": "登出",
|
||||
"Next": "下一页",
|
||||
"Previous": "上一页",
|
||||
"New Subscription": "新订阅",
|
||||
"Import": "导入",
|
||||
"Export": "导出",
|
||||
"There is no category. You must have at least one category.": "目前没有分类.需要有一个已有的分类存在.",
|
||||
"URL": "URL",
|
||||
"Category": "分类",
|
||||
"Find a subscription": "寻找订阅",
|
||||
"Loading...": "载入中...",
|
||||
"Create a category": "新建分类",
|
||||
"There is no category.": "目前没有分类.",
|
||||
"Edit": "编辑",
|
||||
"Remove": "删除",
|
||||
"No feed.": "没有源.",
|
||||
"There is no article in this category.": "该分类下没有文章.",
|
||||
"Original": "原始链接",
|
||||
"Mark this page as read": "标记为已读",
|
||||
"not yet": "尚未",
|
||||
"just now": "刚刚",
|
||||
"1 minute ago": "1 分钟前",
|
||||
"%d minutes ago": "%d 分钟前",
|
||||
"1 hour ago": "1 小时前",
|
||||
"%d hours ago": "%d 小时前",
|
||||
"yesterday": "昨天",
|
||||
"%d days ago": "%d 天前",
|
||||
"%d weeks ago": "%d 周前",
|
||||
"%d months ago": "%d 月前",
|
||||
"%d years ago": "%d 年前",
|
||||
"Date": "日期",
|
||||
"IP Address": "IP地址",
|
||||
"User Agent": "User Agent",
|
||||
"Actions": "操作",
|
||||
"Current session": "当前会话",
|
||||
"Sessions": "会话",
|
||||
"Users": "用户",
|
||||
"Add user": "新建用户",
|
||||
"Choose a Subscription": "选择一个订阅",
|
||||
"Subscribe": "订阅",
|
||||
"New Category": "新分类",
|
||||
"Title": "标题",
|
||||
"Save": "保存",
|
||||
"or": "或",
|
||||
"cancel": "取消",
|
||||
"New User": "新用户",
|
||||
"Confirmation": "确认",
|
||||
"Administrator": "管理员",
|
||||
"Edit Category: %s": "编辑分类 : %s",
|
||||
"Update": "更新",
|
||||
"Edit Feed: %s": "编辑源 : %s",
|
||||
"There is no category!": "没有分类!",
|
||||
"Edit user: %s": "编辑用户: %s",
|
||||
"There is no article for this feed.": "这个源中没有文章.",
|
||||
"Add subscription": "新增订阅",
|
||||
"You don't have any subscription.": "当前没有订阅",
|
||||
"Last check:": "最后检查时间:",
|
||||
"Refresh": "更新",
|
||||
"There is no history at the moment.": "当前没有历史.",
|
||||
"OPML file": "OPML 文件",
|
||||
"Sign In": "登陆",
|
||||
"Sign in": "登陆",
|
||||
"Theme": "主题",
|
||||
"Timezone": "时区",
|
||||
"Language": "语言",
|
||||
"There is no unread article.": "目前没有未读文章.",
|
||||
"You are the only user.": "你是目前仅有的用户.",
|
||||
"Last Login": "最后登录时间",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"This feed already exists (%s)": "源已存在 (%s)",
|
||||
"Unable to fetch feed (statusCode=%d)": "无法获取源 (错误代码=%d)",
|
||||
"Unable to open this link: %v": "无法打开这一链接: %v",
|
||||
"Unable to analyze this page: %v": "无法分析这一页面: %v",
|
||||
"Unable to find any subscription.": "找不到任何订阅.",
|
||||
"The URL and the category are mandatory.": "必须填写URL和分类.",
|
||||
"All fields are mandatory.": "必须填写全部信息.",
|
||||
"Passwords are not the same.": "两次输入的密码不同.",
|
||||
"You must use at least 6 characters.": "请至少使用6个字符.",
|
||||
"The username is mandatory.": "必须填写用户名.",
|
||||
"The username, theme, language and timezone fields are mandatory.": "必须填写用户名,主题,语言和时区.",
|
||||
"The title is mandatory.": "必须填写标题.",
|
||||
"About": "关于",
|
||||
"version": "版本",
|
||||
"Version:": "版本:",
|
||||
"Build Date:": "构建日期:",
|
||||
"Author:": "作者:",
|
||||
"Authors": "作者",
|
||||
"License:": "协议:",
|
||||
"Attachments": "附件",
|
||||
"Download": "下载",
|
||||
"Invalid username or password.": "用户名或密码无效.",
|
||||
"Never": "永不",
|
||||
"Unable to execute request: %v": "无法执行这一请求: %v",
|
||||
"Last Parsing Error": "最后一次解析错误",
|
||||
"There is a problem with this feed": "这一源存在问题",
|
||||
"Unable to parse OPML file: %q": "无法解析OPML文件: %q",
|
||||
"Unable to parse RSS feed: %q": "无法解析RSS源: %q",
|
||||
"Unable to parse Atom feed: %q": "无法解析Atom源: %q",
|
||||
"Unable to parse JSON feed: %q": "无法解析JSON源: %q",
|
||||
"Unable to parse RDF feed: %q": "无法解析RDF源: %q",
|
||||
"Unable to normalize encoding: %q": "无法正则化编码: %q",
|
||||
"Unable to create this category.": "无法建立这个分类.",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"Are you sure?": "您确认吗?",
|
||||
"Work in progress...": "执行中...",
|
||||
"This user already exists.": "用户已存在.",
|
||||
"This category already exists.": "分类已存在.",
|
||||
"Unable to update this category.": "无法更新该分类.",
|
||||
"Integrations": "集成",
|
||||
"Bookmarklet": "书签小应用",
|
||||
"Drag and drop this link to your bookmarks.": "拖动这个链接到书签.",
|
||||
"This special link allows you to subscribe to a website directly by using a bookmark in your web browser.": "你可以打开这个特殊的书签来直接订阅网站.",
|
||||
"Add to Miniflux": "新增到Miniflux",
|
||||
"Refresh all feeds in background": "在后台更新全部源",
|
||||
"Sign in with Google": "使用Google登陆",
|
||||
"Unlink my Google account": "去除Google账号关联",
|
||||
"Link my Google account": "关联我的Google账户",
|
||||
"Category not found for this user": "未找到该用户的这一分类",
|
||||
"Invalid theme.": "无效的主题.",
|
||||
"Entry Sorting": "内容排序",
|
||||
"Older entries first": "旧->新",
|
||||
"Recent entries first": "新->旧",
|
||||
"Saving...": "保存中",
|
||||
"Done!": "完成!",
|
||||
"Save this article": "保存这篇文章",
|
||||
"Mark bookmark as unread": "标记为未读",
|
||||
"Pinboard Tags": "Pinboard 标签",
|
||||
"Pinboard API Token": "Pinboard API Token",
|
||||
"Save articles to Pinboard": "保存文章到Pinboard",
|
||||
"Save articles to Instapaper": "保存文章到Instapaper",
|
||||
"Instapaper Username": "Instapaper 用户名",
|
||||
"Instapaper Password": "Instapaper 密码",
|
||||
"Activate Fever API": "启用 Fever API",
|
||||
"Fever Username": "Fever 用户名",
|
||||
"Fever Password": "Fever 密码",
|
||||
"Fetch original content": "抓取原内容",
|
||||
"Scraper Rules": "Scraper规则",
|
||||
"Rewrite Rules": "重写规则",
|
||||
"Preferences saved!": "偏好已存储!",
|
||||
"Your external account is now linked !": "您的外部账号已关联!",
|
||||
"Save articles to Wallabag": "保存文章到Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag 客户端ID",
|
||||
"Wallabag Client Secret": "Wallabag 客户端Secret",
|
||||
"Wallabag Username": "Wallabag 用户名",
|
||||
"Wallabag Password": "Wallabag 密码",
|
||||
"Save articles to Nunux Keeper": "保存文章到Nunux Keeper",
|
||||
"Nunux Keeper API Endpoint": "Nunux Keeper API Endpoint",
|
||||
"Nunux Keeper API key": "Nunux Keeper API key",
|
||||
"Keyboard Shortcut: %s": "快捷键: %s",
|
||||
"Favorites": "收藏",
|
||||
"Star": "标记星标",
|
||||
"Unstar": "去掉星标",
|
||||
"Starred": "星标",
|
||||
"There is no bookmark at the moment.": "当前没有书签.",
|
||||
"Last checked:": "上次检查:",
|
||||
"ETag header:": "ETag header:",
|
||||
"LastModified header:": "最后修改的Header:",
|
||||
"None": "无",
|
||||
"Keyboard Shortcuts": "快捷键",
|
||||
"Sections Navigation": "分区导航",
|
||||
"Go to unread": "去往未读",
|
||||
"Go to bookmarks": "去往书签",
|
||||
"Go to history": "去往历史",
|
||||
"Go to feeds": "去往源",
|
||||
"Go to categories": "去往分类",
|
||||
"Go to settings": "去往设定",
|
||||
"Show keyboard shortcuts": "显示快捷键",
|
||||
"Items Navigation": "条目导航",
|
||||
"Go to previous item": "上一条目",
|
||||
"Go to next item": "下一条目",
|
||||
"Pages Navigation": "页面导航",
|
||||
"Go to previous page": "上一页",
|
||||
"Go to next page": "下一页",
|
||||
"Open selected item": "打开选定的条目",
|
||||
"Open original link": "打开原始链接",
|
||||
"Toggle read/unread": "切换已读/未读状态",
|
||||
"Mark current page as read": "标记当前",
|
||||
"Download original content": "下载原始内容",
|
||||
"Toggle bookmark": "切换收藏状态",
|
||||
"Close modal dialog": "关闭模态对话窗口",
|
||||
"Save article": "保存文章",
|
||||
"There is already someone associated with this provider!": "该Provider已被关联!",
|
||||
"There is already someone else with the same Fever username!": "Fever用户名已被占用!",
|
||||
"Mark all as read": "全标记为已读",
|
||||
"This feed is empty": "该源是空的",
|
||||
"Flush history": "清理历史",
|
||||
"Site URL": "站点URL",
|
||||
"Feed URL": "源URL",
|
||||
"Logged as %s": "当前登录 %s",
|
||||
"Unread Items": "未读条目",
|
||||
"Change entry status": "更改状态",
|
||||
"Read": "标为已读",
|
||||
"Fever API endpoint:": "Fever API Endpoint:",
|
||||
"Miniflux API": "Miniflux API",
|
||||
"API Endpoint": "API Endpoint",
|
||||
"Your account password": "您账户的密码",
|
||||
"This web page is empty": "该网页是空的",
|
||||
"Invalid SSL certificate (original error: %q)": "无效的SSL证书 (原始错误: %q)",
|
||||
"This website is temporarily unreachable (original error: %q)": "该网站暂时不可达 (原始错误: %q)",
|
||||
"This website is permanently unreachable (original error: %q)": "该网站永久不可达 (原始错误: %q)",
|
||||
"Website unreachable, the request timed out after %d seconds": "网站不可达, 请求已在 %d 秒后超时"
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
var translationsChecksums = map[string]string{
|
||||
"de_DE": "53f7637318ac418ce0e3dd483923dab39ff4f4062b4909e5e03efcc3b693e5d6",
|
||||
"de_DE": "da3e70c096b35c205d89dddd400bbf34927bb495d4ee0f4eb3c3dc04e02b99c1",
|
||||
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
|
||||
"fr_FR": "ae61f82ac14bc2c6c6a3c2d38cf1ad8309ac2eef19b0726b2969ac155ccddc14",
|
||||
"pl_PL": "9c10899ec62f97ebb6d5d4d88cde8c68ac584e514ce840b51ba3aeff9ea3efe3",
|
||||
"fr_FR": "e842d6503b4d50ba5e3cd862b3d92c64f031356cf87f9989d2ac9a1ba0246ac8",
|
||||
"pl_PL": "0d8a76425cf634b96cfc425127d8e83db7662e2d4dbc30674098e3fb6cea7c8d",
|
||||
"zh_CN": "c19cb45a49af7957748fa006b51421edaa9774ef1ab0e91eb2c0552635016b62",
|
||||
}
|
||||
|
||||
@@ -110,12 +110,12 @@
|
||||
"Unable to execute request: %v": "Diese Anfrage konnte nicht ausgeführt werden: %v",
|
||||
"Last Parsing Error": "Letzter Analysefehler",
|
||||
"There is a problem with this feed": "Es gibt ein Problem mit diesem Abonnement",
|
||||
"Unable to parse OPML file: %v.": "OPML Datei konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse RSS feed: %v.": "RSS Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse Atom feed: %v.": "Atom Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse JSON feed: %v.": "JSON Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to parse RDF feed: %v.": "RDF Feed konnte nicht gelesen werden: %v.",
|
||||
"Unable to normalize encoding: %v": "Zeichenkodierung konnte nicht normalisiert werden: %v",
|
||||
"Unable to parse OPML file: %q": "OPML Datei konnte nicht gelesen werden: %q",
|
||||
"Unable to parse RSS feed: %q": "RSS Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to parse Atom feed: %q": "Atom Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to parse JSON feed: %q": "JSON Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to parse RDF feed: %q": "RDF Feed konnte nicht gelesen werden: %q",
|
||||
"Unable to normalize encoding: %q": "Zeichenkodierung konnte nicht normalisiert werden: %q",
|
||||
"Unable to create this category.": "Diese Kategorie konnte nicht angelegt werden.",
|
||||
"yes": "ja",
|
||||
"no": "nein",
|
||||
@@ -162,6 +162,9 @@
|
||||
"Wallabag Client Secret": "Wallabag Client-Secret",
|
||||
"Wallabag Username": "Wallabag Benutzername",
|
||||
"Wallabag Password": "Wallabag Passwort",
|
||||
"Save articles to Nunux Keeper": "Artikel in Nunux Keeper speichern",
|
||||
"Nunux Keeper API Endpoint": "Nunux Keeper API-Endpunkt",
|
||||
"Nunux Keeper API key": "Nunux Keeper API-Schlüssel",
|
||||
"Keyboard Shortcut: %s": "Tastenkürzel: %s",
|
||||
"Favorites": "Lesezeichen",
|
||||
"Star": "Lesezeichen hinzufügen",
|
||||
|
||||
@@ -110,12 +110,12 @@
|
||||
"Unable to execute request: %v": "Impossible d'exécuter cette requête: %v",
|
||||
"Last Parsing Error": "Dernière erreur d'analyse",
|
||||
"There is a problem with this feed": "Il y a un problème avec cet abonnement",
|
||||
"Unable to parse OPML file: %v.": "Impossible de lire ce fichier OPML : %v.",
|
||||
"Unable to parse RSS feed: %v.": "Impossible de lire ce flux RSS: %v.",
|
||||
"Unable to parse Atom feed: %v.": "Impossible de lire ce flux Atom: %v.",
|
||||
"Unable to parse JSON feed: %v.": "Impossible de lire ce flux JSON: %v.",
|
||||
"Unable to parse RDF feed: %v.": "Impossible de lire ce flux RDF: %v.",
|
||||
"Unable to normalize encoding: %v": "Impossible de normaliser l'encodage : %v",
|
||||
"Unable to parse OPML file: %q": "Impossible de lire ce fichier OPML : %q",
|
||||
"Unable to parse RSS feed: %q": "Impossible de lire ce flux RSS : %q",
|
||||
"Unable to parse Atom feed: %q": "Impossible de lire ce flux Atom : %q",
|
||||
"Unable to parse JSON feed: %q": "Impossible de lire ce flux JSON : %q",
|
||||
"Unable to parse RDF feed: %q": "Impossible de lire ce flux RDF : %q",
|
||||
"Unable to normalize encoding: %q": "Impossible de normaliser l'encodage : %q",
|
||||
"Unable to create this category.": "Impossible de créer cette catégorie.",
|
||||
"yes": "oui",
|
||||
"no": "non",
|
||||
@@ -162,6 +162,9 @@
|
||||
"Wallabag Client Secret": "Clé secrète du client Wallabag",
|
||||
"Wallabag Username": "Nom d'utilisateur de Wallabag",
|
||||
"Wallabag Password": "Mot de passe de Wallabag",
|
||||
"Save articles to Nunux Keeper": "Sauvegarder les articles vers Nunux Keeper",
|
||||
"Nunux Keeper API Endpoint": "URL de l'API de Nunux Keeper",
|
||||
"Nunux Keeper API key": "Clé d'API de Nunux Keeper",
|
||||
"Keyboard Shortcut: %s": "Raccourci clavier : %s",
|
||||
"Favorites": "Favoris",
|
||||
"Star": "Favoris",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
],
|
||||
"plural.categories.feed_count": [
|
||||
"Jest %d kanał.",
|
||||
"Są %d kanały.",
|
||||
"Są %d kanały.",
|
||||
"Jest %d kanałów."
|
||||
],
|
||||
"Username": "Nazwa użytkownika",
|
||||
@@ -112,12 +112,12 @@
|
||||
"Unable to execute request: %v": "To polecenie nie mogło zostać wykonane: %v",
|
||||
"Last Parsing Error": "Ostatni błąd analizy",
|
||||
"There is a problem with this feed": "Z tym kanałem jest problem",
|
||||
"Unable to parse OPML file: %v.": "Plik OPML nie mógł zostać odczytany: %v.",
|
||||
"Unable to parse RSS feed: %v.": "Nie można było odczytać kanału RSS: %v.",
|
||||
"Unable to parse Atom feed: %v.": "Nie można było odczytać kanału Atom: %v.",
|
||||
"Unable to parse JSON feed: %v.": "Nie można było odczytać kanału JSON: %v.",
|
||||
"Unable to parse RDF feed: %v.": "Nie można było odczytać kanału RDF: %v.",
|
||||
"Unable to normalize encoding: %v": "Kodowanie znaków nie mogło zostać znormalizowane: %v",
|
||||
"Unable to parse OPML file: %q": "Plik OPML nie mógł zostać odczytany: %q",
|
||||
"Unable to parse RSS feed: %q": "Nie można było odczytać kanału RSS: %q",
|
||||
"Unable to parse Atom feed: %q": "Nie można było odczytać kanału Atom: %q",
|
||||
"Unable to parse JSON feed: %q": "Nie można było odczytać kanału JSON: %q",
|
||||
"Unable to parse RDF feed: %q": "Nie można było odczytać kanału RDF: %q",
|
||||
"Unable to normalize encoding: %q": "Kodowanie znaków nie mogło zostać znormalizowane: %q",
|
||||
"Unable to create this category.": "Ta kategoria nie mogła zostać utworzona.",
|
||||
"yes": "tak",
|
||||
"no": "nie",
|
||||
@@ -164,6 +164,9 @@
|
||||
"Wallabag Client Secret": "Wallabag Client Secret",
|
||||
"Wallabag Username": "Login do Wallabag",
|
||||
"Wallabag Password": "Hasło do Wallabag",
|
||||
"Save articles to Nunux Keeper": "Zapisz artykuly do Nunux Keeper",
|
||||
"Nunux Keeper API Endpoint": "Nunux Keeper URL",
|
||||
"Nunux Keeper API key": "Nunux Keeper API key",
|
||||
"Keyboard Shortcut: %s": "Skróty klawiszowe: %s",
|
||||
"Favorites": "Ulubione",
|
||||
"Star": "Oznacz gwiazdką",
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"plural.feed.error_count": [
|
||||
"%d 错误",
|
||||
"%d 错误"
|
||||
],
|
||||
"plural.categories.feed_count": [
|
||||
"有 %d 个源.",
|
||||
"有 %d 个源."
|
||||
],
|
||||
"Username": "用户名",
|
||||
"Password": "密码",
|
||||
"Unread": "未读",
|
||||
"History": "历史",
|
||||
"Feeds": "源",
|
||||
"Categories": "分类",
|
||||
"Settings": "设置",
|
||||
"Logout": "登出",
|
||||
"Next": "下一页",
|
||||
"Previous": "上一页",
|
||||
"New Subscription": "新订阅",
|
||||
"Import": "导入",
|
||||
"Export": "导出",
|
||||
"There is no category. You must have at least one category.": "目前没有分类.需要有一个已有的分类存在.",
|
||||
"URL": "URL",
|
||||
"Category": "分类",
|
||||
"Find a subscription": "寻找订阅",
|
||||
"Loading...": "载入中...",
|
||||
"Create a category": "新建分类",
|
||||
"There is no category.": "目前没有分类.",
|
||||
"Edit": "编辑",
|
||||
"Remove": "删除",
|
||||
"No feed.": "没有源.",
|
||||
"There is no article in this category.": "该分类下没有文章.",
|
||||
"Original": "原始链接",
|
||||
"Mark this page as read": "标记为已读",
|
||||
"not yet": "尚未",
|
||||
"just now": "刚刚",
|
||||
"1 minute ago": "1 分钟前",
|
||||
"%d minutes ago": "%d 分钟前",
|
||||
"1 hour ago": "1 小时前",
|
||||
"%d hours ago": "%d 小时前",
|
||||
"yesterday": "昨天",
|
||||
"%d days ago": "%d 天前",
|
||||
"%d weeks ago": "%d 周前",
|
||||
"%d months ago": "%d 月前",
|
||||
"%d years ago": "%d 年前",
|
||||
"Date": "日期",
|
||||
"IP Address": "IP地址",
|
||||
"User Agent": "User Agent",
|
||||
"Actions": "操作",
|
||||
"Current session": "当前会话",
|
||||
"Sessions": "会话",
|
||||
"Users": "用户",
|
||||
"Add user": "新建用户",
|
||||
"Choose a Subscription": "选择一个订阅",
|
||||
"Subscribe": "订阅",
|
||||
"New Category": "新分类",
|
||||
"Title": "标题",
|
||||
"Save": "保存",
|
||||
"or": "或",
|
||||
"cancel": "取消",
|
||||
"New User": "新用户",
|
||||
"Confirmation": "确认",
|
||||
"Administrator": "管理员",
|
||||
"Edit Category: %s": "编辑分类 : %s",
|
||||
"Update": "更新",
|
||||
"Edit Feed: %s": "编辑源 : %s",
|
||||
"There is no category!": "没有分类!",
|
||||
"Edit user: %s": "编辑用户: %s",
|
||||
"There is no article for this feed.": "这个源中没有文章.",
|
||||
"Add subscription": "新增订阅",
|
||||
"You don't have any subscription.": "当前没有订阅",
|
||||
"Last check:": "最后检查时间:",
|
||||
"Refresh": "更新",
|
||||
"There is no history at the moment.": "当前没有历史.",
|
||||
"OPML file": "OPML 文件",
|
||||
"Sign In": "登陆",
|
||||
"Sign in": "登陆",
|
||||
"Theme": "主题",
|
||||
"Timezone": "时区",
|
||||
"Language": "语言",
|
||||
"There is no unread article.": "目前没有未读文章.",
|
||||
"You are the only user.": "你是目前仅有的用户.",
|
||||
"Last Login": "最后登录时间",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"This feed already exists (%s)": "源已存在 (%s)",
|
||||
"Unable to fetch feed (statusCode=%d)": "无法获取源 (错误代码=%d)",
|
||||
"Unable to open this link: %v": "无法打开这一链接: %v",
|
||||
"Unable to analyze this page: %v": "无法分析这一页面: %v",
|
||||
"Unable to find any subscription.": "找不到任何订阅.",
|
||||
"The URL and the category are mandatory.": "必须填写URL和分类.",
|
||||
"All fields are mandatory.": "必须填写全部信息.",
|
||||
"Passwords are not the same.": "两次输入的密码不同.",
|
||||
"You must use at least 6 characters.": "请至少使用6个字符.",
|
||||
"The username is mandatory.": "必须填写用户名.",
|
||||
"The username, theme, language and timezone fields are mandatory.": "必须填写用户名,主题,语言和时区.",
|
||||
"The title is mandatory.": "必须填写标题.",
|
||||
"About": "关于",
|
||||
"version": "版本",
|
||||
"Version:": "版本:",
|
||||
"Build Date:": "构建日期:",
|
||||
"Author:": "作者:",
|
||||
"Authors": "作者",
|
||||
"License:": "协议:",
|
||||
"Attachments": "附件",
|
||||
"Download": "下载",
|
||||
"Invalid username or password.": "用户名或密码无效.",
|
||||
"Never": "永不",
|
||||
"Unable to execute request: %v": "无法执行这一请求: %v",
|
||||
"Last Parsing Error": "最后一次解析错误",
|
||||
"There is a problem with this feed": "这一源存在问题",
|
||||
"Unable to parse OPML file: %q": "无法解析OPML文件: %q",
|
||||
"Unable to parse RSS feed: %q": "无法解析RSS源: %q",
|
||||
"Unable to parse Atom feed: %q": "无法解析Atom源: %q",
|
||||
"Unable to parse JSON feed: %q": "无法解析JSON源: %q",
|
||||
"Unable to parse RDF feed: %q": "无法解析RDF源: %q",
|
||||
"Unable to normalize encoding: %q": "无法正则化编码: %q",
|
||||
"Unable to create this category.": "无法建立这个分类.",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"Are you sure?": "您确认吗?",
|
||||
"Work in progress...": "执行中...",
|
||||
"This user already exists.": "用户已存在.",
|
||||
"This category already exists.": "分类已存在.",
|
||||
"Unable to update this category.": "无法更新该分类.",
|
||||
"Integrations": "集成",
|
||||
"Bookmarklet": "书签小应用",
|
||||
"Drag and drop this link to your bookmarks.": "拖动这个链接到书签.",
|
||||
"This special link allows you to subscribe to a website directly by using a bookmark in your web browser.": "你可以打开这个特殊的书签来直接订阅网站.",
|
||||
"Add to Miniflux": "新增到Miniflux",
|
||||
"Refresh all feeds in background": "在后台更新全部源",
|
||||
"Sign in with Google": "使用Google登陆",
|
||||
"Unlink my Google account": "去除Google账号关联",
|
||||
"Link my Google account": "关联我的Google账户",
|
||||
"Category not found for this user": "未找到该用户的这一分类",
|
||||
"Invalid theme.": "无效的主题.",
|
||||
"Entry Sorting": "内容排序",
|
||||
"Older entries first": "旧->新",
|
||||
"Recent entries first": "新->旧",
|
||||
"Saving...": "保存中",
|
||||
"Done!": "完成!",
|
||||
"Save this article": "保存这篇文章",
|
||||
"Mark bookmark as unread": "标记为未读",
|
||||
"Pinboard Tags": "Pinboard 标签",
|
||||
"Pinboard API Token": "Pinboard API Token",
|
||||
"Save articles to Pinboard": "保存文章到Pinboard",
|
||||
"Save articles to Instapaper": "保存文章到Instapaper",
|
||||
"Instapaper Username": "Instapaper 用户名",
|
||||
"Instapaper Password": "Instapaper 密码",
|
||||
"Activate Fever API": "启用 Fever API",
|
||||
"Fever Username": "Fever 用户名",
|
||||
"Fever Password": "Fever 密码",
|
||||
"Fetch original content": "抓取原内容",
|
||||
"Scraper Rules": "Scraper规则",
|
||||
"Rewrite Rules": "重写规则",
|
||||
"Preferences saved!": "偏好已存储!",
|
||||
"Your external account is now linked !": "您的外部账号已关联!",
|
||||
"Save articles to Wallabag": "保存文章到Wallabag",
|
||||
"Wallabag API Endpoint": "Wallabag URL",
|
||||
"Wallabag Client ID": "Wallabag 客户端ID",
|
||||
"Wallabag Client Secret": "Wallabag 客户端Secret",
|
||||
"Wallabag Username": "Wallabag 用户名",
|
||||
"Wallabag Password": "Wallabag 密码",
|
||||
"Save articles to Nunux Keeper": "保存文章到Nunux Keeper",
|
||||
"Nunux Keeper API Endpoint": "Nunux Keeper API Endpoint",
|
||||
"Nunux Keeper API key": "Nunux Keeper API key",
|
||||
"Keyboard Shortcut: %s": "快捷键: %s",
|
||||
"Favorites": "收藏",
|
||||
"Star": "标记星标",
|
||||
"Unstar": "去掉星标",
|
||||
"Starred": "星标",
|
||||
"There is no bookmark at the moment.": "当前没有书签.",
|
||||
"Last checked:": "上次检查:",
|
||||
"ETag header:": "ETag header:",
|
||||
"LastModified header:": "最后修改的Header:",
|
||||
"None": "无",
|
||||
"Keyboard Shortcuts": "快捷键",
|
||||
"Sections Navigation": "分区导航",
|
||||
"Go to unread": "去往未读",
|
||||
"Go to bookmarks": "去往书签",
|
||||
"Go to history": "去往历史",
|
||||
"Go to feeds": "去往源",
|
||||
"Go to categories": "去往分类",
|
||||
"Go to settings": "去往设定",
|
||||
"Show keyboard shortcuts": "显示快捷键",
|
||||
"Items Navigation": "条目导航",
|
||||
"Go to previous item": "上一条目",
|
||||
"Go to next item": "下一条目",
|
||||
"Pages Navigation": "页面导航",
|
||||
"Go to previous page": "上一页",
|
||||
"Go to next page": "下一页",
|
||||
"Open selected item": "打开选定的条目",
|
||||
"Open original link": "打开原始链接",
|
||||
"Toggle read/unread": "切换已读/未读状态",
|
||||
"Mark current page as read": "标记当前",
|
||||
"Download original content": "下载原始内容",
|
||||
"Toggle bookmark": "切换收藏状态",
|
||||
"Close modal dialog": "关闭模态对话窗口",
|
||||
"Save article": "保存文章",
|
||||
"There is already someone associated with this provider!": "该Provider已被关联!",
|
||||
"There is already someone else with the same Fever username!": "Fever用户名已被占用!",
|
||||
"Mark all as read": "全标记为已读",
|
||||
"This feed is empty": "该源是空的",
|
||||
"Flush history": "清理历史",
|
||||
"Site URL": "站点URL",
|
||||
"Feed URL": "源URL",
|
||||
"Logged as %s": "当前登录 %s",
|
||||
"Unread Items": "未读条目",
|
||||
"Change entry status": "更改状态",
|
||||
"Read": "标为已读",
|
||||
"Fever API endpoint:": "Fever API Endpoint:",
|
||||
"Miniflux API": "Miniflux API",
|
||||
"API Endpoint": "API Endpoint",
|
||||
"Your account password": "您账户的密码",
|
||||
"This web page is empty": "该网页是空的",
|
||||
"Invalid SSL certificate (original error: %q)": "无效的SSL证书 (原始错误: %q)",
|
||||
"This website is temporarily unreachable (original error: %q)": "该网站暂时不可达 (原始错误: %q)",
|
||||
"This website is permanently unreachable (original error: %q)": "该网站永久不可达 (原始错误: %q)",
|
||||
"Website unreachable, the request timed out after %d seconds": "网站不可达, 请求已在 %d 秒后超时"
|
||||
}
|
||||
@@ -24,4 +24,7 @@ type Integration struct {
|
||||
WallabagClientSecret string
|
||||
WallabagUsername string
|
||||
WallabagPassword string
|
||||
NunuxKeeperEnabled bool
|
||||
NunuxKeeperURL string
|
||||
NunuxKeeperAPIKey string
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/timezone"
|
||||
)
|
||||
|
||||
// User represents a user in the system.
|
||||
@@ -99,5 +101,19 @@ func (u *User) Merge(override *User) {
|
||||
}
|
||||
}
|
||||
|
||||
// UseTimezone converts last login date to the given timezone.
|
||||
func (u *User) UseTimezone(tz string) {
|
||||
if u.LastLoginAt != nil {
|
||||
*u.LastLoginAt = timezone.Convert(tz, *u.LastLoginAt)
|
||||
}
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []*User
|
||||
|
||||
// UseTimezone converts last login timestamp of all users to the given timezone.
|
||||
func (u Users) UseTimezone(tz string) {
|
||||
for _, user := range u {
|
||||
user.UseTimezone(tz)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-4
@@ -4,8 +4,12 @@
|
||||
|
||||
package model
|
||||
|
||||
import "time"
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/timezone"
|
||||
)
|
||||
|
||||
// UserSession represents a user session in the system.
|
||||
type UserSession struct {
|
||||
@@ -17,9 +21,21 @@ type UserSession struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
func (s *UserSession) String() string {
|
||||
return fmt.Sprintf(`ID="%d", UserID="%d", IP="%s", Token="%s"`, s.ID, s.UserID, s.IP, s.Token)
|
||||
func (u *UserSession) String() string {
|
||||
return fmt.Sprintf(`ID="%d", UserID="%d", IP="%s", Token="%s"`, u.ID, u.UserID, u.IP, u.Token)
|
||||
}
|
||||
|
||||
// UseTimezone converts creation date to the given timezone.
|
||||
func (u *UserSession) UseTimezone(tz string) {
|
||||
u.CreatedAt = timezone.Convert(tz, u.CreatedAt)
|
||||
}
|
||||
|
||||
// UserSessions represents a list of sessions.
|
||||
type UserSessions []*UserSession
|
||||
|
||||
// UseTimezone converts creation date of all sessions to the given timezone.
|
||||
func (u UserSessions) UseTimezone(tz string) {
|
||||
for _, session := range u {
|
||||
session.UseTimezone(tz)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
)
|
||||
|
||||
// Parse returns a normalized feed struct from a Atom feed.
|
||||
func Parse(data io.Reader) (*model.Feed, error) {
|
||||
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
atomFeed := new(atomFeed)
|
||||
decoder := xml.NewDecoder(data)
|
||||
decoder.CharsetReader = encoding.CharsetReader
|
||||
|
||||
err := decoder.Decode(atomFeed)
|
||||
if err != nil {
|
||||
return nil, errors.NewLocalizedError("Unable to parse Atom feed: %v.", err)
|
||||
return nil, errors.NewLocalizedError("Unable to parse Atom feed: %q", err)
|
||||
}
|
||||
|
||||
return atomFeed.Transform(), nil
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
)
|
||||
|
||||
func TestParseAtomSample(t *testing.T) {
|
||||
@@ -430,8 +428,4 @@ func TestParseInvalidXml(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Parse should returns an error")
|
||||
}
|
||||
|
||||
if _, ok := err.(errors.LocalizedError); !ok {
|
||||
t.Error("The error returned must be a LocalizedError")
|
||||
}
|
||||
}
|
||||
|
||||
+26
-17
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
"github.com/miniflux/miniflux/http"
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/reader/icon"
|
||||
@@ -23,14 +24,15 @@ var (
|
||||
errServerFailure = "Unable to fetch feed (statusCode=%d)"
|
||||
errDuplicate = "This feed already exists (%s)"
|
||||
errNotFound = "Feed %d not found"
|
||||
errEncoding = "Unable to normalize encoding: %v"
|
||||
errEncoding = "Unable to normalize encoding: %q"
|
||||
errCategoryNotFound = "Category not found for this user"
|
||||
errEmptyFeed = "This feed is empty"
|
||||
)
|
||||
|
||||
// Handler contains all the logic to create and refresh feeds.
|
||||
type Handler struct {
|
||||
store *storage.Storage
|
||||
store *storage.Storage
|
||||
translator *locale.Translator
|
||||
}
|
||||
|
||||
// CreateFeed fetch, parse and store a new feed.
|
||||
@@ -44,7 +46,7 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
|
||||
client := http.NewClient(url)
|
||||
response, err := client.Get()
|
||||
if err != nil {
|
||||
if _, ok := err.(errors.LocalizedError); ok {
|
||||
if _, ok := err.(*errors.LocalizedError); ok {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.NewLocalizedError(errRequestFailed, err)
|
||||
@@ -68,9 +70,9 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
|
||||
return nil, errors.NewLocalizedError(errEncoding, err)
|
||||
}
|
||||
|
||||
subscription, err := parseFeed(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
subscription, feedErr := parseFeed(body)
|
||||
if feedErr != nil {
|
||||
return nil, feedErr
|
||||
}
|
||||
|
||||
feedProcessor := processor.NewFeedProcessor(userID, h.store, subscription)
|
||||
@@ -110,6 +112,13 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
|
||||
// RefreshFeed fetch and update a feed if necessary.
|
||||
func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Handler:RefreshFeed] feedID=%d", feedID))
|
||||
userLanguage, err := h.store.UserLanguage(userID)
|
||||
if err != nil {
|
||||
logger.Error("[Handler:RefreshFeed] %v", err)
|
||||
userLanguage = "en_US"
|
||||
}
|
||||
|
||||
currentLanguage := h.translator.GetLanguage(userLanguage)
|
||||
|
||||
originalFeed, err := h.store.FeedByID(userID, feedID)
|
||||
if err != nil {
|
||||
@@ -124,14 +133,14 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
response, err := client.Get()
|
||||
if err != nil {
|
||||
var customErr errors.LocalizedError
|
||||
if lerr, ok := err.(errors.LocalizedError); ok {
|
||||
customErr = lerr
|
||||
if lerr, ok := err.(*errors.LocalizedError); ok {
|
||||
customErr = *lerr
|
||||
} else {
|
||||
customErr = errors.NewLocalizedError(errRequestFailed, err)
|
||||
customErr = *errors.NewLocalizedError(errRequestFailed, err)
|
||||
}
|
||||
|
||||
originalFeed.ParsingErrorCount++
|
||||
originalFeed.ParsingErrorMsg = customErr.Error()
|
||||
originalFeed.ParsingErrorMsg = customErr.Localize(currentLanguage)
|
||||
h.store.UpdateFeed(originalFeed)
|
||||
return customErr
|
||||
}
|
||||
@@ -141,7 +150,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
if response.HasServerFailure() {
|
||||
err := errors.NewLocalizedError(errServerFailure, response.StatusCode)
|
||||
originalFeed.ParsingErrorCount++
|
||||
originalFeed.ParsingErrorMsg = err.Error()
|
||||
originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
|
||||
h.store.UpdateFeed(originalFeed)
|
||||
return err
|
||||
}
|
||||
@@ -153,7 +162,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
if response.ContentLength == 0 {
|
||||
err := errors.NewLocalizedError(errEmptyFeed)
|
||||
originalFeed.ParsingErrorCount++
|
||||
originalFeed.ParsingErrorMsg = err.Error()
|
||||
originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
|
||||
h.store.UpdateFeed(originalFeed)
|
||||
return err
|
||||
}
|
||||
@@ -163,10 +172,10 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
return errors.NewLocalizedError(errEncoding, err)
|
||||
}
|
||||
|
||||
subscription, err := parseFeed(body)
|
||||
if err != nil {
|
||||
subscription, parseErr := parseFeed(body)
|
||||
if parseErr != nil {
|
||||
originalFeed.ParsingErrorCount++
|
||||
originalFeed.ParsingErrorMsg = err.Error()
|
||||
originalFeed.ParsingErrorMsg = parseErr.Localize(currentLanguage)
|
||||
h.store.UpdateFeed(originalFeed)
|
||||
return err
|
||||
}
|
||||
@@ -209,6 +218,6 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
|
||||
}
|
||||
|
||||
// NewFeedHandler returns a feed handler.
|
||||
func NewFeedHandler(store *storage.Storage) *Handler {
|
||||
return &Handler{store: store}
|
||||
func NewFeedHandler(store *storage.Storage, translator *locale.Translator) *Handler {
|
||||
return &Handler{store, translator}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ package feed
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
"github.com/miniflux/miniflux/logger"
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/reader/atom"
|
||||
@@ -66,13 +66,13 @@ func DetectFeedFormat(r io.Reader) string {
|
||||
return FormatUnknown
|
||||
}
|
||||
|
||||
func parseFeed(r io.Reader) (*model.Feed, error) {
|
||||
func parseFeed(r io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
defer timer.ExecutionTime(time.Now(), "[Feed:ParseFeed]")
|
||||
|
||||
var buffer bytes.Buffer
|
||||
size, _ := io.Copy(&buffer, r)
|
||||
if size == 0 {
|
||||
return nil, errors.New("This feed is empty")
|
||||
return nil, errors.NewLocalizedError("This feed is empty")
|
||||
}
|
||||
|
||||
str := stripInvalidXMLCharacters(buffer.String())
|
||||
@@ -90,7 +90,7 @@ func parseFeed(r io.Reader) (*model.Feed, error) {
|
||||
case FormatRDF:
|
||||
return rdf.Parse(reader)
|
||||
default:
|
||||
return nil, errors.New("Unsupported feed format")
|
||||
return nil, errors.NewLocalizedError("Unsupported feed format")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
)
|
||||
|
||||
// Parse returns a normalized feed struct from a JON feed.
|
||||
func Parse(data io.Reader) (*model.Feed, error) {
|
||||
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
feed := new(jsonFeed)
|
||||
decoder := json.NewDecoder(data)
|
||||
if err := decoder.Decode(&feed); err != nil {
|
||||
return nil, errors.NewLocalizedError("Unable to parse JSON Feed: %v", err)
|
||||
return nil, errors.NewLocalizedError("Unable to parse JSON Feed: %q", err)
|
||||
}
|
||||
|
||||
return feed.Transform(), nil
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
)
|
||||
|
||||
func TestParseJsonFeed(t *testing.T) {
|
||||
@@ -377,8 +375,4 @@ func TestParseInvalidJSON(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Parse should returns an error")
|
||||
}
|
||||
|
||||
if _, ok := err.(errors.LocalizedError); !ok {
|
||||
t.Error("The error returned must be a LocalizedError")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ import (
|
||||
)
|
||||
|
||||
// Parse reads an OPML file and returns a SubcriptionList.
|
||||
func Parse(data io.Reader) (SubcriptionList, error) {
|
||||
func Parse(data io.Reader) (SubcriptionList, *errors.LocalizedError) {
|
||||
feeds := new(opml)
|
||||
decoder := xml.NewDecoder(data)
|
||||
decoder.CharsetReader = encoding.CharsetReader
|
||||
|
||||
err := decoder.Decode(feeds)
|
||||
if err != nil {
|
||||
return nil, errors.NewLocalizedError("Unable to parse OPML file: %v.", err)
|
||||
return nil, errors.NewLocalizedError("Unable to parse OPML file: %q", err)
|
||||
}
|
||||
|
||||
return feeds.Transform(), nil
|
||||
|
||||
@@ -7,8 +7,6 @@ package opml
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
)
|
||||
|
||||
func TestParseOpmlWithoutCategories(t *testing.T) {
|
||||
@@ -130,8 +128,4 @@ func TestParseInvalidXML(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Parse should generate an error")
|
||||
}
|
||||
|
||||
if _, ok := err.(errors.LocalizedError); !ok {
|
||||
t.Error("The error returned must be a LocalizedError")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
)
|
||||
|
||||
// Parse returns a normalized feed struct from a RDF feed.
|
||||
func Parse(data io.Reader) (*model.Feed, error) {
|
||||
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
feed := new(rdfFeed)
|
||||
decoder := xml.NewDecoder(data)
|
||||
decoder.CharsetReader = encoding.CharsetReader
|
||||
|
||||
err := decoder.Decode(feed)
|
||||
if err != nil {
|
||||
return nil, errors.NewLocalizedError("Unable to parse RDF feed: %v.", err)
|
||||
return nil, errors.NewLocalizedError("Unable to parse RDF feed: %q", err)
|
||||
}
|
||||
|
||||
return feed.Transform(), nil
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
)
|
||||
|
||||
func TestParseRDFSample(t *testing.T) {
|
||||
@@ -330,8 +328,4 @@ func TestParseInvalidXml(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Parse should returns an error")
|
||||
}
|
||||
|
||||
if _, ok := err.(errors.LocalizedError); !ok {
|
||||
t.Error("The error returned must be a LocalizedError")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
)
|
||||
|
||||
// Parse returns a normalized feed struct from a RSS feed.
|
||||
func Parse(data io.Reader) (*model.Feed, error) {
|
||||
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
|
||||
feed := new(rssFeed)
|
||||
decoder := xml.NewDecoder(data)
|
||||
decoder.CharsetReader = encoding.CharsetReader
|
||||
|
||||
err := decoder.Decode(feed)
|
||||
if err != nil {
|
||||
return nil, errors.NewLocalizedError("Unable to parse RSS feed: %v.", err)
|
||||
return nil, errors.NewLocalizedError("Unable to parse RSS feed: %q", err)
|
||||
}
|
||||
|
||||
return feed.Transform(), nil
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/errors"
|
||||
)
|
||||
|
||||
func TestParseRss2Sample(t *testing.T) {
|
||||
@@ -564,8 +562,4 @@ func TestParseInvalidXml(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Error("Parse should returns an error")
|
||||
}
|
||||
|
||||
if _, ok := err.(errors.LocalizedError); !ok {
|
||||
t.Error("The error returned must be a LocalizedError")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func NewFeedScheduler(store *storage.Storage, workerPool *WorkerPool, frequency, batchSize int) {
|
||||
go func() {
|
||||
c := time.Tick(time.Duration(frequency) * time.Minute)
|
||||
for _ = range c {
|
||||
for range c {
|
||||
jobs, err := store.NewBatch(batchSize)
|
||||
if err != nil {
|
||||
logger.Error("[FeedScheduler] %v", err)
|
||||
@@ -31,7 +31,7 @@ func NewFeedScheduler(store *storage.Storage, workerPool *WorkerPool, frequency,
|
||||
func NewSessionScheduler(store *storage.Storage, frequency int) {
|
||||
go func() {
|
||||
c := time.Tick(time.Duration(frequency) * time.Hour)
|
||||
for _ = range c {
|
||||
for range c {
|
||||
nbSessions := store.CleanOldSessions()
|
||||
nbUserSessions := store.CleanOldUserSessions()
|
||||
logger.Info("[SessionScheduler] cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
alter table integrations add column nunux_keeper_enabled bool default 'f';
|
||||
alter table integrations add column nunux_keeper_url text default '';
|
||||
alter table integrations add column nunux_keeper_api_key text default '';
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-02-10 22:26:57.088648586 -0800 PST m=+0.004853992
|
||||
// 2018-02-24 17:47:34.98646993 +0000 GMT
|
||||
|
||||
package sql
|
||||
|
||||
@@ -126,6 +126,9 @@ alter table integrations add column wallabag_password text default '';`,
|
||||
"schema_version_13": `create index entries_user_status_idx on entries(user_id, status);
|
||||
create index feeds_user_category_idx on feeds(user_id, category_id);
|
||||
`,
|
||||
"schema_version_14": `alter table integrations add column nunux_keeper_enabled bool default 'f';
|
||||
alter table integrations add column nunux_keeper_url text default '';
|
||||
alter table integrations add column nunux_keeper_api_key text default '';`,
|
||||
"schema_version_2": `create extension if not exists hstore;
|
||||
alter table users add column extra hstore;
|
||||
create index users_extra_idx on users using gin(extra);
|
||||
@@ -170,6 +173,7 @@ var SqlMapChecksums = map[string]string{
|
||||
"schema_version_11": "dc5bbc302e01e425b49c48ddcd8e29e3ab2bb8e73a6cd1858a6ba9fbec0b5243",
|
||||
"schema_version_12": "a95abab6cdf64811fc744abd37457e2928939d999c5ef00d2bdd9398e16f32fb",
|
||||
"schema_version_13": "9073fae1e796936f4a43a8120ebdb4218442fe7d346ace6387556a357c2d7edf",
|
||||
"schema_version_14": "4622e42c4a5a88b6fe1e61f3d367b295968f7260ab5b96481760775ba9f9e1fe",
|
||||
"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
|
||||
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
|
||||
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
"github.com/miniflux/miniflux/timezone"
|
||||
)
|
||||
|
||||
// EntryQueryBuilder builds a SQL query to fetch entries.
|
||||
@@ -160,7 +161,8 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||
e.url, e.author, e.content, e.status, e.starred,
|
||||
f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
|
||||
f.category_id, c.title as category_title, f.scraper_rules, f.rewrite_rules, f.crawler,
|
||||
fi.icon_id
|
||||
fi.icon_id,
|
||||
u.timezone
|
||||
FROM entries e
|
||||
LEFT JOIN feeds f ON f.id=e.feed_id
|
||||
LEFT JOIN categories c ON c.id=f.category_id
|
||||
@@ -183,6 +185,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||
for rows.Next() {
|
||||
var entry model.Entry
|
||||
var iconID interface{}
|
||||
var tz string
|
||||
|
||||
entry.Feed = &model.Feed{UserID: e.userID}
|
||||
entry.Feed.Category = &model.Category{UserID: e.userID}
|
||||
@@ -210,6 +213,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||
&entry.Feed.RewriteRules,
|
||||
&entry.Feed.Crawler,
|
||||
&iconID,
|
||||
&tz,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -222,6 +226,10 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
|
||||
entry.Feed.Icon.IconID = iconID.(int64)
|
||||
}
|
||||
|
||||
// Make sure that timestamp fields contains timezone information (API)
|
||||
entry.Date = timezone.Convert(tz, entry.Date)
|
||||
entry.Feed.CheckedAt = timezone.Convert(tz, entry.Feed.CheckedAt)
|
||||
|
||||
entry.Feed.ID = entry.FeedID
|
||||
entry.Feed.Icon.FeedID = entry.FeedID
|
||||
entries = append(entries, &entry)
|
||||
|
||||
+11
-2
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
"github.com/miniflux/miniflux/timezone"
|
||||
)
|
||||
|
||||
// FeedExists checks if the given feed exists.
|
||||
@@ -56,7 +57,8 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
|
||||
f.parsing_error_count, f.parsing_error_msg,
|
||||
f.scraper_rules, f.rewrite_rules, f.crawler,
|
||||
f.category_id, c.title as category_title,
|
||||
fi.icon_id
|
||||
fi.icon_id,
|
||||
u.timezone
|
||||
FROM feeds f
|
||||
LEFT JOIN categories c ON c.id=f.category_id
|
||||
LEFT JOIN feed_icons fi ON fi.feed_id=f.id
|
||||
@@ -73,6 +75,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
|
||||
for rows.Next() {
|
||||
var feed model.Feed
|
||||
var iconID interface{}
|
||||
var tz string
|
||||
feed.Category = &model.Category{UserID: userID}
|
||||
|
||||
err := rows.Scan(
|
||||
@@ -92,6 +95,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
|
||||
&feed.Category.ID,
|
||||
&feed.Category.Title,
|
||||
&iconID,
|
||||
&tz,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -102,6 +106,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
|
||||
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
|
||||
}
|
||||
|
||||
feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
|
||||
feeds = append(feeds, &feed)
|
||||
}
|
||||
|
||||
@@ -114,6 +119,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
|
||||
|
||||
var feed model.Feed
|
||||
var iconID interface{}
|
||||
var tz string
|
||||
feed.Category = &model.Category{UserID: userID}
|
||||
|
||||
query := `
|
||||
@@ -123,7 +129,8 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
|
||||
f.parsing_error_count, f.parsing_error_msg,
|
||||
f.scraper_rules, f.rewrite_rules, f.crawler,
|
||||
f.category_id, c.title as category_title,
|
||||
fi.icon_id
|
||||
fi.icon_id,
|
||||
u.timezone
|
||||
FROM feeds f
|
||||
LEFT JOIN categories c ON c.id=f.category_id
|
||||
LEFT JOIN feed_icons fi ON fi.feed_id=f.id
|
||||
@@ -147,6 +154,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
|
||||
&feed.Category.ID,
|
||||
&feed.Category.Title,
|
||||
&iconID,
|
||||
&tz,
|
||||
)
|
||||
|
||||
switch {
|
||||
@@ -160,6 +168,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
|
||||
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
|
||||
}
|
||||
|
||||
feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
|
||||
return &feed, nil
|
||||
}
|
||||
|
||||
|
||||
+15
-3
@@ -67,7 +67,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
wallabag_client_id,
|
||||
wallabag_client_secret,
|
||||
wallabag_username,
|
||||
wallabag_password
|
||||
wallabag_password,
|
||||
nunux_keeper_enabled,
|
||||
nunux_keeper_url,
|
||||
nunux_keeper_api_key
|
||||
FROM integrations
|
||||
WHERE user_id=$1
|
||||
`
|
||||
@@ -91,6 +94,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
|
||||
&integration.WallabagClientSecret,
|
||||
&integration.WallabagUsername,
|
||||
&integration.WallabagPassword,
|
||||
&integration.NunuxKeeperEnabled,
|
||||
&integration.NunuxKeeperURL,
|
||||
&integration.NunuxKeeperAPIKey,
|
||||
)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
@@ -122,8 +128,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
||||
wallabag_client_id=$14,
|
||||
wallabag_client_secret=$15,
|
||||
wallabag_username=$16,
|
||||
wallabag_password=$17
|
||||
WHERE user_id=$18
|
||||
wallabag_password=$17,
|
||||
nunux_keeper_enabled=$18,
|
||||
nunux_keeper_url=$19,
|
||||
nunux_keeper_api_key=$20
|
||||
WHERE user_id=$21
|
||||
`
|
||||
_, err := s.db.Exec(
|
||||
query,
|
||||
@@ -144,6 +153,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
|
||||
integration.WallabagClientSecret,
|
||||
integration.WallabagUsername,
|
||||
integration.WallabagPassword,
|
||||
integration.NunuxKeeperEnabled,
|
||||
integration.NunuxKeeperURL,
|
||||
integration.NunuxKeeperAPIKey,
|
||||
integration.UserID,
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/miniflux/miniflux/sql"
|
||||
)
|
||||
|
||||
const schemaVersion = 13
|
||||
const schemaVersion = 14
|
||||
|
||||
// Migrate run database migrations.
|
||||
func (s *Storage) Migrate() {
|
||||
|
||||
+5
-4
@@ -6,6 +6,7 @@ package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
@@ -14,10 +15,8 @@ import (
|
||||
// Timezones returns all timezones supported by the database.
|
||||
func (s *Storage) Timezones() (map[string]string, error) {
|
||||
defer timer.ExecutionTime(time.Now(), "[Storage:Timezones]")
|
||||
|
||||
timezones := make(map[string]string)
|
||||
query := `select name from pg_timezone_names() order by name asc`
|
||||
rows, err := s.db.Query(query)
|
||||
rows, err := s.db.Query(`SELECT name FROM pg_timezone_names() ORDER BY name ASC`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch timezones: %v", err)
|
||||
}
|
||||
@@ -29,7 +28,9 @@ func (s *Storage) Timezones() (map[string]string, error) {
|
||||
return nil, fmt.Errorf("unable to fetch timezones row: %v", err)
|
||||
}
|
||||
|
||||
timezones[timezone] = timezone
|
||||
if !strings.HasPrefix(timezone, "posix") && !strings.HasPrefix(timezone, "SystemV") && timezone != "localtime" {
|
||||
timezones[timezone] = timezone
|
||||
}
|
||||
}
|
||||
|
||||
return timezones, nil
|
||||
|
||||
+13
-2
@@ -11,11 +11,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq/hstore"
|
||||
|
||||
"github.com/miniflux/miniflux/model"
|
||||
"github.com/miniflux/miniflux/timer"
|
||||
|
||||
"github.com/lib/pq/hstore"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -175,6 +174,18 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserLanguage returns the language of the given user.
|
||||
func (s *Storage) UserLanguage(userID int64) (language string, err error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserLanguage] userID=%d", userID))
|
||||
err = s.db.QueryRow(`SELECT language FROM users WHERE id = $1`, userID).Scan(&language)
|
||||
if err == sql.ErrNoRows {
|
||||
return "en_US", nil
|
||||
} else if err != nil {
|
||||
return "", fmt.Errorf("unable to fetch user language: %v", err)
|
||||
}
|
||||
return language, nil
|
||||
}
|
||||
|
||||
// UserByID finds a user by the ID.
|
||||
func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByID] userID=%d", userID))
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-02-04 14:28:15.225458631 -0800 PST m=+0.036040293
|
||||
// 2018-02-24 17:47:34.998457627 +0000 GMT
|
||||
|
||||
package template
|
||||
|
||||
|
||||
+4
-14
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miniflux/miniflux/locale"
|
||||
"github.com/miniflux/miniflux/timezone"
|
||||
)
|
||||
|
||||
// Texts to be translated if necessary.
|
||||
@@ -28,24 +29,13 @@ var (
|
||||
|
||||
// ElapsedTime returns in a human readable format the elapsed time
|
||||
// since the given datetime.
|
||||
func elapsedTime(language *locale.Language, timezone string, t time.Time) string {
|
||||
func elapsedTime(language *locale.Language, tz string, t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return language.Get(NotYet)
|
||||
}
|
||||
|
||||
var now time.Time
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
now = time.Now()
|
||||
} else {
|
||||
now = time.Now().In(loc)
|
||||
|
||||
// The provided date is already converted to the user timezone by Postgres,
|
||||
// but the timezone information is not set in the time struct.
|
||||
// We cannot use time.In() because the date will be converted a second time.
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc)
|
||||
}
|
||||
|
||||
now := timezone.Now(tz)
|
||||
t = timezone.Convert(tz, t)
|
||||
if now.Before(t) {
|
||||
return language.Get(NotYet)
|
||||
}
|
||||
|
||||
@@ -85,8 +85,9 @@ func (f *funcMap) Map() template.FuncMap {
|
||||
case string:
|
||||
return f.Language.Get(key.(string), args...)
|
||||
case errors.LocalizedError:
|
||||
err := key.(errors.LocalizedError)
|
||||
return err.Localize(f.Language)
|
||||
return key.(errors.LocalizedError).Localize(f.Language)
|
||||
case *errors.LocalizedError:
|
||||
return key.(*errors.LocalizedError).Localize(f.Language)
|
||||
case error:
|
||||
return key.(error).Error()
|
||||
default:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "Edit user %s" .selected_user.Username }}"</h1>
|
||||
<h1>{{ t "Edit user %s" .selected_user.Username }}</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ route "settings" }}">{{ t "Settings" }}</a>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
{{ 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>
|
||||
- <small class="parsing-error-message">{{ .ParsingErrorMsg }}</small>
|
||||
</div>
|
||||
{{ end }}
|
||||
</article>
|
||||
|
||||
@@ -94,6 +94,19 @@
|
||||
<label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
|
||||
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
|
||||
</div>
|
||||
|
||||
<h3>Nunux Keeper</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "Save articles to Nunux Keeper" }}
|
||||
</label>
|
||||
|
||||
<label for="form-nunux-keeper-url">{{ t "Nunux Keeper API Endpoint" }}</label>
|
||||
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper">
|
||||
|
||||
<label for="form-nunux-keeper-api-key">{{ t "Nunux Keeper API key" }}</label>
|
||||
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}">
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
|
||||
|
||||
+19
-6
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-02-04 14:28:15.215257434 -0800 PST m=+0.025839096
|
||||
// 2018-03-01 23:04:58.998374277 -0800 PST m=+0.022158179
|
||||
|
||||
package template
|
||||
|
||||
@@ -416,7 +416,7 @@ var templateViewsMap = map[string]string{
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "Edit user %s" .selected_user.Username }}"</h1>
|
||||
<h1>{{ t "Edit user %s" .selected_user.Username }}</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ route "settings" }}">{{ t "Settings" }}</a>
|
||||
@@ -690,7 +690,7 @@ var templateViewsMap = map[string]string{
|
||||
{{ 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>
|
||||
- <small class="parsing-error-message">{{ .ParsingErrorMsg }}</small>
|
||||
</div>
|
||||
{{ end }}
|
||||
</article>
|
||||
@@ -869,6 +869,19 @@ var templateViewsMap = map[string]string{
|
||||
<label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
|
||||
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
|
||||
</div>
|
||||
|
||||
<h3>Nunux Keeper</h3>
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "Save articles to Nunux Keeper" }}
|
||||
</label>
|
||||
|
||||
<label for="form-nunux-keeper-url">{{ t "Nunux Keeper API Endpoint" }}</label>
|
||||
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper">
|
||||
|
||||
<label for="form-nunux-keeper-api-key">{{ t "Nunux Keeper API key" }}</label>
|
||||
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}">
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
|
||||
@@ -1219,13 +1232,13 @@ var templateViewsMapChecksums = map[string]string{
|
||||
"create_user": "233764778c915754141a20429ec8db9bf80ef2d7704867a2d7232c1e9df233ae",
|
||||
"edit_category": "cee720faadcec58289b707ad30af623d2ee66c1ce23a732965463250d7ff41c5",
|
||||
"edit_feed": "d2c1c8486d7faf4ee58151ccf3e3c690e53bd6872050d291c5db8452a83c3d53",
|
||||
"edit_user": "5edd693460330750ba5ee03319d4e3cb5aabbd9a0e48b3b760799bca72c5ec4e",
|
||||
"edit_user": "321e0a60cf3bf7441bff970f4920e4c5b7c1883f80ab1d1674f8137954b25033",
|
||||
"entry": "27ea028515e79beb546f0b2792a3918c455fd877eea4c41d1a061f8e7b54a430",
|
||||
"feed_entries": "420da786e827a77fecc8794207d158af3a30e489ca2b2019f48d5228919af4a7",
|
||||
"feeds": "0c884b7a9dfc4541b988641516fd95df062a5bf05018d28276a3c0a10323cffd",
|
||||
"feeds": "2a5abe37968ea34a0576dbef52341645cb1fc9562e351382fbf721491da6f4fa",
|
||||
"history": "967bc95236269ab3a77455910aca1939f43f93171fe1af77eb3b1b4eac579e55",
|
||||
"import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
|
||||
"integrations": "958b73d632a3e2a79368bb1582efb8aabc438cef4fa6e8dc1aa4932494916aca",
|
||||
"integrations": "979193f39c2a3b43cec192aa119713cc9cbe2d5fdaedf8d2b3573c752823446c",
|
||||
"login": "7d83c3067c02f1f6aafdd8816c7f97a4eb5a5a4bdaaaa4cc1e2fbb9c17ea65e8",
|
||||
"sessions": "3fa79031dd883847eba92fbafe5f535fa3a4e1614bb610f20588b6f8fc8b3624",
|
||||
"settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2018 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 timezone
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Convert converts provided date time to actual timezone.
|
||||
func Convert(tz string, t time.Time) time.Time {
|
||||
userTimezone := getLocation(tz)
|
||||
|
||||
if t.Location().String() == "" {
|
||||
// In this case, the provided date is already converted to the user timezone by Postgres,
|
||||
// but the timezone information is not set in the time struct.
|
||||
// We cannot use time.In() because the date will be converted a second time.
|
||||
t = time.Date(
|
||||
t.Year(),
|
||||
t.Month(),
|
||||
t.Day(),
|
||||
t.Hour(),
|
||||
t.Minute(),
|
||||
t.Second(),
|
||||
t.Nanosecond(),
|
||||
userTimezone,
|
||||
)
|
||||
} else if t.Location() != userTimezone {
|
||||
t = t.In(userTimezone)
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Now returns the current time with the given timezone.
|
||||
func Now(tz string) time.Time {
|
||||
return time.Now().In(getLocation(tz))
|
||||
}
|
||||
|
||||
func getLocation(tz string) *time.Location {
|
||||
loc, err := time.LoadLocation(tz)
|
||||
if err != nil {
|
||||
loc = time.Local
|
||||
}
|
||||
return loc
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2018 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 timezone
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNow(t *testing.T) {
|
||||
tz := "Europe/Paris"
|
||||
now := Now(tz)
|
||||
|
||||
if now.Location().String() != tz {
|
||||
t.Fatalf(`Unexpected timezone, got %q instead of %q`, now.Location(), tz)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNowWithInvalidTimezone(t *testing.T) {
|
||||
tz := "Invalid Timezone"
|
||||
expected := time.Local
|
||||
now := Now(tz)
|
||||
|
||||
if now.Location().String() != expected.String() {
|
||||
t.Fatalf(`Unexpected timezone, got %q instead of %q`, now.Location(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTimeWithNoTimezoneInformation(t *testing.T) {
|
||||
tz := "Canada/Pacific"
|
||||
input := time.Date(2018, 3, 1, 14, 2, 3, 0, time.FixedZone("", 0))
|
||||
output := Convert(tz, input)
|
||||
|
||||
if output.Location().String() != tz {
|
||||
t.Fatalf(`Unexpected timezone, got %q instead of %s`, output.Location(), tz)
|
||||
}
|
||||
|
||||
hours, minutes, secs := output.Clock()
|
||||
if hours != 14 || minutes != 2 || secs != 3 {
|
||||
t.Fatalf(`Unexpected time, got hours=%d, minutes=%d, secs=%d`, hours, minutes, secs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTimeWithDifferentTimezone(t *testing.T) {
|
||||
tz := "Canada/Central"
|
||||
input := time.Date(2018, 3, 1, 14, 2, 3, 0, time.UTC)
|
||||
output := Convert(tz, input)
|
||||
|
||||
if output.Location().String() != tz {
|
||||
t.Fatalf(`Unexpected timezone, got %q instead of %s`, output.Location(), tz)
|
||||
}
|
||||
|
||||
hours, minutes, secs := output.Clock()
|
||||
if hours != 8 || minutes != 2 || secs != 3 {
|
||||
t.Fatalf(`Unexpected time, got hours=%d, minutes=%d, secs=%d`, hours, minutes, secs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTimeWithIdenticalTimezone(t *testing.T) {
|
||||
tz := "Canada/Central"
|
||||
loc, _ := time.LoadLocation(tz)
|
||||
input := time.Date(2018, 3, 1, 14, 2, 3, 0, loc)
|
||||
output := Convert(tz, input)
|
||||
|
||||
if output.Location().String() != tz {
|
||||
t.Fatalf(`Unexpected timezone, got %q instead of %s`, output.Location(), tz)
|
||||
}
|
||||
|
||||
hours, minutes, secs := output.Clock()
|
||||
if hours != 14 || minutes != 2 || secs != 3 {
|
||||
t.Fatalf(`Unexpected time, got hours=%d, minutes=%d, secs=%d`, hours, minutes, secs)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ type IntegrationForm struct {
|
||||
WallabagClientSecret string
|
||||
WallabagUsername string
|
||||
WallabagPassword string
|
||||
NunuxKeeperEnabled bool
|
||||
NunuxKeeperURL string
|
||||
NunuxKeeperAPIKey string
|
||||
}
|
||||
|
||||
// Merge copy form values to the model.
|
||||
@@ -48,6 +51,9 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
|
||||
integration.WallabagClientSecret = i.WallabagClientSecret
|
||||
integration.WallabagUsername = i.WallabagUsername
|
||||
integration.WallabagPassword = i.WallabagPassword
|
||||
integration.NunuxKeeperEnabled = i.NunuxKeeperEnabled
|
||||
integration.NunuxKeeperURL = i.NunuxKeeperURL
|
||||
integration.NunuxKeeperAPIKey = i.NunuxKeeperAPIKey
|
||||
}
|
||||
|
||||
// NewIntegrationForm returns a new AuthForm.
|
||||
@@ -69,5 +75,8 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
|
||||
WallabagClientSecret: r.FormValue("wallabag_client_secret"),
|
||||
WallabagUsername: r.FormValue("wallabag_username"),
|
||||
WallabagPassword: r.FormValue("wallabag_password"),
|
||||
NunuxKeeperEnabled: r.FormValue("nunux_keeper_enabled") == "1",
|
||||
NunuxKeeperURL: r.FormValue("nunux_keeper_url"),
|
||||
NunuxKeeperAPIKey: r.FormValue("nunux_keeper_api_key"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ func (c *Controller) ShowIntegrations(ctx *handler.Context, request *handler.Req
|
||||
WallabagClientSecret: integration.WallabagClientSecret,
|
||||
WallabagUsername: integration.WallabagUsername,
|
||||
WallabagPassword: integration.WallabagPassword,
|
||||
NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
|
||||
NunuxKeeperURL: integration.NunuxKeeperURL,
|
||||
NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func (c *Controller) ShowSessions(ctx *handler.Context, request *handler.Request
|
||||
return
|
||||
}
|
||||
|
||||
sessions.UseTimezone(user.Timezone)
|
||||
response.HTML().Render("sessions", args.Merge(tplParams{
|
||||
"sessions": sessions,
|
||||
"currentSessionToken": ctx.UserSessionToken(),
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-01-02 21:59:10.082800492 -0800 PST m=+0.010175821
|
||||
// 2018-02-24 17:47:34.994475549 +0000 GMT
|
||||
|
||||
package static
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-02-10 22:33:47.060422476 -0800 PST m=+0.024328540
|
||||
// 2018-02-24 17:47:34.995215527 +0000 GMT
|
||||
|
||||
package static
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// 2018-01-29 20:38:56.472493026 -0800 PST m=+0.027898185
|
||||
// 2018-02-24 17:47:34.995856638 +0000 GMT
|
||||
|
||||
package static
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ func (c *Controller) ShowUsers(ctx *handler.Context, request *handler.Request, r
|
||||
return
|
||||
}
|
||||
|
||||
users.UseTimezone(user.Timezone)
|
||||
response.HTML().Render("users", args.Merge(tplParams{
|
||||
"users": users,
|
||||
"menu": "settings",
|
||||
|
||||
Reference in New Issue
Block a user