diff --git a/api/entry.go b/api/entry.go index 275a775a..ea944589 100644 --- a/api/entry.go +++ b/api/entry.go @@ -193,6 +193,14 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) { 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.WithFeedID(entry.FeedID) feed, err := feedBuilder.GetFeed() @@ -206,7 +214,7 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) { return } - if err := processor.ProcessEntryWebPage(feed, entry); err != nil { + if err := processor.ProcessEntryWebPage(feed, entry, user); err != nil { json.ServerError(w, r, err) return } diff --git a/client/model.go b/client/model.go index 660b2829..c54f1113 100644 --- a/client/model.go +++ b/client/model.go @@ -18,24 +18,26 @@ const ( // User represents a user in the system. type User struct { - ID int64 `json:"id"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - IsAdmin bool `json:"is_admin"` - Theme string `json:"theme"` - Language string `json:"language"` - Timezone string `json:"timezone"` - EntryDirection string `json:"entry_sorting_direction"` - EntryOrder string `json:"entry_sorting_order"` - Stylesheet string `json:"stylesheet"` - GoogleID string `json:"google_id"` - OpenIDConnectID string `json:"openid_connect_id"` - EntriesPerPage int `json:"entries_per_page"` - KeyboardShortcuts bool `json:"keyboard_shortcuts"` - ShowReadingTime bool `json:"show_reading_time"` - EntrySwipe bool `json:"entry_swipe"` - LastLoginAt *time.Time `json:"last_login_at"` - DisplayMode string `json:"display_mode"` + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + IsAdmin bool `json:"is_admin"` + Theme string `json:"theme"` + Language string `json:"language"` + Timezone string `json:"timezone"` + EntryDirection string `json:"entry_sorting_direction"` + EntryOrder string `json:"entry_sorting_order"` + Stylesheet string `json:"stylesheet"` + GoogleID string `json:"google_id"` + OpenIDConnectID string `json:"openid_connect_id"` + EntriesPerPage int `json:"entries_per_page"` + KeyboardShortcuts bool `json:"keyboard_shortcuts"` + ShowReadingTime bool `json:"show_reading_time"` + EntrySwipe bool `json:"entry_swipe"` + LastLoginAt *time.Time `json:"last_login_at"` + DisplayMode string `json:"display_mode"` + DefaultReadingSpeed int `json:"default_reading_speed"` + CJKReadingSpeed int `json:"cjk_reading_speed"` } func (u User) String() string { @@ -53,22 +55,24 @@ type UserCreationRequest struct { // UserModificationRequest represents the request to update a user. type UserModificationRequest struct { - Username *string `json:"username"` - Password *string `json:"password"` - IsAdmin *bool `json:"is_admin"` - Theme *string `json:"theme"` - Language *string `json:"language"` - Timezone *string `json:"timezone"` - EntryDirection *string `json:"entry_sorting_direction"` - EntryOrder *string `json:"entry_sorting_order"` - Stylesheet *string `json:"stylesheet"` - GoogleID *string `json:"google_id"` - OpenIDConnectID *string `json:"openid_connect_id"` - EntriesPerPage *int `json:"entries_per_page"` - KeyboardShortcuts *bool `json:"keyboard_shortcuts"` - ShowReadingTime *bool `json:"show_reading_time"` - EntrySwipe *bool `json:"entry_swipe"` - DisplayMode *string `json:"display_mode"` + Username *string `json:"username"` + Password *string `json:"password"` + IsAdmin *bool `json:"is_admin"` + Theme *string `json:"theme"` + Language *string `json:"language"` + Timezone *string `json:"timezone"` + EntryDirection *string `json:"entry_sorting_direction"` + EntryOrder *string `json:"entry_sorting_order"` + Stylesheet *string `json:"stylesheet"` + GoogleID *string `json:"google_id"` + OpenIDConnectID *string `json:"openid_connect_id"` + EntriesPerPage *int `json:"entries_per_page"` + KeyboardShortcuts *bool `json:"keyboard_shortcuts"` + ShowReadingTime *bool `json:"show_reading_time"` + EntrySwipe *bool `json:"entry_swipe"` + DisplayMode *string `json:"display_mode"` + DefaultReadingSpeed *int `json:"default_reading_speed"` + CJKReadingSpeed *int `json:"cjk_reading_speed"` } // Users represents a list of users. diff --git a/database/migrations.go b/database/migrations.go index 7e68682f..093be271 100644 --- a/database/migrations.go +++ b/database/migrations.go @@ -597,4 +597,11 @@ var migrations = []func(tx *sql.Tx) error{ `) 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 + }, } diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json index 3b7afb13..f4299068 100644 --- a/locale/translations/de_DE.json +++ b/locale/translations/de_DE.json @@ -243,6 +243,7 @@ "error.different_passwords": "Passwörter stimmen nicht überein.", "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_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.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.", "error.feed_already_exists": "Dieser Feed existiert bereits.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Thema", "form.prefs.label.entry_sorting": "Sortierung der Artikel", "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.select.older_first": "Älteste Artikel zuerst", "form.prefs.select.recent_first": "Neueste Artikel zuerst", diff --git a/locale/translations/el_EL.json b/locale/translations/el_EL.json index f17a2f5b..8525ad29 100644 --- a/locale/translations/el_EL.json +++ b/locale/translations/el_EL.json @@ -248,6 +248,7 @@ "error.different_passwords": "Οι κωδικοί πρόσβασης δεν είναι οι ίδιοι.", "error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.", "error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.", + "error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.", "error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.", "error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.", "error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Θέμα", "form.prefs.label.entry_sorting": "Ταξινόμηση", "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.select.older_first": "Παλαιότερες καταχωρήσεις πρώτα", "form.prefs.select.recent_first": "Πρόσφατες καταχωρήσεις πρώτα", diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json index d6aacce4..85623f9e 100644 --- a/locale/translations/en_US.json +++ b/locale/translations/en_US.json @@ -248,6 +248,7 @@ "error.different_passwords": "Passwords are not the same.", "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_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.feed_mandatory_fields": "The URL and the category are mandatory.", "error.feed_already_exists": "This feed already exists.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Theme", "form.prefs.label.entry_sorting": "Entry Sorting", "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.select.older_first": "Older entries first", "form.prefs.select.recent_first": "Recent entries first", diff --git a/locale/translations/es_ES.json b/locale/translations/es_ES.json index 38a13444..1cac1de8 100644 --- a/locale/translations/es_ES.json +++ b/locale/translations/es_ES.json @@ -243,6 +243,7 @@ "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.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.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.", "error.feed_already_exists": "Este feed ya existe.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Tema", "form.prefs.label.entry_sorting": "Clasificación de entradas", "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.select.older_first": "Entradas más viejas primero", "form.prefs.select.recent_first": "Entradas recientes primero", diff --git a/locale/translations/fi_FI.json b/locale/translations/fi_FI.json index 89d74d14..f5ab63cb 100644 --- a/locale/translations/fi_FI.json +++ b/locale/translations/fi_FI.json @@ -248,6 +248,7 @@ "error.different_passwords": "Salasanat eivät ole samat.", "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_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.", "error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.", "error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.", "error.feed_already_exists": "Tämä syöte on jo olemassa.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Teema", "form.prefs.label.entry_sorting": "Lajittelu", "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.select.older_first": "Vanhin ensin", "form.prefs.select.recent_first": "Uusin ensin", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index 505f9e8e..679ef80d 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -243,6 +243,7 @@ "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.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.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.feed_already_exists": "Ce flux existe déjà.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Thème", "form.prefs.label.entry_sorting": "Ordre des éléments", "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.select.older_first": "Ancien éléments en premier", "form.prefs.select.recent_first": "Éléments récents en premier", diff --git a/locale/translations/hi_IN.json b/locale/translations/hi_IN.json index c9695863..8c889079 100644 --- a/locale/translations/hi_IN.json +++ b/locale/translations/hi_IN.json @@ -248,6 +248,7 @@ "error.different_passwords": "पासवर्ड एक जैसे नहीं हैं।", "error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।", "error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।", + "error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।", "error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।", "error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।", "error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "थीम", "form.prefs.label.entry_sorting": "प्रवेश छँटाई", "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.select.older_first": "पहले पुरानी प्रविष्टियाँ", "form.prefs.select.recent_first": "हाल की प्रविष्टियाँ पहले", diff --git a/locale/translations/it_IT.json b/locale/translations/it_IT.json index e46249c4..fe8476b8 100644 --- a/locale/translations/it_IT.json +++ b/locale/translations/it_IT.json @@ -243,6 +243,7 @@ "error.different_passwords": "Le password non coincidono.", "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_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.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.", "error.feed_already_exists": "Questo feed esiste già.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Tema", "form.prefs.label.entry_sorting": "Ordinamento articoli", "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.select.older_first": "Prima i più vecchi", "form.prefs.select.recent_first": "Prima i più recenti", diff --git a/locale/translations/ja_JP.json b/locale/translations/ja_JP.json index 1535c3ee..8b9d53c8 100644 --- a/locale/translations/ja_JP.json +++ b/locale/translations/ja_JP.json @@ -243,6 +243,7 @@ "error.different_passwords": "パスワードが一致しません。", "error.password_min_length": "パスワードは6文字以上である必要があります。", "error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。", + "error.settings_reading_speed_is_positive": "読み取り速度は正の整数でなければならない。", "error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。", "error.feed_mandatory_fields": "URL と カテゴリが必要です。", "error.feed_already_exists": "このフィードはすでに存在します。", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "テーマ", "form.prefs.label.entry_sorting": "記事の並べ替え", "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.select.older_first": "古い記事を最初に", "form.prefs.select.recent_first": "新しい記事を最初に", diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json index 291cf85d..79adda7c 100644 --- a/locale/translations/nl_NL.json +++ b/locale/translations/nl_NL.json @@ -243,6 +243,7 @@ "error.different_passwords": "Wachtwoorden zijn niet hetzelfde.", "error.password_min_length": "Je moet minstens 6 tekens gebruiken.", "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.feed_mandatory_fields": "The URL en de categorie zijn verplicht.", "error.feed_already_exists": "Deze feed bestaat al.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Skin", "form.prefs.label.entry_sorting": "Volgorde van items", "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.select.older_first": "Oudere items eerst", "form.prefs.select.recent_first": "Recente items eerst", diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json index 28abe75b..58fa35fc 100644 --- a/locale/translations/pl_PL.json +++ b/locale/translations/pl_PL.json @@ -245,6 +245,7 @@ "error.different_passwords": "Hasła nie są identyczne.", "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_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.feed_mandatory_fields": "URL i kategoria są obowiązkowe.", "error.feed_already_exists": "Ten kanał już istnieje.", @@ -294,6 +295,8 @@ "form.prefs.label.theme": "Wygląd", "form.prefs.label.entry_sorting": "Sortowanie artykułów", "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.select.older_first": "Najstarsze wpisy jako pierwsze", "form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe", diff --git a/locale/translations/pt_BR.json b/locale/translations/pt_BR.json index 15e338fd..73cb3ba9 100644 --- a/locale/translations/pt_BR.json +++ b/locale/translations/pt_BR.json @@ -243,6 +243,7 @@ "error.different_passwords": "As senhas não são iguais.", "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_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.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.", "error.feed_already_exists": "Este feed já existe.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Tema", "form.prefs.label.entry_sorting": "Ordenação dos itens", "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.select.older_first": "Itens mais velhos primeiro", "form.prefs.select.recent_first": "Itens mais recentes", diff --git a/locale/translations/ru_RU.json b/locale/translations/ru_RU.json index 4fe38f5f..9e402949 100644 --- a/locale/translations/ru_RU.json +++ b/locale/translations/ru_RU.json @@ -245,6 +245,7 @@ "error.different_passwords": "Пароли не совпадают.", "error.password_min_length": "Вы должны использовать минимум 6 символов.", "error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.", + "error.settings_reading_speed_is_positive": "Скорости считывания должны быть целыми положительными числами.", "error.entries_per_page_invalid": "Количество записей на странице недействительно.", "error.feed_mandatory_fields": "URL и категория обязательны.", "error.feed_already_exists": "Этот фид уже существует.", @@ -294,6 +295,8 @@ "form.prefs.label.theme": "Тема", "form.prefs.label.entry_sorting": "Сортировка записей", "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.select.older_first": "Сначала старые записи", "form.prefs.select.recent_first": "Сначала последние записи", diff --git a/locale/translations/tr_TR.json b/locale/translations/tr_TR.json index 6a3abeff..ae83346f 100644 --- a/locale/translations/tr_TR.json +++ b/locale/translations/tr_TR.json @@ -248,6 +248,7 @@ "error.different_passwords": "Parolalar eşleşmiyor.", "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_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.feed_mandatory_fields": "URL ve kategori zorunlu.", "error.feed_already_exists": "Bu besleme zaten mevcut.", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "Tema", "form.prefs.label.entry_sorting": "İleti Sıralaması", "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.select.older_first": "Önce eski iletiler", "form.prefs.select.recent_first": "Önce yeni iletiler", diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json index 1ead7e4e..586d08d3 100644 --- a/locale/translations/zh_CN.json +++ b/locale/translations/zh_CN.json @@ -249,6 +249,7 @@ "error.feed_url_not_empty": "订阅源的网址不能为空。", "error.site_url_not_empty": "源网站的网址不能为空。", "error.feed_title_not_empty": "订阅源的标题不能为空。", + "error.settings_reading_speed_is_positive": "阅读速度必须是正整数。", "error.feed_category_not_found": "此类别不存在或不属于该用户。", "error.feed_invalid_blocklist_rule": "阻止列表规则无效。", "error.feed_invalid_keeplist_rule": "保留列表规则无效。", @@ -291,6 +292,8 @@ "form.prefs.label.entry_sorting": "文章排序", "form.prefs.label.entries_per_page": "每页文章数", "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.recent_first": "新->旧", "form.prefs.select.fullscreen": "全屏", diff --git a/locale/translations/zh_TW.json b/locale/translations/zh_TW.json index 12f9a3db..fe25c882 100644 --- a/locale/translations/zh_TW.json +++ b/locale/translations/zh_TW.json @@ -243,6 +243,7 @@ "error.different_passwords": "兩次輸入的密碼不同", "error.password_min_length": "請至少輸入 6 個字元", "error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區", + "error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.", "error.entries_per_page_invalid": "每頁的文章數無效。", "error.feed_mandatory_fields": "必須填寫網址和分類", "error.feed_already_exists": "此Feed已存在。", @@ -292,6 +293,8 @@ "form.prefs.label.theme": "主題", "form.prefs.label.entry_sorting": "文章排序", "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.select.older_first": "舊->新", "form.prefs.select.recent_first": "新->舊", diff --git a/model/user.go b/model/user.go index 6ff7332a..b1f335c9 100644 --- a/model/user.go +++ b/model/user.go @@ -12,24 +12,26 @@ import ( // User represents a user in the system. type User struct { - ID int64 `json:"id"` - Username string `json:"username"` - Password string `json:"-"` - IsAdmin bool `json:"is_admin"` - Theme string `json:"theme"` - Language string `json:"language"` - Timezone string `json:"timezone"` - EntryDirection string `json:"entry_sorting_direction"` - EntryOrder string `json:"entry_sorting_order"` - Stylesheet string `json:"stylesheet"` - GoogleID string `json:"google_id"` - OpenIDConnectID string `json:"openid_connect_id"` - EntriesPerPage int `json:"entries_per_page"` - KeyboardShortcuts bool `json:"keyboard_shortcuts"` - ShowReadingTime bool `json:"show_reading_time"` - EntrySwipe bool `json:"entry_swipe"` - LastLoginAt *time.Time `json:"last_login_at"` - DisplayMode string `json:"display_mode"` + ID int64 `json:"id"` + Username string `json:"username"` + Password string `json:"-"` + IsAdmin bool `json:"is_admin"` + Theme string `json:"theme"` + Language string `json:"language"` + Timezone string `json:"timezone"` + EntryDirection string `json:"entry_sorting_direction"` + EntryOrder string `json:"entry_sorting_order"` + Stylesheet string `json:"stylesheet"` + GoogleID string `json:"google_id"` + OpenIDConnectID string `json:"openid_connect_id"` + EntriesPerPage int `json:"entries_per_page"` + KeyboardShortcuts bool `json:"keyboard_shortcuts"` + ShowReadingTime bool `json:"show_reading_time"` + EntrySwipe bool `json:"entry_swipe"` + LastLoginAt *time.Time `json:"last_login_at"` + 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. @@ -43,22 +45,24 @@ type UserCreationRequest struct { // UserModificationRequest represents the request to update a user. type UserModificationRequest struct { - Username *string `json:"username"` - Password *string `json:"password"` - Theme *string `json:"theme"` - Language *string `json:"language"` - Timezone *string `json:"timezone"` - EntryDirection *string `json:"entry_sorting_direction"` - EntryOrder *string `json:"entry_sorting_order"` - Stylesheet *string `json:"stylesheet"` - GoogleID *string `json:"google_id"` - OpenIDConnectID *string `json:"openid_connect_id"` - EntriesPerPage *int `json:"entries_per_page"` - IsAdmin *bool `json:"is_admin"` - KeyboardShortcuts *bool `json:"keyboard_shortcuts"` - ShowReadingTime *bool `json:"show_reading_time"` - EntrySwipe *bool `json:"entry_swipe"` - DisplayMode *string `json:"display_mode"` + Username *string `json:"username"` + Password *string `json:"password"` + Theme *string `json:"theme"` + Language *string `json:"language"` + Timezone *string `json:"timezone"` + EntryDirection *string `json:"entry_sorting_direction"` + EntryOrder *string `json:"entry_sorting_order"` + Stylesheet *string `json:"stylesheet"` + GoogleID *string `json:"google_id"` + OpenIDConnectID *string `json:"openid_connect_id"` + EntriesPerPage *int `json:"entries_per_page"` + IsAdmin *bool `json:"is_admin"` + KeyboardShortcuts *bool `json:"keyboard_shortcuts"` + ShowReadingTime *bool `json:"show_reading_time"` + EntrySwipe *bool `json:"entry_swipe"` + 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. @@ -126,6 +130,14 @@ func (u *UserModificationRequest) Patch(user *User) { if u.DisplayMode != nil { 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. diff --git a/reader/handler/handler.go b/reader/handler/handler.go index bd775104..be9d6a63 100644 --- a/reader/handler/handler.go +++ b/reader/handler/handler.go @@ -32,6 +32,11 @@ var ( 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)) + user, storeErr := store.UserByID(userID) + if storeErr != nil { + return nil, storeErr + } + if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) { return nil, errors.NewLocalizedError(errCategoryNotFound) } @@ -79,7 +84,7 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model subscription.WithClientResponse(response) subscription.CheckedNow() - processor.ProcessFeedEntries(store, subscription) + processor.ProcessFeedEntries(store, subscription, user) if storeErr := store.CreateFeed(subscription); storeErr != nil { return nil, storeErr @@ -101,8 +106,12 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model // RefreshFeed refreshes a feed. func RefreshFeed(store *storage.Storage, userID, feedID int64) error { defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[RefreshFeed] feedID=%d", feedID)) - userLanguage := store.UserLanguage(userID) - printer := locale.NewPrinter(userLanguage) + user, storeErr := store.UserByID(userID) + if storeErr != nil { + return storeErr + } + + printer := locale.NewPrinter(user.Language) originalFeed, storeErr := store.FeedByID(userID, feedID) if storeErr != nil { @@ -164,7 +173,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64) error { } 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). if storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, !originalFeed.Crawler); storeErr != nil { diff --git a/reader/processor/processor.go b/reader/processor/processor.go index 4df0e510..26b5b87e 100644 --- a/reader/processor/processor.go +++ b/reader/processor/processor.go @@ -38,7 +38,7 @@ var ( ) // 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 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) } @@ -127,7 +127,7 @@ func isAllowedEntry(feed *model.Feed, entry *model.Entry) bool { } // 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() url := getUrlFromEntry(feed, entry) @@ -157,7 +157,7 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry) error { if content != "" { entry.Content = content - entry.ReadingTime = calculateReadingTime(content) + entry.ReadingTime = calculateReadingTime(content, user) } return nil @@ -179,7 +179,7 @@ func getUrlFromEntry(feed *model.Feed, entry *model.Entry) string { 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 entryIsNew { 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. 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 } -func calculateReadingTime(content string) int { +func calculateReadingTime(content string, user *model.User) int { sanitizedContent := sanitizer.StripTags(content) languageInfo := getlang.FromString(sanitizedContent) var timeToReadInt int 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 { nbOfWords := len(strings.Fields(sanitizedContent)) - timeToReadInt = int(math.Ceil(float64(nbOfWords) / 265)) + timeToReadInt = int(math.Ceil(float64(nbOfWords) / float64(user.DefaultReadingSpeed))) } return timeToReadInt diff --git a/storage/user.go b/storage/user.go index 2195ddfc..dded2185 100644 --- a/storage/user.go +++ b/storage/user.go @@ -85,7 +85,9 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m google_id, openid_connect_id, display_mode, - entry_order + entry_order, + default_reading_speed, + cjk_reading_speed ` tx, err := s.db.Begin() @@ -118,6 +120,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m &user.OpenIDConnectID, &user.DisplayMode, &user.EntryOrder, + &user.DefaultReadingSpeed, + &user.CJKReadingSpeed, ) if err != nil { tx.Rollback() @@ -168,9 +172,11 @@ func (s *Storage) UpdateUser(user *model.User) error { google_id=$13, openid_connect_id=$14, display_mode=$15, - entry_order=$16 + entry_order=$16, + default_reading_speed=$17, + cjk_reading_speed=$18 WHERE - id=$17 + id=$19 ` _, err = s.db.Exec( @@ -191,6 +197,8 @@ func (s *Storage) UpdateUser(user *model.User) error { user.OpenIDConnectID, user.DisplayMode, user.EntryOrder, + user.DefaultReadingSpeed, + user.CJKReadingSpeed, user.ID, ) if err != nil { @@ -213,9 +221,11 @@ func (s *Storage) UpdateUser(user *model.User) error { google_id=$12, openid_connect_id=$13, display_mode=$14, - entry_order=$15 + entry_order=$15, + default_reading_speed=$16, + cjk_reading_speed=$17 WHERE - id=$16 + id=$18 ` _, err := s.db.Exec( @@ -235,6 +245,8 @@ func (s *Storage) UpdateUser(user *model.User) error { user.OpenIDConnectID, user.DisplayMode, user.EntryOrder, + user.DefaultReadingSpeed, + user.CJKReadingSpeed, user.ID, ) @@ -276,7 +288,9 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) { google_id, openid_connect_id, display_mode, - entry_order + entry_order, + default_reading_speed, + cjk_reading_speed FROM users WHERE @@ -305,7 +319,9 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) { google_id, openid_connect_id, display_mode, - entry_order + entry_order, + default_reading_speed, + cjk_reading_speed FROM users WHERE @@ -334,7 +350,9 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) { google_id, openid_connect_id, display_mode, - entry_order + entry_order, + default_reading_speed, + cjk_reading_speed FROM users WHERE @@ -370,7 +388,9 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) { u.google_id, u.openid_connect_id, u.display_mode, - u.entry_order + u.entry_order, + u.default_reading_speed, + u.cjk_reading_speed FROM users u LEFT JOIN @@ -401,6 +421,8 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err &user.OpenIDConnectID, &user.DisplayMode, &user.EntryOrder, + &user.DefaultReadingSpeed, + &user.CJKReadingSpeed, ) if err == sql.ErrNoRows { @@ -492,7 +514,9 @@ func (s *Storage) Users() (model.Users, error) { google_id, openid_connect_id, display_mode, - entry_order + entry_order, + default_reading_speed, + cjk_reading_speed FROM users ORDER BY username ASC @@ -524,6 +548,8 @@ func (s *Storage) Users() (model.Users, error) { &user.OpenIDConnectID, &user.DisplayMode, &user.EntryOrder, + &user.DefaultReadingSpeed, + &user.CJKReadingSpeed, ) if err != nil { diff --git a/template/templates/views/settings.html b/template/templates/views/settings.html index 3826473c..87a77bbb 100644 --- a/template/templates/views/settings.html +++ b/template/templates/views/settings.html @@ -72,6 +72,12 @@ + + + + + +
diff --git a/tests/user_test.go b/tests/user_test.go index de7668ec..74e86cef 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -2,6 +2,7 @@ // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. +//go:build integration // +build integration package tests @@ -86,6 +87,14 @@ func TestGetUsers(t *testing.T) { if users[0].DisplayMode != "standalone" { 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) { @@ -135,6 +144,14 @@ func TestCreateStandardUser(t *testing.T) { if user.DisplayMode != "standalone" { 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) { @@ -207,6 +224,14 @@ func TestGetUserByID(t *testing.T) { if user.DisplayMode != "standalone" { 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) { @@ -266,6 +291,14 @@ func TestGetUserByUsername(t *testing.T) { if user.DisplayMode != "standalone" { 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) { @@ -299,11 +332,15 @@ func TestUpdateUserFields(t *testing.T) { swipe := false entriesPerPage := 5 displayMode := "fullscreen" + defaultReadingSpeed := 380 + cjkReadingSpeed := 200 user, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{ - Stylesheet: &stylesheet, - EntrySwipe: &swipe, - EntriesPerPage: &entriesPerPage, - DisplayMode: &displayMode, + Stylesheet: &stylesheet, + EntrySwipe: &swipe, + EntriesPerPage: &entriesPerPage, + DisplayMode: &displayMode, + DefaultReadingSpeed: &defaultReadingSpeed, + CJKReadingSpeed: &cjkReadingSpeed, }) if err != nil { t.Fatal(err) @@ -324,6 +361,14 @@ func TestUpdateUserFields(t *testing.T) { if 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) { diff --git a/ui/entry_scraper.go b/ui/entry_scraper.go index a17427c1..a7cc0403 100644 --- a/ui/entry_scraper.go +++ b/ui/entry_scraper.go @@ -34,6 +34,14 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) { 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.WithFeedID(entry.FeedID) feed, err := feedBuilder.GetFeed() @@ -47,12 +55,14 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) { return } - if err := processor.ProcessEntryWebPage(feed, entry); err != nil { + if err := processor.ProcessEntryWebPage(feed, entry, user); err != nil { json.ServerError(w, r, err) 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)}) } diff --git a/ui/form/settings.go b/ui/form/settings.go index 47a17b2a..f82c1ae8 100644 --- a/ui/form/settings.go +++ b/ui/form/settings.go @@ -14,20 +14,22 @@ import ( // SettingsForm represents the settings form. type SettingsForm struct { - Username string - Password string - Confirmation string - Theme string - Language string - Timezone string - EntryDirection string - EntryOrder string - EntriesPerPage int - KeyboardShortcuts bool - ShowReadingTime bool - CustomCSS string - EntrySwipe bool - DisplayMode string + Username string + Password string + Confirmation string + Theme string + Language string + Timezone string + EntryDirection string + EntryOrder string + EntriesPerPage int + KeyboardShortcuts bool + ShowReadingTime bool + CustomCSS string + EntrySwipe bool + DisplayMode string + DefaultReadingSpeed int + CJKReadingSpeed int } // 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.EntrySwipe = s.EntrySwipe user.DisplayMode = s.DisplayMode + user.CJKReadingSpeed = s.CJKReadingSpeed + user.DefaultReadingSpeed = s.DefaultReadingSpeed if s.Password != "" { user.Password = s.Password @@ -58,6 +62,10 @@ func (s *SettingsForm) Validate() error { 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 == "" { // Firefox insists on auto-completing the password field. // If the confirmation field is blank, the user probably @@ -78,20 +86,30 @@ func NewSettingsForm(r *http.Request) *SettingsForm { if err != nil { 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{ - Username: r.FormValue("username"), - Password: r.FormValue("password"), - Confirmation: r.FormValue("confirmation"), - Theme: r.FormValue("theme"), - Language: r.FormValue("language"), - Timezone: r.FormValue("timezone"), - EntryDirection: r.FormValue("entry_direction"), - EntryOrder: r.FormValue("entry_order"), - EntriesPerPage: int(entriesPerPage), - KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1", - ShowReadingTime: r.FormValue("show_reading_time") == "1", - CustomCSS: r.FormValue("custom_css"), - EntrySwipe: r.FormValue("entry_swipe") == "1", - DisplayMode: r.FormValue("display_mode"), + Username: r.FormValue("username"), + Password: r.FormValue("password"), + Confirmation: r.FormValue("confirmation"), + Theme: r.FormValue("theme"), + Language: r.FormValue("language"), + Timezone: r.FormValue("timezone"), + EntryDirection: r.FormValue("entry_direction"), + EntryOrder: r.FormValue("entry_order"), + EntriesPerPage: int(entriesPerPage), + KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1", + ShowReadingTime: r.FormValue("show_reading_time") == "1", + CustomCSS: r.FormValue("custom_css"), + EntrySwipe: r.FormValue("entry_swipe") == "1", + DisplayMode: r.FormValue("display_mode"), + DefaultReadingSpeed: int(defaultReadingSpeed), + CJKReadingSpeed: int(cjkReadingSpeed), } } diff --git a/ui/form/settings_test.go b/ui/form/settings_test.go index cf3aaa73..e6c973d1 100644 --- a/ui/form/settings_test.go +++ b/ui/form/settings_test.go @@ -6,15 +6,17 @@ import ( func TestValid(t *testing.T) { settings := &SettingsForm{ - Username: "user", - Password: "hunter2", - Confirmation: "hunter2", - Theme: "default", - Language: "en_US", - Timezone: "UTC", - EntryDirection: "asc", - EntriesPerPage: 50, - DisplayMode: "standalone", + Username: "user", + Password: "hunter2", + Confirmation: "hunter2", + Theme: "default", + Language: "en_US", + Timezone: "UTC", + EntryDirection: "asc", + EntriesPerPage: 50, + DisplayMode: "standalone", + DefaultReadingSpeed: 35, + CJKReadingSpeed: 25, } err := settings.Validate() @@ -25,15 +27,17 @@ func TestValid(t *testing.T) { func TestConfirmationEmpty(t *testing.T) { settings := &SettingsForm{ - Username: "user", - Password: "hunter2", - Confirmation: "", - Theme: "default", - Language: "en_US", - Timezone: "UTC", - EntryDirection: "asc", - EntriesPerPage: 50, - DisplayMode: "standalone", + Username: "user", + Password: "hunter2", + Confirmation: "", + Theme: "default", + Language: "en_US", + Timezone: "UTC", + EntryDirection: "asc", + EntriesPerPage: 50, + DisplayMode: "standalone", + DefaultReadingSpeed: 35, + CJKReadingSpeed: 25, } err := settings.Validate() @@ -48,15 +52,17 @@ func TestConfirmationEmpty(t *testing.T) { func TestConfirmationIncorrect(t *testing.T) { settings := &SettingsForm{ - Username: "user", - Password: "hunter2", - Confirmation: "unter2", - Theme: "default", - Language: "en_US", - Timezone: "UTC", - EntryDirection: "asc", - EntriesPerPage: 50, - DisplayMode: "standalone", + Username: "user", + Password: "hunter2", + Confirmation: "unter2", + Theme: "default", + Language: "en_US", + Timezone: "UTC", + EntryDirection: "asc", + EntriesPerPage: 50, + DisplayMode: "standalone", + DefaultReadingSpeed: 35, + CJKReadingSpeed: 25, } err := settings.Validate() diff --git a/ui/settings_show.go b/ui/settings_show.go index 14a3c30d..595f32e8 100644 --- a/ui/settings_show.go +++ b/ui/settings_show.go @@ -27,18 +27,20 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { } settingsForm := form.SettingsForm{ - Username: user.Username, - Theme: user.Theme, - Language: user.Language, - Timezone: user.Timezone, - EntryDirection: user.EntryDirection, - EntryOrder: user.EntryOrder, - EntriesPerPage: user.EntriesPerPage, - KeyboardShortcuts: user.KeyboardShortcuts, - ShowReadingTime: user.ShowReadingTime, - CustomCSS: user.Stylesheet, - EntrySwipe: user.EntrySwipe, - DisplayMode: user.DisplayMode, + Username: user.Username, + Theme: user.Theme, + Language: user.Language, + Timezone: user.Timezone, + EntryDirection: user.EntryDirection, + EntryOrder: user.EntryOrder, + EntriesPerPage: user.EntriesPerPage, + KeyboardShortcuts: user.KeyboardShortcuts, + ShowReadingTime: user.ShowReadingTime, + CustomCSS: user.Stylesheet, + EntrySwipe: user.EntrySwipe, + DisplayMode: user.DisplayMode, + DefaultReadingSpeed: user.DefaultReadingSpeed, + CJKReadingSpeed: user.CJKReadingSpeed, } timezones, err := h.store.Timezones() diff --git a/ui/settings_update.go b/ui/settings_update.go index e58bf8a2..06812561 100644 --- a/ui/settings_update.go +++ b/ui/settings_update.go @@ -53,14 +53,16 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) { } userModificationRequest := &model.UserModificationRequest{ - Username: model.OptionalString(settingsForm.Username), - Password: model.OptionalString(settingsForm.Password), - Theme: model.OptionalString(settingsForm.Theme), - Language: model.OptionalString(settingsForm.Language), - Timezone: model.OptionalString(settingsForm.Timezone), - EntryDirection: model.OptionalString(settingsForm.EntryDirection), - EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage), - DisplayMode: model.OptionalString(settingsForm.DisplayMode), + Username: model.OptionalString(settingsForm.Username), + Password: model.OptionalString(settingsForm.Password), + Theme: model.OptionalString(settingsForm.Theme), + Language: model.OptionalString(settingsForm.Language), + Timezone: model.OptionalString(settingsForm.Timezone), + EntryDirection: model.OptionalString(settingsForm.EntryDirection), + EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage), + 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 { diff --git a/validator/user.go b/validator/user.go index 9f1a7b48..0e049461 100644 --- a/validator/user.go +++ b/validator/user.go @@ -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 }