Add swipe as option for gesture navigation between entries.

* Refactor `TouchHandler` to handle double-tap and swipe gestures.
  * Renamed existing `onTouch` JavaScript methods to `onItemTouch` and
    added `onContentTouch` methods for swipe gesture.
  * Refactor double-tap. It's now a method in `TouchHandler` versus
    anonymous functions in `listen()` method.
* Updated CSS classes.
  * Added `touch-action` CSS for `.entry-content`.
  * Renamed CSS classes for adding events in `TouchHandler`.
* Updated users settings to replace checkbox for double tap with select
  for none, double tap, or swipe.
* Added database migrations for new gesture_nav option.
  * Rename `users.double_tap` to `users.gesture_nav` and migrate
    existing user settings.
* Updated translation files. (Non-English updated with Google
  Translate.)

Resolves #1449, closes #1495
This commit is contained in:
dzaikos 2022-09-30 05:37:57 +00:00 committed by Frédéric Guillot
parent 140a40acaf
commit 7d252ea45b
32 changed files with 238 additions and 81 deletions

View file

@ -34,7 +34,7 @@ type User 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"`
DoubleTap bool `json:"double_tap"` GestureNav string `json:"gesture_nav"`
LastLoginAt *time.Time `json:"last_login_at"` LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"` DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"` DefaultReadingSpeed int `json:"default_reading_speed"`
@ -73,7 +73,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"`
DoubleTap *bool `json:"double_tap"` GestureNav *string `json:"gesture_nav"`
DisplayMode *string `json:"display_mode"` DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"` DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"` CJKReadingSpeed *int `json:"cjk_reading_speed"`

View file

@ -644,4 +644,13 @@ var migrations = []func(tx *sql.Tx) error{
`) `)
return 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
},
} }

View file

@ -264,6 +264,7 @@
"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": "Progressive Web App (PWA) Anzeigemodus", "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!", "error.invalid_default_home_page": "Ungültige Standard-Startseite!",
"form.feed.label.title": "Titel", "form.feed.label.title": "Titel",
"form.feed.label.site_url": "Webseite-URL", "form.feed.label.site_url": "Webseite-URL",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Eintrag erstellt Zeit", "form.prefs.select.created_time": "Eintrag erstellt Zeit",
"form.prefs.select.alphabetical": "Alphabetisch", "form.prefs.select.alphabetical": "Alphabetisch",
"form.prefs.select.unread_count": "Ungelesen zählen", "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.keyboard_shortcuts": "Tastaturkürzel aktivieren",
"form.prefs.label.entry_swipe": "Aktivieren Sie das Streichen von Einträgen auf Touchscreens", "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.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
"form.prefs.label.custom_css": "Benutzerdefiniertes CSS", "form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
"form.prefs.label.entry_order": "Eintrag Sortierspalte", "form.prefs.label.entry_order": "Eintrag Sortierspalte",

View file

@ -242,6 +242,7 @@
"error.invalid_timezone": "Μη έγκυρη ζώνη ώρας.", "error.invalid_timezone": "Μη έγκυρη ζώνη ώρας.",
"error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.", "error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.",
"error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.", "error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.",
"error.invalid_gesture_nav": "Μη έγκυρη πλοήγηση με χειρονομίες.",
"error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!", "error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!",
"error.empty_file": "Αυτό το αρχείο είναι κενό.", "error.empty_file": "Αυτό το αρχείο είναι κενό.",
"error.bad_credentials": "Μη έγκυρο όνομα χρήστη ή κωδικό πρόσβασης.", "error.bad_credentials": "Μη έγκυρο όνομα χρήστη ή κωδικό πρόσβασης.",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Χρόνος δημιουργίας καταχώρησης", "form.prefs.select.created_time": "Χρόνος δημιουργίας καταχώρησης",
"form.prefs.select.alphabetical": "Αλφαβητική σειρά", "form.prefs.select.alphabetical": "Αλφαβητική σειρά",
"form.prefs.select.unread_count": "Αριθμός μη αναγνωσμένων", "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.keyboard_shortcuts": "Ενεργοποίηση συντομεύσεων πληκτρολογίου",
"form.prefs.label.entry_swipe": "Ενεργοποιήστε το σάρωση καταχώρισης στις οθόνες αφής", "form.prefs.label.entry_swipe": "Ενεργοποιήστε το σάρωση καταχώρισης στις οθόνες αφής",
"form.prefs.label.double_tap": "Ενεργοποιήστε το διπλό πάτημα για πλοήγηση μεταξύ των καταχωρήσεων", "form.prefs.label.gesture_nav": "Χειρονομία για πλοήγηση μεταξύ των καταχωρήσεων",
"form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα", "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
"form.prefs.label.custom_css": "Προσαρμοσμένο CSS", "form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
"form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου", "form.prefs.label.entry_order": "Στήλη ταξινόμησης εισόδου",

View file

@ -242,6 +242,7 @@
"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.invalid_display_mode": "Invalid web app display mode.",
"error.invalid_gesture_nav": "Invalid gesture navigation.",
"error.invalid_default_home_page": "Invalid default homepage!", "error.invalid_default_home_page": "Invalid default homepage!",
"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.",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Entry created time", "form.prefs.select.created_time": "Entry created time",
"form.prefs.select.alphabetical": "Alphabetical", "form.prefs.select.alphabetical": "Alphabetical",
"form.prefs.select.unread_count": "Unread count", "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.keyboard_shortcuts": "Enable keyboard shortcuts",
"form.prefs.label.entry_swipe": "Enable entry swipe on touch screens", "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.show_reading_time": "Show estimated reading time for entries",
"form.prefs.label.custom_css": "Custom CSS", "form.prefs.label.custom_css": "Custom CSS",
"form.prefs.label.entry_order": "Entry sorting column", "form.prefs.label.entry_order": "Entry sorting column",

View file

@ -264,6 +264,7 @@
"error.invalid_timezone": "Zona horaria no válida.", "error.invalid_timezone": "Zona horaria no válida.",
"error.invalid_entry_direction": "Dirección de artículo 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_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!", "error.invalid_default_home_page": "¡Página de inicio por defecto no válida!",
"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",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Hora de creación del artículo", "form.prefs.select.created_time": "Hora de creación del artículo",
"form.prefs.select.alphabetical": "Alfabético", "form.prefs.select.alphabetical": "Alfabético",
"form.prefs.select.unread_count": "Recuento de no leídos", "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.keyboard_shortcuts": "Habilitar atajos de teclado",
"form.prefs.label.entry_swipe": "Habilitar deslizamiento de entrada en pantallas táctiles", "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.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
"form.prefs.label.custom_css": "CSS personalizado", "form.prefs.label.custom_css": "CSS personalizado",
"form.prefs.label.entry_order": "Columna de clasificación de artículos", "form.prefs.label.entry_order": "Columna de clasificación de artículos",

View file

@ -242,6 +242,7 @@
"error.invalid_timezone": "Virheellinen aikavyöhyke.", "error.invalid_timezone": "Virheellinen aikavyöhyke.",
"error.invalid_entry_direction": "Invalid entry direction.", "error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.", "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.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!",
"error.empty_file": "Tiedosto on tyhjä.", "error.empty_file": "Tiedosto on tyhjä.",
"error.bad_credentials": "Virheellinen käyttäjänimi tai salasana.", "error.bad_credentials": "Virheellinen käyttäjänimi tai salasana.",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Luomisaika", "form.prefs.select.created_time": "Luomisaika",
"form.prefs.select.alphabetical": "Aakkosjärjestys", "form.prefs.select.alphabetical": "Aakkosjärjestys",
"form.prefs.select.unread_count": "Lukemattomien määrä", "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.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.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.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
"form.prefs.label.custom_css": "Mukautettu CSS", "form.prefs.label.custom_css": "Mukautettu CSS",
"form.prefs.label.entry_order": "Lajittele sarakkeen mukaan", "form.prefs.label.entry_order": "Lajittele sarakkeen mukaan",

View file

@ -264,6 +264,7 @@
"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.", "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 !", "error.invalid_default_home_page": "Page d'accueil par défaut invalide !",
"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",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Heure de création de l'entrée", "form.prefs.select.created_time": "Heure de création de l'entrée",
"form.prefs.select.alphabetical": "Alphabétique", "form.prefs.select.alphabetical": "Alphabétique",
"form.prefs.select.unread_count": "Nombre d'articles non lus", "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.keyboard_shortcuts": "Activer les raccourcis clavier",
"form.prefs.label.entry_swipe": "Activer le balayage des entrées sur les écrans tactiles", "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.show_reading_time": "Afficher le temps de lecture estimé des articles",
"form.prefs.label.custom_css": "CSS personnalisé", "form.prefs.label.custom_css": "CSS personnalisé",
"form.prefs.label.entry_order": "Colonne de tri des entrées", "form.prefs.label.entry_order": "Colonne de tri des entrées",

View file

@ -242,6 +242,7 @@
"error.invalid_timezone": "अमान्य समयक्षेत्र.", "error.invalid_timezone": "अमान्य समयक्षेत्र.",
"error.invalid_entry_direction": "अमान्य प्रवेश दिशा।", "error.invalid_entry_direction": "अमान्य प्रवेश दिशा।",
"error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.", "error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.",
"error.invalid_gesture_nav": "अमान्य इशारा नेविगेशन।",
"error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!", "error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!",
"error.empty_file": "यह फ़ाइल खाली है।", "error.empty_file": "यह फ़ाइल खाली है।",
"error.bad_credentials": "अमान्य उपयोगकर्ता नाम या पासवर्ड।", "error.bad_credentials": "अमान्य उपयोगकर्ता नाम या पासवर्ड।",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "प्रवेश बनाया समय", "form.prefs.select.created_time": "प्रवेश बनाया समय",
"form.prefs.select.alphabetical": "वर्णक्रम", "form.prefs.select.alphabetical": "वर्णक्रम",
"form.prefs.select.unread_count": "अपठित गणना", "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.keyboard_shortcuts": "कीबोर्ड शॉर्टकट सक्षम करें",
"form.prefs.label.entry_swipe": "टच स्क्रीन पर एंट्री स्वाइप सक्षम करें", "form.prefs.label.entry_swipe": "टच स्क्रीन पर एंट्री स्वाइप सक्षम करें",
"form.prefs.label.double_tap": "प्रविष्टियों के बीच नेविगेट करने के लिए डबल टैप सक्षम करें", "form.prefs.label.gesture_nav": "प्रविष्टियों के बीच नेविगेट करने के लिए इशारा",
"form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं", "form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
"form.prefs.label.custom_css": "कस्टम सीएसएस", "form.prefs.label.custom_css": "कस्टम सीएसएस",
"form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम", "form.prefs.label.entry_order": "प्रवेश छँटाई कॉलम",

View file

@ -239,6 +239,7 @@
"error.invalid_timezone": "Zona waktu tidak valid.", "error.invalid_timezone": "Zona waktu tidak valid.",
"error.invalid_entry_direction": "Urutan entri tidak valid.", "error.invalid_entry_direction": "Urutan entri tidak valid.",
"error.invalid_display_mode": "Mode tampilan aplikasi web 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.invalid_default_home_page": "Beranda baku tidak valid!",
"error.empty_file": "Berkas ini kosong.", "error.empty_file": "Berkas ini kosong.",
"error.bad_credentials": "Nama pengguna atau kata sandi tidak valid.", "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.created_time": "Waktu entri dibuat",
"form.prefs.select.alphabetical": "Secara alfabet", "form.prefs.select.alphabetical": "Secara alfabet",
"form.prefs.select.unread_count": "Jumlah yang belum dibaca", "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.keyboard_shortcuts": "Aktifkan pintasan papan tik",
"form.prefs.label.entry_swipe": "Aktifkan tindakan geser pada entri di ponsel", "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.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
"form.prefs.label.custom_css": "Modifikasi CSS", "form.prefs.label.custom_css": "Modifikasi CSS",
"form.prefs.label.entry_order": "Pengurutan Kolom Entri", "form.prefs.label.entry_order": "Pengurutan Kolom Entri",

View file

@ -264,6 +264,7 @@
"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.", "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!", "error.invalid_default_home_page": "Pagina iniziale predefinita 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",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Tempo di creazione dell'entrata", "form.prefs.select.created_time": "Tempo di creazione dell'entrata",
"form.prefs.select.alphabetical": "In ordine alfabetico", "form.prefs.select.alphabetical": "In ordine alfabetico",
"form.prefs.select.unread_count": "Conteggio dei non letti", "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.keyboard_shortcuts": "Abilita le scorciatoie da tastiera",
"form.prefs.label.entry_swipe": "Abilita lo scorrimento della voce sui touch screen", "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.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
"form.prefs.label.custom_css": "CSS personalizzati", "form.prefs.label.custom_css": "CSS personalizzati",
"form.prefs.label.entry_order": "Colonna di ordinamento delle voci", "form.prefs.label.entry_order": "Colonna di ordinamento delle voci",

View file

@ -242,6 +242,7 @@
"error.invalid_timezone": "タイムゾーンが無効です。", "error.invalid_timezone": "タイムゾーンが無効です。",
"error.invalid_entry_direction": "記事の表示順が無効です。", "error.invalid_entry_direction": "記事の表示順が無効です。",
"error.invalid_display_mode": "Web アプリの表示モードが無効です。", "error.invalid_display_mode": "Web アプリの表示モードが無効です。",
"error.invalid_gesture_nav": "ジェスチャー ナビゲーションが無効です。",
"error.invalid_default_home_page": "デフォルトのトップページが無効です", "error.invalid_default_home_page": "デフォルトのトップページが無効です",
"error.empty_file": "このファイルは空です。", "error.empty_file": "このファイルは空です。",
"error.bad_credentials": "ユーザー名かパスワードが間違っています。", "error.bad_credentials": "ユーザー名かパスワードが間違っています。",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "記事の取得時刻", "form.prefs.select.created_time": "記事の取得時刻",
"form.prefs.select.alphabetical": "アルファベット順", "form.prefs.select.alphabetical": "アルファベット順",
"form.prefs.select.unread_count": "未読数", "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.keyboard_shortcuts": "キーボードショートカットを有効にする",
"form.prefs.label.entry_swipe": "タッチスクリーンでスワイプ入力を有効にする", "form.prefs.label.entry_swipe": "タッチスクリーンでスワイプ入力を有効にする",
"form.prefs.label.double_tap": "ダブルタップで記事間を移動する", "form.prefs.label.gesture_nav": "エントリ間を移動するジェスチャー",
"form.prefs.label.show_reading_time": "記事の推定読書時間を表示する", "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
"form.prefs.label.custom_css": "カスタム CSS", "form.prefs.label.custom_css": "カスタム CSS",
"form.prefs.label.entry_order": "記事の表示順の基準", "form.prefs.label.entry_order": "記事の表示順の基準",

View file

@ -264,6 +264,7 @@
"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.", "error.invalid_display_mode": "Ongeldige weergavemodus voor webapp.",
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
"error.invalid_default_home_page": "Ongeldige standaard homepage!", "error.invalid_default_home_page": "Ongeldige standaard homepage!",
"form.feed.label.title": "Naam", "form.feed.label.title": "Naam",
"form.feed.label.site_url": "Website URL", "form.feed.label.site_url": "Website URL",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Tijdstip van binnenkomst", "form.prefs.select.created_time": "Tijdstip van binnenkomst",
"form.prefs.select.alphabetical": "Alfabetisch", "form.prefs.select.alphabetical": "Alfabetisch",
"form.prefs.select.unread_count": "Ongelezen tellen", "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.keyboard_shortcuts": "Schakel sneltoetsen in",
"form.prefs.label.entry_swipe": "Invoervegen inschakelen op aanraakschermen", "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.show_reading_time": "Toon geschatte leestijd voor artikelen",
"form.prefs.label.custom_css": "Aangepaste CSS", "form.prefs.label.custom_css": "Aangepaste CSS",
"form.prefs.label.entry_order": "Ingang Sorteerkolom", "form.prefs.label.entry_order": "Ingang Sorteerkolom",

View file

@ -266,6 +266,7 @@
"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.", "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!", "error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!",
"form.feed.label.title": "Tytuł", "form.feed.label.title": "Tytuł",
"form.feed.label.site_url": "URL strony", "form.feed.label.site_url": "URL strony",
@ -303,7 +304,7 @@
"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 machnięcie wpisu na ekranach dotykowych", "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.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.fullscreen": "Pełny ekran",
@ -314,6 +315,9 @@
"form.prefs.select.created_time": "Czas utworzenia wpisu", "form.prefs.select.created_time": "Czas utworzenia wpisu",
"form.prefs.select.alphabetical": "Alfabetycznie", "form.prefs.select.alphabetical": "Alfabetycznie",
"form.prefs.select.unread_count": "Liczba nieprzeczytanych", "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.custom_css": "Niestandardowy CSS",
"form.prefs.label.entry_order": "Kolumna sortowania wpisów", "form.prefs.label.entry_order": "Kolumna sortowania wpisów",
"form.prefs.label.default_home_page": "Domyślna strona główna", "form.prefs.label.default_home_page": "Domyślna strona główna",

View file

@ -264,6 +264,7 @@
"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.", "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!", "error.invalid_default_home_page": "Página inicial por defeito inválida!",
"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",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "Entrada tempo criado", "form.prefs.select.created_time": "Entrada tempo criado",
"form.prefs.select.alphabetical": "Por ordem alfabética", "form.prefs.select.alphabetical": "Por ordem alfabética",
"form.prefs.select.unread_count": "Contagem não lida", "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.keyboard_shortcuts": "Habilitar atalhos do teclado",
"form.prefs.label.entry_swipe": "Ativar entrada de furto em telas sensíveis ao toque", "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.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
"form.prefs.label.custom_css": "CSS customizado", "form.prefs.label.custom_css": "CSS customizado",
"form.prefs.label.entry_order": "Coluna de Ordenação de Entrada", "form.prefs.label.entry_order": "Coluna de Ordenação de Entrada",

View file

@ -266,6 +266,7 @@
"error.invalid_timezone": "Неверный часовой пояс.", "error.invalid_timezone": "Неверный часовой пояс.",
"error.invalid_entry_direction": "Неверное направление входа.", "error.invalid_entry_direction": "Неверное направление входа.",
"error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.", "error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.",
"error.invalid_gesture_nav": "Неверная жестовая навигация.",
"error.invalid_default_home_page": "Неверная домашняя страница по умолчанию!", "error.invalid_default_home_page": "Неверная домашняя страница по умолчанию!",
"form.feed.label.title": "Название", "form.feed.label.title": "Название",
"form.feed.label.site_url": "URL сайта", "form.feed.label.site_url": "URL сайта",
@ -310,9 +311,12 @@
"form.prefs.select.created_time": "Время создания записи", "form.prefs.select.created_time": "Время создания записи",
"form.prefs.select.alphabetical": "По алфавиту", "form.prefs.select.alphabetical": "По алфавиту",
"form.prefs.select.unread_count": "Количество непрочитанных", "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.keyboard_shortcuts": "Включить сочетания клавиш",
"form.prefs.label.entry_swipe": "Включить пролистывание ввода на сенсорных экранах", "form.prefs.label.entry_swipe": "Включить пролистывание ввода на сенсорных экранах",
"form.prefs.label.double_tap": "Включить двойное касание для перехода между записями", "form.prefs.label.gesture_nav": "Жест для перехода между записями",
"form.prefs.label.show_reading_time": "Показать примерное время чтения статей", "form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
"form.prefs.label.custom_css": "Пользовательские CSS", "form.prefs.label.custom_css": "Пользовательские CSS",
"form.prefs.label.entry_order": "Колонка сортировки ввода", "form.prefs.label.entry_order": "Колонка сортировки ввода",

View file

@ -242,6 +242,7 @@
"error.invalid_timezone": "Geçersiz saat dilimi", "error.invalid_timezone": "Geçersiz saat dilimi",
"error.invalid_entry_direction": "Geçersiz giriş yönü.", "error.invalid_entry_direction": "Geçersiz giriş yönü.",
"error.invalid_display_mode": "Geçersiz web uygulaması görüntüleme modu.", "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.invalid_default_home_page": "Geçersiz varsayılan ana sayfa!",
"error.empty_file": "Bu dosya boş.", "error.empty_file": "Bu dosya boş.",
"error.bad_credentials": "Geçersiz kullanıcı veya parola.", "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.created_time": "Girişin oluşturulma zamanı",
"form.prefs.select.alphabetical": "Alfabetik", "form.prefs.select.alphabetical": "Alfabetik",
"form.prefs.select.unread_count": "Okunmamış sayısı", "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.keyboard_shortcuts": "Klavye kısayollarını etkinleştir",
"form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах", "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.show_reading_time": "Makaleler için tahmini okuma süresini göster",
"form.prefs.label.custom_css": "Özel CSS", "form.prefs.label.custom_css": "Özel CSS",
"form.prefs.label.entry_order": "Giriş Sıralama Sütunu", "form.prefs.label.entry_order": "Giriş Sıralama Sütunu",

View file

@ -241,6 +241,7 @@
"error.invalid_timezone": "Недійсний часовий пояс.", "error.invalid_timezone": "Недійсний часовий пояс.",
"error.invalid_entry_direction": "Недійсний напрямок запису.", "error.invalid_entry_direction": "Недійсний напрямок запису.",
"error.invalid_display_mode": "Недійсний режим відображення.", "error.invalid_display_mode": "Недійсний режим відображення.",
"error.invalid_gesture_nav": "Недійсна навігація жестами.",
"error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!", "error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!",
"error.empty_file": "Цей файл порожній.", "error.empty_file": "Цей файл порожній.",
"error.bad_credentials": "Невірне ім’я користувача або пароль.", "error.bad_credentials": "Невірне ім’я користувача або пароль.",
@ -307,9 +308,12 @@
"form.prefs.select.created_time": "Дата створення запису", "form.prefs.select.created_time": "Дата створення запису",
"form.prefs.select.alphabetical": "За алфавітом", "form.prefs.select.alphabetical": "За алфавітом",
"form.prefs.select.unread_count": "Кількість непрочитаних", "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.keyboard_shortcuts": "Увімкнути комбінації клавиш",
"form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах", "form.prefs.label.entry_swipe": "Увімкніть введення пальцем на сенсорних екранах",
"form.prefs.label.double_tap": "Увімкніть подвійне торкання, щоб переходити між записами", "form.prefs.label.gesture_nav": "Жест для переходу між записами",
"form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів", "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
"form.prefs.label.custom_css": "Спеціальний CSS", "form.prefs.label.custom_css": "Спеціальний CSS",
"form.prefs.label.entry_order": "Стовпець сортування записів", "form.prefs.label.entry_order": "Стовпець сортування записів",

View file

@ -262,6 +262,7 @@
"error.invalid_timezone": "无效的时区。", "error.invalid_timezone": "无效的时区。",
"error.invalid_entry_direction": "无效的输入方向。", "error.invalid_entry_direction": "无效的输入方向。",
"error.invalid_display_mode": "无效的网页应用显示模式。", "error.invalid_display_mode": "无效的网页应用显示模式。",
"error.invalid_gesture_nav": "手势导航无效。",
"error.invalid_default_home_page": "无效的默认主页!", "error.invalid_default_home_page": "无效的默认主页!",
"form.feed.label.title": "标题", "form.feed.label.title": "标题",
"form.feed.label.site_url": "源网站 URL", "form.feed.label.site_url": "源网站 URL",
@ -306,9 +307,12 @@
"form.prefs.select.created_time": "文章创建时间", "form.prefs.select.created_time": "文章创建时间",
"form.prefs.select.alphabetical": "按字母顺序", "form.prefs.select.alphabetical": "按字母顺序",
"form.prefs.select.unread_count": "未读计数", "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.keyboard_shortcuts": "启用键盘快捷键",
"form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动", "form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动",
"form.prefs.label.double_tap": "启用双击以在条目之间导航", "form.prefs.label.gesture_nav": "在条目之间导航的手势",
"form.prefs.label.show_reading_time": "显示文章的预计阅读时间", "form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
"form.prefs.label.custom_css": "自定义 CSS", "form.prefs.label.custom_css": "自定义 CSS",
"form.prefs.label.entry_order": "文章排序依据", "form.prefs.label.entry_order": "文章排序依据",

View file

@ -264,6 +264,7 @@
"error.invalid_timezone": "無效的時區。", "error.invalid_timezone": "無效的時區。",
"error.invalid_entry_direction": "無效的輸入方向。", "error.invalid_entry_direction": "無效的輸入方向。",
"error.invalid_display_mode": "無效的網頁應用顯示模式。", "error.invalid_display_mode": "無效的網頁應用顯示模式。",
"error.invalid_gesture_nav": "手勢導航無效.",
"error.invalid_default_home_page": "默認主頁無效!", "error.invalid_default_home_page": "默認主頁無效!",
"form.feed.label.title": "標題", "form.feed.label.title": "標題",
"form.feed.label.site_url": "網站 URL", "form.feed.label.site_url": "網站 URL",
@ -308,9 +309,12 @@
"form.prefs.select.created_time": "文章建立時間", "form.prefs.select.created_time": "文章建立時間",
"form.prefs.select.alphabetical": "按字母順序", "form.prefs.select.alphabetical": "按字母順序",
"form.prefs.select.unread_count": "未讀計數", "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.keyboard_shortcuts": "啟用鍵盤快捷鍵",
"form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动", "form.prefs.label.entry_swipe": "在触摸屏上启用输入滑动",
"form.prefs.label.double_tap": "啟用雙擊以在條目之間導航", "form.prefs.label.gesture_nav": "在條目之間導航的手勢",
"form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間", "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
"form.prefs.label.custom_css": "自定義 CSS", "form.prefs.label.custom_css": "自定義 CSS",
"form.prefs.label.entry_order": "文章排序依據", "form.prefs.label.entry_order": "文章排序依據",

View file

@ -28,7 +28,7 @@ type User 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"`
DoubleTap bool `json:"double_tap"` GestureNav string `json:"gesture_nav"`
LastLoginAt *time.Time `json:"last_login_at"` LastLoginAt *time.Time `json:"last_login_at"`
DisplayMode string `json:"display_mode"` DisplayMode string `json:"display_mode"`
DefaultReadingSpeed int `json:"default_reading_speed"` DefaultReadingSpeed int `json:"default_reading_speed"`
@ -63,7 +63,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"`
DoubleTap *bool `json:"double_tap"` GestureNav *string `json:"gesture_nav"`
DisplayMode *string `json:"display_mode"` DisplayMode *string `json:"display_mode"`
DefaultReadingSpeed *int `json:"default_reading_speed"` DefaultReadingSpeed *int `json:"default_reading_speed"`
CJKReadingSpeed *int `json:"cjk_reading_speed"` CJKReadingSpeed *int `json:"cjk_reading_speed"`
@ -133,8 +133,8 @@ func (u *UserModificationRequest) Patch(user *User) {
user.EntrySwipe = *u.EntrySwipe user.EntrySwipe = *u.EntrySwipe
} }
if u.DoubleTap != nil { if u.GestureNav != nil {
user.DoubleTap = *u.DoubleTap user.GestureNav = *u.GestureNav
} }
if u.DisplayMode != nil { if u.DisplayMode != nil {

View file

@ -81,7 +81,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
keyboard_shortcuts, keyboard_shortcuts,
show_reading_time, show_reading_time,
entry_swipe, entry_swipe,
double_tap, gesture_nav,
stylesheet, stylesheet,
google_id, google_id,
openid_connect_id, openid_connect_id,
@ -118,7 +118,7 @@ func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*m
&user.KeyboardShortcuts, &user.KeyboardShortcuts,
&user.ShowReadingTime, &user.ShowReadingTime,
&user.EntrySwipe, &user.EntrySwipe,
&user.DoubleTap, &user.GestureNav,
&user.Stylesheet, &user.Stylesheet,
&user.GoogleID, &user.GoogleID,
&user.OpenIDConnectID, &user.OpenIDConnectID,
@ -174,7 +174,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
keyboard_shortcuts=$9, keyboard_shortcuts=$9,
show_reading_time=$10, show_reading_time=$10,
entry_swipe=$11, entry_swipe=$11,
double_tap=$12, gesture_nav=$12,
stylesheet=$13, stylesheet=$13,
google_id=$14, google_id=$14,
openid_connect_id=$15, openid_connect_id=$15,
@ -201,7 +201,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.KeyboardShortcuts, user.KeyboardShortcuts,
user.ShowReadingTime, user.ShowReadingTime,
user.EntrySwipe, user.EntrySwipe,
user.DoubleTap, user.GestureNav,
user.Stylesheet, user.Stylesheet,
user.GoogleID, user.GoogleID,
user.OpenIDConnectID, user.OpenIDConnectID,
@ -229,7 +229,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
keyboard_shortcuts=$8, keyboard_shortcuts=$8,
show_reading_time=$9, show_reading_time=$9,
entry_swipe=$10, entry_swipe=$10,
double_tap=$11, gesture_nav=$11,
stylesheet=$12, stylesheet=$12,
google_id=$13, google_id=$13,
openid_connect_id=$14, openid_connect_id=$14,
@ -255,7 +255,7 @@ func (s *Storage) UpdateUser(user *model.User) error {
user.KeyboardShortcuts, user.KeyboardShortcuts,
user.ShowReadingTime, user.ShowReadingTime,
user.EntrySwipe, user.EntrySwipe,
user.DoubleTap, user.GestureNav,
user.Stylesheet, user.Stylesheet,
user.GoogleID, user.GoogleID,
user.OpenIDConnectID, user.OpenIDConnectID,
@ -301,7 +301,7 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
keyboard_shortcuts, keyboard_shortcuts,
show_reading_time, show_reading_time,
entry_swipe, entry_swipe,
double_tap, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
@ -335,7 +335,7 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
keyboard_shortcuts, keyboard_shortcuts,
show_reading_time, show_reading_time,
entry_swipe, entry_swipe,
double_tap, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
@ -369,7 +369,7 @@ func (s *Storage) UserByField(field, value string) (*model.User, error) {
keyboard_shortcuts, keyboard_shortcuts,
show_reading_time, show_reading_time,
entry_swipe, entry_swipe,
double_tap, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
@ -410,7 +410,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
u.keyboard_shortcuts, u.keyboard_shortcuts,
u.show_reading_time, u.show_reading_time,
u.entry_swipe, u.entry_swipe,
u.double_tap, u.gesture_nav,
u.last_login_at, u.last_login_at,
u.stylesheet, u.stylesheet,
u.google_id, u.google_id,
@ -445,7 +445,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
&user.KeyboardShortcuts, &user.KeyboardShortcuts,
&user.ShowReadingTime, &user.ShowReadingTime,
&user.EntrySwipe, &user.EntrySwipe,
&user.DoubleTap, &user.GestureNav,
&user.LastLoginAt, &user.LastLoginAt,
&user.Stylesheet, &user.Stylesheet,
&user.GoogleID, &user.GoogleID,
@ -542,7 +542,7 @@ func (s *Storage) Users() (model.Users, error) {
keyboard_shortcuts, keyboard_shortcuts,
show_reading_time, show_reading_time,
entry_swipe, entry_swipe,
double_tap, gesture_nav,
last_login_at, last_login_at,
stylesheet, stylesheet,
google_id, google_id,
@ -578,7 +578,7 @@ func (s *Storage) Users() (model.Users, error) {
&user.KeyboardShortcuts, &user.KeyboardShortcuts,
&user.ShowReadingTime, &user.ShowReadingTime,
&user.EntrySwipe, &user.EntrySwipe,
&user.DoubleTap, &user.GestureNav,
&user.LastLoginAt, &user.LastLoginAt,
&user.Stylesheet, &user.Stylesheet,
&user.GoogleID, &user.GoogleID,

View file

@ -143,7 +143,7 @@
</div> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}
<article role="article" class="entry-content {{ if $.user.DoubleTap }}double-tap{{ end }}" dir="auto"> <article role="article" class="entry-content gesture-nav-{{ $.user.GestureNav }}" dir="auto">
{{ if .user }} {{ if .user }}
{{ noescape (proxyFilter .entry.Content) }} {{ noescape (proxyFilter .entry.Content) }}
{{ else }} {{ else }}

View file

@ -90,7 +90,12 @@
<label><input type="checkbox" name="entry_swipe" value="1" {{ if .form.EntrySwipe }}checked{{ end }}> {{ t "form.prefs.label.entry_swipe" }}</label> <label><input type="checkbox" name="entry_swipe" value="1" {{ if .form.EntrySwipe }}checked{{ end }}> {{ t "form.prefs.label.entry_swipe" }}</label>
<label><input type="checkbox" name="double_tap" value="1" {{ if .form.DoubleTap }}checked{{ end }}> {{ t "form.prefs.label.double_tap" }}</label> <label for="form-gesture-nav">{{ t "form.prefs.label.gesture_nav" }}</label>
<select id="form-gesture-nav" name="gesture_nav">
<option value="none" {{ if eq "none" $.form.GestureNav }}selected="selected"{{ end }}>{{ t "form.prefs.select.none" }}</option>
<option value="tap" {{ if eq "tap" $.form.GestureNav }}selected="selected"{{ end }}>{{ t "form.prefs.select.tap" }}</option>
<option value="swipe" {{ if eq "swipe" $.form.GestureNav }}selected="selected"{{ end }}>{{ t "form.prefs.select.swipe" }}</option>
</select>
<label><input type="checkbox" name="show_reading_time" value="1" {{ if .form.ShowReadingTime }}checked{{ end }}> {{ t "form.prefs.label.show_reading_time" }}</label> <label><input type="checkbox" name="show_reading_time" value="1" {{ if .form.ShowReadingTime }}checked{{ end }}> {{ t "form.prefs.label.show_reading_time" }}</label>

View file

@ -88,6 +88,10 @@ func TestGetUsers(t *testing.T) {
t.Fatalf(`Invalid web app display mode, got "%v"`, users[0].DisplayMode) 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 { if users[0].DefaultReadingSpeed != 265 {
t.Fatalf(`Invalid default reading speed, got "%v"`, users[0].DefaultReadingSpeed) t.Fatalf(`Invalid default reading speed, got "%v"`, users[0].DefaultReadingSpeed)
} }

View file

@ -27,7 +27,7 @@ type SettingsForm struct {
ShowReadingTime bool ShowReadingTime bool
CustomCSS string CustomCSS string
EntrySwipe bool EntrySwipe bool
DoubleTap bool GestureNav string
DisplayMode string DisplayMode string
DefaultReadingSpeed int DefaultReadingSpeed int
CJKReadingSpeed int CJKReadingSpeed int
@ -48,7 +48,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.DoubleTap = s.DoubleTap user.GestureNav = s.GestureNav
user.DisplayMode = s.DisplayMode user.DisplayMode = s.DisplayMode
user.CJKReadingSpeed = s.CJKReadingSpeed user.CJKReadingSpeed = s.CJKReadingSpeed
user.DefaultReadingSpeed = s.DefaultReadingSpeed user.DefaultReadingSpeed = s.DefaultReadingSpeed
@ -114,7 +114,7 @@ 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",
DoubleTap: r.FormValue("double_tap") == "1", GestureNav: r.FormValue("gesture_nav"),
DisplayMode: r.FormValue("display_mode"), DisplayMode: r.FormValue("display_mode"),
DefaultReadingSpeed: int(defaultReadingSpeed), DefaultReadingSpeed: int(defaultReadingSpeed),
CJKReadingSpeed: int(cjkReadingSpeed), CJKReadingSpeed: int(cjkReadingSpeed),

View file

@ -15,6 +15,7 @@ func TestValid(t *testing.T) {
EntryDirection: "asc", EntryDirection: "asc",
EntriesPerPage: 50, EntriesPerPage: 50,
DisplayMode: "standalone", DisplayMode: "standalone",
GestureNav: "tap",
DefaultReadingSpeed: 35, DefaultReadingSpeed: 35,
CJKReadingSpeed: 25, CJKReadingSpeed: 25,
DefaultHomePage: "unread", DefaultHomePage: "unread",
@ -37,6 +38,7 @@ func TestConfirmationEmpty(t *testing.T) {
EntryDirection: "asc", EntryDirection: "asc",
EntriesPerPage: 50, EntriesPerPage: 50,
DisplayMode: "standalone", DisplayMode: "standalone",
GestureNav: "tap",
DefaultReadingSpeed: 35, DefaultReadingSpeed: 35,
CJKReadingSpeed: 25, CJKReadingSpeed: 25,
DefaultHomePage: "unread", DefaultHomePage: "unread",
@ -63,6 +65,7 @@ func TestConfirmationIncorrect(t *testing.T) {
EntryDirection: "asc", EntryDirection: "asc",
EntriesPerPage: 50, EntriesPerPage: 50,
DisplayMode: "standalone", DisplayMode: "standalone",
GestureNav: "tap",
DefaultReadingSpeed: 35, DefaultReadingSpeed: 35,
CJKReadingSpeed: 25, CJKReadingSpeed: 25,
DefaultHomePage: "unread", DefaultHomePage: "unread",

View file

@ -38,7 +38,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,
DoubleTap: user.DoubleTap, GestureNav: user.GestureNav,
DisplayMode: user.DisplayMode, DisplayMode: user.DisplayMode,
DefaultReadingSpeed: user.DefaultReadingSpeed, DefaultReadingSpeed: user.DefaultReadingSpeed,
CJKReadingSpeed: user.CJKReadingSpeed, CJKReadingSpeed: user.CJKReadingSpeed,

View file

@ -61,6 +61,7 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
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), DisplayMode: model.OptionalString(settingsForm.DisplayMode),
GestureNav: model.OptionalString(settingsForm.GestureNav),
DefaultReadingSpeed: model.OptionalInt(settingsForm.DefaultReadingSpeed), DefaultReadingSpeed: model.OptionalInt(settingsForm.DefaultReadingSpeed),
CJKReadingSpeed: model.OptionalInt(settingsForm.CJKReadingSpeed), CJKReadingSpeed: model.OptionalInt(settingsForm.CJKReadingSpeed),
DefaultHomePage: model.OptionalString(settingsForm.DefaultHomePage), DefaultHomePage: model.OptionalString(settingsForm.DefaultHomePage),

View file

@ -882,6 +882,7 @@ article.category-has-unread {
color: var(--entry-content-color); color: var(--entry-content-color);
line-height: 1.4em; line-height: 1.4em;
overflow-wrap: break-word; overflow-wrap: break-word;
touch-action: pan-y pinch-zoom;
} }
.entry-content h1, h2, h3, h4, h5, h6 { .entry-content h1, h2, h3, h4, h5, h6 {

View file

@ -8,6 +8,7 @@ class TouchHandler {
start: { x: -1, y: -1 }, start: { x: -1, y: -1 },
move: { x: -1, y: -1 }, move: { x: -1, y: -1 },
moved: false, moved: false,
time: 0,
element: null element: null
}; };
} }
@ -33,7 +34,7 @@ class TouchHandler {
return DomHelper.findParent(element, "entry-swipe"); return DomHelper.findParent(element, "entry-swipe");
} }
onTouchStart(event) { onItemTouchStart(event) {
if (event.touches === undefined || event.touches.length !== 1) { if (event.touches === undefined || event.touches.length !== 1) {
return; return;
} }
@ -45,7 +46,7 @@ class TouchHandler {
this.touch.element.style.transitionDuration = "0s"; this.touch.element.style.transitionDuration = "0s";
} }
onTouchMove(event) { onItemTouchMove(event) {
if (event.touches === undefined || event.touches.length !== 1 || this.element === null) { if (event.touches === undefined || event.touches.length !== 1 || this.element === null) {
return; return;
} }
@ -71,15 +72,15 @@ class TouchHandler {
} }
} }
onTouchEnd(event) { onItemTouchEnd(event) {
if (event.touches === undefined) { if (event.touches === undefined) {
return; return;
} }
if (this.touch.element !== null) { 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); toggleEntryStatus(this.touch.element);
} }
@ -92,47 +93,95 @@ class TouchHandler {
this.reset(); 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() { listen() {
let elements = document.querySelectorAll(".entry-swipe");
let hasPassiveOption = DomHelper.hasPassiveEventListenerOption(); let hasPassiveOption = DomHelper.hasPassiveEventListenerOption();
let elements = document.querySelectorAll(".entry-swipe");
elements.forEach((element) => { elements.forEach((element) => {
element.addEventListener("touchstart", (e) => this.onTouchStart(e), hasPassiveOption ? { passive: true } : false); element.addEventListener("touchstart", (e) => this.onItemTouchStart(e), hasPassiveOption ? { passive: true } : false);
element.addEventListener("touchmove", (e) => this.onTouchMove(e), hasPassiveOption ? { passive: false } : false); element.addEventListener("touchmove", (e) => this.onItemTouchMove(e), hasPassiveOption ? { passive: false } : false);
element.addEventListener("touchend", (e) => this.onTouchEnd(e), hasPassiveOption ? { passive: true } : false); element.addEventListener("touchend", (e) => this.onItemTouchEnd(e), hasPassiveOption ? { passive: true } : false);
element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false); element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false);
}); });
let entryContentElement = document.querySelector(".entry-content"); let element = document.querySelector(".entry-content");
if (entryContentElement && entryContentElement.classList.contains('double-tap')) {
let doubleTapTimers = {
previous: null,
next: null
};
const detectDoubleTap = (doubleTapTimer, event) => { if (element) {
const timer = doubleTapTimers[doubleTapTimer]; if (element.classList.contains("gesture-nav-tap")) {
if (timer === null) { element.addEventListener("touchend", (e) => this.onTapEnd(e), hasPassiveOption ? { passive: true } : false);
doubleTapTimers[doubleTapTimer] = setTimeout(() => { element.addEventListener("touchmove", () => this.reset(), hasPassiveOption ? { passive: true } : false);
doubleTapTimers[doubleTapTimer] = null; element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false);
}, 200); } else if (element.classList.contains("gesture-nav-swipe")) {
} else { element.addEventListener("touchstart", (e) => this.onContentTouchStart(e), hasPassiveOption ? { passive: true } : false);
event.preventDefault(); element.addEventListener("touchmove", (e) => this.onContentTouchMove(e), hasPassiveOption ? { passive: true } : false);
goToPage(doubleTapTimer); element.addEventListener("touchend", (e) => this.onContentTouchEnd(e), hasPassiveOption ? { passive: true } : false);
element.addEventListener("touchcancel", () => this.reset(), hasPassiveOption ? { passive: true } : false);
} }
};
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);
});
} }
} }
} }

View file

@ -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 changes.DefaultReadingSpeed != nil {
if err := validateReadingSpeed(*changes.DefaultReadingSpeed); err != nil { if err := validateReadingSpeed(*changes.DefaultReadingSpeed); err != nil {
return err return err
@ -163,6 +169,13 @@ func validateDisplayMode(displayMode string) *ValidationError {
return nil 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 { func validateDefaultHomePage(defaultHomePage string) *ValidationError {
defaultHomePages := model.HomePages() defaultHomePages := model.HomePages()
if _, found := defaultHomePages[defaultHomePage]; !found { if _, found := defaultHomePages[defaultHomePage]; !found {