Make reading speed user-configurable
This commit is contained in:
parent
3a0aaddafd
commit
6e50ce3293
31 changed files with 395 additions and 173 deletions
10
api/entry.go
10
api/entry.go
|
@ -193,6 +193,14 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := h.store.UserByID(entry.UserID)
|
||||||
|
if err != nil {
|
||||||
|
json.ServerError(w, r, err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
json.NotFound(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
feedBuilder := storage.NewFeedQueryBuilder(h.store, loggedUserID)
|
feedBuilder := storage.NewFeedQueryBuilder(h.store, loggedUserID)
|
||||||
feedBuilder.WithFeedID(entry.FeedID)
|
feedBuilder.WithFeedID(entry.FeedID)
|
||||||
feed, err := feedBuilder.GetFeed()
|
feed, err := feedBuilder.GetFeed()
|
||||||
|
@ -206,7 +214,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := processor.ProcessEntryWebPage(feed, entry); err != nil {
|
if err := processor.ProcessEntryWebPage(feed, entry, user); err != nil {
|
||||||
json.ServerError(w, r, err)
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,24 +18,26 @@ const (
|
||||||
|
|
||||||
// User represents a user in the system.
|
// User represents a user in the system.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
EntryDirection string `json:"entry_sorting_direction"`
|
EntryDirection string `json:"entry_sorting_direction"`
|
||||||
EntryOrder string `json:"entry_sorting_order"`
|
EntryOrder string `json:"entry_sorting_order"`
|
||||||
Stylesheet string `json:"stylesheet"`
|
Stylesheet string `json:"stylesheet"`
|
||||||
GoogleID string `json:"google_id"`
|
GoogleID string `json:"google_id"`
|
||||||
OpenIDConnectID string `json:"openid_connect_id"`
|
OpenIDConnectID string `json:"openid_connect_id"`
|
||||||
EntriesPerPage int `json:"entries_per_page"`
|
EntriesPerPage int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime bool `json:"show_reading_time"`
|
ShowReadingTime bool `json:"show_reading_time"`
|
||||||
EntrySwipe bool `json:"entry_swipe"`
|
EntrySwipe bool `json:"entry_swipe"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
DisplayMode string `json:"display_mode"`
|
DisplayMode string `json:"display_mode"`
|
||||||
|
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||||
|
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) String() string {
|
func (u User) String() string {
|
||||||
|
@ -53,22 +55,24 @@ type UserCreationRequest struct {
|
||||||
|
|
||||||
// UserModificationRequest represents the request to update a user.
|
// UserModificationRequest represents the request to update a user.
|
||||||
type UserModificationRequest struct {
|
type UserModificationRequest struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Language *string `json:"language"`
|
Language *string `json:"language"`
|
||||||
Timezone *string `json:"timezone"`
|
Timezone *string `json:"timezone"`
|
||||||
EntryDirection *string `json:"entry_sorting_direction"`
|
EntryDirection *string `json:"entry_sorting_direction"`
|
||||||
EntryOrder *string `json:"entry_sorting_order"`
|
EntryOrder *string `json:"entry_sorting_order"`
|
||||||
Stylesheet *string `json:"stylesheet"`
|
Stylesheet *string `json:"stylesheet"`
|
||||||
GoogleID *string `json:"google_id"`
|
GoogleID *string `json:"google_id"`
|
||||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||||
EntriesPerPage *int `json:"entries_per_page"`
|
EntriesPerPage *int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime *bool `json:"show_reading_time"`
|
ShowReadingTime *bool `json:"show_reading_time"`
|
||||||
EntrySwipe *bool `json:"entry_swipe"`
|
EntrySwipe *bool `json:"entry_swipe"`
|
||||||
DisplayMode *string `json:"display_mode"`
|
DisplayMode *string `json:"display_mode"`
|
||||||
|
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||||
|
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users represents a list of users.
|
// Users represents a list of users.
|
||||||
|
|
|
@ -597,4 +597,11 @@ var migrations = []func(tx *sql.Tx) error{
|
||||||
`)
|
`)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
|
func(tx *sql.Tx) (err error) {
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
ALTER TABLE users ADD COLUMN default_reading_speed int default 265;
|
||||||
|
ALTER TABLE users ADD COLUMN cjk_reading_speed int default 500;
|
||||||
|
`)
|
||||||
|
return
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "Passwörter stimmen nicht überein.",
|
"error.different_passwords": "Passwörter stimmen nicht überein.",
|
||||||
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
|
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
|
||||||
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
|
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Die Lesegeschwindigkeiten müssen positive ganze Zahlen sein.",
|
||||||
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
|
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
|
||||||
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
|
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
|
||||||
"error.feed_already_exists": "Dieser Feed existiert bereits.",
|
"error.feed_already_exists": "Dieser Feed existiert bereits.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Thema",
|
"form.prefs.label.theme": "Thema",
|
||||||
"form.prefs.label.entry_sorting": "Sortierung der Artikel",
|
"form.prefs.label.entry_sorting": "Sortierung der Artikel",
|
||||||
"form.prefs.label.entries_per_page": "Einträge pro Seite",
|
"form.prefs.label.entries_per_page": "Einträge pro Seite",
|
||||||
|
"form.prefs.label.default_reading_speed": "Lesegeschwindigkeit für andere Sprachen (Wörter pro Minute)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Lesegeschwindigkeit für Chinesisch, Koreanisch und Japanisch (Zeichen pro Minute)",
|
||||||
"form.prefs.label.display_mode": "Anzeigemodus der Web-App (muss neu installiert werden)",
|
"form.prefs.label.display_mode": "Anzeigemodus der Web-App (muss neu installiert werden)",
|
||||||
"form.prefs.select.older_first": "Älteste Artikel zuerst",
|
"form.prefs.select.older_first": "Älteste Artikel zuerst",
|
||||||
"form.prefs.select.recent_first": "Neueste Artikel zuerst",
|
"form.prefs.select.recent_first": "Neueste Artikel zuerst",
|
||||||
|
|
|
@ -248,6 +248,7 @@
|
||||||
"error.different_passwords": "Οι κωδικοί πρόσβασης δεν είναι οι ίδιοι.",
|
"error.different_passwords": "Οι κωδικοί πρόσβασης δεν είναι οι ίδιοι.",
|
||||||
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
|
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
|
||||||
"error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
|
"error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.",
|
||||||
"error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
|
"error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
|
||||||
"error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
|
"error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
|
||||||
"error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
|
"error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Θέμα",
|
"form.prefs.label.theme": "Θέμα",
|
||||||
"form.prefs.label.entry_sorting": "Ταξινόμηση",
|
"form.prefs.label.entry_sorting": "Ταξινόμηση",
|
||||||
"form.prefs.label.entries_per_page": "Καταχωρήσεις ανά σελίδα",
|
"form.prefs.label.entries_per_page": "Καταχωρήσεις ανά σελίδα",
|
||||||
|
"form.prefs.label.default_reading_speed": "Ταχύτητα ανάγνωσης άλλων γλωσσών (λέξεις ανά λεπτό)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Ταχύτητα ανάγνωσης για κινέζικα, κορεάτικα και ιαπωνικά (χαρακτήρες ανά λεπτό)",
|
||||||
"form.prefs.label.display_mode": "Λειτουργία προβολής εφαρμογών ιστού (χρειάζεται επανεγκατάσταση)",
|
"form.prefs.label.display_mode": "Λειτουργία προβολής εφαρμογών ιστού (χρειάζεται επανεγκατάσταση)",
|
||||||
"form.prefs.select.older_first": "Παλαιότερες καταχωρήσεις πρώτα",
|
"form.prefs.select.older_first": "Παλαιότερες καταχωρήσεις πρώτα",
|
||||||
"form.prefs.select.recent_first": "Πρόσφατες καταχωρήσεις πρώτα",
|
"form.prefs.select.recent_first": "Πρόσφατες καταχωρήσεις πρώτα",
|
||||||
|
|
|
@ -248,6 +248,7 @@
|
||||||
"error.different_passwords": "Passwords are not the same.",
|
"error.different_passwords": "Passwords are not the same.",
|
||||||
"error.password_min_length": "The password must have at least 6 characters.",
|
"error.password_min_length": "The password must have at least 6 characters.",
|
||||||
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
|
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
|
||||||
|
"error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
|
||||||
"error.entries_per_page_invalid": "The number of entries per page is not valid.",
|
"error.entries_per_page_invalid": "The number of entries per page is not valid.",
|
||||||
"error.feed_mandatory_fields": "The URL and the category are mandatory.",
|
"error.feed_mandatory_fields": "The URL and the category are mandatory.",
|
||||||
"error.feed_already_exists": "This feed already exists.",
|
"error.feed_already_exists": "This feed already exists.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Theme",
|
"form.prefs.label.theme": "Theme",
|
||||||
"form.prefs.label.entry_sorting": "Entry Sorting",
|
"form.prefs.label.entry_sorting": "Entry Sorting",
|
||||||
"form.prefs.label.entries_per_page": "Entries per page",
|
"form.prefs.label.entries_per_page": "Entries per page",
|
||||||
|
"form.prefs.label.default_reading_speed": "Reading speed for other languages (words per minute)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
|
||||||
"form.prefs.label.display_mode": "Web app display mode (needs reinstalling)",
|
"form.prefs.label.display_mode": "Web app display mode (needs reinstalling)",
|
||||||
"form.prefs.select.older_first": "Older entries first",
|
"form.prefs.select.older_first": "Older entries first",
|
||||||
"form.prefs.select.recent_first": "Recent entries first",
|
"form.prefs.select.recent_first": "Recent entries first",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "Las contraseñas no son las mismas.",
|
"error.different_passwords": "Las contraseñas no son las mismas.",
|
||||||
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
|
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
|
||||||
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
|
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Las velocidades de lectura deben ser números enteros positivos.",
|
||||||
"error.entries_per_page_invalid": "El número de entradas por página no es válido.",
|
"error.entries_per_page_invalid": "El número de entradas por página no es válido.",
|
||||||
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
|
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
|
||||||
"error.feed_already_exists": "Este feed ya existe.",
|
"error.feed_already_exists": "Este feed ya existe.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.entry_sorting": "Clasificación de entradas",
|
"form.prefs.label.entry_sorting": "Clasificación de entradas",
|
||||||
"form.prefs.label.entries_per_page": "Entradas por página",
|
"form.prefs.label.entries_per_page": "Entradas por página",
|
||||||
|
"form.prefs.label.default_reading_speed": "Velocidad de lectura de otras lenguas (palabras por minuto)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Velocidad de lectura en chino, coreano y japonés (caracteres por minuto)",
|
||||||
"form.prefs.label.display_mode": "Modo de visualización de la aplicación web (necesita reinstalación)",
|
"form.prefs.label.display_mode": "Modo de visualización de la aplicación web (necesita reinstalación)",
|
||||||
"form.prefs.select.older_first": "Entradas más viejas primero",
|
"form.prefs.select.older_first": "Entradas más viejas primero",
|
||||||
"form.prefs.select.recent_first": "Entradas recientes primero",
|
"form.prefs.select.recent_first": "Entradas recientes primero",
|
||||||
|
|
|
@ -248,6 +248,7 @@
|
||||||
"error.different_passwords": "Salasanat eivät ole samat.",
|
"error.different_passwords": "Salasanat eivät ole samat.",
|
||||||
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
||||||
"error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
|
"error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.",
|
||||||
"error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
|
"error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
|
||||||
"error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
|
"error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
|
||||||
"error.feed_already_exists": "Tämä syöte on jo olemassa.",
|
"error.feed_already_exists": "Tämä syöte on jo olemassa.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Teema",
|
"form.prefs.label.theme": "Teema",
|
||||||
"form.prefs.label.entry_sorting": "Lajittelu",
|
"form.prefs.label.entry_sorting": "Lajittelu",
|
||||||
"form.prefs.label.entries_per_page": "Artikkelia sivulla",
|
"form.prefs.label.entries_per_page": "Artikkelia sivulla",
|
||||||
|
"form.prefs.label.default_reading_speed": "Muiden kielten lukunopeus (sanaa minuutissa)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Kiinan, Korean ja Japanin lukunopeus (merkkejä minuutissa)",
|
||||||
"form.prefs.label.display_mode": "Verkkosovelluksen näyttötila (vaatii uudelleenasennuksen)",
|
"form.prefs.label.display_mode": "Verkkosovelluksen näyttötila (vaatii uudelleenasennuksen)",
|
||||||
"form.prefs.select.older_first": "Vanhin ensin",
|
"form.prefs.select.older_first": "Vanhin ensin",
|
||||||
"form.prefs.select.recent_first": "Uusin ensin",
|
"form.prefs.select.recent_first": "Uusin ensin",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "Les mots de passe ne sont pas les mêmes.",
|
"error.different_passwords": "Les mots de passe ne sont pas les mêmes.",
|
||||||
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
|
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
|
||||||
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
|
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Les vitesses de lecture doivent être des entiers positifs.",
|
||||||
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
|
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
|
||||||
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
|
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
|
||||||
"error.feed_already_exists": "Ce flux existe déjà.",
|
"error.feed_already_exists": "Ce flux existe déjà.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Thème",
|
"form.prefs.label.theme": "Thème",
|
||||||
"form.prefs.label.entry_sorting": "Ordre des éléments",
|
"form.prefs.label.entry_sorting": "Ordre des éléments",
|
||||||
"form.prefs.label.entries_per_page": "Entrées par page",
|
"form.prefs.label.entries_per_page": "Entrées par page",
|
||||||
|
"form.prefs.label.default_reading_speed": "Vitesse de lecture pour les autres langues (mots par minute)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Vitesse de lecture pour le Chinois, le Coréen et le Japonais (caractères par minute)",
|
||||||
"form.prefs.label.display_mode": "Mode d'affichage de l'application web (doit être réinstallé)",
|
"form.prefs.label.display_mode": "Mode d'affichage de l'application web (doit être réinstallé)",
|
||||||
"form.prefs.select.older_first": "Ancien éléments en premier",
|
"form.prefs.select.older_first": "Ancien éléments en premier",
|
||||||
"form.prefs.select.recent_first": "Éléments récents en premier",
|
"form.prefs.select.recent_first": "Éléments récents en premier",
|
||||||
|
|
|
@ -248,6 +248,7 @@
|
||||||
"error.different_passwords": "पासवर्ड एक जैसे नहीं हैं।",
|
"error.different_passwords": "पासवर्ड एक जैसे नहीं हैं।",
|
||||||
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
||||||
"error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
|
"error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
|
||||||
|
"error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।",
|
||||||
"error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
|
"error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
|
||||||
"error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
|
"error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
|
||||||
"error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
|
"error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "थीम",
|
"form.prefs.label.theme": "थीम",
|
||||||
"form.prefs.label.entry_sorting": "प्रवेश छँटाई",
|
"form.prefs.label.entry_sorting": "प्रवेश छँटाई",
|
||||||
"form.prefs.label.entries_per_page": "प्रति पृष्ठ प्रविष्टियाँ",
|
"form.prefs.label.entries_per_page": "प्रति पृष्ठ प्रविष्टियाँ",
|
||||||
|
"form.prefs.label.default_reading_speed": "अन्य भाषाओं के लिए पढ़ने की गति (प्रति मिनट शब्द)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "चीनी, कोरियाई और जापानी के लिए पढ़ने की गति (प्रति मिनट वर्ण)",
|
||||||
"form.prefs.label.display_mode": "वेब ऐप डिस्प्ले मोड (पुनः स्थापित करने की आवश्यकता है)",
|
"form.prefs.label.display_mode": "वेब ऐप डिस्प्ले मोड (पुनः स्थापित करने की आवश्यकता है)",
|
||||||
"form.prefs.select.older_first": "पहले पुरानी प्रविष्टियाँ",
|
"form.prefs.select.older_first": "पहले पुरानी प्रविष्टियाँ",
|
||||||
"form.prefs.select.recent_first": "हाल की प्रविष्टियाँ पहले",
|
"form.prefs.select.recent_first": "हाल की प्रविष्टियाँ पहले",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "Le password non coincidono.",
|
"error.different_passwords": "Le password non coincidono.",
|
||||||
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
||||||
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
|
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Le velocità di lettura devono essere numeri interi positivi.",
|
||||||
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
|
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
|
||||||
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
|
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
|
||||||
"error.feed_already_exists": "Questo feed esiste già.",
|
"error.feed_already_exists": "Questo feed esiste già.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.entry_sorting": "Ordinamento articoli",
|
"form.prefs.label.entry_sorting": "Ordinamento articoli",
|
||||||
"form.prefs.label.entries_per_page": "Articoli per pagina",
|
"form.prefs.label.entries_per_page": "Articoli per pagina",
|
||||||
|
"form.prefs.label.default_reading_speed": "Velocità di lettura di altre lingue (parole al minuto)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Velocità di lettura per cinese, coreano e giapponese (caratteri al minuto)",
|
||||||
"form.prefs.label.display_mode": "Modalità di visualizzazione web app (necessita la reinstallazione)",
|
"form.prefs.label.display_mode": "Modalità di visualizzazione web app (necessita la reinstallazione)",
|
||||||
"form.prefs.select.older_first": "Prima i più vecchi",
|
"form.prefs.select.older_first": "Prima i più vecchi",
|
||||||
"form.prefs.select.recent_first": "Prima i più recenti",
|
"form.prefs.select.recent_first": "Prima i più recenti",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "パスワードが一致しません。",
|
"error.different_passwords": "パスワードが一致しません。",
|
||||||
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
||||||
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
|
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
|
||||||
|
"error.settings_reading_speed_is_positive": "読み取り速度は正の整数でなければならない。",
|
||||||
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
|
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
|
||||||
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
|
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
|
||||||
"error.feed_already_exists": "このフィードはすでに存在します。",
|
"error.feed_already_exists": "このフィードはすでに存在します。",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "テーマ",
|
"form.prefs.label.theme": "テーマ",
|
||||||
"form.prefs.label.entry_sorting": "記事の並べ替え",
|
"form.prefs.label.entry_sorting": "記事の並べ替え",
|
||||||
"form.prefs.label.entries_per_page": "ページあたりのエントリ",
|
"form.prefs.label.entries_per_page": "ページあたりのエントリ",
|
||||||
|
"form.prefs.label.default_reading_speed": "他言語の読解速度(単語/分)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "中国語、韓国語、日本語の読書速度(1分間あたりの文字数)",
|
||||||
"form.prefs.label.display_mode": "Webアプリの表示モード (再インストールが必要)",
|
"form.prefs.label.display_mode": "Webアプリの表示モード (再インストールが必要)",
|
||||||
"form.prefs.select.older_first": "古い記事を最初に",
|
"form.prefs.select.older_first": "古い記事を最初に",
|
||||||
"form.prefs.select.recent_first": "新しい記事を最初に",
|
"form.prefs.select.recent_first": "新しい記事を最初に",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "Wachtwoorden zijn niet hetzelfde.",
|
"error.different_passwords": "Wachtwoorden zijn niet hetzelfde.",
|
||||||
"error.password_min_length": "Je moet minstens 6 tekens gebruiken.",
|
"error.password_min_length": "Je moet minstens 6 tekens gebruiken.",
|
||||||
"error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
|
"error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
|
||||||
|
"error.settings_reading_speed_is_positive": "De leessnelheden moeten positieve gehele getallen zijn.",
|
||||||
"error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
|
"error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
|
||||||
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
|
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
|
||||||
"error.feed_already_exists": "Deze feed bestaat al.",
|
"error.feed_already_exists": "Deze feed bestaat al.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Skin",
|
"form.prefs.label.theme": "Skin",
|
||||||
"form.prefs.label.entry_sorting": "Volgorde van items",
|
"form.prefs.label.entry_sorting": "Volgorde van items",
|
||||||
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
|
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
|
||||||
|
"form.prefs.label.default_reading_speed": "Leessnelheid voor andere talen (woorden per minuut)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
|
||||||
"form.prefs.label.display_mode": "Weergavemodus voor webapp (moet opnieuw worden geïnstalleerd)",
|
"form.prefs.label.display_mode": "Weergavemodus voor webapp (moet opnieuw worden geïnstalleerd)",
|
||||||
"form.prefs.select.older_first": "Oudere items eerst",
|
"form.prefs.select.older_first": "Oudere items eerst",
|
||||||
"form.prefs.select.recent_first": "Recente items eerst",
|
"form.prefs.select.recent_first": "Recente items eerst",
|
||||||
|
|
|
@ -245,6 +245,7 @@
|
||||||
"error.different_passwords": "Hasła nie są identyczne.",
|
"error.different_passwords": "Hasła nie są identyczne.",
|
||||||
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
|
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
|
||||||
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
|
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Prędkości odczytu muszą być dodatnimi liczbami całkowitymi.",
|
||||||
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
|
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
|
||||||
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
|
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
|
||||||
"error.feed_already_exists": "Ten kanał już istnieje.",
|
"error.feed_already_exists": "Ten kanał już istnieje.",
|
||||||
|
@ -294,6 +295,8 @@
|
||||||
"form.prefs.label.theme": "Wygląd",
|
"form.prefs.label.theme": "Wygląd",
|
||||||
"form.prefs.label.entry_sorting": "Sortowanie artykułów",
|
"form.prefs.label.entry_sorting": "Sortowanie artykułów",
|
||||||
"form.prefs.label.entries_per_page": "Wpisy na stronie",
|
"form.prefs.label.entries_per_page": "Wpisy na stronie",
|
||||||
|
"form.prefs.label.default_reading_speed": "Prędkość czytania dla innych języków (słowa na minutę)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Prędkość czytania dla języka chińskiego, koreańskiego i japońskiego (znaki na minutę)",
|
||||||
"form.prefs.label.display_mode": "Tryb wyświetlania aplikacji internetowej (wymaga ponownej instalacji)",
|
"form.prefs.label.display_mode": "Tryb wyświetlania aplikacji internetowej (wymaga ponownej instalacji)",
|
||||||
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
|
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
|
||||||
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
|
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "As senhas não são iguais.",
|
"error.different_passwords": "As senhas não são iguais.",
|
||||||
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
|
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
|
||||||
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
|
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
|
||||||
|
"error.settings_reading_speed_is_positive": "As velocidades de leitura devem ser inteiros positivos.",
|
||||||
"error.entries_per_page_invalid": "O número de itens por página é inválido.",
|
"error.entries_per_page_invalid": "O número de itens por página é inválido.",
|
||||||
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
|
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
|
||||||
"error.feed_already_exists": "Este feed já existe.",
|
"error.feed_already_exists": "Este feed já existe.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.entry_sorting": "Ordenação dos itens",
|
"form.prefs.label.entry_sorting": "Ordenação dos itens",
|
||||||
"form.prefs.label.entries_per_page": "Itens por página",
|
"form.prefs.label.entries_per_page": "Itens por página",
|
||||||
|
"form.prefs.label.default_reading_speed": "Velocidade de leitura para outros idiomas (palavras por minuto)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Velocidade de leitura para chinês, coreano e japonês (caracteres por minuto)",
|
||||||
"form.prefs.label.display_mode": "Modo de exibição do aplicativo Web (precisa ser reinstalado)",
|
"form.prefs.label.display_mode": "Modo de exibição do aplicativo Web (precisa ser reinstalado)",
|
||||||
"form.prefs.select.older_first": "Itens mais velhos primeiro",
|
"form.prefs.select.older_first": "Itens mais velhos primeiro",
|
||||||
"form.prefs.select.recent_first": "Itens mais recentes",
|
"form.prefs.select.recent_first": "Itens mais recentes",
|
||||||
|
|
|
@ -245,6 +245,7 @@
|
||||||
"error.different_passwords": "Пароли не совпадают.",
|
"error.different_passwords": "Пароли не совпадают.",
|
||||||
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
|
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
|
||||||
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Скорости считывания должны быть целыми положительными числами.",
|
||||||
"error.entries_per_page_invalid": "Количество записей на странице недействительно.",
|
"error.entries_per_page_invalid": "Количество записей на странице недействительно.",
|
||||||
"error.feed_mandatory_fields": "URL и категория обязательны.",
|
"error.feed_mandatory_fields": "URL и категория обязательны.",
|
||||||
"error.feed_already_exists": "Этот фид уже существует.",
|
"error.feed_already_exists": "Этот фид уже существует.",
|
||||||
|
@ -294,6 +295,8 @@
|
||||||
"form.prefs.label.theme": "Тема",
|
"form.prefs.label.theme": "Тема",
|
||||||
"form.prefs.label.entry_sorting": "Сортировка записей",
|
"form.prefs.label.entry_sorting": "Сортировка записей",
|
||||||
"form.prefs.label.entries_per_page": "Записи на странице",
|
"form.prefs.label.entries_per_page": "Записи на странице",
|
||||||
|
"form.prefs.label.default_reading_speed": "Скорость чтения на других языках (слов в минуту)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Скорость чтения на китайском, корейском и японском языках (знаков в минуту)",
|
||||||
"form.prefs.label.display_mode": "Режим отображения веб-приложения (требуется переустановка)",
|
"form.prefs.label.display_mode": "Режим отображения веб-приложения (требуется переустановка)",
|
||||||
"form.prefs.select.older_first": "Сначала старые записи",
|
"form.prefs.select.older_first": "Сначала старые записи",
|
||||||
"form.prefs.select.recent_first": "Сначала последние записи",
|
"form.prefs.select.recent_first": "Сначала последние записи",
|
||||||
|
|
|
@ -248,6 +248,7 @@
|
||||||
"error.different_passwords": "Parolalar eşleşmiyor.",
|
"error.different_passwords": "Parolalar eşleşmiyor.",
|
||||||
"error.password_min_length": "Parola en az 6 karakter içermeli.",
|
"error.password_min_length": "Parola en az 6 karakter içermeli.",
|
||||||
"error.settings_mandatory_fields": "Kullanıcı ad, tema, dil ve saat dilimi zorunlu.",
|
"error.settings_mandatory_fields": "Kullanıcı ad, tema, dil ve saat dilimi zorunlu.",
|
||||||
|
"error.settings_reading_speed_is_positive": "Okuma hızları pozitif tam sayılar olmalıdır.",
|
||||||
"error.entries_per_page_invalid": "Sayfa başına ileti sayısı geçersiz.",
|
"error.entries_per_page_invalid": "Sayfa başına ileti sayısı geçersiz.",
|
||||||
"error.feed_mandatory_fields": "URL ve kategori zorunlu.",
|
"error.feed_mandatory_fields": "URL ve kategori zorunlu.",
|
||||||
"error.feed_already_exists": "Bu besleme zaten mevcut.",
|
"error.feed_already_exists": "Bu besleme zaten mevcut.",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "Tema",
|
"form.prefs.label.theme": "Tema",
|
||||||
"form.prefs.label.entry_sorting": "İleti Sıralaması",
|
"form.prefs.label.entry_sorting": "İleti Sıralaması",
|
||||||
"form.prefs.label.entries_per_page": "Sayfa başına ileti",
|
"form.prefs.label.entries_per_page": "Sayfa başına ileti",
|
||||||
|
"form.prefs.label.default_reading_speed": "Diğer diller için okuma hızı (dakika başına kelime)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)",
|
||||||
"form.prefs.label.display_mode": "Web uygulaması görüntüleme modu (yeniden kurulum gerektirir)",
|
"form.prefs.label.display_mode": "Web uygulaması görüntüleme modu (yeniden kurulum gerektirir)",
|
||||||
"form.prefs.select.older_first": "Önce eski iletiler",
|
"form.prefs.select.older_first": "Önce eski iletiler",
|
||||||
"form.prefs.select.recent_first": "Önce yeni iletiler",
|
"form.prefs.select.recent_first": "Önce yeni iletiler",
|
||||||
|
|
|
@ -249,6 +249,7 @@
|
||||||
"error.feed_url_not_empty": "订阅源的网址不能为空。",
|
"error.feed_url_not_empty": "订阅源的网址不能为空。",
|
||||||
"error.site_url_not_empty": "源网站的网址不能为空。",
|
"error.site_url_not_empty": "源网站的网址不能为空。",
|
||||||
"error.feed_title_not_empty": "订阅源的标题不能为空。",
|
"error.feed_title_not_empty": "订阅源的标题不能为空。",
|
||||||
|
"error.settings_reading_speed_is_positive": "阅读速度必须是正整数。",
|
||||||
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
|
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
|
||||||
"error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
|
"error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
|
||||||
"error.feed_invalid_keeplist_rule": "保留列表规则无效。",
|
"error.feed_invalid_keeplist_rule": "保留列表规则无效。",
|
||||||
|
@ -291,6 +292,8 @@
|
||||||
"form.prefs.label.entry_sorting": "文章排序",
|
"form.prefs.label.entry_sorting": "文章排序",
|
||||||
"form.prefs.label.entries_per_page": "每页文章数",
|
"form.prefs.label.entries_per_page": "每页文章数",
|
||||||
"form.prefs.label.display_mode": "渐进式网页应用显示模式(需要重新添加)",
|
"form.prefs.label.display_mode": "渐进式网页应用显示模式(需要重新添加)",
|
||||||
|
"form.prefs.label.default_reading_speed": "其他语言的阅读速度(每分钟字数)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "中文、韩文和日文的阅读速度(每分钟字符数)",
|
||||||
"form.prefs.select.older_first": "旧->新",
|
"form.prefs.select.older_first": "旧->新",
|
||||||
"form.prefs.select.recent_first": "新->旧",
|
"form.prefs.select.recent_first": "新->旧",
|
||||||
"form.prefs.select.fullscreen": "全屏",
|
"form.prefs.select.fullscreen": "全屏",
|
||||||
|
|
|
@ -243,6 +243,7 @@
|
||||||
"error.different_passwords": "兩次輸入的密碼不同",
|
"error.different_passwords": "兩次輸入的密碼不同",
|
||||||
"error.password_min_length": "請至少輸入 6 個字元",
|
"error.password_min_length": "請至少輸入 6 個字元",
|
||||||
"error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區",
|
"error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區",
|
||||||
|
"error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
|
||||||
"error.entries_per_page_invalid": "每頁的文章數無效。",
|
"error.entries_per_page_invalid": "每頁的文章數無效。",
|
||||||
"error.feed_mandatory_fields": "必須填寫網址和分類",
|
"error.feed_mandatory_fields": "必須填寫網址和分類",
|
||||||
"error.feed_already_exists": "此Feed已存在。",
|
"error.feed_already_exists": "此Feed已存在。",
|
||||||
|
@ -292,6 +293,8 @@
|
||||||
"form.prefs.label.theme": "主題",
|
"form.prefs.label.theme": "主題",
|
||||||
"form.prefs.label.entry_sorting": "文章排序",
|
"form.prefs.label.entry_sorting": "文章排序",
|
||||||
"form.prefs.label.entries_per_page": "每頁文章數",
|
"form.prefs.label.entries_per_page": "每頁文章數",
|
||||||
|
"form.prefs.label.default_reading_speed": "Reading speed for other languages (words per minute)",
|
||||||
|
"form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
|
||||||
"form.prefs.label.display_mode": "漸進式網頁應用顯示模式(需要重新新增)",
|
"form.prefs.label.display_mode": "漸進式網頁應用顯示模式(需要重新新增)",
|
||||||
"form.prefs.select.older_first": "舊->新",
|
"form.prefs.select.older_first": "舊->新",
|
||||||
"form.prefs.select.recent_first": "新->舊",
|
"form.prefs.select.recent_first": "新->舊",
|
||||||
|
|
|
@ -12,24 +12,26 @@ import (
|
||||||
|
|
||||||
// User represents a user in the system.
|
// User represents a user in the system.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
EntryDirection string `json:"entry_sorting_direction"`
|
EntryDirection string `json:"entry_sorting_direction"`
|
||||||
EntryOrder string `json:"entry_sorting_order"`
|
EntryOrder string `json:"entry_sorting_order"`
|
||||||
Stylesheet string `json:"stylesheet"`
|
Stylesheet string `json:"stylesheet"`
|
||||||
GoogleID string `json:"google_id"`
|
GoogleID string `json:"google_id"`
|
||||||
OpenIDConnectID string `json:"openid_connect_id"`
|
OpenIDConnectID string `json:"openid_connect_id"`
|
||||||
EntriesPerPage int `json:"entries_per_page"`
|
EntriesPerPage int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime bool `json:"show_reading_time"`
|
ShowReadingTime bool `json:"show_reading_time"`
|
||||||
EntrySwipe bool `json:"entry_swipe"`
|
EntrySwipe bool `json:"entry_swipe"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
DisplayMode string `json:"display_mode"`
|
DisplayMode string `json:"display_mode"`
|
||||||
|
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||||
|
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCreationRequest represents the request to create a user.
|
// UserCreationRequest represents the request to create a user.
|
||||||
|
@ -43,22 +45,24 @@ type UserCreationRequest struct {
|
||||||
|
|
||||||
// UserModificationRequest represents the request to update a user.
|
// UserModificationRequest represents the request to update a user.
|
||||||
type UserModificationRequest struct {
|
type UserModificationRequest struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Language *string `json:"language"`
|
Language *string `json:"language"`
|
||||||
Timezone *string `json:"timezone"`
|
Timezone *string `json:"timezone"`
|
||||||
EntryDirection *string `json:"entry_sorting_direction"`
|
EntryDirection *string `json:"entry_sorting_direction"`
|
||||||
EntryOrder *string `json:"entry_sorting_order"`
|
EntryOrder *string `json:"entry_sorting_order"`
|
||||||
Stylesheet *string `json:"stylesheet"`
|
Stylesheet *string `json:"stylesheet"`
|
||||||
GoogleID *string `json:"google_id"`
|
GoogleID *string `json:"google_id"`
|
||||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||||
EntriesPerPage *int `json:"entries_per_page"`
|
EntriesPerPage *int `json:"entries_per_page"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime *bool `json:"show_reading_time"`
|
ShowReadingTime *bool `json:"show_reading_time"`
|
||||||
EntrySwipe *bool `json:"entry_swipe"`
|
EntrySwipe *bool `json:"entry_swipe"`
|
||||||
DisplayMode *string `json:"display_mode"`
|
DisplayMode *string `json:"display_mode"`
|
||||||
|
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||||
|
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch updates the User object with the modification request.
|
// Patch updates the User object with the modification request.
|
||||||
|
@ -126,6 +130,14 @@ func (u *UserModificationRequest) Patch(user *User) {
|
||||||
if u.DisplayMode != nil {
|
if u.DisplayMode != nil {
|
||||||
user.DisplayMode = *u.DisplayMode
|
user.DisplayMode = *u.DisplayMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u.DefaultReadingSpeed != nil {
|
||||||
|
user.DefaultReadingSpeed = *u.DefaultReadingSpeed
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.CJKReadingSpeed != nil {
|
||||||
|
user.CJKReadingSpeed = *u.CJKReadingSpeed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseTimezone converts last login date to the given timezone.
|
// UseTimezone converts last login date to the given timezone.
|
||||||
|
|
|
@ -32,6 +32,11 @@ var (
|
||||||
func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model.FeedCreationRequest) (*model.Feed, error) {
|
func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model.FeedCreationRequest) (*model.Feed, error) {
|
||||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[CreateFeed] FeedURL=%s", feedCreationRequest.FeedURL))
|
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[CreateFeed] FeedURL=%s", feedCreationRequest.FeedURL))
|
||||||
|
|
||||||
|
user, storeErr := store.UserByID(userID)
|
||||||
|
if storeErr != nil {
|
||||||
|
return nil, storeErr
|
||||||
|
}
|
||||||
|
|
||||||
if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
|
if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
|
||||||
return nil, errors.NewLocalizedError(errCategoryNotFound)
|
return nil, errors.NewLocalizedError(errCategoryNotFound)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +84,7 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
|
||||||
subscription.WithClientResponse(response)
|
subscription.WithClientResponse(response)
|
||||||
subscription.CheckedNow()
|
subscription.CheckedNow()
|
||||||
|
|
||||||
processor.ProcessFeedEntries(store, subscription)
|
processor.ProcessFeedEntries(store, subscription, user)
|
||||||
|
|
||||||
if storeErr := store.CreateFeed(subscription); storeErr != nil {
|
if storeErr := store.CreateFeed(subscription); storeErr != nil {
|
||||||
return nil, storeErr
|
return nil, storeErr
|
||||||
|
@ -101,8 +106,12 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
|
||||||
// RefreshFeed refreshes a feed.
|
// RefreshFeed refreshes a feed.
|
||||||
func RefreshFeed(store *storage.Storage, userID, feedID int64) error {
|
func RefreshFeed(store *storage.Storage, userID, feedID int64) error {
|
||||||
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[RefreshFeed] feedID=%d", feedID))
|
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[RefreshFeed] feedID=%d", feedID))
|
||||||
userLanguage := store.UserLanguage(userID)
|
user, storeErr := store.UserByID(userID)
|
||||||
printer := locale.NewPrinter(userLanguage)
|
if storeErr != nil {
|
||||||
|
return storeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
printer := locale.NewPrinter(user.Language)
|
||||||
|
|
||||||
originalFeed, storeErr := store.FeedByID(userID, feedID)
|
originalFeed, storeErr := store.FeedByID(userID, feedID)
|
||||||
if storeErr != nil {
|
if storeErr != nil {
|
||||||
|
@ -164,7 +173,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
originalFeed.Entries = updatedFeed.Entries
|
originalFeed.Entries = updatedFeed.Entries
|
||||||
processor.ProcessFeedEntries(store, originalFeed)
|
processor.ProcessFeedEntries(store, originalFeed, user)
|
||||||
|
|
||||||
// We don't update existing entries when the crawler is enabled (we crawl only inexisting entries).
|
// We don't update existing entries when the crawler is enabled (we crawl only inexisting entries).
|
||||||
if storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, !originalFeed.Crawler); storeErr != nil {
|
if storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, !originalFeed.Crawler); storeErr != nil {
|
||||||
|
|
|
@ -38,7 +38,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProcessFeedEntries downloads original web page for entries and apply filters.
|
// ProcessFeedEntries downloads original web page for entries and apply filters.
|
||||||
func ProcessFeedEntries(store *storage.Storage, feed *model.Feed) {
|
func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, user *model.User) {
|
||||||
var filteredEntries model.Entries
|
var filteredEntries model.Entries
|
||||||
|
|
||||||
for _, entry := range feed.Entries {
|
for _, entry := range feed.Entries {
|
||||||
|
@ -96,7 +96,7 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEntryReadingTime(store, feed, entry, entryIsNew)
|
updateEntryReadingTime(store, feed, entry, entryIsNew, user)
|
||||||
filteredEntries = append(filteredEntries, entry)
|
filteredEntries = append(filteredEntries, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ func isAllowedEntry(feed *model.Feed, entry *model.Entry) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessEntryWebPage downloads the entry web page and apply rewrite rules.
|
// ProcessEntryWebPage downloads the entry web page and apply rewrite rules.
|
||||||
func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry) error {
|
func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User) error {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
url := getUrlFromEntry(feed, entry)
|
url := getUrlFromEntry(feed, entry)
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry) error {
|
||||||
|
|
||||||
if content != "" {
|
if content != "" {
|
||||||
entry.Content = content
|
entry.Content = content
|
||||||
entry.ReadingTime = calculateReadingTime(content)
|
entry.ReadingTime = calculateReadingTime(content, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -179,7 +179,7 @@ func getUrlFromEntry(feed *model.Feed, entry *model.Entry) string {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateEntryReadingTime(store *storage.Storage, feed *model.Feed, entry *model.Entry, entryIsNew bool) {
|
func updateEntryReadingTime(store *storage.Storage, feed *model.Feed, entry *model.Entry, entryIsNew bool, user *model.User) {
|
||||||
if shouldFetchYouTubeWatchTime(entry) {
|
if shouldFetchYouTubeWatchTime(entry) {
|
||||||
if entryIsNew {
|
if entryIsNew {
|
||||||
watchTime, err := fetchYouTubeWatchTime(entry.URL)
|
watchTime, err := fetchYouTubeWatchTime(entry.URL)
|
||||||
|
@ -194,7 +194,7 @@ func updateEntryReadingTime(store *storage.Storage, feed *model.Feed, entry *mod
|
||||||
|
|
||||||
// Handle YT error case and non-YT entries.
|
// Handle YT error case and non-YT entries.
|
||||||
if entry.ReadingTime == 0 {
|
if entry.ReadingTime == 0 {
|
||||||
entry.ReadingTime = calculateReadingTime(entry.Content)
|
entry.ReadingTime = calculateReadingTime(entry.Content, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,16 +269,16 @@ func parseISO8601(from string) (time.Duration, error) {
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateReadingTime(content string) int {
|
func calculateReadingTime(content string, user *model.User) int {
|
||||||
sanitizedContent := sanitizer.StripTags(content)
|
sanitizedContent := sanitizer.StripTags(content)
|
||||||
languageInfo := getlang.FromString(sanitizedContent)
|
languageInfo := getlang.FromString(sanitizedContent)
|
||||||
|
|
||||||
var timeToReadInt int
|
var timeToReadInt int
|
||||||
if languageInfo.LanguageCode() == "ko" || languageInfo.LanguageCode() == "zh" || languageInfo.LanguageCode() == "jp" {
|
if languageInfo.LanguageCode() == "ko" || languageInfo.LanguageCode() == "zh" || languageInfo.LanguageCode() == "jp" {
|
||||||
timeToReadInt = int(math.Ceil(float64(utf8.RuneCountInString(sanitizedContent)) / 500))
|
timeToReadInt = int(math.Ceil(float64(utf8.RuneCountInString(sanitizedContent)) / float64(user.CJKReadingSpeed)))
|
||||||
} else {
|
} else {
|
||||||
nbOfWords := len(strings.Fields(sanitizedContent))
|
nbOfWords := len(strings.Fields(sanitizedContent))
|
||||||
timeToReadInt = int(math.Ceil(float64(nbOfWords) / 265))
|
timeToReadInt = int(math.Ceil(float64(nbOfWords) / float64(user.DefaultReadingSpeed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return timeToReadInt
|
return timeToReadInt
|
||||||
|
|
|
@ -85,7 +85,9 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
|
||||||
google_id,
|
google_id,
|
||||||
openid_connect_id,
|
openid_connect_id,
|
||||||
display_mode,
|
display_mode,
|
||||||
entry_order
|
entry_order,
|
||||||
|
default_reading_speed,
|
||||||
|
cjk_reading_speed
|
||||||
`
|
`
|
||||||
|
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
|
@ -118,6 +120,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
|
||||||
&user.OpenIDConnectID,
|
&user.OpenIDConnectID,
|
||||||
&user.DisplayMode,
|
&user.DisplayMode,
|
||||||
&user.EntryOrder,
|
&user.EntryOrder,
|
||||||
|
&user.DefaultReadingSpeed,
|
||||||
|
&user.CJKReadingSpeed,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
@ -168,9 +172,11 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
google_id=$13,
|
google_id=$13,
|
||||||
openid_connect_id=$14,
|
openid_connect_id=$14,
|
||||||
display_mode=$15,
|
display_mode=$15,
|
||||||
entry_order=$16
|
entry_order=$16,
|
||||||
|
default_reading_speed=$17,
|
||||||
|
cjk_reading_speed=$18
|
||||||
WHERE
|
WHERE
|
||||||
id=$17
|
id=$19
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err = s.db.Exec(
|
_, err = s.db.Exec(
|
||||||
|
@ -191,6 +197,8 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
user.OpenIDConnectID,
|
user.OpenIDConnectID,
|
||||||
user.DisplayMode,
|
user.DisplayMode,
|
||||||
user.EntryOrder,
|
user.EntryOrder,
|
||||||
|
user.DefaultReadingSpeed,
|
||||||
|
user.CJKReadingSpeed,
|
||||||
user.ID,
|
user.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -213,9 +221,11 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
google_id=$12,
|
google_id=$12,
|
||||||
openid_connect_id=$13,
|
openid_connect_id=$13,
|
||||||
display_mode=$14,
|
display_mode=$14,
|
||||||
entry_order=$15
|
entry_order=$15,
|
||||||
|
default_reading_speed=$16,
|
||||||
|
cjk_reading_speed=$17
|
||||||
WHERE
|
WHERE
|
||||||
id=$16
|
id=$18
|
||||||
`
|
`
|
||||||
|
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
|
@ -235,6 +245,8 @@ func (s *Storage) UpdateUser(user *model.User) error {
|
||||||
user.OpenIDConnectID,
|
user.OpenIDConnectID,
|
||||||
user.DisplayMode,
|
user.DisplayMode,
|
||||||
user.EntryOrder,
|
user.EntryOrder,
|
||||||
|
user.DefaultReadingSpeed,
|
||||||
|
user.CJKReadingSpeed,
|
||||||
user.ID,
|
user.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -276,7 +288,9 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
|
||||||
google_id,
|
google_id,
|
||||||
openid_connect_id,
|
openid_connect_id,
|
||||||
display_mode,
|
display_mode,
|
||||||
entry_order
|
entry_order,
|
||||||
|
default_reading_speed,
|
||||||
|
cjk_reading_speed
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -305,7 +319,9 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
|
||||||
google_id,
|
google_id,
|
||||||
openid_connect_id,
|
openid_connect_id,
|
||||||
display_mode,
|
display_mode,
|
||||||
entry_order
|
entry_order,
|
||||||
|
default_reading_speed,
|
||||||
|
cjk_reading_speed
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -334,7 +350,9 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
|
||||||
google_id,
|
google_id,
|
||||||
openid_connect_id,
|
openid_connect_id,
|
||||||
display_mode,
|
display_mode,
|
||||||
entry_order
|
entry_order,
|
||||||
|
default_reading_speed,
|
||||||
|
cjk_reading_speed
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -370,7 +388,9 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
|
||||||
u.google_id,
|
u.google_id,
|
||||||
u.openid_connect_id,
|
u.openid_connect_id,
|
||||||
u.display_mode,
|
u.display_mode,
|
||||||
u.entry_order
|
u.entry_order,
|
||||||
|
u.default_reading_speed,
|
||||||
|
u.cjk_reading_speed
|
||||||
FROM
|
FROM
|
||||||
users u
|
users u
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
@ -401,6 +421,8 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
|
||||||
&user.OpenIDConnectID,
|
&user.OpenIDConnectID,
|
||||||
&user.DisplayMode,
|
&user.DisplayMode,
|
||||||
&user.EntryOrder,
|
&user.EntryOrder,
|
||||||
|
&user.DefaultReadingSpeed,
|
||||||
|
&user.CJKReadingSpeed,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
@ -492,7 +514,9 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
google_id,
|
google_id,
|
||||||
openid_connect_id,
|
openid_connect_id,
|
||||||
display_mode,
|
display_mode,
|
||||||
entry_order
|
entry_order,
|
||||||
|
default_reading_speed,
|
||||||
|
cjk_reading_speed
|
||||||
FROM
|
FROM
|
||||||
users
|
users
|
||||||
ORDER BY username ASC
|
ORDER BY username ASC
|
||||||
|
@ -524,6 +548,8 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
&user.OpenIDConnectID,
|
&user.OpenIDConnectID,
|
||||||
&user.DisplayMode,
|
&user.DisplayMode,
|
||||||
&user.EntryOrder,
|
&user.EntryOrder,
|
||||||
|
&user.DefaultReadingSpeed,
|
||||||
|
&user.CJKReadingSpeed,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -72,6 +72,12 @@
|
||||||
|
|
||||||
<label><input type="checkbox" name="entry_swipe" value="1" {{ if .form.EntrySwipe }}checked{{ end }}> {{ t "form.prefs.label.entry_swipe" }}</label>
|
<label><input type="checkbox" name="entry_swipe" value="1" {{ if .form.EntrySwipe }}checked{{ end }}> {{ t "form.prefs.label.entry_swipe" }}</label>
|
||||||
|
|
||||||
|
<label for="form-cjk-reading-speed">{{ t "form.prefs.label.cjk_reading_speed" }}</label>
|
||||||
|
<input type="number" name="cjk_reading_speed" id="form-cjk-reading-speed" value="{{ .form.CJKReadingSpeed }}" min="1">
|
||||||
|
|
||||||
|
<label for="form-default-reading-speed">{{ t "form.prefs.label.default_reading_speed" }}</label>
|
||||||
|
<input type="number" name="default_reading_speed" id="form-default-reading-speed" value="{{ .form.DefaultReadingSpeed }}" min="1">
|
||||||
|
|
||||||
<label>{{t "form.prefs.label.custom_css" }}</label><textarea name="custom_css" cols="40" rows="8" spellcheck="false">{{ .form.CustomCSS }}</textarea>
|
<label>{{t "form.prefs.label.custom_css" }}</label><textarea name="custom_css" cols="40" rows="8" spellcheck="false">{{ .form.CustomCSS }}</textarea>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// Use of this source code is governed by the Apache 2.0
|
// Use of this source code is governed by the Apache 2.0
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build integration
|
||||||
// +build integration
|
// +build integration
|
||||||
|
|
||||||
package tests
|
package tests
|
||||||
|
@ -86,6 +87,14 @@ func TestGetUsers(t *testing.T) {
|
||||||
if users[0].DisplayMode != "standalone" {
|
if users[0].DisplayMode != "standalone" {
|
||||||
t.Fatalf(`Invalid web app display mode, got "%v"`, users[0].DisplayMode)
|
t.Fatalf(`Invalid web app display mode, got "%v"`, users[0].DisplayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if users[0].DefaultReadingSpeed != 265 {
|
||||||
|
t.Fatalf(`Invalid default reading speed, got "%v"`, users[0].DefaultReadingSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if users[0].CJKReadingSpeed != 500 {
|
||||||
|
t.Fatalf(`Invalid cjk reading speed, got "%v"`, users[0].CJKReadingSpeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateStandardUser(t *testing.T) {
|
func TestCreateStandardUser(t *testing.T) {
|
||||||
|
@ -135,6 +144,14 @@ func TestCreateStandardUser(t *testing.T) {
|
||||||
if user.DisplayMode != "standalone" {
|
if user.DisplayMode != "standalone" {
|
||||||
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
|
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.DefaultReadingSpeed != 265 {
|
||||||
|
t.Fatalf(`Invalid default reading speed, got "%v"`, user.DefaultReadingSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.CJKReadingSpeed != 500 {
|
||||||
|
t.Fatalf(`Invalid cjk reading speed, got "%v"`, user.CJKReadingSpeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoveUser(t *testing.T) {
|
func TestRemoveUser(t *testing.T) {
|
||||||
|
@ -207,6 +224,14 @@ func TestGetUserByID(t *testing.T) {
|
||||||
if user.DisplayMode != "standalone" {
|
if user.DisplayMode != "standalone" {
|
||||||
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
|
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.DefaultReadingSpeed != 265 {
|
||||||
|
t.Fatalf(`Invalid default reading speed, got "%v"`, user.DefaultReadingSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.CJKReadingSpeed != 500 {
|
||||||
|
t.Fatalf(`Invalid cjk reading speed, got "%v"`, user.CJKReadingSpeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserByUsername(t *testing.T) {
|
func TestGetUserByUsername(t *testing.T) {
|
||||||
|
@ -266,6 +291,14 @@ func TestGetUserByUsername(t *testing.T) {
|
||||||
if user.DisplayMode != "standalone" {
|
if user.DisplayMode != "standalone" {
|
||||||
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
|
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.DefaultReadingSpeed != 265 {
|
||||||
|
t.Fatalf(`Invalid default reading speed, got "%v"`, user.DefaultReadingSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.CJKReadingSpeed != 500 {
|
||||||
|
t.Fatalf(`Invalid cjk reading speed, got "%v"`, user.CJKReadingSpeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUserTheme(t *testing.T) {
|
func TestUpdateUserTheme(t *testing.T) {
|
||||||
|
@ -299,11 +332,15 @@ func TestUpdateUserFields(t *testing.T) {
|
||||||
swipe := false
|
swipe := false
|
||||||
entriesPerPage := 5
|
entriesPerPage := 5
|
||||||
displayMode := "fullscreen"
|
displayMode := "fullscreen"
|
||||||
|
defaultReadingSpeed := 380
|
||||||
|
cjkReadingSpeed := 200
|
||||||
user, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{
|
user, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{
|
||||||
Stylesheet: &stylesheet,
|
Stylesheet: &stylesheet,
|
||||||
EntrySwipe: &swipe,
|
EntrySwipe: &swipe,
|
||||||
EntriesPerPage: &entriesPerPage,
|
EntriesPerPage: &entriesPerPage,
|
||||||
DisplayMode: &displayMode,
|
DisplayMode: &displayMode,
|
||||||
|
DefaultReadingSpeed: &defaultReadingSpeed,
|
||||||
|
CJKReadingSpeed: &cjkReadingSpeed,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -324,6 +361,14 @@ func TestUpdateUserFields(t *testing.T) {
|
||||||
if user.DisplayMode != displayMode {
|
if user.DisplayMode != displayMode {
|
||||||
t.Fatalf(`Unable to update user DisplayMode: got %q instead of %q`, user.DisplayMode, displayMode)
|
t.Fatalf(`Unable to update user DisplayMode: got %q instead of %q`, user.DisplayMode, displayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.DefaultReadingSpeed != defaultReadingSpeed {
|
||||||
|
t.Fatalf(`Invalid default reading speed, got %v instead of %v`, user.DefaultReadingSpeed, defaultReadingSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.CJKReadingSpeed != cjkReadingSpeed {
|
||||||
|
t.Fatalf(`Invalid cjk reading speed, got %v instead of %v`, user.CJKReadingSpeed, cjkReadingSpeed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUserThemeWithInvalidValue(t *testing.T) {
|
func TestUpdateUserThemeWithInvalidValue(t *testing.T) {
|
||||||
|
|
|
@ -34,6 +34,14 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := h.store.UserByID(entry.UserID)
|
||||||
|
if err != nil {
|
||||||
|
json.ServerError(w, r, err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
json.NotFound(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
feedBuilder := storage.NewFeedQueryBuilder(h.store, loggedUserID)
|
feedBuilder := storage.NewFeedQueryBuilder(h.store, loggedUserID)
|
||||||
feedBuilder.WithFeedID(entry.FeedID)
|
feedBuilder.WithFeedID(entry.FeedID)
|
||||||
feed, err := feedBuilder.GetFeed()
|
feed, err := feedBuilder.GetFeed()
|
||||||
|
@ -47,12 +55,14 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := processor.ProcessEntryWebPage(feed, entry); err != nil {
|
if err := processor.ProcessEntryWebPage(feed, entry, user); err != nil {
|
||||||
json.ServerError(w, r, err)
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.store.UpdateEntryContent(entry)
|
if err := h.store.UpdateEntryContent(entry); err != nil {
|
||||||
|
json.ServerError(w, r, err)
|
||||||
|
}
|
||||||
|
|
||||||
json.OK(w, r, map[string]string{"content": proxy.ImageProxyRewriter(h.router, entry.Content)})
|
json.OK(w, r, map[string]string{"content": proxy.ImageProxyRewriter(h.router, entry.Content)})
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,20 +14,22 @@ import (
|
||||||
|
|
||||||
// SettingsForm represents the settings form.
|
// SettingsForm represents the settings form.
|
||||||
type SettingsForm struct {
|
type SettingsForm struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Confirmation string
|
Confirmation string
|
||||||
Theme string
|
Theme string
|
||||||
Language string
|
Language string
|
||||||
Timezone string
|
Timezone string
|
||||||
EntryDirection string
|
EntryDirection string
|
||||||
EntryOrder string
|
EntryOrder string
|
||||||
EntriesPerPage int
|
EntriesPerPage int
|
||||||
KeyboardShortcuts bool
|
KeyboardShortcuts bool
|
||||||
ShowReadingTime bool
|
ShowReadingTime bool
|
||||||
CustomCSS string
|
CustomCSS string
|
||||||
EntrySwipe bool
|
EntrySwipe bool
|
||||||
DisplayMode string
|
DisplayMode string
|
||||||
|
DefaultReadingSpeed int
|
||||||
|
CJKReadingSpeed int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge updates the fields of the given user.
|
// Merge updates the fields of the given user.
|
||||||
|
@ -44,6 +46,8 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
|
||||||
user.Stylesheet = s.CustomCSS
|
user.Stylesheet = s.CustomCSS
|
||||||
user.EntrySwipe = s.EntrySwipe
|
user.EntrySwipe = s.EntrySwipe
|
||||||
user.DisplayMode = s.DisplayMode
|
user.DisplayMode = s.DisplayMode
|
||||||
|
user.CJKReadingSpeed = s.CJKReadingSpeed
|
||||||
|
user.DefaultReadingSpeed = s.DefaultReadingSpeed
|
||||||
|
|
||||||
if s.Password != "" {
|
if s.Password != "" {
|
||||||
user.Password = s.Password
|
user.Password = s.Password
|
||||||
|
@ -58,6 +62,10 @@ func (s *SettingsForm) Validate() error {
|
||||||
return errors.NewLocalizedError("error.settings_mandatory_fields")
|
return errors.NewLocalizedError("error.settings_mandatory_fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.CJKReadingSpeed <= 0 || s.DefaultReadingSpeed <= 0 {
|
||||||
|
return errors.NewLocalizedError("error.settings_reading_speed_is_positive")
|
||||||
|
}
|
||||||
|
|
||||||
if s.Confirmation == "" {
|
if s.Confirmation == "" {
|
||||||
// Firefox insists on auto-completing the password field.
|
// Firefox insists on auto-completing the password field.
|
||||||
// If the confirmation field is blank, the user probably
|
// If the confirmation field is blank, the user probably
|
||||||
|
@ -78,20 +86,30 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
entriesPerPage = 0
|
entriesPerPage = 0
|
||||||
}
|
}
|
||||||
|
defaultReadingSpeed, err := strconv.ParseInt(r.FormValue("default_reading_speed"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
defaultReadingSpeed = 0
|
||||||
|
}
|
||||||
|
cjkReadingSpeed, err := strconv.ParseInt(r.FormValue("cjk_reading_speed"), 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
cjkReadingSpeed = 0
|
||||||
|
}
|
||||||
return &SettingsForm{
|
return &SettingsForm{
|
||||||
Username: r.FormValue("username"),
|
Username: r.FormValue("username"),
|
||||||
Password: r.FormValue("password"),
|
Password: r.FormValue("password"),
|
||||||
Confirmation: r.FormValue("confirmation"),
|
Confirmation: r.FormValue("confirmation"),
|
||||||
Theme: r.FormValue("theme"),
|
Theme: r.FormValue("theme"),
|
||||||
Language: r.FormValue("language"),
|
Language: r.FormValue("language"),
|
||||||
Timezone: r.FormValue("timezone"),
|
Timezone: r.FormValue("timezone"),
|
||||||
EntryDirection: r.FormValue("entry_direction"),
|
EntryDirection: r.FormValue("entry_direction"),
|
||||||
EntryOrder: r.FormValue("entry_order"),
|
EntryOrder: r.FormValue("entry_order"),
|
||||||
EntriesPerPage: int(entriesPerPage),
|
EntriesPerPage: int(entriesPerPage),
|
||||||
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
|
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
|
||||||
ShowReadingTime: r.FormValue("show_reading_time") == "1",
|
ShowReadingTime: r.FormValue("show_reading_time") == "1",
|
||||||
CustomCSS: r.FormValue("custom_css"),
|
CustomCSS: r.FormValue("custom_css"),
|
||||||
EntrySwipe: r.FormValue("entry_swipe") == "1",
|
EntrySwipe: r.FormValue("entry_swipe") == "1",
|
||||||
DisplayMode: r.FormValue("display_mode"),
|
DisplayMode: r.FormValue("display_mode"),
|
||||||
|
DefaultReadingSpeed: int(defaultReadingSpeed),
|
||||||
|
CJKReadingSpeed: int(cjkReadingSpeed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,17 @@ import (
|
||||||
|
|
||||||
func TestValid(t *testing.T) {
|
func TestValid(t *testing.T) {
|
||||||
settings := &SettingsForm{
|
settings := &SettingsForm{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "hunter2",
|
Password: "hunter2",
|
||||||
Confirmation: "hunter2",
|
Confirmation: "hunter2",
|
||||||
Theme: "default",
|
Theme: "default",
|
||||||
Language: "en_US",
|
Language: "en_US",
|
||||||
Timezone: "UTC",
|
Timezone: "UTC",
|
||||||
EntryDirection: "asc",
|
EntryDirection: "asc",
|
||||||
EntriesPerPage: 50,
|
EntriesPerPage: 50,
|
||||||
DisplayMode: "standalone",
|
DisplayMode: "standalone",
|
||||||
|
DefaultReadingSpeed: 35,
|
||||||
|
CJKReadingSpeed: 25,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := settings.Validate()
|
err := settings.Validate()
|
||||||
|
@ -25,15 +27,17 @@ func TestValid(t *testing.T) {
|
||||||
|
|
||||||
func TestConfirmationEmpty(t *testing.T) {
|
func TestConfirmationEmpty(t *testing.T) {
|
||||||
settings := &SettingsForm{
|
settings := &SettingsForm{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "hunter2",
|
Password: "hunter2",
|
||||||
Confirmation: "",
|
Confirmation: "",
|
||||||
Theme: "default",
|
Theme: "default",
|
||||||
Language: "en_US",
|
Language: "en_US",
|
||||||
Timezone: "UTC",
|
Timezone: "UTC",
|
||||||
EntryDirection: "asc",
|
EntryDirection: "asc",
|
||||||
EntriesPerPage: 50,
|
EntriesPerPage: 50,
|
||||||
DisplayMode: "standalone",
|
DisplayMode: "standalone",
|
||||||
|
DefaultReadingSpeed: 35,
|
||||||
|
CJKReadingSpeed: 25,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := settings.Validate()
|
err := settings.Validate()
|
||||||
|
@ -48,15 +52,17 @@ func TestConfirmationEmpty(t *testing.T) {
|
||||||
|
|
||||||
func TestConfirmationIncorrect(t *testing.T) {
|
func TestConfirmationIncorrect(t *testing.T) {
|
||||||
settings := &SettingsForm{
|
settings := &SettingsForm{
|
||||||
Username: "user",
|
Username: "user",
|
||||||
Password: "hunter2",
|
Password: "hunter2",
|
||||||
Confirmation: "unter2",
|
Confirmation: "unter2",
|
||||||
Theme: "default",
|
Theme: "default",
|
||||||
Language: "en_US",
|
Language: "en_US",
|
||||||
Timezone: "UTC",
|
Timezone: "UTC",
|
||||||
EntryDirection: "asc",
|
EntryDirection: "asc",
|
||||||
EntriesPerPage: 50,
|
EntriesPerPage: 50,
|
||||||
DisplayMode: "standalone",
|
DisplayMode: "standalone",
|
||||||
|
DefaultReadingSpeed: 35,
|
||||||
|
CJKReadingSpeed: 25,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := settings.Validate()
|
err := settings.Validate()
|
||||||
|
|
|
@ -27,18 +27,20 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsForm := form.SettingsForm{
|
settingsForm := form.SettingsForm{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Theme: user.Theme,
|
Theme: user.Theme,
|
||||||
Language: user.Language,
|
Language: user.Language,
|
||||||
Timezone: user.Timezone,
|
Timezone: user.Timezone,
|
||||||
EntryDirection: user.EntryDirection,
|
EntryDirection: user.EntryDirection,
|
||||||
EntryOrder: user.EntryOrder,
|
EntryOrder: user.EntryOrder,
|
||||||
EntriesPerPage: user.EntriesPerPage,
|
EntriesPerPage: user.EntriesPerPage,
|
||||||
KeyboardShortcuts: user.KeyboardShortcuts,
|
KeyboardShortcuts: user.KeyboardShortcuts,
|
||||||
ShowReadingTime: user.ShowReadingTime,
|
ShowReadingTime: user.ShowReadingTime,
|
||||||
CustomCSS: user.Stylesheet,
|
CustomCSS: user.Stylesheet,
|
||||||
EntrySwipe: user.EntrySwipe,
|
EntrySwipe: user.EntrySwipe,
|
||||||
DisplayMode: user.DisplayMode,
|
DisplayMode: user.DisplayMode,
|
||||||
|
DefaultReadingSpeed: user.DefaultReadingSpeed,
|
||||||
|
CJKReadingSpeed: user.CJKReadingSpeed,
|
||||||
}
|
}
|
||||||
|
|
||||||
timezones, err := h.store.Timezones()
|
timezones, err := h.store.Timezones()
|
||||||
|
|
|
@ -53,14 +53,16 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
userModificationRequest := &model.UserModificationRequest{
|
userModificationRequest := &model.UserModificationRequest{
|
||||||
Username: model.OptionalString(settingsForm.Username),
|
Username: model.OptionalString(settingsForm.Username),
|
||||||
Password: model.OptionalString(settingsForm.Password),
|
Password: model.OptionalString(settingsForm.Password),
|
||||||
Theme: model.OptionalString(settingsForm.Theme),
|
Theme: model.OptionalString(settingsForm.Theme),
|
||||||
Language: model.OptionalString(settingsForm.Language),
|
Language: model.OptionalString(settingsForm.Language),
|
||||||
Timezone: model.OptionalString(settingsForm.Timezone),
|
Timezone: model.OptionalString(settingsForm.Timezone),
|
||||||
EntryDirection: model.OptionalString(settingsForm.EntryDirection),
|
EntryDirection: model.OptionalString(settingsForm.EntryDirection),
|
||||||
EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage),
|
EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage),
|
||||||
DisplayMode: model.OptionalString(settingsForm.DisplayMode),
|
DisplayMode: model.OptionalString(settingsForm.DisplayMode),
|
||||||
|
DefaultReadingSpeed: model.OptionalInt(settingsForm.DefaultReadingSpeed),
|
||||||
|
CJKReadingSpeed: model.OptionalInt(settingsForm.CJKReadingSpeed),
|
||||||
}
|
}
|
||||||
|
|
||||||
if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil {
|
if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil {
|
||||||
|
|
|
@ -79,6 +79,25 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if changes.DefaultReadingSpeed != nil {
|
||||||
|
if err := validateReadingSpeed(*changes.DefaultReadingSpeed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.CJKReadingSpeed != nil {
|
||||||
|
if err := validateReadingSpeed(*changes.CJKReadingSpeed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateReadingSpeed(readingSpeed int) *ValidationError {
|
||||||
|
if readingSpeed <= 0 {
|
||||||
|
return NewValidationError("error.settings_reading_speed_is_positive")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue