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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 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>
<div class="buttons">
<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
// 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) {

View file

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

View file

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

View file

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

View file

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

View file

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

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
}