Переглянути джерело

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
1pav 5 роки тому
батько
коміт
0d935a863f

+ 2 - 0
client/model.go

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

+ 8 - 0
database/migrations.go

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

+ 6 - 0
locale/translations/de_DE.json

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

+ 6 - 0
locale/translations/en_US.json

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

+ 6 - 0
locale/translations/es_ES.json

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

+ 6 - 0
locale/translations/fr_FR.json

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

+ 6 - 0
locale/translations/it_IT.json

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

+ 6 - 0
locale/translations/ja_JP.json

@@ -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": "記事の推定読書時間を表示する",

+ 6 - 0
locale/translations/nl_NL.json

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

+ 6 - 0
locale/translations/pl_PL.json

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

+ 6 - 0
locale/translations/pt_BR.json

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

+ 6 - 0
locale/translations/ru_RU.json

@@ -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": "Показать примерное время чтения статей",

+ 6 - 0
locale/translations/zh_CN.json

@@ -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": "显示文章的预计阅读时间",

+ 6 - 0
model/user.go

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

+ 23 - 10
storage/user.go

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

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

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

+ 37 - 0
tests/user_test.go

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

+ 4 - 1
ui/form/settings.go

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

+ 3 - 0
ui/form/settings_test.go

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

+ 1 - 0
ui/settings_show.go

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

+ 1 - 0
ui/settings_update.go

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

+ 10 - 1
ui/static_manifest.go

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

+ 13 - 0
validator/user.go

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