Make web app display mode configurable

The change is visible after reinstalling the web app. 

It's not compatible with all browsers.

See https://developer.mozilla.org/en-US/docs/Web/Manifest/display
This commit is contained in:
1pav 2021-02-28 22:29:51 +01:00 committed by GitHub
parent 053b1d0f8d
commit 0d935a863f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 182 additions and 12 deletions

View file

@ -34,6 +34,7 @@ type User struct {
ShowReadingTime bool `json:"show_reading_time"` ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"` EntrySwipe bool `json:"entry_swipe"`
LastLoginAt *time.Time `json:"last_login_at"` LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
} }
func (u User) String() string { func (u User) String() string {
@ -65,6 +66,7 @@ type UserModificationRequest struct {
KeyboardShortcuts *bool `json:"keyboard_shortcuts"` KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"` ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"` EntrySwipe *bool `json:"entry_swipe"`
DisplayMode *string `json:"display_mode"`
} }
// Users represents a list of users. // Users represents a list of users.

View file

@ -521,4 +521,12 @@ var migrations = []func(tx *sql.Tx) error{
`) `)
return err return err
}, },
func(tx *sql.Tx) (err error) {
sql := `
CREATE TYPE webapp_display_mode AS enum('fullscreen', 'standalone', 'minimal-ui', 'browser');
ALTER TABLE users ADD COLUMN display_mode webapp_display_mode default 'standalone';
`
_, err = tx.Exec(sql)
return err
},
} }

View file

@ -254,6 +254,7 @@
"error.invalid_language": "Ungültige Sprache.", "error.invalid_language": "Ungültige Sprache.",
"error.invalid_timezone": "Ungültige Zeitzone.", "error.invalid_timezone": "Ungültige Zeitzone.",
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.", "error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
"error.invalid_display_mode": "Ungültiger Web-App-Anzeigemodus.",
"form.feed.label.title": "Titel", "form.feed.label.title": "Titel",
"form.feed.label.site_url": "Webseite-URL", "form.feed.label.site_url": "Webseite-URL",
"form.feed.label.feed_url": "Abonnement-URL", "form.feed.label.feed_url": "Abonnement-URL",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Thema", "form.prefs.label.theme": "Thema",
"form.prefs.label.entry_sorting": "Sortierung der Artikel", "form.prefs.label.entry_sorting": "Sortierung der Artikel",
"form.prefs.label.entries_per_page": "Einträge pro Seite", "form.prefs.label.entries_per_page": "Einträge pro Seite",
"form.prefs.label.display_mode": "Anzeigemodus der Web-App (muss neu installiert werden)",
"form.prefs.select.older_first": "Älteste Artikel zuerst", "form.prefs.select.older_first": "Älteste Artikel zuerst",
"form.prefs.select.recent_first": "Neueste Artikel zuerst", "form.prefs.select.recent_first": "Neueste Artikel zuerst",
"form.prefs.select.fullscreen": "Vollbildschirm",
"form.prefs.select.standalone": "Eigenständige",
"form.prefs.select.minimal_ui": "Minimal",
"form.prefs.select.browser": "Browser",
"form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren", "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.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.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",

View file

@ -233,6 +233,7 @@
"error.invalid_language": "Invalid language.", "error.invalid_language": "Invalid language.",
"error.invalid_timezone": "Invalid timezone.", "error.invalid_timezone": "Invalid timezone.",
"error.invalid_entry_direction": "Invalid entry direction.", "error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_display_mode": "Invalid web app display mode.",
"error.empty_file": "This file is empty.", "error.empty_file": "This file is empty.",
"error.bad_credentials": "Invalid username or password.", "error.bad_credentials": "Invalid username or password.",
"error.fields_mandatory": "All fields are mandatory.", "error.fields_mandatory": "All fields are mandatory.",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Theme", "form.prefs.label.theme": "Theme",
"form.prefs.label.entry_sorting": "Entry Sorting", "form.prefs.label.entry_sorting": "Entry Sorting",
"form.prefs.label.entries_per_page": "Entries per page", "form.prefs.label.entries_per_page": "Entries per page",
"form.prefs.label.display_mode": "Web app display mode (needs reinstalling)",
"form.prefs.select.older_first": "Older entries first", "form.prefs.select.older_first": "Older entries first",
"form.prefs.select.recent_first": "Recent entries first", "form.prefs.select.recent_first": "Recent entries first",
"form.prefs.select.fullscreen": "Fullscreen",
"form.prefs.select.standalone": "Standalone",
"form.prefs.select.minimal_ui": "Minimal",
"form.prefs.select.browser": "Browser",
"form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts", "form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts",
"form.prefs.label.entry_swipe": "Enable swipe gesture on entries on mobile", "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.show_reading_time": "Show estimated reading time for articles",

View file

@ -254,6 +254,7 @@
"error.invalid_language": "Idioma no válido.", "error.invalid_language": "Idioma no válido.",
"error.invalid_timezone": "Zona horaria no válida.", "error.invalid_timezone": "Zona horaria no válida.",
"error.invalid_entry_direction": "Dirección de entrada no válida.", "error.invalid_entry_direction": "Dirección de entrada no válida.",
"error.invalid_display_mode": "Modo de visualización de la aplicación web no válido.",
"form.feed.label.title": "Título", "form.feed.label.title": "Título",
"form.feed.label.site_url": "URL del sitio", "form.feed.label.site_url": "URL del sitio",
"form.feed.label.feed_url": "URL de la fuente", "form.feed.label.feed_url": "URL de la fuente",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Tema", "form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Clasificación de entradas", "form.prefs.label.entry_sorting": "Clasificación de entradas",
"form.prefs.label.entries_per_page": "Entradas por página", "form.prefs.label.entries_per_page": "Entradas por página",
"form.prefs.label.display_mode": "Modo de visualización de la aplicación web (necesita reinstalación)",
"form.prefs.select.older_first": "Entradas más viejas primero", "form.prefs.select.older_first": "Entradas más viejas primero",
"form.prefs.select.recent_first": "Entradas recientes primero", "form.prefs.select.recent_first": "Entradas recientes primero",
"form.prefs.select.fullscreen": "Pantalla completa",
"form.prefs.select.standalone": "Ser único",
"form.prefs.select.minimal_ui": "Mínimo",
"form.prefs.select.browser": "Navegador",
"form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado", "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.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.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",

View file

@ -254,6 +254,7 @@
"error.invalid_language": "Langue non valide.", "error.invalid_language": "Langue non valide.",
"error.invalid_timezone": "Fuseau horaire non valide.", "error.invalid_timezone": "Fuseau horaire non valide.",
"error.invalid_entry_direction": "Ordre de trie non valide.", "error.invalid_entry_direction": "Ordre de trie non valide.",
"error.invalid_display_mode": "Mode d'affichage de l'application web non valide.",
"form.feed.label.title": "Titre", "form.feed.label.title": "Titre",
"form.feed.label.site_url": "URL du site web", "form.feed.label.site_url": "URL du site web",
"form.feed.label.feed_url": "URL du flux", "form.feed.label.feed_url": "URL du flux",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Thème", "form.prefs.label.theme": "Thème",
"form.prefs.label.entry_sorting": "Ordre des éléments", "form.prefs.label.entry_sorting": "Ordre des éléments",
"form.prefs.label.entries_per_page": "Entrées par page", "form.prefs.label.entries_per_page": "Entrées par page",
"form.prefs.label.display_mode": "Mode d'affichage de l'application web (doit être réinstallé)",
"form.prefs.select.older_first": "Ancien éléments en premier", "form.prefs.select.older_first": "Ancien éléments en premier",
"form.prefs.select.recent_first": "Éléments récents en premier", "form.prefs.select.recent_first": "Éléments récents en premier",
"form.prefs.select.fullscreen": "Plein écran",
"form.prefs.select.standalone": "Autonome",
"form.prefs.select.minimal_ui": "Minimal",
"form.prefs.select.browser": "Navigateur",
"form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier", "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.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.show_reading_time": "Afficher le temps de lecture estimé des articles",

View file

@ -254,6 +254,7 @@
"error.invalid_language": "Lingua non valida.", "error.invalid_language": "Lingua non valida.",
"error.invalid_timezone": "Fuso orario non valido.", "error.invalid_timezone": "Fuso orario non valido.",
"error.invalid_entry_direction": "Ordinamento non valido.", "error.invalid_entry_direction": "Ordinamento non valido.",
"error.invalid_display_mode": "Modalità di visualizzazione web app non valida.",
"form.feed.label.title": "Titolo", "form.feed.label.title": "Titolo",
"form.feed.label.site_url": "URL del sito", "form.feed.label.site_url": "URL del sito",
"form.feed.label.feed_url": "URL del feed", "form.feed.label.feed_url": "URL del feed",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Tema", "form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Ordinamento articoli", "form.prefs.label.entry_sorting": "Ordinamento articoli",
"form.prefs.label.entries_per_page": "Articoli per pagina", "form.prefs.label.entries_per_page": "Articoli per pagina",
"form.prefs.label.display_mode": "Modalità di visualizzazione web app (necessita la reinstallazione)",
"form.prefs.select.older_first": "Prima i più vecchi", "form.prefs.select.older_first": "Prima i più vecchi",
"form.prefs.select.recent_first": "Prima i più recenti", "form.prefs.select.recent_first": "Prima i più recenti",
"form.prefs.select.fullscreen": "Schermo intero",
"form.prefs.select.standalone": "Autonoma",
"form.prefs.select.minimal_ui": "Minimale",
"form.prefs.select.browser": "Browser",
"form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera", "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.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.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",

View file

@ -254,6 +254,7 @@
"error.invalid_language": "言語が無効です。", "error.invalid_language": "言語が無効です。",
"error.invalid_timezone": "タイムゾーンが無効です。", "error.invalid_timezone": "タイムゾーンが無効です。",
"error.invalid_entry_direction": "ソート順が無効です。", "error.invalid_entry_direction": "ソート順が無効です。",
"error.invalid_display_mode": "Webアプリの表示モードが無効です。",
"form.feed.label.title": "タイトル", "form.feed.label.title": "タイトル",
"form.feed.label.site_url": "サイト URL", "form.feed.label.site_url": "サイト URL",
"form.feed.label.feed_url": "フィード URL", "form.feed.label.feed_url": "フィード URL",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "テーマ", "form.prefs.label.theme": "テーマ",
"form.prefs.label.entry_sorting": "記事の並べ替え", "form.prefs.label.entry_sorting": "記事の並べ替え",
"form.prefs.label.entries_per_page": "ページあたりのエントリ", "form.prefs.label.entries_per_page": "ページあたりのエントリ",
"form.prefs.label.display_mode": "Webアプリの表示モード (再インストールが必要)",
"form.prefs.select.older_first": "古い記事を最初に", "form.prefs.select.older_first": "古い記事を最初に",
"form.prefs.select.recent_first": "新しい記事を最初に", "form.prefs.select.recent_first": "新しい記事を最初に",
"form.prefs.select.fullscreen": "全画面表示",
"form.prefs.select.standalone": "スタンドアロン",
"form.prefs.select.minimal_ui": "最小限",
"form.prefs.select.browser": "ブラウザ",
"form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする", "form.prefs.label.keyboard_shortcuts": "キーボード・ショートカットを有効にする",
"form.prefs.label.entry_swipe": "モバイルのエントリでスワイプジェスチャーを有効にする", "form.prefs.label.entry_swipe": "モバイルのエントリでスワイプジェスチャーを有効にする",
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する", "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",

View file

@ -254,6 +254,7 @@
"error.invalid_language": "Ongeldige taal.", "error.invalid_language": "Ongeldige taal.",
"error.invalid_timezone": "Ongeldige tijdzone.", "error.invalid_timezone": "Ongeldige tijdzone.",
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.", "error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
"error.invalid_display_mode": "Ongeldige weergavemodus voor webapp.",
"form.feed.label.title": "Naam", "form.feed.label.title": "Naam",
"form.feed.label.site_url": "Website URL", "form.feed.label.site_url": "Website URL",
"form.feed.label.feed_url": "Feed URL", "form.feed.label.feed_url": "Feed URL",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Skin", "form.prefs.label.theme": "Skin",
"form.prefs.label.entry_sorting": "Volgorde van items", "form.prefs.label.entry_sorting": "Volgorde van items",
"form.prefs.label.entries_per_page": "Inzendingen per pagina", "form.prefs.label.entries_per_page": "Inzendingen per pagina",
"form.prefs.label.display_mode": "Weergavemodus voor webapp (moet opnieuw worden geïnstalleerd)",
"form.prefs.select.older_first": "Oudere items eerst", "form.prefs.select.older_first": "Oudere items eerst",
"form.prefs.select.recent_first": "Recente items eerst", "form.prefs.select.recent_first": "Recente items eerst",
"form.prefs.select.fullscreen": "Volledig scherm",
"form.prefs.select.standalone": "Standalone",
"form.prefs.select.minimal_ui": "Minimaal",
"form.prefs.select.browser": "Browser",
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in", "form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
"form.prefs.label.entry_swipe": "Schakel veegbewegingen in voor items op mobiel", "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.show_reading_time": "Toon geschatte leestijd voor artikelen",

View file

@ -256,6 +256,7 @@
"error.invalid_language": "Nieprawidłowy język.", "error.invalid_language": "Nieprawidłowy język.",
"error.invalid_timezone": "Nieprawidłowa strefa czasowa.", "error.invalid_timezone": "Nieprawidłowa strefa czasowa.",
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.", "error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji internetowej.",
"form.feed.label.title": "Tytuł", "form.feed.label.title": "Tytuł",
"form.feed.label.site_url": "URL strony", "form.feed.label.site_url": "URL strony",
"form.feed.label.feed_url": "URL kanału", "form.feed.label.feed_url": "URL kanału",
@ -282,11 +283,16 @@
"form.prefs.label.theme": "Wygląd", "form.prefs.label.theme": "Wygląd",
"form.prefs.label.entry_sorting": "Sortowanie artykułów", "form.prefs.label.entry_sorting": "Sortowanie artykułów",
"form.prefs.label.entries_per_page": "Wpisy na stronie", "form.prefs.label.entries_per_page": "Wpisy na stronie",
"form.prefs.label.display_mode": "Tryb wyświetlania aplikacji internetowej (wymaga ponownej instalacji)",
"form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze", "form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze",
"form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe", "form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe",
"form.prefs.label.entry_swipe": "Włącz gest przesuwania na wpisach na telefonie komórkowym", "form.prefs.label.entry_swipe": "Włącz gest przesuwania na wpisach na telefonie komórkowym",
"form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów", "form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów",
"form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze", "form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze",
"form.prefs.select.fullscreen": "Pełny ekran",
"form.prefs.select.standalone": "Samodzielny",
"form.prefs.select.minimal_ui": "Minimalny",
"form.prefs.select.browser": "Przeglądarka",
"form.prefs.label.custom_css": "Niestandardowy CSS", "form.prefs.label.custom_css": "Niestandardowy CSS",
"form.import.label.file": "Plik OPML", "form.import.label.file": "Plik OPML",
"form.import.label.url": "URL", "form.import.label.url": "URL",

View file

@ -254,6 +254,7 @@
"error.invalid_language": "Idioma inválido.", "error.invalid_language": "Idioma inválido.",
"error.invalid_timezone": "Fuso horário inválido.", "error.invalid_timezone": "Fuso horário inválido.",
"error.invalid_entry_direction": "Direção de entrada inválida.", "error.invalid_entry_direction": "Direção de entrada inválida.",
"error.invalid_display_mode": "Modo de exibição de aplicativo inválido da web.",
"form.feed.label.title": "Título", "form.feed.label.title": "Título",
"form.feed.label.site_url": "URL do site", "form.feed.label.site_url": "URL do site",
"form.feed.label.feed_url": "URL da fonte", "form.feed.label.feed_url": "URL da fonte",
@ -280,8 +281,13 @@
"form.prefs.label.theme": "Tema", "form.prefs.label.theme": "Tema",
"form.prefs.label.entry_sorting": "Ordenação dos itens", "form.prefs.label.entry_sorting": "Ordenação dos itens",
"form.prefs.label.entries_per_page": "Itens por página", "form.prefs.label.entries_per_page": "Itens por página",
"form.prefs.label.display_mode": "Modo de exibição do aplicativo Web (precisa ser reinstalado)",
"form.prefs.select.older_first": "Itens mais velhos primeiro", "form.prefs.select.older_first": "Itens mais velhos primeiro",
"form.prefs.select.recent_first": "Itens mais recentes", "form.prefs.select.recent_first": "Itens mais recentes",
"form.prefs.select.fullscreen": "Tela completa",
"form.prefs.select.standalone": "Autônomo",
"form.prefs.select.minimal_ui": "Mínimo",
"form.prefs.select.browser": "Navegador",
"form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado", "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.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.show_reading_time": "Mostrar tempo estimado de leitura de artigos",

View file

@ -256,6 +256,7 @@
"error.invalid_language": "Неверный язык.", "error.invalid_language": "Неверный язык.",
"error.invalid_timezone": "Неверный часовой пояс.", "error.invalid_timezone": "Неверный часовой пояс.",
"error.invalid_entry_direction": "Неверное направление входа.", "error.invalid_entry_direction": "Неверное направление входа.",
"error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.",
"form.feed.label.title": "Название", "form.feed.label.title": "Название",
"form.feed.label.site_url": "URL сайта", "form.feed.label.site_url": "URL сайта",
"form.feed.label.feed_url": "URL подписки", "form.feed.label.feed_url": "URL подписки",
@ -282,8 +283,13 @@
"form.prefs.label.theme": "Тема", "form.prefs.label.theme": "Тема",
"form.prefs.label.entry_sorting": "Сортировка записей", "form.prefs.label.entry_sorting": "Сортировка записей",
"form.prefs.label.entries_per_page": "Записи на странице", "form.prefs.label.entries_per_page": "Записи на странице",
"form.prefs.label.display_mode": "Режим отображения веб-приложения (требуется переустановка)",
"form.prefs.select.older_first": "Сначала старые записи", "form.prefs.select.older_first": "Сначала старые записи",
"form.prefs.select.recent_first": "Сначала последние записи", "form.prefs.select.recent_first": "Сначала последние записи",
"form.prefs.select.fullscreen": "Полноэкранный",
"form.prefs.select.standalone": "Автономный",
"form.prefs.select.minimal_ui": "Минимальный",
"form.prefs.select.browser": "Браузер",
"form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш", "form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш",
"form.prefs.label.entry_swipe": "Включить жест смахивания для записей на мобильном устройстве", "form.prefs.label.entry_swipe": "Включить жест смахивания для записей на мобильном устройстве",
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей", "form.prefs.label.show_reading_time": "Показать примерное время чтения статей",

View file

@ -252,6 +252,7 @@
"error.invalid_language": "语言无效。", "error.invalid_language": "语言无效。",
"error.invalid_timezone": "无效的时区。", "error.invalid_timezone": "无效的时区。",
"error.invalid_entry_direction": "无效的输入方向。", "error.invalid_entry_direction": "无效的输入方向。",
"error.invalid_display_mode": "无效的Web应用显示模式。",
"form.feed.label.title": "标题", "form.feed.label.title": "标题",
"form.feed.label.site_url": "站点 URL", "form.feed.label.site_url": "站点 URL",
"form.feed.label.feed_url": "源 URL", "form.feed.label.feed_url": "源 URL",
@ -278,8 +279,13 @@
"form.prefs.label.theme": "主题", "form.prefs.label.theme": "主题",
"form.prefs.label.entry_sorting": "内容排序", "form.prefs.label.entry_sorting": "内容排序",
"form.prefs.label.entries_per_page": "每页条目", "form.prefs.label.entries_per_page": "每页条目",
"form.prefs.label.display_mode": "Web应用程序显示模式 (需要重新安装)",
"form.prefs.select.older_first": "旧->新", "form.prefs.select.older_first": "旧->新",
"form.prefs.select.recent_first": "新->旧", "form.prefs.select.recent_first": "新->旧",
"form.prefs.select.fullscreen": "全屏",
"form.prefs.select.standalone": "单机版",
"form.prefs.select.minimal_ui": "最小的",
"form.prefs.select.browser": "浏览器",
"form.prefs.label.keyboard_shortcuts": "启用键盘快捷键", "form.prefs.label.keyboard_shortcuts": "启用键盘快捷键",
"form.prefs.label.entry_swipe": "在移动设备上的条目上启用滑动手势", "form.prefs.label.entry_swipe": "在移动设备上的条目上启用滑动手势",
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间", "form.prefs.label.show_reading_time": "显示文章的预计阅读时间",

View file

@ -28,6 +28,7 @@ type User struct {
ShowReadingTime bool `json:"show_reading_time"` ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"` EntrySwipe bool `json:"entry_swipe"`
LastLoginAt *time.Time `json:"last_login_at"` LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"`
} }
// UserCreationRequest represents the request to create a user. // UserCreationRequest represents the request to create a user.
@ -55,6 +56,7 @@ type UserModificationRequest struct {
KeyboardShortcuts *bool `json:"keyboard_shortcuts"` KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"` ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"` EntrySwipe *bool `json:"entry_swipe"`
DisplayMode *string `json:"display_mode"`
} }
// Patch updates the User object with the modification request. // Patch updates the User object with the modification request.
@ -114,6 +116,10 @@ func (u *UserModificationRequest) Patch(user *User) {
if u.EntrySwipe != nil { if u.EntrySwipe != nil {
user.EntrySwipe = *u.EntrySwipe user.EntrySwipe = *u.EntrySwipe
} }
if u.DisplayMode != nil {
user.DisplayMode = *u.DisplayMode
}
} }
// UseTimezone converts last login date to the given timezone. // UseTimezone converts last login date to the given timezone.

View file

@ -83,7 +83,8 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
entry_swipe, entry_swipe,
stylesheet, stylesheet,
google_id, google_id,
openid_connect_id openid_connect_id,
display_mode
` `
tx, err := s.db.Begin() tx, err := s.db.Begin()
@ -114,6 +115,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.Stylesheet, &user.Stylesheet,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
&user.DisplayMode,
) )
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
@ -162,9 +164,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
entry_swipe=$11, entry_swipe=$11,
stylesheet=$12, stylesheet=$12,
google_id=$13, google_id=$13,
openid_connect_id=$14 openid_connect_id=$14,
display_mode=$15
WHERE WHERE
id=$15 id=$16
` `
_, err = s.db.Exec( _, err = s.db.Exec(
@ -183,6 +186,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.Stylesheet, user.Stylesheet,
user.GoogleID, user.GoogleID,
user.OpenIDConnectID, user.OpenIDConnectID,
user.DisplayMode,
user.ID, user.ID,
) )
if err != nil { if err != nil {
@ -203,9 +207,10 @@ func (s *Storage) UpdateUser(user *model.User) error {
entry_swipe=$10, entry_swipe=$10,
stylesheet=$11, stylesheet=$11,
google_id=$12, google_id=$12,
openid_connect_id=$13 openid_connect_id=$13,
display_mode=$14
WHERE WHERE
id=$14 id=$15
` `
_, err := s.db.Exec( _, err := s.db.Exec(
@ -223,6 +228,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.Stylesheet, user.Stylesheet,
user.GoogleID, user.GoogleID,
user.OpenIDConnectID, user.OpenIDConnectID,
user.DisplayMode,
user.ID, user.ID,
) )
@ -262,7 +268,8 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
openid_connect_id openid_connect_id,
display_mode
FROM FROM
users users
WHERE WHERE
@ -289,7 +296,8 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
openid_connect_id openid_connect_id,
display_mode
FROM FROM
users users
WHERE WHERE
@ -316,7 +324,8 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
openid_connect_id openid_connect_id,
display_mode
FROM FROM
users users
WHERE WHERE
@ -350,7 +359,8 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.last_login_at, u.last_login_at,
u.stylesheet, u.stylesheet,
u.google_id, u.google_id,
u.openid_connect_id u.openid_connect_id,
u.display_mode
FROM FROM
users u users u
LEFT JOIN LEFT JOIN
@ -379,6 +389,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.Stylesheet, &user.Stylesheet,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
&user.DisplayMode,
) )
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -442,7 +453,8 @@ func (s *Storage) Users() (model.Users, error) {
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
openid_connect_id openid_connect_id,
display_mode
FROM FROM
users users
ORDER BY username ASC ORDER BY username ASC
@ -472,6 +484,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.Stylesheet, &user.Stylesheet,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
&user.DisplayMode,
) )
if err != nil { if err != nil {

View file

@ -43,6 +43,14 @@
{{ end }} {{ end }}
</select> </select>
<label for="form-display-mode">{{ t "form.prefs.label.display_mode" }}</label>
<select id="form-display-mode" name="display_mode">
<option value="fullscreen" {{ if eq "fullscreen" $.form.DisplayMode }}selected="selected"{{ end }}>{{ t "form.prefs.select.fullscreen" }}</option>
<option value="standalone" {{ if eq "standalone" $.form.DisplayMode }}selected="selected"{{ end }}>{{ t "form.prefs.select.standalone" }}</option>
<option value="minimal-ui" {{ if eq "minimal-ui" $.form.DisplayMode }}selected="selected"{{ end }}>{{ t "form.prefs.select.minimal_ui" }}</option>
<option value="browser" {{ if eq "browser" $.form.DisplayMode }}selected="selected"{{ end }}>{{ t "form.prefs.select.browser" }}</option>
</select>
<label for="form-entry-direction">{{ t "form.prefs.label.entry_sorting" }}</label> <label for="form-entry-direction">{{ t "form.prefs.label.entry_sorting" }}</label>
<select id="form-entry-direction" name="entry_direction"> <select id="form-entry-direction" name="entry_direction">
<option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "form.prefs.select.older_first" }}</option> <option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "form.prefs.select.older_first" }}</option>

View file

@ -82,6 +82,10 @@ func TestGetUsers(t *testing.T) {
if users[0].EntriesPerPage != 100 { if users[0].EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, users[0].EntriesPerPage) t.Fatalf(`Invalid entries per page, got "%v"`, users[0].EntriesPerPage)
} }
if users[0].DisplayMode != "standalone" {
t.Fatalf(`Invalid web app display mode, got "%v"`, users[0].DisplayMode)
}
} }
func TestCreateStandardUser(t *testing.T) { func TestCreateStandardUser(t *testing.T) {
@ -127,6 +131,10 @@ func TestCreateStandardUser(t *testing.T) {
if user.EntriesPerPage != 100 { if user.EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage) t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage)
} }
if user.DisplayMode != "standalone" {
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
}
} }
func TestRemoveUser(t *testing.T) { func TestRemoveUser(t *testing.T) {
@ -195,6 +203,10 @@ func TestGetUserByID(t *testing.T) {
if user.EntriesPerPage != 100 { if user.EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage) t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage)
} }
if user.DisplayMode != "standalone" {
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
}
} }
func TestGetUserByUsername(t *testing.T) { func TestGetUserByUsername(t *testing.T) {
@ -250,6 +262,10 @@ func TestGetUserByUsername(t *testing.T) {
if user.EntriesPerPage != 100 { if user.EntriesPerPage != 100 {
t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage) t.Fatalf(`Invalid entries per page, got "%v"`, user.EntriesPerPage)
} }
if user.DisplayMode != "standalone" {
t.Fatalf(`Invalid web app display mode, got "%v"`, user.DisplayMode)
}
} }
func TestUpdateUserTheme(t *testing.T) { func TestUpdateUserTheme(t *testing.T) {
@ -282,10 +298,12 @@ func TestUpdateUserFields(t *testing.T) {
stylesheet := "body { color: red }" stylesheet := "body { color: red }"
swipe := false swipe := false
entriesPerPage := 5 entriesPerPage := 5
displayMode := "fullscreen"
user, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{ user, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{
Stylesheet: &stylesheet, Stylesheet: &stylesheet,
EntrySwipe: &swipe, EntrySwipe: &swipe,
EntriesPerPage: &entriesPerPage, EntriesPerPage: &entriesPerPage,
DisplayMode: &displayMode,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -302,6 +320,10 @@ func TestUpdateUserFields(t *testing.T) {
if user.EntriesPerPage != entriesPerPage { if user.EntriesPerPage != entriesPerPage {
t.Fatalf(`Unable to update user EntriesPerPage: got %q instead of %q`, user.EntriesPerPage, entriesPerPage) t.Fatalf(`Unable to update user EntriesPerPage: got %q instead of %q`, user.EntriesPerPage, entriesPerPage)
} }
if user.DisplayMode != displayMode {
t.Fatalf(`Unable to update user DisplayMode: got %q instead of %q`, user.DisplayMode, displayMode)
}
} }
func TestUpdateUserThemeWithInvalidValue(t *testing.T) { func TestUpdateUserThemeWithInvalidValue(t *testing.T) {
@ -394,6 +416,21 @@ func TestUpdateUserPasswordWithInvalidValue(t *testing.T) {
} }
} }
func TestUpdateUserDisplayModeWithInvalidValue(t *testing.T) {
username := getRandomUsername()
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
user, err := client.CreateUser(username, testStandardPassword, false)
if err != nil {
t.Fatal(err)
}
displayMode := "invalid"
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{DisplayMode: &displayMode})
if err == nil {
t.Fatal(`Updating a user web app display mode with an invalid value should raise an error`)
}
}
func TestUpdateUserWithEmptyUsernameValue(t *testing.T) { func TestUpdateUserWithEmptyUsernameValue(t *testing.T) {
username := getRandomUsername() username := getRandomUsername()
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword) client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)

View file

@ -26,6 +26,7 @@ type SettingsForm struct {
ShowReadingTime bool ShowReadingTime bool
CustomCSS string CustomCSS string
EntrySwipe bool EntrySwipe bool
DisplayMode string
} }
// Merge updates the fields of the given user. // Merge updates the fields of the given user.
@ -40,6 +41,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
user.ShowReadingTime = s.ShowReadingTime user.ShowReadingTime = s.ShowReadingTime
user.Stylesheet = s.CustomCSS user.Stylesheet = s.CustomCSS
user.EntrySwipe = s.EntrySwipe user.EntrySwipe = s.EntrySwipe
user.DisplayMode = s.DisplayMode
if s.Password != "" { if s.Password != "" {
user.Password = s.Password user.Password = s.Password
@ -50,7 +52,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
// Validate makes sure the form values are valid. // Validate makes sure the form values are valid.
func (s *SettingsForm) Validate() error { func (s *SettingsForm) Validate() error {
if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" { if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" || s.DisplayMode == "" {
return errors.NewLocalizedError("error.settings_mandatory_fields") return errors.NewLocalizedError("error.settings_mandatory_fields")
} }
@ -87,5 +89,6 @@ func NewSettingsForm(r *http.Request) *SettingsForm {
ShowReadingTime: r.FormValue("show_reading_time") == "1", ShowReadingTime: r.FormValue("show_reading_time") == "1",
CustomCSS: r.FormValue("custom_css"), CustomCSS: r.FormValue("custom_css"),
EntrySwipe: r.FormValue("entry_swipe") == "1", EntrySwipe: r.FormValue("entry_swipe") == "1",
DisplayMode: r.FormValue("display_mode"),
} }
} }

View file

@ -14,6 +14,7 @@ func TestValid(t *testing.T) {
Timezone: "UTC", Timezone: "UTC",
EntryDirection: "asc", EntryDirection: "asc",
EntriesPerPage: 50, EntriesPerPage: 50,
DisplayMode: "standalone",
} }
err := settings.Validate() err := settings.Validate()
@ -32,6 +33,7 @@ func TestConfirmationEmpty(t *testing.T) {
Timezone: "UTC", Timezone: "UTC",
EntryDirection: "asc", EntryDirection: "asc",
EntriesPerPage: 50, EntriesPerPage: 50,
DisplayMode: "standalone",
} }
err := settings.Validate() err := settings.Validate()
@ -54,6 +56,7 @@ func TestConfirmationIncorrect(t *testing.T) {
Timezone: "UTC", Timezone: "UTC",
EntryDirection: "asc", EntryDirection: "asc",
EntriesPerPage: 50, EntriesPerPage: 50,
DisplayMode: "standalone",
} }
err := settings.Validate() err := settings.Validate()

View file

@ -37,6 +37,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
ShowReadingTime: user.ShowReadingTime, ShowReadingTime: user.ShowReadingTime,
CustomCSS: user.Stylesheet, CustomCSS: user.Stylesheet,
EntrySwipe: user.EntrySwipe, EntrySwipe: user.EntrySwipe,
DisplayMode: user.DisplayMode,
} }
timezones, err := h.store.Timezones() timezones, err := h.store.Timezones()

View file

@ -60,6 +60,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
Timezone: model.OptionalString(settingsForm.Timezone), Timezone: model.OptionalString(settingsForm.Timezone),
EntryDirection: model.OptionalString(settingsForm.EntryDirection), EntryDirection: model.OptionalString(settingsForm.EntryDirection),
EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage), EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage),
DisplayMode: model.OptionalString(settingsForm.DisplayMode),
} }
if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil { if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil {

View file

@ -44,12 +44,21 @@ func (h *handler) showWebManifest(w http.ResponseWriter, r *http.Request) {
BackgroundColor string `json:"background_color"` BackgroundColor string `json:"background_color"`
} }
displayMode := "standalone"
if request.IsAuthenticated(r) {
user, err := h.store.UserByID(request.UserID(r))
if err != nil {
json.ServerError(w, r, err)
return
}
displayMode = user.DisplayMode
}
themeColor := model.ThemeColor(request.UserTheme(r)) themeColor := model.ThemeColor(request.UserTheme(r))
manifest := &webManifest{ manifest := &webManifest{
Name: "Miniflux", Name: "Miniflux",
ShortName: "Miniflux", ShortName: "Miniflux",
Description: "Minimalist Feed Reader", Description: "Minimalist Feed Reader",
Display: "standalone", Display: displayMode,
StartURL: route.Path(h.router, "unread"), StartURL: route.Path(h.router, "unread"),
ThemeColor: themeColor, ThemeColor: themeColor,
BackgroundColor: themeColor, BackgroundColor: themeColor,

View file

@ -73,6 +73,12 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod
} }
} }
if changes.DisplayMode != nil {
if err := validateDisplayMode(*changes.DisplayMode); err != nil {
return err
}
}
return nil return nil
} }
@ -124,3 +130,10 @@ func validateEntriesPerPage(entriesPerPage int) *ValidationError {
} }
return nil return nil
} }
func validateDisplayMode(displayMode string) *ValidationError {
if displayMode != "fullscreen" && displayMode != "standalone" && displayMode != "minimal-ui" && displayMode != "browser" {
return NewValidationError("error.invalid_display_mode")
}
return nil
}