diff --git a/client/model.go b/client/model.go index 170b6cfe..0e18c29a 100644 --- a/client/model.go +++ b/client/model.go @@ -34,7 +34,7 @@ type User struct { KeyboardShortcuts bool `json:"keyboard_shortcuts"` ShowReadingTime bool `json:"show_reading_time"` EntrySwipe bool `json:"entry_swipe"` - DoubleTap bool `json:"double_tap"` + GestureNav string `json:"gesture_nav"` LastLoginAt *time.Time `json:"last_login_at"` DisplayMode string `json:"display_mode"` DefaultReadingSpeed int `json:"default_reading_speed"` @@ -73,7 +73,7 @@ type UserModificationRequest struct { KeyboardShortcuts *bool `json:"keyboard_shortcuts"` ShowReadingTime *bool `json:"show_reading_time"` EntrySwipe *bool `json:"entry_swipe"` - DoubleTap *bool `json:"double_tap"` + GestureNav *string `json:"gesture_nav"` DisplayMode *string `json:"display_mode"` DefaultReadingSpeed *int `json:"default_reading_speed"` CJKReadingSpeed *int `json:"cjk_reading_speed"` diff --git a/database/migrations.go b/database/migrations.go index 433120a4..141917dc 100644 --- a/database/migrations.go +++ b/database/migrations.go @@ -644,4 +644,13 @@ var migrations = []func(tx *sql.Tx) error{ `) return }, + func(tx *sql.Tx) (err error) { + sql := ` + ALTER TABLE users RENAME double_tap TO gesture_nav; + ALTER TABLE users ALTER COLUMN gesture_nav SET DATA TYPE text using case when gesture_nav = true then 'tap' when gesture_nav = false then 'none' end; + ALTER TABLE users ALTER COLUMN gesture_nav SET default 'tap'; + ` + _, err = tx.Exec(sql) + return err + }, } diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json index e374e6c9..17184438 100644 --- a/locale/translations/de_DE.json +++ b/locale/translations/de_DE.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "Ungültige Zeitzone.", "error.invalid_entry_direction": "Ungültige Sortierreihenfolge.", "error.invalid_display_mode": "Progressive Web App (PWA) Anzeigemodus", + "error.invalid_gesture_nav": "Ungültige Gestennavigation.", "error.invalid_default_home_page": "Ungültige Standard-Startseite!", "form.feed.label.title": "Titel", "form.feed.label.site_url": "Webseite-URL", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "Eintrag erstellt Zeit", "form.prefs.select.alphabetical": "Alphabetisch", "form.prefs.select.unread_count": "Ungelesen zählen", + "form.prefs.select.none": "Keiner", + "form.prefs.select.tap": "Doppeltippen", + "form.prefs.select.swipe": "Wischen", "form.prefs.label.keyboard_shortcuts": "Tastaturkürzel aktivieren", "form.prefs.label.entry_swipe": "Aktivieren Sie das Streichen von Einträgen auf Touchscreens", - "form.prefs.label.double_tap": "Doppeltippen aktivieren, um zwischen Einträgen zu navigieren", + "form.prefs.label.gesture_nav": "Geste zum Navigieren zwischen Einträgen", "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", diff --git a/locale/translations/el_EL.json b/locale/translations/el_EL.json index 014f5db6..c8b8340a 100644 --- a/locale/translations/el_EL.json +++ b/locale/translations/el_EL.json @@ -242,6 +242,7 @@ "error.invalid_timezone": "Μη έγκυρη ζώνη ώρας.", "error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.", "error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.", + "error.invalid_gesture_nav": "Μη έγκυρη πλοήγηση με χειρονομίες.", "error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!", "error.empty_file": "Αυτό το αρχείο είναι κενό.", "error.bad_credentials": "Μη έγκυρο όνομα χρήστη ή κωδικό πρόσβασης.", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "Χρόνος δημιουργίας καταχώρησης", "form.prefs.select.alphabetical": "Αλφαβητική σειρά", "form.prefs.select.unread_count": "Αριθμός μη αναγνωσμένων", + "form.prefs.select.none": "Κανένας", + "form.prefs.select.tap": "Διπλό χτύπημα", + "form.prefs.select.swipe": "Σουφρώνω", "form.prefs.label.keyboard_shortcuts": "Ενεργοποίηση συντομεύσεων πληκτρολογίου", "form.prefs.label.entry_swipe": "Ενεργοποιήστε το σάρωση καταχώρισης στις οθόνες αφής", - "form.prefs.label.double_tap": "Ενεργοποιήστε το διπλό πάτημα για πλοήγηση μεταξύ των καταχωρήσεων", + "form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων", "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα", "form.prefs.label.custom_css": "Προσαρμοσμένο CSS", "form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου", diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json index a06ea944..a62ea3a5 100644 --- a/locale/translations/en_US.json +++ b/locale/translations/en_US.json @@ -242,6 +242,7 @@ "error.invalid_timezone": "Invalid timezone.", "error.invalid_entry_direction": "Invalid entry direction.", "error.invalid_display_mode": "Invalid web app display mode.", + "error.invalid_gesture_nav": "Invalid gesture navigation.", "error.invalid_default_home_page": "Invalid default homepage!", "error.empty_file": "This file is empty.", "error.bad_credentials": "Invalid username or password.", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "Entry created time", "form.prefs.select.alphabetical": "Alphabetical", "form.prefs.select.unread_count": "Unread count", + "form.prefs.select.none": "None", + "form.prefs.select.tap": "Double tap", + "form.prefs.select.swipe": "Swipe", "form.prefs.label.keyboard_shortcuts": "Enable keyboard shortcuts", "form.prefs.label.entry_swipe": "Enable entry swipe on touch screens", - "form.prefs.label.double_tap": "Enable double tap to navigate between entries", + "form.prefs.label.gesture_nav": "Gesture to navigate between entries", "form.prefs.label.show_reading_time": "Show estimated reading time for entries", "form.prefs.label.custom_css": "Custom CSS", "form.prefs.label.entry_order": "Entry sorting column", diff --git a/locale/translations/es_ES.json b/locale/translations/es_ES.json index 0fbdbddf..8ae726ff 100644 --- a/locale/translations/es_ES.json +++ b/locale/translations/es_ES.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "Zona horaria no válida.", "error.invalid_entry_direction": "Dirección de artículo no válida.", "error.invalid_display_mode": "Modo de visualización de la aplicación web no válido.", + "error.invalid_gesture_nav": "Navegación por gestos no válida.", "error.invalid_default_home_page": "¡Página de inicio por defecto no válida!", "form.feed.label.title": "Título", "form.feed.label.site_url": "URL del sitio", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "Hora de creación del artículo", "form.prefs.select.alphabetical": "Alfabético", "form.prefs.select.unread_count": "Recuento de no leídos", + "form.prefs.select.none": "Ninguno", + "form.prefs.select.tap": "Doble toque", + "form.prefs.select.swipe": "Golpe fuerte", "form.prefs.label.keyboard_shortcuts": "Habilitar atajos de teclado", "form.prefs.label.entry_swipe": "Habilitar deslizamiento de entrada en pantallas táctiles", - "form.prefs.label.double_tap": "Habilite el doble toque para navegar entre las entradas", + "form.prefs.label.gesture_nav": "Gesto para navegar entre entradas", "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 artículos", diff --git a/locale/translations/fi_FI.json b/locale/translations/fi_FI.json index 67e416ce..9f49ce66 100644 --- a/locale/translations/fi_FI.json +++ b/locale/translations/fi_FI.json @@ -242,6 +242,7 @@ "error.invalid_timezone": "Virheellinen aikavyöhyke.", "error.invalid_entry_direction": "Invalid entry direction.", "error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.", + "error.invalid_gesture_nav": "Virheellinen ele-navigointi.", "error.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!", "error.empty_file": "Tiedosto on tyhjä.", "error.bad_credentials": "Virheellinen käyttäjänimi tai salasana.", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "Luomisaika", "form.prefs.select.alphabetical": "Aakkosjärjestys", "form.prefs.select.unread_count": "Lukemattomien määrä", + "form.prefs.select.none": "Ei mitään", + "form.prefs.select.tap": "Kaksoisnapauta", + "form.prefs.select.swipe": "Pyyhkäise", "form.prefs.label.keyboard_shortcuts": "Ota pikanäppäimet käyttöön", "form.prefs.label.entry_swipe": "Ota syöttöpyyhkäisy käyttöön kosketusnäytöissä", - "form.prefs.label.double_tap": "Ota kaksoisnapautus käyttöön siirtyäksesi merkintöjen välillä", + "form.prefs.label.gesture_nav": "Ele siirtyäksesi merkintöjen välillä", "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", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index bf41b130..5ed90ddf 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "Fuseau horaire non valide.", "error.invalid_entry_direction": "Ordre de trie non valide.", "error.invalid_display_mode": "Mode d'affichage de l'application web non valide.", + "error.invalid_gesture_nav": "Navigation gestuelle non valide.", "error.invalid_default_home_page": "Page d'accueil par défaut invalide !", "form.feed.label.title": "Titre", "form.feed.label.site_url": "URL du site web", @@ -308,9 +309,12 @@ "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.select.none": "Aucun", + "form.prefs.select.tap": "Tapez deux fois", + "form.prefs.select.swipe": "Glisser", "form.prefs.label.keyboard_shortcuts": "Activer les raccourcis clavier", "form.prefs.label.entry_swipe": "Activer le balayage des entrées sur les écrans tactiles", - "form.prefs.label.double_tap": "Activer le double tap pour naviguer entre les entrées", + "form.prefs.label.gesture_nav": "Geste pour naviguer entre les entrées", "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", diff --git a/locale/translations/hi_IN.json b/locale/translations/hi_IN.json index 3d9b5e32..179fe59a 100644 --- a/locale/translations/hi_IN.json +++ b/locale/translations/hi_IN.json @@ -242,6 +242,7 @@ "error.invalid_timezone": "अमान्य समयक्षेत्र.", "error.invalid_entry_direction": "अमान्य प्रवेश दिशा।", "error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.", + "error.invalid_gesture_nav": "अमान्य इशारा नेविगेशन।", "error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!", "error.empty_file": "यह फ़ाइल खाली है।", "error.bad_credentials": "अमान्य उपयोगकर्ता नाम या पासवर्ड।", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "प्रवेश बनाया समय", "form.prefs.select.alphabetical": "वर्णक्रम", "form.prefs.select.unread_count": "अपठित गणना", + "form.prefs.select.none": "कोई नहीं", + "form.prefs.select.tap": "दो बार टैप", + "form.prefs.select.swipe": "कड़ी चोट", "form.prefs.label.keyboard_shortcuts": "कीबोर्ड शॉर्टकट सक्षम करें", "form.prefs.label.entry_swipe": "टच स्क्रीन पर एंट्री स्वाइप सक्षम करें", - "form.prefs.label.double_tap": "प्रविष्टियों के बीच नेविगेट करने के लिए डबल टैप सक्षम करें", + "form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा", "form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं", "form.prefs.label.custom_css": "कस्टम सीएसएस", "form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम", diff --git a/locale/translations/id_ID.json b/locale/translations/id_ID.json index dd1f956a..6748ea41 100644 --- a/locale/translations/id_ID.json +++ b/locale/translations/id_ID.json @@ -239,6 +239,7 @@ "error.invalid_timezone": "Zona waktu tidak valid.", "error.invalid_entry_direction": "Urutan entri tidak valid.", "error.invalid_display_mode": "Mode tampilan aplikasi web tidak valid.", + "error.invalid_gesture_nav": "Navigasi gestur tidak valid.", "error.invalid_default_home_page": "Beranda baku tidak valid!", "error.empty_file": "Berkas ini kosong.", "error.bad_credentials": "Nama pengguna atau kata sandi tidak valid.", @@ -305,9 +306,12 @@ "form.prefs.select.created_time": "Waktu entri dibuat", "form.prefs.select.alphabetical": "Secara alfabet", "form.prefs.select.unread_count": "Jumlah yang belum dibaca", + "form.prefs.select.none": "Tidak ada", + "form.prefs.select.tap": "Ketuk dua kali", + "form.prefs.select.swipe": "Geser", "form.prefs.label.keyboard_shortcuts": "Aktifkan pintasan papan tik", "form.prefs.label.entry_swipe": "Aktifkan tindakan geser pada entri di ponsel", - "form.prefs.label.double_tap": "Aktifkan ketuk dua kali untuk navigasi antar entri", + "form.prefs.label.gesture_nav": "Isyarat untuk menavigasi antar entri", "form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel", "form.prefs.label.custom_css": "Modifikasi CSS", "form.prefs.label.entry_order": "Pengurutan Kolom Entri", diff --git a/locale/translations/it_IT.json b/locale/translations/it_IT.json index d56f07fb..d244c79a 100644 --- a/locale/translations/it_IT.json +++ b/locale/translations/it_IT.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "Fuso orario non valido.", "error.invalid_entry_direction": "Ordinamento non valido.", "error.invalid_display_mode": "Modalità di visualizzazione web app non valida.", + "error.invalid_gesture_nav": "Navigazione gestuale non valida.", "error.invalid_default_home_page": "Pagina iniziale predefinita non valida!", "form.feed.label.title": "Titolo", "form.feed.label.site_url": "URL del sito", @@ -308,9 +309,12 @@ "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.select.none": "Nessuno", + "form.prefs.select.tap": "Tocca due volte", + "form.prefs.select.swipe": "Scorri", "form.prefs.label.keyboard_shortcuts": "Abilita le scorciatoie da tastiera", "form.prefs.label.entry_swipe": "Abilita lo scorrimento della voce sui touch screen", - "form.prefs.label.double_tap": "Abilita il doppio tocco per navigare tra le voci", + "form.prefs.label.gesture_nav": "Gesto per navigare tra le voci", "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", diff --git a/locale/translations/ja_JP.json b/locale/translations/ja_JP.json index 0d3246fd..cd394f3d 100644 --- a/locale/translations/ja_JP.json +++ b/locale/translations/ja_JP.json @@ -242,6 +242,7 @@ "error.invalid_timezone": "タイムゾーンが無効です。", "error.invalid_entry_direction": "記事の表示順が無効です。", "error.invalid_display_mode": "Web アプリの表示モードが無効です。", + "error.invalid_gesture_nav": "ジェスチャー ナビゲーションが無効です。", "error.invalid_default_home_page": "デフォルトのトップページが無効です", "error.empty_file": "このファイルは空です。", "error.bad_credentials": "ユーザー名かパスワードが間違っています。", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "記事の取得時刻", "form.prefs.select.alphabetical": "アルファベット順", "form.prefs.select.unread_count": "未読数", + "form.prefs.select.none": "なし", + "form.prefs.select.tap": "ダブルタップ", + "form.prefs.select.swipe": "スワイプ", "form.prefs.label.keyboard_shortcuts": "キーボードショートカットを有効にする", "form.prefs.label.entry_swipe": "タッチスクリーンでスワイプ入力を有効にする", - "form.prefs.label.double_tap": "ダブルタップで記事間を移動する", + "form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー", "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する", "form.prefs.label.custom_css": "カスタム CSS", "form.prefs.label.entry_order": "記事の表示順の基準", diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json index 0d856424..a21e2454 100644 --- a/locale/translations/nl_NL.json +++ b/locale/translations/nl_NL.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "Ongeldige tijdzone.", "error.invalid_entry_direction": "Ongeldige sorteervolgorde.", "error.invalid_display_mode": "Ongeldige weergavemodus voor webapp.", + "error.invalid_gesture_nav": "Ongeldige gebarennavigatie.", "error.invalid_default_home_page": "Ongeldige standaard homepage!", "form.feed.label.title": "Naam", "form.feed.label.site_url": "Website URL", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "Tijdstip van binnenkomst", "form.prefs.select.alphabetical": "Alfabetisch", "form.prefs.select.unread_count": "Ongelezen tellen", + "form.prefs.select.none": "Geen", + "form.prefs.select.tap": "Dubbeltik", + "form.prefs.select.swipe": "Vegen", "form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in", "form.prefs.label.entry_swipe": "Invoervegen inschakelen op aanraakschermen", - "form.prefs.label.double_tap": "Schakel dubbeltikken in om tussen vermeldingen te navigeren", + "form.prefs.label.gesture_nav": "Gebaar om tussen ingangen te navigeren", "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", diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json index c14f67e0..ea1ef740 100644 --- a/locale/translations/pl_PL.json +++ b/locale/translations/pl_PL.json @@ -266,6 +266,7 @@ "error.invalid_timezone": "Nieprawidłowa strefa czasowa.", "error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.", "error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji internetowej.", + "error.invalid_gesture_nav": "Nieprawidłowa nawigacja gestami.", "error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!", "form.feed.label.title": "Tytuł", "form.feed.label.site_url": "URL strony", @@ -303,7 +304,7 @@ "form.prefs.select.older_first": "Najstarsze wpisy jako pierwsze", "form.prefs.label.keyboard_shortcuts": "Włącz skróty klawiaturowe", "form.prefs.label.entry_swipe": "Włącz machnięcie wpisu na ekranach dotykowych", - "form.prefs.label.double_tap": "Włącz podwójne dotknięcie, aby przechodzić między wpisami", + "form.prefs.label.gesture_nav": "Gest, aby poruszać się między wpisami", "form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania artykułów", "form.prefs.select.recent_first": "Najnowsze wpisy jako pierwsze", "form.prefs.select.fullscreen": "Pełny ekran", @@ -314,6 +315,9 @@ "form.prefs.select.created_time": "Czas utworzenia wpisu", "form.prefs.select.alphabetical": "Alfabetycznie", "form.prefs.select.unread_count": "Liczba nieprzeczytanych", + "form.prefs.select.none": "Nic", + "form.prefs.select.tap": "Podwójne wciśnięcie", + "form.prefs.select.swipe": "Trzepnąć", "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", diff --git a/locale/translations/pt_BR.json b/locale/translations/pt_BR.json index 8e048a72..01df91f4 100644 --- a/locale/translations/pt_BR.json +++ b/locale/translations/pt_BR.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "Fuso horário inválido.", "error.invalid_entry_direction": "Direção de entrada inválida.", "error.invalid_display_mode": "Modo de exibição de aplicativo inválido da web.", + "error.invalid_gesture_nav": "Navegação por gestos inválida.", "error.invalid_default_home_page": "Página inicial por defeito inválida!", "form.feed.label.title": "Título", "form.feed.label.site_url": "URL do site", @@ -308,9 +309,12 @@ "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.select.none": "Nenhum", + "form.prefs.select.tap": "Toque duplo", + "form.prefs.select.swipe": "Deslize", "form.prefs.label.keyboard_shortcuts": "Habilitar atalhos do teclado", "form.prefs.label.entry_swipe": "Ativar entrada de furto em telas sensíveis ao toque", - "form.prefs.label.double_tap": "Ative o toque duplo para navegar entre as entradas", + "form.prefs.label.gesture_nav": "Gesto para navegar entre as entradas", "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", diff --git a/locale/translations/ru_RU.json b/locale/translations/ru_RU.json index 9e9603ff..9a73d61b 100644 --- a/locale/translations/ru_RU.json +++ b/locale/translations/ru_RU.json @@ -266,6 +266,7 @@ "error.invalid_timezone": "Неверный часовой пояс.", "error.invalid_entry_direction": "Неверное направление входа.", "error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.", + "error.invalid_gesture_nav": "Неверная жестовая навигация.", "error.invalid_default_home_page": "Неверная домашняя страница по умолчанию!", "form.feed.label.title": "Название", "form.feed.label.site_url": "URL сайта", @@ -310,9 +311,12 @@ "form.prefs.select.created_time": "Время создания записи", "form.prefs.select.alphabetical": "По алфавиту", "form.prefs.select.unread_count": "Количество непрочитанных", + "form.prefs.select.none": "Никто", + "form.prefs.select.tap": "Двойное нажатие", + "form.prefs.select.swipe": "Проведите", "form.prefs.label.keyboard_shortcuts": "Включить сочетания клавиш", "form.prefs.label.entry_swipe": "Включить пролистывание ввода на сенсорных экранах", - "form.prefs.label.double_tap": "Включить двойное касание для перехода между записями", + "form.prefs.label.gesture_nav": "Жест для перехода между записями", "form.prefs.label.show_reading_time": "Показать примерное время чтения статей", "form.prefs.label.custom_css": "Пользовательские CSS", "form.prefs.label.entry_order": "Колонка сортировки ввода", diff --git a/locale/translations/tr_TR.json b/locale/translations/tr_TR.json index 285c1d1a..a495c9d6 100644 --- a/locale/translations/tr_TR.json +++ b/locale/translations/tr_TR.json @@ -242,6 +242,7 @@ "error.invalid_timezone": "Geçersiz saat dilimi", "error.invalid_entry_direction": "Geçersiz giriş yönü.", "error.invalid_display_mode": "Geçersiz web uygulaması görüntüleme modu.", + "error.invalid_gesture_nav": "Hareketle gezinme geçersiz.", "error.invalid_default_home_page": "Geçersiz varsayılan ana sayfa!", "error.empty_file": "Bu dosya boş.", "error.bad_credentials": "Geçersiz kullanıcı veya parola.", @@ -308,9 +309,12 @@ "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.select.none": "Hiçbiri", + "form.prefs.select.tap": "çift dokunma", + "form.prefs.select.swipe": "Tokatlamak", "form.prefs.label.keyboard_shortcuts": "Klavye kısayollarını etkinleştir", "form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах", - "form.prefs.label.double_tap": "Girişler arasında gezinmek için çift dokunmayı etkinleştirin", + "form.prefs.label.gesture_nav": "Girişler arasında gezinmek için hareket", "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", diff --git a/locale/translations/uk_UA.json b/locale/translations/uk_UA.json index 0a45a5c5..abc47b35 100644 --- a/locale/translations/uk_UA.json +++ b/locale/translations/uk_UA.json @@ -241,6 +241,7 @@ "error.invalid_timezone": "Недійсний часовий пояс.", "error.invalid_entry_direction": "Недійсний напрямок запису.", "error.invalid_display_mode": "Недійсний режим відображення.", + "error.invalid_gesture_nav": "Недійсна навігація жестами.", "error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!", "error.empty_file": "Цей файл порожній.", "error.bad_credentials": "Невірне ім’я користувача або пароль.", @@ -307,9 +308,12 @@ "form.prefs.select.created_time": "Дата створення запису", "form.prefs.select.alphabetical": "За алфавітом", "form.prefs.select.unread_count": "Кількість непрочитаних", + "form.prefs.select.none": "Жодного", + "form.prefs.select.tap": "Двічі натисніть", + "form.prefs.select.swipe": "Проведіть пальцем", "form.prefs.label.keyboard_shortcuts": "Увімкнути комбінації клавиш", "form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах", - "form.prefs.label.double_tap": "Увімкніть подвійне торкання, щоб переходити між записами", + "form.prefs.label.gesture_nav": "Жест для переходу між записами", "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів", "form.prefs.label.custom_css": "Спеціальний CSS", "form.prefs.label.entry_order": "Стовпець сортування записів", diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json index affcdc2f..b867cb3d 100644 --- a/locale/translations/zh_CN.json +++ b/locale/translations/zh_CN.json @@ -262,6 +262,7 @@ "error.invalid_timezone": "无效的时区。", "error.invalid_entry_direction": "无效的输入方向。", "error.invalid_display_mode": "无效的网页应用显示模式。", + "error.invalid_gesture_nav": "手势导航无效。", "error.invalid_default_home_page": "无效的默认主页!", "form.feed.label.title": "标题", "form.feed.label.site_url": "源网站 URL", @@ -306,9 +307,12 @@ "form.prefs.select.created_time": "文章创建时间", "form.prefs.select.alphabetical": "按字母顺序", "form.prefs.select.unread_count": "未读计数", + "form.prefs.select.none": "没有任何", + "form.prefs.select.tap": "双击", + "form.prefs.select.swipe": "滑动", "form.prefs.label.keyboard_shortcuts": "启用键盘快捷键", "form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动", - "form.prefs.label.double_tap": "启用双击以在条目之间导航", + "form.prefs.label.gesture_nav": "在条目之间导航的手势", "form.prefs.label.show_reading_time": "显示文章的预计阅读时间", "form.prefs.label.custom_css": "自定义 CSS", "form.prefs.label.entry_order": "文章排序依据", diff --git a/locale/translations/zh_TW.json b/locale/translations/zh_TW.json index 8c81190e..3e8f85d8 100644 --- a/locale/translations/zh_TW.json +++ b/locale/translations/zh_TW.json @@ -264,6 +264,7 @@ "error.invalid_timezone": "無效的時區。", "error.invalid_entry_direction": "無效的輸入方向。", "error.invalid_display_mode": "無效的網頁應用顯示模式。", + "error.invalid_gesture_nav": "手勢導航無效.", "error.invalid_default_home_page": "默認主頁無效!", "form.feed.label.title": "標題", "form.feed.label.site_url": "網站 URL", @@ -308,9 +309,12 @@ "form.prefs.select.created_time": "文章建立時間", "form.prefs.select.alphabetical": "按字母順序", "form.prefs.select.unread_count": "未讀計數", + "form.prefs.select.none": "沒有任何", + "form.prefs.select.tap": "雙擊", + "form.prefs.select.swipe": "滑動", "form.prefs.label.keyboard_shortcuts": "啟用鍵盤快捷鍵", "form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动", - "form.prefs.label.double_tap": "啟用雙擊以在條目之間導航", + "form.prefs.label.gesture_nav": "在條目之間導航的手勢", "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間", "form.prefs.label.custom_css": "自定義 CSS", "form.prefs.label.entry_order": "文章排序依據", diff --git a/model/user.go b/model/user.go index 08608900..62a0594d 100644 --- a/model/user.go +++ b/model/user.go @@ -28,7 +28,7 @@ type User struct { KeyboardShortcuts bool `json:"keyboard_shortcuts"` ShowReadingTime bool `json:"show_reading_time"` EntrySwipe bool `json:"entry_swipe"` - DoubleTap bool `json:"double_tap"` + GestureNav string `json:"gesture_nav"` LastLoginAt *time.Time `json:"last_login_at"` DisplayMode string `json:"display_mode"` DefaultReadingSpeed int `json:"default_reading_speed"` @@ -63,7 +63,7 @@ type UserModificationRequest struct { KeyboardShortcuts *bool `json:"keyboard_shortcuts"` ShowReadingTime *bool `json:"show_reading_time"` EntrySwipe *bool `json:"entry_swipe"` - DoubleTap *bool `json:"double_tap"` + GestureNav *string `json:"gesture_nav"` DisplayMode *string `json:"display_mode"` DefaultReadingSpeed *int `json:"default_reading_speed"` CJKReadingSpeed *int `json:"cjk_reading_speed"` @@ -133,8 +133,8 @@ func (u *UserModificationRequest) Patch(user *User) { user.EntrySwipe = *u.EntrySwipe } - if u.DoubleTap != nil { - user.DoubleTap = *u.DoubleTap + if u.GestureNav != nil { + user.GestureNav = *u.GestureNav } if u.DisplayMode != nil { diff --git a/storage/user.go b/storage/user.go index 9f5a2768..4b0737f2 100644 --- a/storage/user.go +++ b/storage/user.go @@ -81,7 +81,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m keyboard_shortcuts, show_reading_time, entry_swipe, - double_tap, + gesture_nav, stylesheet, google_id, openid_connect_id, @@ -118,7 +118,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m &user.KeyboardShortcuts, &user.ShowReadingTime, &user.EntrySwipe, - &user.DoubleTap, + &user.GestureNav, &user.Stylesheet, &user.GoogleID, &user.OpenIDConnectID, @@ -174,7 +174,7 @@ func (s *Storage) UpdateUser(user *model.User) error { keyboard_shortcuts=$9, show_reading_time=$10, entry_swipe=$11, - double_tap=$12, + gesture_nav=$12, stylesheet=$13, google_id=$14, openid_connect_id=$15, @@ -201,7 +201,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.KeyboardShortcuts, user.ShowReadingTime, user.EntrySwipe, - user.DoubleTap, + user.GestureNav, user.Stylesheet, user.GoogleID, user.OpenIDConnectID, @@ -229,7 +229,7 @@ func (s *Storage) UpdateUser(user *model.User) error { keyboard_shortcuts=$8, show_reading_time=$9, entry_swipe=$10, - double_tap=$11, + gesture_nav=$11, stylesheet=$12, google_id=$13, openid_connect_id=$14, @@ -255,7 +255,7 @@ func (s *Storage) UpdateUser(user *model.User) error { user.KeyboardShortcuts, user.ShowReadingTime, user.EntrySwipe, - user.DoubleTap, + user.GestureNav, user.Stylesheet, user.GoogleID, user.OpenIDConnectID, @@ -301,7 +301,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) { keyboard_shortcuts, show_reading_time, entry_swipe, - double_tap, + gesture_nav, last_login_at, stylesheet, google_id, @@ -335,7 +335,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) { keyboard_shortcuts, show_reading_time, entry_swipe, - double_tap, + gesture_nav, last_login_at, stylesheet, google_id, @@ -369,7 +369,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) { keyboard_shortcuts, show_reading_time, entry_swipe, - double_tap, + gesture_nav, last_login_at, stylesheet, google_id, @@ -410,7 +410,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) { u.keyboard_shortcuts, u.show_reading_time, u.entry_swipe, - u.double_tap, + u.gesture_nav, u.last_login_at, u.stylesheet, u.google_id, @@ -445,7 +445,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err &user.KeyboardShortcuts, &user.ShowReadingTime, &user.EntrySwipe, - &user.DoubleTap, + &user.GestureNav, &user.LastLoginAt, &user.Stylesheet, &user.GoogleID, @@ -542,7 +542,7 @@ func (s *Storage) Users() (model.Users, error) { keyboard_shortcuts, show_reading_time, entry_swipe, - double_tap, + gesture_nav, last_login_at, stylesheet, google_id, @@ -578,7 +578,7 @@ func (s *Storage) Users() (model.Users, error) { &user.KeyboardShortcuts, &user.ShowReadingTime, &user.EntrySwipe, - &user.DoubleTap, + &user.GestureNav, &user.LastLoginAt, &user.Stylesheet, &user.GoogleID, diff --git a/template/templates/views/entry.html b/template/templates/views/entry.html index 1cc17039..a0fec9b7 100644 --- a/template/templates/views/entry.html +++ b/template/templates/views/entry.html @@ -143,7 +143,7 @@ {{ end }} {{ end }} -
+
{{ if .user }} {{ noescape (proxyFilter .entry.Content) }} {{ else }} diff --git a/template/templates/views/settings.html b/template/templates/views/settings.html index 8a33b0dd..a7d7e26b 100644 --- a/template/templates/views/settings.html +++ b/template/templates/views/settings.html @@ -90,7 +90,12 @@ - + + diff --git a/tests/user_test.go b/tests/user_test.go index 74e86cef..e8369f77 100644 --- a/tests/user_test.go +++ b/tests/user_test.go @@ -88,6 +88,10 @@ func TestGetUsers(t *testing.T) { t.Fatalf(`Invalid web app display mode, got "%v"`, users[0].DisplayMode) } + if users[0].GestureNav != "tap" { + t.Fatalf(`Invalid gesture navigation, got "%v"`, users[0].GestureNav) + } + if users[0].DefaultReadingSpeed != 265 { t.Fatalf(`Invalid default reading speed, got "%v"`, users[0].DefaultReadingSpeed) } diff --git a/ui/form/settings.go b/ui/form/settings.go index ef508743..4a6c56e3 100644 --- a/ui/form/settings.go +++ b/ui/form/settings.go @@ -27,7 +27,7 @@ type SettingsForm struct { ShowReadingTime bool CustomCSS string EntrySwipe bool - DoubleTap bool + GestureNav string DisplayMode string DefaultReadingSpeed int CJKReadingSpeed int @@ -48,7 +48,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User { user.ShowReadingTime = s.ShowReadingTime user.Stylesheet = s.CustomCSS user.EntrySwipe = s.EntrySwipe - user.DoubleTap = s.DoubleTap + user.GestureNav = s.GestureNav user.DisplayMode = s.DisplayMode user.CJKReadingSpeed = s.CJKReadingSpeed user.DefaultReadingSpeed = s.DefaultReadingSpeed @@ -114,7 +114,7 @@ func NewSettingsForm(r *http.Request) *SettingsForm { ShowReadingTime: r.FormValue("show_reading_time") == "1", CustomCSS: r.FormValue("custom_css"), EntrySwipe: r.FormValue("entry_swipe") == "1", - DoubleTap: r.FormValue("double_tap") == "1", + GestureNav: r.FormValue("gesture_nav"), DisplayMode: r.FormValue("display_mode"), DefaultReadingSpeed: int(defaultReadingSpeed), CJKReadingSpeed: int(cjkReadingSpeed), diff --git a/ui/form/settings_test.go b/ui/form/settings_test.go index 07b0d162..655c9fe2 100644 --- a/ui/form/settings_test.go +++ b/ui/form/settings_test.go @@ -15,6 +15,7 @@ func TestValid(t *testing.T) { EntryDirection: "asc", EntriesPerPage: 50, DisplayMode: "standalone", + GestureNav: "tap", DefaultReadingSpeed: 35, CJKReadingSpeed: 25, DefaultHomePage: "unread", @@ -37,6 +38,7 @@ func TestConfirmationEmpty(t *testing.T) { EntryDirection: "asc", EntriesPerPage: 50, DisplayMode: "standalone", + GestureNav: "tap", DefaultReadingSpeed: 35, CJKReadingSpeed: 25, DefaultHomePage: "unread", @@ -63,6 +65,7 @@ func TestConfirmationIncorrect(t *testing.T) { EntryDirection: "asc", EntriesPerPage: 50, DisplayMode: "standalone", + GestureNav: "tap", DefaultReadingSpeed: 35, CJKReadingSpeed: 25, DefaultHomePage: "unread", diff --git a/ui/settings_show.go b/ui/settings_show.go index fa2e1480..7b04d4e0 100644 --- a/ui/settings_show.go +++ b/ui/settings_show.go @@ -38,7 +38,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) { ShowReadingTime: user.ShowReadingTime, CustomCSS: user.Stylesheet, EntrySwipe: user.EntrySwipe, - DoubleTap: user.DoubleTap, + GestureNav: user.GestureNav, DisplayMode: user.DisplayMode, DefaultReadingSpeed: user.DefaultReadingSpeed, CJKReadingSpeed: user.CJKReadingSpeed, diff --git a/ui/settings_update.go b/ui/settings_update.go index 6c4550cc..ce516eb6 100644 --- a/ui/settings_update.go +++ b/ui/settings_update.go @@ -61,6 +61,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) { EntryDirection: model.OptionalString(settingsForm.EntryDirection), EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage), DisplayMode: model.OptionalString(settingsForm.DisplayMode), + GestureNav: model.OptionalString(settingsForm.GestureNav), DefaultReadingSpeed: model.OptionalInt(settingsForm.DefaultReadingSpeed), CJKReadingSpeed: model.OptionalInt(settingsForm.CJKReadingSpeed), DefaultHomePage: model.OptionalString(settingsForm.DefaultHomePage), diff --git a/ui/static/css/common.css b/ui/static/css/common.css index 1fa93c3e..f30289c7 100644 --- a/ui/static/css/common.css +++ b/ui/static/css/common.css @@ -882,6 +882,7 @@ article.category-has-unread { color: var(--entry-content-color); line-height: 1.4em; overflow-wrap: break-word; + touch-action: pan-y pinch-zoom; } .entry-content h1, h2, h3, h4, h5, h6 { diff --git a/ui/static/js/touch_handler.js b/ui/static/js/touch_handler.js index e06d81aa..99c1d5b2 100644 --- a/ui/static/js/touch_handler.js +++ b/ui/static/js/touch_handler.js @@ -8,6 +8,7 @@ class TouchHandler { start: { x: -1, y: -1 }, move: { x: -1, y: -1 }, moved: false, + time: 0, element: null }; } @@ -33,7 +34,7 @@ class TouchHandler { return DomHelper.findParent(element, "entry-swipe"); } - onTouchStart(event) { + onItemTouchStart(event) { if (event.touches === undefined || event.touches.length !== 1) { return; } @@ -45,7 +46,7 @@ class TouchHandler { this.touch.element.style.transitionDuration = "0s"; } - onTouchMove(event) { + onItemTouchMove(event) { if (event.touches === undefined || event.touches.length !== 1 || this.element === null) { return; } @@ -71,15 +72,15 @@ class TouchHandler { } } - onTouchEnd(event) { + onItemTouchEnd(event) { if (event.touches === undefined) { return; } if (this.touch.element !== null) { - let distance = Math.abs(this.calculateDistance()); + let absDistance = Math.abs(this.calculateDistance()); - if (distance > 75) { + if (absDistance > 75) { toggleEntryStatus(this.touch.element); } @@ -92,47 +93,95 @@ class TouchHandler { this.reset(); } + onContentTouchStart(event) { + if (event.touches === undefined || event.touches.length !== 1) { + return; + } + + this.reset(); + this.touch.start.x = event.touches[0].clientX; + this.touch.start.y = event.touches[0].clientY; + this.touch.time = Date.now(); + } + + onContentTouchMove(event) { + if (event.touches === undefined || event.touches.length !== 1 || this.element === null) { + return; + } + + this.touch.move.x = event.touches[0].clientX; + this.touch.move.y = event.touches[0].clientY; + } + + onContentTouchEnd(event) { + if (event.touches === undefined) { + return; + } + + let distance = this.calculateDistance(); + let absDistance = Math.abs(distance); + let now = Date.now(); + + if (now - this.touch.time <= 1000 && absDistance > 75) { + if (distance > 0) { + goToPage("previous"); + } else { + goToPage("next"); + } + } + + this.reset(); + } + + onTapEnd(event) { + if (event.touches === undefined) { + return; + } + + let now = Date.now(); + + if (this.touch.start.x !== -1 && now - this.touch.time <= 200) { + let innerWidthHalf = window.innerWidth / 2; + + if (this.touch.start.x >= innerWidthHalf && event.changedTouches[0].clientX >= innerWidthHalf) { + goToPage("next"); + } else if (this.touch.start.x < innerWidthHalf && event.changedTouches[0].clientX < innerWidthHalf) { + goToPage("previous"); + } + + this.reset(); + } else { + this.reset(); + this.touch.start.x = event.changedTouches[0].clientX; + this.touch.time = now; + } + } + listen() { - let elements = document.querySelectorAll(".entry-swipe"); let hasPassiveOption = DomHelper.hasPassiveEventListenerOption(); + let elements = document.querySelectorAll(".entry-swipe"); + elements.forEach((element) => { - element.addEventListener("touchstart", (e) => this.onTouchStart(e), hasPassiveOption ? { passive: true } : false); - element.addEventListener("touchmove", (e) => this.onTouchMove(e), hasPassiveOption ? { passive: false } : false); - element.addEventListener("touchend", (e) => this.onTouchEnd(e), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchstart", (e) => this.onItemTouchStart(e), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchmove", (e) => this.onItemTouchMove(e), hasPassiveOption ? { passive: false } : false); + element.addEventListener("touchend", (e) => this.onItemTouchEnd(e), hasPassiveOption ? { passive: true } : false); element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); }); - let entryContentElement = document.querySelector(".entry-content"); - if (entryContentElement && entryContentElement.classList.contains('double-tap')) { - let doubleTapTimers = { - previous: null, - next: null - }; + let element = document.querySelector(".entry-content"); - const detectDoubleTap = (doubleTapTimer, event) => { - const timer = doubleTapTimers[doubleTapTimer]; - if (timer === null) { - doubleTapTimers[doubleTapTimer] = setTimeout(() => { - doubleTapTimers[doubleTapTimer] = null; - }, 200); - } else { - event.preventDefault(); - goToPage(doubleTapTimer); - } - }; - - entryContentElement.addEventListener("touchend", (e) => { - if (e.changedTouches[0].clientX >= (entryContentElement.offsetWidth / 2)) { - detectDoubleTap("next", e); - } else { - detectDoubleTap("previous", e); - } - }, hasPassiveOption ? { passive: false } : false); - - entryContentElement.addEventListener("touchmove", (e) => { - Object.keys(doubleTapTimers).forEach(timer => doubleTapTimers[timer] = null); - }); + if (element) { + if (element.classList.contains("gesture-nav-tap")) { + element.addEventListener("touchend", (e) => this.onTapEnd(e), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchmove", () => this.reset(), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); + } else if (element.classList.contains("gesture-nav-swipe")) { + element.addEventListener("touchstart", (e) => this.onContentTouchStart(e), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchmove", (e) => this.onContentTouchMove(e), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchend", (e) => this.onContentTouchEnd(e), hasPassiveOption ? { passive: true } : false); + element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); + } } } } diff --git a/validator/user.go b/validator/user.go index c302241d..f7f271c0 100644 --- a/validator/user.go +++ b/validator/user.go @@ -79,6 +79,12 @@ func ValidateUserModification(store *storage.Storage, userID int64, changes *mod } } + if changes.GestureNav != nil { + if err := validateGestureNav(*changes.GestureNav); err != nil { + return err + } + } + if changes.DefaultReadingSpeed != nil { if err := validateReadingSpeed(*changes.DefaultReadingSpeed); err != nil { return err @@ -163,6 +169,13 @@ func validateDisplayMode(displayMode string) *ValidationError { return nil } +func validateGestureNav(gestureNav string) *ValidationError { + if gestureNav != "none" && gestureNav != "tap" && gestureNav != "swipe" { + return NewValidationError("error.invalid_gesture_nav") + } + return nil +} + func validateDefaultHomePage(defaultHomePage string) *ValidationError { defaultHomePages := model.HomePages() if _, found := defaultHomePages[defaultHomePage]; !found {