فهرست منبع

Add optional sort option in category page
closes #1552

Romain de Laage 3 سال پیش
والد
کامیت
83e1f154b5

+ 2 - 0
client/model.go

@@ -39,6 +39,7 @@ type User struct {
 	DefaultReadingSpeed int        `json:"default_reading_speed"`
 	CJKReadingSpeed     int        `json:"cjk_reading_speed"`
 	DefaultHomePage     string     `json:"default_home_page"`
+	CategoriesSortOrder string     `json:"categories_sort_order"`
 }
 
 func (u User) String() string {
@@ -75,6 +76,7 @@ type UserModificationRequest struct {
 	DefaultReadingSpeed *int    `json:"default_reading_speed"`
 	CJKReadingSpeed     *int    `json:"cjk_reading_speed"`
 	DefaultHomePage     *string `json:"default_home_page"`
+	CategoriesSortOrder *string `json:"categories_sort_order"`
 }
 
 // Users represents a list of users.

+ 6 - 0
database/migrations.go

@@ -616,4 +616,10 @@ var migrations = []func(tx *sql.Tx) error{
 		`)
 		return
 	},
+	func(tx *sql.Tx) (err error) {
+		_, err = tx.Exec(`
+			ALTER TABLE users ADD COLUMN categories_sort_order text not null default 'unread_count';
+		`)
+		return
+	},
 }

+ 3 - 0
locale/translations/de_DE.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Browser",
     "form.prefs.select.publish_time": "Eintrag veröffentlichte Zeit",
     "form.prefs.select.created_time": "Eintrag erstellt Zeit",
+    "form.prefs.select.alphabetical": "Alphabetisch",
+    "form.prefs.select.unread_count": "Ungelesen zählen",
     "form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren",
     "form.prefs.label.entry_swipe": "Wischgeste für Einträge auf dem Handy aktivieren",
     "form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
     "form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
     "form.prefs.label.entry_order": "Eintrag Sortierspalte",
     "form.prefs.label.default_home_page": "Standard Startseite",
+    "form.prefs.label.categories_sort_order": "Kategorien sortieren",
     "form.import.label.file": "OPML Datei",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Fever API aktivieren",

+ 3 - 0
locale/translations/el_EL.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Περιηγητής",
     "form.prefs.select.publish_time": "Δημοσιευμένος χρόνος εισόδου",
     "form.prefs.select.created_time": "Χρόνος δημιουργίας καταχώρησης",
+    "form.prefs.select.alphabetical": "Αλφαβητική σειρά",
+    "form.prefs.select.unread_count": "Αριθμός μη αναγνωσμένων",
     "form.prefs.label.keyboard_shortcuts": "Ενεργοποίηση συντομεύσεων πληκτρολογίου",
     "form.prefs.label.entry_swipe": "Ενεργοποιήστε τη χειρονομία σάρωσης στις καταχωρήσεις στο κινητό",
     "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
     "form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
     "form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",
     "form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
+    "form.prefs.label.categories_sort_order": "Ταξινόμηση κατηγοριών",
     "form.import.label.file": "Αρχείο OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Ενεργοποιήστε το Fever API",

+ 3 - 0
locale/translations/en_US.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Browser",
     "form.prefs.select.publish_time": "Entry published time",
     "form.prefs.select.created_time": "Entry created time",
+    "form.prefs.select.alphabetical": "Alphabetical",
+    "form.prefs.select.unread_count": "Unread count",
     "form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
     "form.prefs.label.entry_swipe": "Enable swipe gesture on entries on mobile",
     "form.prefs.label.show_reading_time": "Show estimated reading time for articles",
     "form.prefs.label.custom_css": "Custom CSS",
     "form.prefs.label.entry_order": "Entry Sorting Column",
     "form.prefs.label.default_home_page": "Default home page",
+    "form.prefs.label.categories_sort_order": "Categories Sorting",
     "form.import.label.file": "OPML file",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activate Fever API",

+ 3 - 0
locale/translations/es_ES.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Navegador",
     "form.prefs.select.publish_time": "Hora de publicación de la entrada",
     "form.prefs.select.created_time": "Hora de creación de la entrada",
+    "form.prefs.select.alphabetical": "Alfabético",
+    "form.prefs.select.unread_count": "Recuento de no leídos",
     "form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado",
     "form.prefs.label.entry_swipe": "Habilitar el gesto de deslizar el dedo en las entradas en el móvil",
     "form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
     "form.prefs.label.custom_css": "CSS personalizado",
     "form.prefs.label.entry_order": "Columna de clasificación de entradas",
     "form.prefs.label.default_home_page": "Página de inicio por defecto",
+    "form.prefs.label.categories_sort_order": "Clasificación por categorías",
     "form.import.label.file": "Archivo OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activar API de Fever",

+ 3 - 0
locale/translations/fi_FI.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Selain",
     "form.prefs.select.publish_time": "Julkaisuaika",
     "form.prefs.select.created_time": "Luomisaika",
+    "form.prefs.select.alphabetical": "Aakkosjärjestys",
+    "form.prefs.select.unread_count": "Lukemattomien määrä",
     "form.prefs.label.keyboard_shortcuts": "Ota pikanäppäimet käyttöön",
     "form.prefs.label.entry_swipe": "Ota pyyhkäisyele käyttöön mobiililaitteella",
     "form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
     "form.prefs.label.custom_css": "Mukautettu CSS",
     "form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",
     "form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
+    "form.prefs.label.categories_sort_order": "Kategorioiden lajittelu",
     "form.import.label.file": "OPML-tiedosto",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Ota Fever API käyttöön",

+ 3 - 0
locale/translations/fr_FR.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Navigateur",
     "form.prefs.select.publish_time": "Heure de publication de l'entrée",
     "form.prefs.select.created_time": "Heure de création de l'entrée",
+    "form.prefs.select.alphabetical": "Alphabétique",
+    "form.prefs.select.unread_count": "Nombre d'articles non lus",
     "form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier",
     "form.prefs.label.entry_swipe": "Activer le geste de balayage sur les entrées sur mobile",
     "form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
     "form.prefs.label.custom_css": "CSS personnalisé",
     "form.prefs.label.entry_order": "Colonne de tri des entrées",
     "form.prefs.label.default_home_page": "Page d'accueil par défaut",
+    "form.prefs.label.categories_sort_order": "Colonne de tri des catégories",
     "form.import.label.file": "Fichier OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activer l'API de Fever",

+ 3 - 0
locale/translations/hi_IN.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "ब्राउज़र",
     "form.prefs.select.publish_time": "प्रवेश प्रकाशित समय",
     "form.prefs.select.created_time": "प्रवेश बनाया समय",
+    "form.prefs.select.alphabetical": "वर्णक्रम",
+    "form.prefs.select.unread_count": "अपठित गणना",
     "form.prefs.label.keyboard_shortcuts": "कीबोर्ड शॉर्टकट सक्षम करें",
     "form.prefs.label.entry_swipe": "मोबाइल पर प्रविष्टियों पर स्वाइप जेस्चर सक्षम करें",
     "form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
     "form.prefs.label.custom_css": "कस्टम सीएसएस",
     "form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम",
     "form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
+    "form.prefs.label.categories_sort_order": "श्रेणियाँ छँटाई",
     "form.import.label.file": "ओपीएमएल फ़ाइल",
     "form.import.label.url": "यूआरएल",
     "form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",

+ 3 - 0
locale/translations/it_IT.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Browser",
     "form.prefs.select.publish_time": "Ora di pubblicazione dell'entrata",
     "form.prefs.select.created_time": "Tempo di creazione dell'entrata",
+    "form.prefs.select.alphabetical": "In ordine alfabetico",
+    "form.prefs.select.unread_count": "Conteggio dei non letti",
     "form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
     "form.prefs.label.entry_swipe": "Abilita il gesto di scorrimento sulle voci sul cellulare",
     "form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
     "form.prefs.label.custom_css": "CSS personalizzati",
     "form.prefs.label.entry_order": "Colonna di ordinamento delle voci",
     "form.prefs.label.default_home_page": "Pagina iniziale predefinita",
+    "form.prefs.label.categories_sort_order": "Ordinamento delle categorie",
     "form.import.label.file": "File OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Abilita l'API di Fever",

+ 3 - 0
locale/translations/ja_JP.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "ブラウザ",
     "form.prefs.select.publish_time": "エントリー公開時間",
     "form.prefs.select.created_time": "エントリー作成時間",
+    "form.prefs.select.alphabetical": "アルファベット順",
+    "form.prefs.select.unread_count": "未読数",
     "form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",
     "form.prefs.label.entry_swipe": "モバイルのエントリでスワイプジェスチャーを有効にする",
     "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
     "form.prefs.label.custom_css": "カスタムCSS",
     "form.prefs.label.entry_order": "エントリーソートカラム",
     "form.prefs.label.default_home_page": "デフォルトのトップページ",
+    "form.prefs.label.categories_sort_order": "カテゴリの並べ替え",
     "form.import.label.file": "OPML ファイル",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Fever API を有効にする",

+ 3 - 0
locale/translations/nl_NL.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Browser",
     "form.prefs.select.publish_time": "Tijd van binnenkomst",
     "form.prefs.select.created_time": "Tijdstip van binnenkomst",
+    "form.prefs.select.alphabetical": "Alfabetisch",
+    "form.prefs.select.unread_count": "Ongelezen tellen",
     "form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
     "form.prefs.label.entry_swipe": "Schakel veegbewegingen in voor items op mobiel",
     "form.prefs.label.show_reading_time": "Toon geschatte leestijd voor artikelen",
     "form.prefs.label.custom_css": "Aangepaste CSS",
     "form.prefs.label.entry_order": "Ingang Sorteerkolom",
     "form.prefs.label.default_home_page": "Standaard startpagina",
+    "form.prefs.label.categories_sort_order": "Categorieën sorteren",
     "form.import.label.file": "OPML-bestand",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Activeer Fever API",

+ 3 - 0
locale/translations/pl_PL.json

@@ -310,9 +310,12 @@
     "form.prefs.select.browser": "Przeglądarka",
     "form.prefs.select.publish_time": "Czas publikacji wpisu",
     "form.prefs.select.created_time": "Czas utworzenia wpisu",
+    "form.prefs.select.alphabetical": "Alfabetycznie",
+    "form.prefs.select.unread_count": "Liczba nieprzeczytanych",
     "form.prefs.label.custom_css": "Niestandardowy CSS",
     "form.prefs.label.entry_order": "Kolumna sortowania wpisów",
     "form.prefs.label.default_home_page": "Domyślna strona główna",
+    "form.prefs.label.categories_sort_order": "Sortowanie kategorii",
     "form.import.label.file": "Plik OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Aktywuj Fever API",

+ 3 - 0
locale/translations/pt_BR.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Navegador",
     "form.prefs.select.publish_time": "Entrada hora de publicação",
     "form.prefs.select.created_time": "Entrada tempo criado",
+    "form.prefs.select.alphabetical": "Por ordem alfabética",
+    "form.prefs.select.unread_count": "Contagem não lida",
     "form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado",
     "form.prefs.label.entry_swipe": "Ativar gesto de deslizar nas entradas no celular",
     "form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
     "form.prefs.label.custom_css": "CSS customizado",
     "form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",
     "form.prefs.label.default_home_page": "Página inicial predefinida",
+    "form.prefs.label.categories_sort_order": "Classificação das categorias",
     "form.import.label.file": "Arquivo OPML",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Ativar API do Fever",

+ 3 - 0
locale/translations/ru_RU.json

@@ -307,12 +307,15 @@
     "form.prefs.select.browser": "Браузер",
     "form.prefs.select.publish_time": "Время публикации заявки",
     "form.prefs.select.created_time": "Время создания записи",
+    "form.prefs.select.alphabetical": "По алфавиту",
+    "form.prefs.select.unread_count": "Количество непрочитанных",
     "form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",
     "form.prefs.label.entry_swipe": "Включить жест смахивания для записей на мобильном устройстве",
     "form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
     "form.prefs.label.custom_css": "Пользовательские CSS",
     "form.prefs.label.entry_order": "Колонка сортировки ввода",
     "form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
+    "form.prefs.label.categories_sort_order": "Сортировка категорий",
     "form.import.label.file": "OPML файл",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Активировать Fever API",

+ 3 - 0
locale/translations/tr_TR.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "Tarayıcı",
     "form.prefs.select.publish_time": "Giriş yayınlanma zamanı",
     "form.prefs.select.created_time": "Girişin oluşturulma zamanı",
+    "form.prefs.select.alphabetical": "Alfabetik",
+    "form.prefs.select.unread_count": "Okunmamış sayısı",
     "form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
     "form.prefs.label.entry_swipe": "Mobil cihazlarda iletiler için kaydırma hareketlerini etkinleştir",
     "form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
     "form.prefs.label.custom_css": "Özel CSS",
     "form.prefs.label.entry_order": "Giriş Sıralama Sütunu",
     "form.prefs.label.default_home_page": "Varsayılan ana sayfa",
+    "form.prefs.label.categories_sort_order": "Kategoriler sıralama",
     "form.import.label.file": "OPML dosyası",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "Fever API'yi Etkinleştir",

+ 3 - 0
locale/translations/uk_UA.json

@@ -304,12 +304,15 @@
   "form.prefs.select.browser": "Браузер",
   "form.prefs.select.publish_time": "Дата публікації запису",
   "form.prefs.select.created_time": "Дата створення запису",
+    "form.prefs.select.alphabetical": "За алфавітом",
+    "form.prefs.select.unread_count": "Кількість непрочитаних",
   "form.prefs.label.keyboard_shortcuts": "Увімкнути комбінації клавиш",
   "form.prefs.label.entry_swipe": "Увімкнути жест гортання для записів на мобільних пристроях",
   "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
   "form.prefs.label.custom_css": "Спеціальний CSS",
   "form.prefs.label.entry_order": "Стовпець сортування записів",
   "form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
+    "form.prefs.label.categories_sort_order": "Сортування за категоріями",
   "form.import.label.file": "Файл OPML",
   "form.import.label.url": "URL-адреса",
   "form.integration.fever_activate": "Увімкнути API Fever",

+ 3 - 0
locale/translations/zh_CN.json

@@ -303,12 +303,15 @@
     "form.prefs.select.browser": "浏览器",
     "form.prefs.select.publish_time": "文章发布时间",
     "form.prefs.select.created_time": "文章创建时间",
+    "form.prefs.select.alphabetical": "按字母顺序",
+    "form.prefs.select.unread_count": "未读计数",
     "form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
     "form.prefs.label.entry_swipe": "在移动设备上启用滑动手势",
     "form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
     "form.prefs.label.custom_css": "自定义 CSS",
     "form.prefs.label.entry_order": "文章排序依据",
     "form.prefs.label.default_home_page": "默认主页",
+    "form.prefs.label.categories_sort_order": "分类排序",
     "form.import.label.file": "OPML 文件",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "启用 Fever API",

+ 3 - 0
locale/translations/zh_TW.json

@@ -305,12 +305,15 @@
     "form.prefs.select.browser": "瀏覽器",
     "form.prefs.select.publish_time": "文章釋出時間",
     "form.prefs.select.created_time": "文章建立時間",
+    "form.prefs.select.alphabetical": "按字母順序",
+    "form.prefs.select.unread_count": "未讀計數",
     "form.prefs.label.keyboard_shortcuts": "啟用鍵盤快捷鍵",
     "form.prefs.label.entry_swipe": "在移動裝置上啟用滑動手勢",
     "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
     "form.prefs.label.custom_css": "自定義 CSS",
     "form.prefs.label.entry_order": "文章排序依據",
     "form.prefs.label.default_home_page": "默認主頁",
+    "form.prefs.label.categories_sort_order": "分類排序",
     "form.import.label.file": "OPML 檔案",
     "form.import.label.url": "URL",
     "form.integration.fever_activate": "啟用 Fever API",

+ 13 - 0
model/categories_sort_options.go

@@ -0,0 +1,13 @@
+// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package model // import "miniflux.app/model"
+
+// HomePages returns the list of available home pages.
+func CategoriesSortOptions() map[string]string {
+	return map[string]string{
+		"unread_count": "form.prefs.select.unread_count",
+		"alphabetical": "form.prefs.select.alphabetical",
+	}
+}

+ 6 - 0
model/user.go

@@ -33,6 +33,7 @@ type User struct {
 	DefaultReadingSpeed int        `json:"default_reading_speed"`
 	CJKReadingSpeed     int        `json:"cjk_reading_speed"`
 	DefaultHomePage     string     `json:"default_home_page"`
+	CategoriesSortOrder string     `json:"categories_sort_order"`
 }
 
 // UserCreationRequest represents the request to create a user.
@@ -65,6 +66,7 @@ type UserModificationRequest struct {
 	DefaultReadingSpeed *int    `json:"default_reading_speed"`
 	CJKReadingSpeed     *int    `json:"cjk_reading_speed"`
 	DefaultHomePage     *string `json:"default_home_page"`
+	CategoriesSortOrder *string `json:"categories_sort_order"`
 }
 
 // Patch updates the User object with the modification request.
@@ -144,6 +146,10 @@ func (u *UserModificationRequest) Patch(user *User) {
 	if u.DefaultHomePage != nil {
 		user.DefaultHomePage = *u.DefaultHomePage
 	}
+
+	if u.CategoriesSortOrder != nil {
+		user.CategoriesSortOrder = *u.CategoriesSortOrder
+	}
 }
 
 // UseTimezone converts last login date to the given timezone.

+ 18 - 3
storage/category.go

@@ -112,6 +112,11 @@ func (s *Storage) Categories(userID int64) (model.Categories, error) {
 
 // CategoriesWithFeedCount returns all categories with the number of feeds.
 func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error) {
+	user, err := s.UserByID(userID)
+	if err != nil {
+		return nil, err
+	}
+
 	query := `
 		SELECT
 			c.id,
@@ -126,11 +131,21 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
 		FROM categories c
 		WHERE
 			user_id=$1
-		ORDER BY
-			count_unread DESC,
-			c.title ASC
 	`
 
+	if user.CategoriesSortOrder == "alphabetical" {
+		query = query + `
+			ORDER BY
+				c.title ASC
+		`
+	} else {
+		query = query + `
+			ORDER BY
+				count_unread DESC,
+				c.title ASC
+		`
+	}
+
 	rows, err := s.db.Query(query, userID)
 	if err != nil {
 		return nil, fmt.Errorf(`store: unable to fetch categories: %v`, err)

+ 23 - 10
storage/user.go

@@ -88,7 +88,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 			entry_order,
 		    default_reading_speed,
 		    cjk_reading_speed,
-		    default_home_page
+		    default_home_page,
+		    categories_sort_order
 	`
 
 	tx, err := s.db.Begin()
@@ -124,6 +125,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
 		&user.DefaultReadingSpeed,
 		&user.CJKReadingSpeed,
 		&user.DefaultHomePage,
+		&user.CategoriesSortOrder,
 	)
 	if err != nil {
 		tx.Rollback()
@@ -177,9 +179,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				entry_order=$16,
 				default_reading_speed=$17,
 				cjk_reading_speed=$18,
-				default_home_page=$19
+				default_home_page=$19,
+				categories_sort_order=$20
 			WHERE
-				id=$20
+				id=$21
 		`
 
 		_, err = s.db.Exec(
@@ -203,6 +206,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.DefaultReadingSpeed,
 			user.CJKReadingSpeed,
 			user.DefaultHomePage,
+			user.CategoriesSortOrder,
 			user.ID,
 		)
 		if err != nil {
@@ -228,9 +232,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
 				entry_order=$15,
 				default_reading_speed=$16,
 				cjk_reading_speed=$17,
-				default_home_page=$18
+				default_home_page=$18,
+				categories_sort_order=$19
 			WHERE
-				id=$19
+				id=$20
 		`
 
 		_, err := s.db.Exec(
@@ -253,6 +258,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
 			user.DefaultReadingSpeed,
 			user.CJKReadingSpeed,
 			user.DefaultHomePage,
+			user.CategoriesSortOrder,
 			user.ID,
 		)
 
@@ -297,7 +303,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
 			entry_order,
 			default_reading_speed,
 			cjk_reading_speed,
-			default_home_page
+			default_home_page,
+			categories_sort_order
 		FROM
 			users
 		WHERE
@@ -329,7 +336,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
 			entry_order,
 			default_reading_speed,
 			cjk_reading_speed,
-			default_home_page
+			default_home_page,
+			categories_sort_order
 		FROM
 			users
 		WHERE
@@ -361,7 +369,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
 			entry_order,
 			default_reading_speed,
 			cjk_reading_speed,
-			default_home_page
+			default_home_page,
+			categories_sort_order
 		FROM
 			users
 		WHERE
@@ -400,7 +409,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
 			u.entry_order,
 			u.default_reading_speed,
 			u.cjk_reading_speed,
-			u.default_home_page
+			u.default_home_page,
+			u.categories_sort_order
 		FROM
 			users u
 		LEFT JOIN
@@ -434,6 +444,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
 		&user.DefaultReadingSpeed,
 		&user.CJKReadingSpeed,
 		&user.DefaultHomePage,
+		&user.CategoriesSortOrder,
 	)
 
 	if err == sql.ErrNoRows {
@@ -528,7 +539,8 @@ func (s *Storage) Users() (model.Users, error) {
 			entry_order,
 			default_reading_speed,
 			cjk_reading_speed,
-			default_home_page
+			default_home_page,
+			categories_sort_order
 		FROM
 			users
 		ORDER BY username ASC
@@ -563,6 +575,7 @@ func (s *Storage) Users() (model.Users, error) {
 			&user.DefaultReadingSpeed,
 			&user.CJKReadingSpeed,
 			&user.DefaultHomePage,
+			&user.CategoriesSortOrder,
 		)
 
 		if err != nil {

+ 7 - 0
template/templates/views/settings.html

@@ -70,6 +70,13 @@
     {{ end }}
     </select>
 
+    <label for="form-categories-sort-order">{{ t "form.prefs.label.categories_sort_order" }}</label>
+    <select id="form-categories-sort-order" name="categories_sort_order">
+    {{ range $key, $value := .categories_sort_options }}
+        <option value="{{ $key }}" {{ if eq $key $.form.CategoriesSortOrder }}selected="selected"{{ end }}>{{ t $value }}</option>
+    {{ end }}
+    </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">
 

+ 3 - 0
ui/form/settings.go

@@ -31,6 +31,7 @@ type SettingsForm struct {
 	DefaultReadingSpeed int
 	CJKReadingSpeed     int
 	DefaultHomePage     string
+	CategoriesSortOrder string
 }
 
 // Merge updates the fields of the given user.
@@ -50,6 +51,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 	user.CJKReadingSpeed = s.CJKReadingSpeed
 	user.DefaultReadingSpeed = s.DefaultReadingSpeed
 	user.DefaultHomePage = s.DefaultHomePage
+	user.CategoriesSortOrder = s.CategoriesSortOrder
 
 	if s.Password != "" {
 		user.Password = s.Password
@@ -114,5 +116,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
 		DefaultReadingSpeed: int(defaultReadingSpeed),
 		CJKReadingSpeed:     int(cjkReadingSpeed),
 		DefaultHomePage:     r.FormValue("default_home_page"),
+		CategoriesSortOrder: r.FormValue("categories_sort_order"),
 	}
 }

+ 2 - 0
ui/settings_show.go

@@ -42,6 +42,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 		DefaultReadingSpeed: user.DefaultReadingSpeed,
 		CJKReadingSpeed:     user.CJKReadingSpeed,
 		DefaultHomePage:     user.DefaultHomePage,
+		CategoriesSortOrder: user.CategoriesSortOrder,
 	}
 
 	timezones, err := h.store.Timezones()
@@ -59,6 +60,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
 	view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
 	view.Set("default_home_pages", model.HomePages())
+	view.Set("categories_sort_options", model.CategoriesSortOptions())
 
 	html.OK(w, r, view.Render("settings"))
 }