Make reading speed user-configurable

This commit is contained in:
Gabriel Augendre 2021-08-30 16:53:05 +02:00 committed by Frédéric Guillot
parent 3a0aaddafd
commit 6e50ce3293
31 changed files with 395 additions and 173 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "Πρόσφατες καταχωρήσεις πρώτα",

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "हाल की प्रविष्टियाँ पहले",

View file

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

View file

@ -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": "新しい記事を最初に",

View file

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

View file

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

View file

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

View file

@ -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": "Сначала последние записи",

View file

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

View file

@ -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": "全屏",

View file

@ -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": "新->舊",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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