Browse Source

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

logan 5 years ago
parent
commit
5f266319a3

+ 5 - 0
api/payload.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) {

+ 2 - 0
client/core.go

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

+ 1 - 1
database/migration.go

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

+ 3 - 0
database/sql.go

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

+ 1 - 0
database/sql/schema_version_32.sql

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

+ 30 - 10
locale/translations.go

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

+ 2 - 0
locale/translations/de_DE.json

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

+ 2 - 0
locale/translations/en_US.json

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

+ 2 - 0
locale/translations/es_ES.json

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

+ 2 - 0
locale/translations/fr_FR.json

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

+ 2 - 0
locale/translations/it_IT.json

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

+ 2 - 0
locale/translations/ja_JP.json

@@ -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": "キーボード・ショートカットを有効にする",

+ 2 - 0
locale/translations/nl_NL.json

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

+ 2 - 0
locale/translations/pl_PL.json

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

+ 2 - 0
locale/translations/ru_RU.json

@@ -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": "Включить сочетания клавиш",

+ 2 - 0
locale/translations/zh_CN.json

@@ -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": "启用键盘快捷键",

+ 1 - 0
model/user.go

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

+ 17 - 5
storage/user.go

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

+ 3 - 0
template/html/settings.html

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

+ 4 - 1
template/views.go

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

+ 16 - 0
tests/user_test.go

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

+ 2 - 2
ui/bookmark_entries.go

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

+ 2 - 2
ui/category_entries.go

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

+ 2 - 2
ui/category_entries_all.go

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

+ 2 - 2
ui/feed_entries.go

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

+ 2 - 2
ui/feed_entries_all.go

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

+ 12 - 0
ui/form/settings.go

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

+ 21 - 0
ui/form/settings_test.go

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

+ 2 - 2
ui/history_entries.go

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

+ 1 - 5
ui/pagination.go

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

+ 2 - 2
ui/search_entries.go

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

+ 1 - 0
ui/settings_show.go

@@ -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 it is too large
+ 0 - 0
ui/static/css.go


+ 8 - 2
ui/static/css/common.css

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

+ 2 - 2
ui/unread_entries.go

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

Some files were not shown because too many files changed in this diff