Add option to change the number of entries per page (fixes #40)

pull/730/head
logan 4 years ago committed by Frédéric Guillot
parent e32fa059e5
commit 5f266319a3
  1. 5
      api/payload.go
  2. 2
      client/core.go
  3. 2
      database/migration.go
  4. 3
      database/sql.go
  5. 1
      database/sql/schema_version_32.sql
  6. 40
      locale/translations.go
  7. 2
      locale/translations/de_DE.json
  8. 2
      locale/translations/en_US.json
  9. 2
      locale/translations/es_ES.json
  10. 2
      locale/translations/fr_FR.json
  11. 2
      locale/translations/it_IT.json
  12. 2
      locale/translations/ja_JP.json
  13. 2
      locale/translations/nl_NL.json
  14. 2
      locale/translations/pl_PL.json
  15. 2
      locale/translations/ru_RU.json
  16. 2
      locale/translations/zh_CN.json
  17. 1
      model/user.go
  18. 22
      storage/user.go
  19. 3
      template/html/settings.html
  20. 5
      template/views.go
  21. 16
      tests/user_test.go
  22. 4
      ui/bookmark_entries.go
  23. 4
      ui/category_entries.go
  24. 4
      ui/category_entries_all.go
  25. 4
      ui/feed_entries.go
  26. 4
      ui/feed_entries_all.go
  27. 12
      ui/form/settings.go
  28. 21
      ui/form/settings_test.go
  29. 4
      ui/history_entries.go
  30. 6
      ui/pagination.go
  31. 4
      ui/search_entries.go
  32. 1
      ui/settings_show.go
  33. 24
      ui/static/css.go
  34. 10
      ui/static/css/common.css
  35. 4
      ui/unread_entries.go

@ -109,6 +109,7 @@ type userModification struct {
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntriesPerPage *int `json:"entries_per_page"`
}
func (u *userModification) Update(user *model.User) {
@ -139,6 +140,10 @@ func (u *userModification) Update(user *model.User) {
if u.EntryDirection != nil {
user.EntryDirection = *u.EntryDirection
}
if u.EntriesPerPage != nil {
user.EntriesPerPage = *u.EntriesPerPage
}
}
func decodeUserModificationPayload(r io.ReadCloser) (*userModification, error) {

@ -26,6 +26,7 @@ type User struct {
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntriesPerPage int `json:"entries_per_page"`
LastLoginAt *time.Time `json:"last_login_at"`
Extra map[string]string `json:"extra"`
}
@ -43,6 +44,7 @@ type UserModification struct {
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
EntriesPerPage *int `json:"entries_per_page"`
}
// Users represents a list of users.

@ -12,7 +12,7 @@ import (
"miniflux.app/logger"
)
const schemaVersion = 31
const schemaVersion = 32
// Migrate executes database migrations.
func Migrate(db *sql.DB) {

@ -183,6 +183,8 @@ create unique index entries_share_code_idx on entries using btree(share_code) wh
create index entries_user_feed_idx on entries (user_id, feed_id);
`,
"schema_version_31": `alter table feeds add column ignore_http_cache bool default false;`,
"schema_version_32": `alter table users add column entries_per_page int default 100;
`,
"schema_version_4": `create type entry_sorting_direction as enum('asc', 'desc');
alter table users add column entry_direction entry_sorting_direction default 'asc';
`,
@ -237,6 +239,7 @@ var SqlMapChecksums = map[string]string{
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
"schema_version_30": "3ec48a9b2e7a0fc32c85f31652f723565c34213f5f2d7e5e5076aad8f0b40d23",
"schema_version_31": "9290ef295731b03ddfe32dcaded0be70d41b63572420ad379cf2874a9b54581c",
"schema_version_32": "5b4de8dd2d7e3c6ae4150e0e3931df2ee989f2c667145bd67294e5a5f3fae456",
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
"schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c",
"schema_version_6": "9d05b4fb223f0e60efc716add5048b0ca9c37511cf2041721e20505d6d798ce4",

@ -0,0 +1 @@
alter table users add column entries_per_page int default 100;

@ -234,6 +234,7 @@ var translations = map[string]string{
"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.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.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
@ -259,6 +260,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Zeitzone",
"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.select.older_first": "Älteste Artikel zuerst",
"form.prefs.select.recent_first": "Neueste Artikel zuerst",
"form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren",
@ -572,6 +574,7 @@ var translations = map[string]string{
"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.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.user_mandatory_fields": "The username is mandatory.",
"error.api_key_already_exists": "This API Key already exists.",
@ -597,6 +600,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Timezone",
"form.prefs.label.theme": "Theme",
"form.prefs.label.entry_sorting": "Entry Sorting",
"form.prefs.label.entries_per_page": "Entries per page",
"form.prefs.select.older_first": "Older entries first",
"form.prefs.select.recent_first": "Recent entries first",
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
@ -890,6 +894,7 @@ var translations = map[string]string{
"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.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.user_mandatory_fields": "El nombre de usuario es obligatorio.",
"error.api_key_already_exists": "Esta clave API ya existe.",
@ -915,6 +920,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Zona horaria",
"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.select.older_first": "Entradas más viejas primero",
"form.prefs.select.recent_first": "Entradas recientes primero",
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",
@ -1208,6 +1214,7 @@ var translations = map[string]string{
"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.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.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
@ -1233,6 +1240,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Fuseau horaire",
"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.select.older_first": "Ancien éléments en premier",
"form.prefs.select.recent_first": "Éléments récents en premier",
"form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier",
@ -1546,6 +1554,7 @@ var translations = map[string]string{
"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.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
"error.api_key_already_exists": "Questa chiave API esiste già.",
@ -1571,6 +1580,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Fuso orario",
"form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Ordinamento articoli",
"form.prefs.label.entries_per_page": "Articoli per pagina",
"form.prefs.select.older_first": "Prima i più vecchi",
"form.prefs.select.recent_first": "Prima i più recenti",
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
@ -1864,6 +1874,7 @@ var translations = map[string]string{
"error.different_passwords": "パスワードが一致しません。",
"error.password_min_length": "パスワードは6文字以上である必要があります。",
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
"error.user_mandatory_fields": "ユーザー名が必要です。",
"error.api_key_already_exists": "このAPIキーは既に存在します。",
@ -1889,6 +1900,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "タイムゾーン",
"form.prefs.label.theme": "テーマ",
"form.prefs.label.entry_sorting": "記事の並べ替え",
"form.prefs.label.entries_per_page": "ページあたりのエントリ",
"form.prefs.select.older_first": "古い記事を最初に",
"form.prefs.select.recent_first": "新しい記事を最初に",
"form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",
@ -2182,6 +2194,7 @@ var translations = map[string]string{
"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.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
"error.api_key_already_exists": "This API Key already exists.",
@ -2207,6 +2220,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Tijdzone",
"form.prefs.label.theme": "Skin",
"form.prefs.label.entry_sorting": "Volgorde van items",
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
"form.prefs.select.older_first": "Oudere items eerst",
"form.prefs.select.recent_first": "Recente items eerst",
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
@ -2520,6 +2534,7 @@ var translations = map[string]string{
"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.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
@ -2545,6 +2560,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Strefa czasowa",
"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.select.older_first": "Najstarsze wpisy jako pierwsze",
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",
@ -2864,6 +2880,7 @@ var translations = map[string]string{
"error.different_passwords": "Пароли не совпадают.",
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
"error.entries_per_page_invalid": "Количество записей на странице недействительно.",
"error.feed_mandatory_fields": "URL и категория обязательны.",
"error.user_mandatory_fields": "Имя пользователя обязательно.",
"error.api_key_already_exists": "Этот ключ API уже существует.",
@ -2889,6 +2906,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "Часовой пояс",
"form.prefs.label.theme": "Тема",
"form.prefs.label.entry_sorting": "Сортировка записей",
"form.prefs.label.entries_per_page": "Записи на странице",
"form.prefs.select.older_first": "Сначала старые записи",
"form.prefs.select.recent_first": "Сначала последние записи",
"form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",
@ -3186,6 +3204,7 @@ var translations = map[string]string{
"error.different_passwords": "两次输入的密码不同",
"error.password_min_length": "请至少使用6个字符",
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
"error.entries_per_page_invalid": "每页的条目数无效。",
"error.feed_mandatory_fields": "必须填写 URL 和分类",
"error.user_mandatory_fields": "必须填写用户名",
"error.api_key_already_exists": "此API密钥已存在。",
@ -3211,6 +3230,7 @@ var translations = map[string]string{
"form.prefs.label.timezone": "时区",
"form.prefs.label.theme": "主题",
"form.prefs.label.entry_sorting": "内容排序",
"form.prefs.label.entries_per_page": "每页条目",
"form.prefs.select.older_first": "旧->新",
"form.prefs.select.recent_first": "新->旧",
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
@ -3289,14 +3309,14 @@ var translations = map[string]string{
}
var translationsChecksums = map[string]string{
"de_DE": "ddb063682852c86361af350be616d3bd328373ecb927804824008d016aa7c67c",
"en_US": "350b835f759212abd2110322394aa00b666fbf27d752532a7700fb52d5af3f02",
"es_ES": "26efc79faaf35efe5a33528cedc2522496987d290c9e86d8fff3a9bcbed3e441",
"fr_FR": "e8736791d5373b955cacce215b3ae67d56280bfa5d4596899e4e5e37ff962afd",
"it_IT": "8ec2311e00c45b4d2b939ad0280fe49277f5c851a4cd521f42be1a88baef4c34",
"ja_JP": "237f49939be015b509d4b3a02890691c3766df8878109114493624cfd13c0cad",
"nl_NL": "c70e1eaa3c2e8c0130522189c3932b52ee6e9ff91c91b0090eb9178f2f23c588",
"pl_PL": "1d5e05789a3150a8f1ddbe57616d509d1d33c61b60200c563a5e23571671209e",
"ru_RU": "b0408b7a150bd79e411376ced3acb706a12e6b28e564a6abfedbdebc2d552915",
"zh_CN": "7732905e498d087c9a11ecc3eae8736e758c6b053da13de64fd6599ca40d8ee6",
"de_DE": "e986a40b1748968725ddede18ae6451e4d1ae270b9c4c033daa81ee50b1d306e",
"en_US": "b27169fc7767e51e6f7610ff1844708e8111e527c7931e3f888864a66826e293",
"es_ES": "20a713468ca6ce00e899a80354912e927ded61cf8a79ad9d976c78f515e242dd",
"fr_FR": "251eb14fe8521bde772d293fa748307ecd4cae4b0597da03aad39e745a382f11",
"it_IT": "8ab664ec8d826aa3702a4f5294c3a3e87193437e64b0ef4990a3a9609b782786",
"ja_JP": "7dc146dc5815a8d6dbae2f7f467deea598a85099bbee63e92bf3862d445519af",
"nl_NL": "fd106f08b2f8902712a68716a0e33b063bdce32a8440f7a2b296b4f822088403",
"pl_PL": "85de665d29e873f6099ef5ea40efe569a05ec3cbf08e4ca7741778bf3d5c8593",
"ru_RU": "6e765e44e250469fe1c5666f8ff24e5e07e6b04098c1325c2663a1f722e0bfe9",
"zh_CN": "0dc8c5b86a03f0ce58f6d2633ab3011d9bc8004af18f922944a65d151e54beda",
}

@ -229,6 +229,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.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.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "Zeitzone",
"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.select.older_first": "Älteste Artikel zuerst",
"form.prefs.select.recent_first": "Neueste Artikel zuerst",
"form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren",

@ -229,6 +229,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.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.user_mandatory_fields": "The username is mandatory.",
"error.api_key_already_exists": "This API Key already exists.",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "Timezone",
"form.prefs.label.theme": "Theme",
"form.prefs.label.entry_sorting": "Entry Sorting",
"form.prefs.label.entries_per_page": "Entries per page",
"form.prefs.select.older_first": "Older entries first",
"form.prefs.select.recent_first": "Recent entries first",
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",

@ -229,6 +229,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.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.user_mandatory_fields": "El nombre de usuario es obligatorio.",
"error.api_key_already_exists": "Esta clave API ya existe.",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "Zona horaria",
"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.select.older_first": "Entradas más viejas primero",
"form.prefs.select.recent_first": "Entradas recientes primero",
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",

@ -229,6 +229,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.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.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "Fuseau horaire",
"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.select.older_first": "Ancien éléments en premier",
"form.prefs.select.recent_first": "Éléments récents en premier",
"form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier",

@ -229,6 +229,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.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
"error.api_key_already_exists": "Questa chiave API esiste già.",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "Fuso orario",
"form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Ordinamento articoli",
"form.prefs.label.entries_per_page": "Articoli per pagina",
"form.prefs.select.older_first": "Prima i più vecchi",
"form.prefs.select.recent_first": "Prima i più recenti",
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",

@ -229,6 +229,7 @@
"error.different_passwords": "パスワードが一致しません。",
"error.password_min_length": "パスワードは6文字以上である必要があります。",
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。",
"error.entries_per_page_invalid": "ページあたりのエントリ数が無効です。",
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
"error.user_mandatory_fields": "ユーザー名が必要です。",
"error.api_key_already_exists": "このAPIキーは既に存在します。",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "タイムゾーン",
"form.prefs.label.theme": "テーマ",
"form.prefs.label.entry_sorting": "記事の並べ替え",
"form.prefs.label.entries_per_page": "ページあたりのエントリ",
"form.prefs.select.older_first": "古い記事を最初に",
"form.prefs.select.recent_first": "新しい記事を最初に",
"form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",

@ -229,6 +229,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.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
"error.api_key_already_exists": "This API Key already exists.",
@ -254,6 +255,7 @@
"form.prefs.label.timezone": "Tijdzone",
"form.prefs.label.theme": "Skin",
"form.prefs.label.entry_sorting": "Volgorde van items",
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
"form.prefs.select.older_first": "Oudere items eerst",
"form.prefs.select.recent_first": "Recente items eerst",
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",

@ -231,6 +231,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.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
@ -256,6 +257,7 @@
"form.prefs.label.timezone": "Strefa czasowa",
"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.select.older_first": "Najstarsze wpisy jako pierwsze",
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",

@ -231,6 +231,7 @@
"error.different_passwords": "Пароли не совпадают.",
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
"error.entries_per_page_invalid": "Количество записей на странице недействительно.",
"error.feed_mandatory_fields": "URL и категория обязательны.",
"error.user_mandatory_fields": "Имя пользователя обязательно.",
"error.api_key_already_exists": "Этот ключ API уже существует.",
@ -256,6 +257,7 @@
"form.prefs.label.timezone": "Часовой пояс",
"form.prefs.label.theme": "Тема",
"form.prefs.label.entry_sorting": "Сортировка записей",
"form.prefs.label.entries_per_page": "Записи на странице",
"form.prefs.select.older_first": "Сначала старые записи",
"form.prefs.select.recent_first": "Сначала последние записи",
"form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",

@ -227,6 +227,7 @@
"error.different_passwords": "两次输入的密码不同",
"error.password_min_length": "请至少使用6个字符",
"error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区",
"error.entries_per_page_invalid": "每页的条目数无效。",
"error.feed_mandatory_fields": "必须填写 URL 和分类",
"error.user_mandatory_fields": "必须填写用户名",
"error.api_key_already_exists": "此API密钥已存在。",
@ -252,6 +253,7 @@
"form.prefs.label.timezone": "时区",
"form.prefs.label.theme": "主题",
"form.prefs.label.entry_sorting": "内容排序",
"form.prefs.label.entries_per_page": "每页条目",
"form.prefs.select.older_first": "旧->新",
"form.prefs.select.recent_first": "新->旧",
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",

@ -21,6 +21,7 @@ type User struct {
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
Extra map[string]string `json:"extra"`

@ -64,7 +64,7 @@ func (s *Storage) CreateUser(user *model.User) (err error) {
VALUES
(LOWER($1), $2, $3, $4)
RETURNING
id, username, is_admin, language, theme, timezone, entry_direction, keyboard_shortcuts
id, username, is_admin, language, theme, timezone, entry_direction, entries_per_page, keyboard_shortcuts
`
err = s.db.QueryRow(query, user.Username, password, user.IsAdmin, extra).Scan(
@ -75,6 +75,7 @@ func (s *Storage) CreateUser(user *model.User) (err error) {
&user.Theme,
&user.Timezone,
&user.EntryDirection,
&user.EntriesPerPage,
&user.KeyboardShortcuts,
)
if err != nil {
@ -123,9 +124,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
language=$5,
timezone=$6,
entry_direction=$7,
keyboard_shortcuts=$8
entries_per_page=$8,
keyboard_shortcuts=$9
WHERE
id=$9
id=$10
`
_, err = s.db.Exec(
@ -137,6 +139,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.Language,
user.Timezone,
user.EntryDirection,
user.EntriesPerPage,
user.KeyboardShortcuts,
user.ID,
)
@ -152,9 +155,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
language=$4,
timezone=$5,
entry_direction=$6,
keyboard_shortcuts=$7
entries_per_page=$7,
keyboard_shortcuts=$8
WHERE
id=$8
id=$9
`
_, err := s.db.Exec(
@ -165,6 +169,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.Language,
user.Timezone,
user.EntryDirection,
user.EntriesPerPage,
user.KeyboardShortcuts,
user.ID,
)
@ -202,6 +207,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
language,
timezone,
entry_direction,
entries_per_page,
keyboard_shortcuts,
last_login_at,
extra
@ -224,6 +230,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
language,
timezone,
entry_direction,
entries_per_page,
keyboard_shortcuts,
last_login_at,
extra
@ -246,6 +253,7 @@ func (s *Storage) UserByExtraField(field, value string) (*model.User, error) {
language,
timezone,
entry_direction,
entries_per_page,
keyboard_shortcuts,
last_login_at,
extra
@ -268,6 +276,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.language,
u.timezone,
u.entry_direction,
u.entries_per_page,
u.keyboard_shortcuts,
u.last_login_at,
u.extra
@ -293,6 +302,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.Language,
&user.Timezone,
&user.EntryDirection,
&user.EntriesPerPage,
&user.KeyboardShortcuts,
&user.LastLoginAt,
&extra,
@ -348,6 +358,7 @@ func (s *Storage) Users() (model.Users, error) {
language,
timezone,
entry_direction,
entries_per_page,
keyboard_shortcuts,
last_login_at,
extra
@ -373,6 +384,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.Language,
&user.Timezone,
&user.EntryDirection,
&user.EntriesPerPage,
&user.KeyboardShortcuts,
&user.LastLoginAt,
&extra,

@ -49,6 +49,9 @@
<option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "form.prefs.select.recent_first" }}</option>
</select>
<label for="form-entries-per-page">{{ t "form.prefs.label.entries_per_page" }}</label>
<input type="number" name="entries_per_page" id="form-entries-per-page" value="{{ .form.EntriesPerPage }}" min="1">
<label><input type="checkbox" name="keyboard_shortcuts" value="1" {{ if .form.KeyboardShortcuts }}checked{{ end }}> {{ t "form.prefs.label.keyboard_shortcuts" }}</label>
<label>{{t "form.prefs.label.custom_css" }}</label><textarea name="custom_css" cols="40" rows="5">{{ .form.CustomCSS }}</textarea>

@ -1320,6 +1320,9 @@ var templateViewsMap = map[string]string{
<option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "form.prefs.select.recent_first" }}</option>
</select>
<label for="form-entries-per-page">{{ t "form.prefs.label.entries_per_page" }}</label>
<input type="number" name="entries_per_page" id="form-entries-per-page" value="{{ .form.EntriesPerPage }}" min="1">
<label><input type="checkbox" name="keyboard_shortcuts" value="1" {{ if .form.KeyboardShortcuts }}checked{{ end }}> {{ t "form.prefs.label.keyboard_shortcuts" }}</label>
<label>{{t "form.prefs.label.custom_css" }}</label><textarea name="custom_css" cols="40" rows="5">{{ .form.CustomCSS }}</textarea>
@ -1565,7 +1568,7 @@ var templateViewsMapChecksums = map[string]string{
"login": "79ff2ca488c0a19b37c8fa227a21f73e94472eb357a51a077197c852f7713f11",
"search_entries": "c0786ddc6b17e865007b975eefb97417935cbc601f5917cca1ee0d3f584594bc",
"sessions": "5d5c677bddbd027e0b0c9f7a0dd95b66d9d95b4e130959f31fb955b926c2201c",
"settings": "3ab566c3220c62edc3edc51f2e93c1101b728e9f62f52f23de6bc6322d86aeb6",
"settings": "3d6dd0d7fa0ca48cfd9a5edb43c055af8b816eb4460f16b71ae22db40ed9b754",
"shared_entries": "1494d81e46f6af534a73cf6a91f8dfda1932a477bb3a70143513896ac0f0220b",
"unread_entries": "e0080d0cf3583cda51d865422960137c8556c432853657086e43daf6bd5b73be",
"users": "d7ff52efc582bbad10504f4a04fa3adcc12d15890e45dff51cac281e0c446e45",

@ -78,6 +78,10 @@ func TestGetUsers(t *testing.T) {
if !users[0].IsAdmin {
t.Fatalf(`Invalid role, got "%v"`, users[0].IsAdmin)
}
if users[0].EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, users[0].EntriesPerPage)
}
}
func TestCreateStandardUser(t *testing.T) {
@ -119,6 +123,10 @@ func TestCreateStandardUser(t *testing.T) {
if user.LastLoginAt != nil {
t.Fatalf(`Invalid last login date, got "%v"`, user.LastLoginAt)
}
if user.EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage)
}
}
func TestRemoveUser(t *testing.T) {
@ -183,6 +191,10 @@ func TestGetUserByID(t *testing.T) {
if user.LastLoginAt != nil {
t.Fatalf(`Invalid last login date, got "%v"`, user.LastLoginAt)
}
if user.EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage)
}
}
func TestGetUserByUsername(t *testing.T) {
@ -234,6 +246,10 @@ func TestGetUserByUsername(t *testing.T) {
if user.LastLoginAt != nil {
t.Fatalf(`Invalid last login date, got "%v"`, user.LastLoginAt)
}
if user.EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage)
}
}
func TestUpdateUserTheme(t *testing.T) {

@ -29,7 +29,7 @@ func (h *handler) showStarredPage(w http.ResponseWriter, r *http.Request) {
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(user.EntryDirection)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -48,7 +48,7 @@ func (h *handler) showStarredPage(w http.ResponseWriter, r *http.Request) {
view.Set("total", count)
view.Set("entries", entries)
view.Set("pagination", getPagination(route.Path(h.router, "starred"), count, offset))
view.Set("pagination", getPagination(route.Path(h.router, "starred"), count, offset, user.EntriesPerPage))
view.Set("menu", "starred")
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))

@ -41,7 +41,7 @@ func (h *handler) showCategoryEntriesPage(w http.ResponseWriter, r *http.Request
builder.WithDirection(user.EntryDirection)
builder.WithStatus(model.EntryStatusUnread)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -60,7 +60,7 @@ func (h *handler) showCategoryEntriesPage(w http.ResponseWriter, r *http.Request
view.Set("category", category)
view.Set("total", count)
view.Set("entries", entries)
view.Set("pagination", getPagination(route.Path(h.router, "categoryEntries", "categoryID", category.ID), count, offset))
view.Set("pagination", getPagination(route.Path(h.router, "categoryEntries", "categoryID", category.ID), count, offset, user.EntriesPerPage))
view.Set("menu", "categories")
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))

@ -41,7 +41,7 @@ func (h *handler) showCategoryEntriesAllPage(w http.ResponseWriter, r *http.Requ
builder.WithDirection(user.EntryDirection)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -60,7 +60,7 @@ func (h *handler) showCategoryEntriesAllPage(w http.ResponseWriter, r *http.Requ
view.Set("category", category)
view.Set("total", count)
view.Set("entries", entries)
view.Set("pagination", getPagination(route.Path(h.router, "categoryEntriesAll", "categoryID", category.ID), count, offset))
view.Set("pagination", getPagination(route.Path(h.router, "categoryEntriesAll", "categoryID", category.ID), count, offset, user.EntriesPerPage))
view.Set("menu", "categories")
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))

@ -41,7 +41,7 @@ func (h *handler) showFeedEntriesPage(w http.ResponseWriter, r *http.Request) {
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(user.EntryDirection)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -60,7 +60,7 @@ func (h *handler) showFeedEntriesPage(w http.ResponseWriter, r *http.Request) {
view.Set("feed", feed)
view.Set("entries", entries)
view.Set("total", count)
view.Set("pagination", getPagination(route.Path(h.router, "feedEntries", "feedID", feed.ID), count, offset))
view.Set("pagination", getPagination(route.Path(h.router, "feedEntries", "feedID", feed.ID), count, offset, user.EntriesPerPage))
view.Set("menu", "feeds")
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))

@ -41,7 +41,7 @@ func (h *handler) showFeedEntriesAllPage(w http.ResponseWriter, r *http.Request)
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(user.EntryDirection)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -60,7 +60,7 @@ func (h *handler) showFeedEntriesAllPage(w http.ResponseWriter, r *http.Request)
view.Set("feed", feed)
view.Set("entries", entries)
view.Set("total", count)
view.Set("pagination", getPagination(route.Path(h.router, "feedEntriesAll", "feedID", feed.ID), count, offset))
view.Set("pagination", getPagination(route.Path(h.router, "feedEntriesAll", "feedID", feed.ID), count, offset, user.EntriesPerPage))
view.Set("menu", "feeds")
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))

@ -6,6 +6,7 @@ package form // import "miniflux.app/ui/form"
import (
"net/http"
"strconv"
"miniflux.app/errors"
"miniflux.app/model"
@ -20,6 +21,7 @@ type SettingsForm struct {
Language string
Timezone string
EntryDirection string
EntriesPerPage int
KeyboardShortcuts bool
CustomCSS string
}
@ -31,6 +33,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.Language = s.Language
user.Timezone = s.Timezone
user.EntryDirection = s.EntryDirection
user.EntriesPerPage = s.EntriesPerPage
user.KeyboardShortcuts = s.KeyboardShortcuts
user.Extra["custom_css"] = s.CustomCSS
@ -47,6 +50,10 @@ func (s *SettingsForm) Validate() error {
return errors.NewLocalizedError("error.settings_mandatory_fields")
}
if s.EntriesPerPage < 1 {
return errors.NewLocalizedError("error.entries_per_page_invalid")
}
if s.Confirmation == "" {
// Firefox insists on auto-completing the password field.
// If the confirmation field is blank, the user probably
@ -67,6 +74,10 @@ func (s *SettingsForm) Validate() error {
// NewSettingsForm returns a new SettingsForm.
func NewSettingsForm(r *http.Request) *SettingsForm {
entriesPerPage, err := strconv.ParseInt(r.FormValue("entries_per_page"), 10, 64)
if err != nil {
entriesPerPage = 0
}
return &SettingsForm{
Username: r.FormValue("username"),
Password: r.FormValue("password"),
@ -75,6 +86,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
Language: r.FormValue("language"),
Timezone: r.FormValue("timezone"),
EntryDirection: r.FormValue("entry_direction"),
EntriesPerPage: int(entriesPerPage),
KeyboardShortcuts: r.FormValue("keyboard_shortcuts") == "1",
CustomCSS: r.FormValue("custom_css"),
}

@ -13,6 +13,7 @@ func TestValid(t *testing.T) {
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 50,
}
err := settings.Validate()
@ -30,6 +31,7 @@ func TestConfirmationEmpty(t *testing.T) {
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 50,
}
err := settings.Validate()
@ -51,6 +53,25 @@ func TestConfirmationIncorrect(t *testing.T) {
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 50,
}
err := settings.Validate()
if err == nil {
t.Error("Validate should return an error")
}
}
func TestEntriesPerPageNotValid(t *testing.T) {
settings := &SettingsForm{
Username: "user",
Password: "hunter2",
Confirmation: "hunter2",
Theme: "default",
Language: "en_US",
Timezone: "UTC",
EntryDirection: "asc",
EntriesPerPage: 0,
}
err := settings.Validate()

@ -28,7 +28,7 @@ func (h *handler) showHistoryPage(w http.ResponseWriter, r *http.Request) {
builder.WithOrder("changed_at")
builder.WithDirection("desc")
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -46,7 +46,7 @@ func (h *handler) showHistoryPage(w http.ResponseWriter, r *http.Request) {
view := view.New(h.tpl, r, sess)
view.Set("entries", entries)
view.Set("total", count)
view.Set("pagination", getPagination(route.Path(h.router, "history"), count, offset))
view.Set("pagination", getPagination(route.Path(h.router, "history"), count, offset, user.EntriesPerPage))
view.Set("menu", "history")
view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))

@ -4,10 +4,6 @@
package ui // import "miniflux.app/ui"
const (
nbItemsPerPage = 100
)
type pagination struct {
Route string
Total int
@ -20,7 +16,7 @@ type pagination struct {
SearchQuery string
}
func getPagination(route string, total, offset int) pagination {
func getPagination(route string, total, offset, nbItemsPerPage int) pagination {
nextOffset := 0
prevOffset := 0
showNext := (total - offset) > nbItemsPerPage

@ -28,7 +28,7 @@ func (h *handler) showSearchEntriesPage(w http.ResponseWriter, r *http.Request)
builder.WithSearchQuery(searchQuery)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
@ -44,7 +44,7 @@ func (h *handler) showSearchEntriesPage(w http.ResponseWriter, r *http.Request)
sess := session.New(h.store, request.SessionID(r))
view := view.New(h.tpl, r, sess)
pagination := getPagination(route.Path(h.router, "searchEntries"), count, offset)
pagination := getPagination(route.Path(h.router, "searchEntries"), count, offset, user.EntriesPerPage)
pagination.SearchQuery = searchQuery
view.Set("searchQuery", searchQuery)

@ -32,6 +32,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
Language: user.Language,
Timezone: user.Timezone,
EntryDirection: user.EntryDirection,
EntriesPerPage: user.EntriesPerPage,
KeyboardShortcuts: user.KeyboardShortcuts,
CustomCSS: user.Extra["custom_css"],
}

File diff suppressed because one or more lines are too long

@ -353,7 +353,8 @@ select {
input[type="search"],
input[type="url"],
input[type="password"],
input[type="text"] {
input[type="text"],
input[type="number"] {
color: var(--input-color);
background: var(--input-background);
border: var(--input-border);
@ -369,13 +370,18 @@ input[type="text"] {
input[type="search"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
input[type="text"]:focus {
input[type="text"]:focus,
input[type="number"]:focus {
color: var(--input-focus-color);
border-color: var(--input-focus-border-color);
outline: 0;
box-shadow: var(--input-focus-box-shadow);
}
#form-entries-per-page {
max-width: 80px;
}
input[type="checkbox"] {
margin-bottom: 15px;
}

@ -43,7 +43,7 @@ func (h *handler) showUnreadPage(w http.ResponseWriter, r *http.Request) {
builder.WithOrder(model.DefaultSortingOrder)
builder.WithDirection(user.EntryDirection)
builder.WithOffset(offset)
builder.WithLimit(nbItemsPerPage)
builder.WithLimit(user.EntriesPerPage)
entries, err := builder.GetEntries()
if err != nil {
html.ServerError(w, r, err)
@ -51,7 +51,7 @@ func (h *handler) showUnreadPage(w http.ResponseWriter, r *http.Request) {
}
view.Set("entries", entries)
view.Set("pagination", getPagination(route.Path(h.router, "unread"), countUnread, offset))
view.Set("pagination", getPagination(route.Path(h.router, "unread"), countUnread, offset, user.EntriesPerPage))
view.Set("menu", "unread")
view.Set("user", user)
view.Set("countUnread", countUnread)

Loading…
Cancel
Save