Bladeren bron

feat: adding support for saving entries to karakeep

Signed-off-by: Jesse Jaggars <jhjaggars@gmail.com>
Jesse Jaggars 10 maanden geleden
bovenliggende
commit
43d302e768

+ 9 - 0
internal/database/migrations.go

@@ -1077,4 +1077,13 @@ var migrations = []func(tx *sql.Tx, driver string) error{
 		_, err = tx.Exec(`ALTER TABLE users ADD COLUMN always_open_external_links bool default 'f'`)
 		return err
 	},
+	func(tx *sql.Tx, _ string) (err error) {
+		sql := `
+			ALTER TABLE integrations ADD COLUMN karakeep_enabled bool default 'f';
+			ALTER TABLE integrations ADD COLUMN karakeep_api_key text default '';
+			ALTER TABLE integrations ADD COLUMN karakeep_url text default '';
+		`
+		_, err = tx.Exec(sql)
+		return
+	},
 }

+ 19 - 0
internal/integration/integration.go

@@ -13,6 +13,7 @@ import (
 	"miniflux.app/v2/internal/integration/discord"
 	"miniflux.app/v2/internal/integration/espial"
 	"miniflux.app/v2/internal/integration/instapaper"
+	"miniflux.app/v2/internal/integration/karakeep"
 	"miniflux.app/v2/internal/integration/linkace"
 	"miniflux.app/v2/internal/integration/linkding"
 	"miniflux.app/v2/internal/integration/linkwarden"
@@ -428,6 +429,24 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
 		}
 	}
 
+	if userIntegrations.KarakeepEnabled {
+		slog.Debug("Sending entry to Karakeep",
+			slog.Int64("user_id", userIntegrations.UserID),
+			slog.Int64("entry_id", entry.ID),
+			slog.String("entry_url", entry.URL),
+		)
+
+		client := karakeep.NewClient(userIntegrations.KarakeepAPIKey, userIntegrations.KarakeepURL)
+		if err := client.SaveUrl(entry.URL); err != nil {
+			slog.Error("Unable to send entry to Karakeep",
+				slog.Int64("user_id", userIntegrations.UserID),
+				slog.Int64("entry_id", entry.ID),
+				slog.String("entry_url", entry.URL),
+				slog.Any("error", err),
+			)
+		}
+	}
+
 	if userIntegrations.RaindropEnabled {
 		slog.Debug("Sending entry to Raindrop",
 			slog.Int64("user_id", userIntegrations.UserID),

+ 89 - 0
internal/integration/karakeep/karakeep.go

@@ -0,0 +1,89 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package karakeep // import "miniflux.app/v2/internal/integration/karakeep"
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	"miniflux.app/v2/internal/version"
+)
+
+const defaultClientTimeout = 10 * time.Second
+
+type errorResponse struct {
+	Code  string `json:"code"`
+	Error string `json:"error"`
+}
+
+type successResponse struct {
+	CreatedAt string `json:"createdAt"`
+	Content   struct {
+		Type string `json:"type"`
+		Url  string `json:"url"`
+	}
+}
+
+type Client interface {
+	SaveUrl(url string) error
+}
+
+type client struct {
+	wrapped     *http.Client
+	apiEndpoint string
+	apiToken    string
+}
+
+func NewClient(apiToken string, apiEndpoint string) Client {
+	return &client{wrapped: &http.Client{Timeout: defaultClientTimeout}, apiEndpoint: apiEndpoint, apiToken: apiToken}
+}
+
+func (c *client) SaveUrl(url string) error {
+	var payload = map[string]interface{}{
+		"type": "link",
+		"url":  url,
+	}
+	b, err := json.Marshal(payload)
+	if err != nil {
+		return err
+	}
+	req, err := http.NewRequest(http.MethodPost, c.apiEndpoint, bytes.NewReader(b))
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiToken))
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+	resp, err := c.wrapped.Do(req)
+	if err != nil {
+		return err
+	}
+
+	defer resp.Body.Close()
+	b, err = io.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("karakeep: failed to parse response: %s", err)
+	}
+
+	if resp.StatusCode >= 400 {
+		var errResponse errorResponse
+		if err = json.Unmarshal(b, &errResponse); err != nil {
+			return fmt.Errorf("karakeep: failed to save URL: status=%d %s", resp.StatusCode, string(b))
+		}
+		return fmt.Errorf("karakeep: failed to save URL: status=%d errorcode=%s %s", resp.StatusCode, errResponse.Code, errResponse.Error)
+	}
+
+	var successReponse successResponse
+	if err = json.Unmarshal(b, &successReponse); err != nil {
+		return fmt.Errorf("karakeep: failed to parse response, however the request appears successful, is the url correct?: status=%d %s", resp.StatusCode, string(b))
+	}
+
+	return nil
+}

+ 4 - 1
internal/locale/translations/de_DE.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Einträge in Instapaper speichern",
     "form.integration.instapaper_password": "Instapaper-Passwort",
     "form.integration.instapaper_username": "Instapaper-Benutzername",
+    "form.integration.karakeep_activate": "Einträge in Karakeep speichern",
+    "form.integration.karakeep_api_key": "Karakeep-API-Schlüssel",
+    "form.integration.karakeep_url": "Karakeep-API-Endpunkt",
     "form.integration.linkace_activate": "Einträge in LinkAce speichern",
     "form.integration.linkace_api_key": "LinkAce-API-Schlüssel",
     "form.integration.linkace_check_disabled": "Linkprüfung deaktivieren",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Globale Feedeinstellungen",
     "form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
     "form.prefs.help.external_font_hosts": "Per Leerzeichen getrennte Liste externer Schriftarten-Hosts, die erlaubt werden sollen. Beispiel: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Artikel immer mit Öffnen der Links lesen",
     "form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
     "form.prefs.label.cjk_reading_speed": "Lesegeschwindigkeit für Chinesisch, Koreanisch und Japanisch (Zeichen pro Minute)",
     "form.prefs.label.custom_css": "Benutzerdefiniertes CSS",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden. Audio/Video bei 90%% Wiedergabe als gelesen markieren",
     "form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
-    "form.prefs.label.always_open_external_links": "Artikel immer mit Öffnen der Links lesen",
     "form.prefs.label.show_reading_time": "Geschätzte Lesezeit für Artikel anzeigen",
     "form.prefs.label.theme": "Thema",
     "form.prefs.label.timezone": "Zeitzone",

+ 4 - 1
internal/locale/translations/el_EL.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Αποθήκευση άρθρων στο Instapaper",
     "form.integration.instapaper_password": "Κωδικός Πρόσβασης Instapaper",
     "form.integration.instapaper_username": "Όνομα Χρήστη Instapaper",
+    "form.integration.karakeep_activate": "Αποθήκευση άρθρων στο Karakeep",
+    "form.integration.karakeep_api_key": "Κλειδί API Karakeep",
+    "form.integration.karakeep_url": "Τελικό σημείο Karakeep API",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
     "form.prefs.label.cjk_reading_speed": "Ταχύτητα ανάγνωσης για κινέζικα, κορεάτικα και ιαπωνικά (χαρακτήρες ανά λεπτό)",
     "form.prefs.label.custom_css": "Προσαρμοσμένο CSS",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Εμφάνιση εκτιμώμενου χρόνου ανάγνωσης για άρθρα",
     "form.prefs.label.theme": "Θέμα",
     "form.prefs.label.timezone": "Ζώνη Ώρας",

+ 4 - 1
internal/locale/translations/en_US.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Save entries to Instapaper",
     "form.integration.instapaper_password": "Instapaper Password",
     "form.integration.instapaper_username": "Instapaper Username",
+    "form.integration.karakeep_activate": "Save entries to Karakeep",
+    "form.integration.karakeep_api_key": "Karakeep API key",
+    "form.integration.karakeep_url": "Karakeep API Endpoint",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Categories sorting",
     "form.prefs.label.cjk_reading_speed": "Reading speed for Chinese, Korean and Japanese (characters per minute)",
     "form.prefs.label.custom_css": "Custom CSS",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Show estimated reading time for entries",
     "form.prefs.label.theme": "Theme",
     "form.prefs.label.timezone": "Timezone",

+ 4 - 1
internal/locale/translations/es_ES.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Enviar artículos a Instapaper",
     "form.integration.instapaper_password": "Contraseña de Instapaper",
     "form.integration.instapaper_username": "Nombre de usuario de Instapaper",
+    "form.integration.karakeep_activate": "Enviar artículos a Karakeep",
+    "form.integration.karakeep_api_key": "Clave de API de Karakeep",
+    "form.integration.karakeep_url": "Acceso API de Karakeep",
     "form.integration.linkace_activate": "Guardar artículos en LinkAce",
     "form.integration.linkace_api_key": "Clave API de LinkAce",
     "form.integration.linkace_check_disabled": "Deshabilitar la comprobación de enlace",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Ajustes globales del feed",
     "form.prefs.fieldset.reader_settings": "Ajustes del lector",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Clasificación por categorías",
     "form.prefs.label.cjk_reading_speed": "Velocidad de lectura en chino, coreano y japonés (caracteres por minuto)",
     "form.prefs.label.custom_css": "CSS personalizado",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Marcar las entradas como leídas cuando se vean. Para audio/video, marcar como leído al 90%% de finalización",
     "form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Mostrar el tiempo estimado de lectura de los artículos",
     "form.prefs.label.theme": "Tema",
     "form.prefs.label.timezone": "Zona horaria",

+ 4 - 1
internal/locale/translations/fi_FI.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Tallenna artikkelit Instapaperiin",
     "form.integration.instapaper_password": "Instapaper-salasana",
     "form.integration.instapaper_username": "Instapaper-käyttäjätunnus",
+    "form.integration.karakeep_activate": "Tallenna artikkelit Karakeepiin",
+    "form.integration.karakeep_api_key": "Karakeep API-avain",
+    "form.integration.karakeep_url": "Karakeep API-päätepiste",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
     "form.prefs.label.cjk_reading_speed": "Kiinan, Korean ja Japanin lukunopeus (merkkejä minuutissa)",
     "form.prefs.label.custom_css": "Mukautettu CSS",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Näytä artikkeleiden arvioitu lukuaika",
     "form.prefs.label.theme": "Teema",
     "form.prefs.label.timezone": "Aikavyöhyke",

+ 4 - 1
internal/locale/translations/fr_FR.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Sauvegarder les articles vers Instapaper",
     "form.integration.instapaper_password": "Mot de passe Instapaper",
     "form.integration.instapaper_username": "Nom d'utilisateur Instapaper",
+    "form.integration.karakeep_activate": "Sauvegarder les articles vers Karakeep",
+    "form.integration.karakeep_api_key": "Clé d'API de Karakeep",
+    "form.integration.karakeep_url": "URL de l'API de Karakeep",
     "form.integration.linkace_activate": "Enregistrer les entrées vers LinkAce",
     "form.integration.linkace_api_key": "Clé d'API LinkAce",
     "form.integration.linkace_check_disabled": "Désactiver la vérification des liens",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements",
     "form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
     "form.prefs.help.external_font_hosts": "Liste de domaine externes autorisés, séparés par des espaces. Par exemple : « fonts.gstatic.com fonts.googleapis.com ».",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
     "form.prefs.label.cjk_reading_speed": "Vitesse de lecture pour le chinois, le coréen et le japonais (caractères par minute)",
     "form.prefs.label.custom_css": "Feuille de style personnalisée",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées. Pour l'audio/vidéo, marquer comme lues après 90%%",
     "form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Afficher le temps de lecture estimé des articles",
     "form.prefs.label.theme": "Thème",
     "form.prefs.label.timezone": "Fuseau horaire",

+ 4 - 1
internal/locale/translations/hi_IN.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "विषय-वस्तु को इंस्टापेपर में सहेजें",
     "form.integration.instapaper_password": "इंस्टापेपर पासवर्ड",
     "form.integration.instapaper_username": "इंस्टापेपर यूजरनेम",
+    "form.integration.karakeep_activate": "Save entries to Karakeep",
+    "form.integration.karakeep_api_key": "Karakeep API key",
+    "form.integration.karakeep_url": "Karakeep API Endpoint",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
     "form.prefs.label.cjk_reading_speed": "चीनी, कोरियाई और जापानी के लिए पढ़ने की गति (प्रति मिनट वर्ण)",
     "form.prefs.label.custom_css": "कस्टम सीएसएस",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "विषय के लिए अनुमानित पढ़ने का समय दिखाएं",
     "form.prefs.label.theme": "थीम",
     "form.prefs.label.timezone": "समय क्षेत्र",

+ 4 - 1
internal/locale/translations/id_ID.json

@@ -231,6 +231,9 @@
     "form.integration.instapaper_activate": "Simpan artikel ke Instapaper",
     "form.integration.instapaper_password": "Kata Sandi Instapaper",
     "form.integration.instapaper_username": "Nama Pengguna Instapaper",
+    "form.integration.karakeep_activate": "Simpan artikel ke Karakeep",
+    "form.integration.karakeep_api_key": "Kunci API Karakeep",
+    "form.integration.karakeep_url": "Titik URL API Karakeep",
     "form.integration.linkace_activate": "Simpan artikel ke LinkAce",
     "form.integration.linkace_api_key": "Kunci API LinkAce",
     "form.integration.linkace_check_disabled": "Matikan pemeriksaan tautan",
@@ -326,6 +329,7 @@
     "form.prefs.fieldset.global_feed_settings": "Pengaturan Umpan Global",
     "form.prefs.fieldset.reader_settings": "Pengaturan Pembaca",
     "form.prefs.help.external_font_hosts": "Daftar yang dipisah spasi untuk peladen penyedia fonta eksternal yang diperbolehkan. Seperti: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Baca artikel dengan membuka tautan eksternal",
     "form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
     "form.prefs.label.cjk_reading_speed": "Kecepatan membaca untuk bahasa Tiongkok, Korea, dan Jepang (karakter per menit)",
     "form.prefs.label.custom_css": "Modifikasi CSS",
@@ -346,7 +350,6 @@
     "form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Tandai entri sebagai telah dibaca ketika dilihat. Untuk audio/video, tandai sebagai telah dibaca ketika sudah 90% didengar/ditonton.",
     "form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
-    "form.prefs.label.always_open_external_links": "Baca artikel dengan membuka tautan eksternal",
     "form.prefs.label.show_reading_time": "Tampilkan perkiraan waktu baca untuk artikel",
     "form.prefs.label.theme": "Tema",
     "form.prefs.label.timezone": "Zona Waktu",

+ 4 - 1
internal/locale/translations/it_IT.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Salva gli articoli su Instapaper",
     "form.integration.instapaper_password": "Password dell'account Instapaper",
     "form.integration.instapaper_username": "Nome utente dell'account Instapaper",
+    "form.integration.karakeep_activate": "Salva gli articoli su Karakeep",
+    "form.integration.karakeep_api_key": "API key dell'account Karakeep",
+    "form.integration.karakeep_url": "Endpoint dell'API di Karakeep",
     "form.integration.linkace_activate": "Salva gli articoli su LinkAce",
     "form.integration.linkace_api_key": "API key dell'account LinkAce",
     "form.integration.linkace_check_disabled": "Disabilita i controlli",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
     "form.prefs.label.cjk_reading_speed": "Velocità di lettura per cinese, coreano e giapponese (caratteri al minuto)",
     "form.prefs.label.custom_css": "CSS personalizzati",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Mostra il tempo di lettura stimato per gli articoli",
     "form.prefs.label.theme": "Tema",
     "form.prefs.label.timezone": "Fuso orario",

+ 4 - 1
internal/locale/translations/ja_JP.json

@@ -231,6 +231,9 @@
     "form.integration.instapaper_activate": "Instapaper に記事を保存する",
     "form.integration.instapaper_password": "Instapaper のパスワード",
     "form.integration.instapaper_username": "Instapaper のユーザー名",
+    "form.integration.karakeep_activate": "Karakeep に記事を保存する",
+    "form.integration.karakeep_api_key": "Karakeep の API key",
+    "form.integration.karakeep_url": "Karakeep の API Endpoint",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -326,6 +329,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "カテゴリの表示順",
     "form.prefs.label.cjk_reading_speed": "中国語、韓国語、日本語の読書速度(文字数/分)",
     "form.prefs.label.custom_css": "カスタム CSS",
@@ -346,7 +350,6 @@
     "form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "記事の推定読書時間を表示する",
     "form.prefs.label.theme": "テーマ",
     "form.prefs.label.timezone": "タイムゾーン",

+ 4 - 1
internal/locale/translations/nan_Latn_pehoeji.json

@@ -231,6 +231,9 @@
     "form.integration.instapaper_activate": "Pó-chûn siau-sit kàu Instapaper",
     "form.integration.instapaper_password": "Instapaper bi̍t-bé",
     "form.integration.instapaper_username": "Instapaper Kháu-chō miâ",
+    "form.integration.karakeep_activate": "Pó-chûn siau-sit kàu Karakeep",
+    "form.integration.karakeep_api_key": "Karakeep API só-sî",
+    "form.integration.karakeep_url": "Karakeep API thâu",
     "form.integration.linkace_activate": "Pó-chûn siau-sit kàu LinkAce",
     "form.integration.linkace_api_key": "LinkAce API só-sî",
     "form.integration.linkace_check_disabled": "Thêng iōng liân-kiat kiám-cha",
@@ -326,6 +329,7 @@
     "form.prefs.fieldset.global_feed_settings": "Choân-he̍k siau-sit lâi-goân siat-tēng",
     "form.prefs.fieldset.reader_settings": "Ia̍t-tha̍k khì siat-tēng",
     "form.prefs.help.external_font_hosts": "Iōng khang-keh keh khui ún-chún ê gōa-pō͘ lī-hêng lâi-goân. Phì-lû \"fonts.gstatic.com fonts.googleapis.com\"",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Lūi-pia̍t hián-sī sūn-sū",
     "form.prefs.label.cjk_reading_speed": "Tiong-bûn, Hân-bûn, Li̍t-bûn tha̍k ê sok-tō͘ (múi hun-cheng ē-sái tha̍k kúi ê lī-goân)",
     "form.prefs.label.custom_css": "Chū tēng ê CSS",
@@ -346,7 +350,6 @@
     "form.prefs.label.mark_read_on_view": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Phah khui ê sî-chūn sūn-sòa kā siau-sit chù chòe tha̍k kè, m̄-koh nā-sī im-sìn, sī-sìn tio̍h tī hòng-sàng kàu 90%% ê si-chun chiah lâi chù",
     "form.prefs.label.media_playback_rate": "Im-sìn, sī-sìn pàng ê sok-tō͘",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Hián-sī siau-sit àn-sǹg ài gōa-kú lâi tha̍k",
     "form.prefs.label.theme": "Chú-tôe",
     "form.prefs.label.timezone": "Sî-khu",

+ 4 - 1
internal/locale/translations/nl_NL.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Artikelen opslaan in Instapaper",
     "form.integration.instapaper_password": "Instapaper wachtwoord",
     "form.integration.instapaper_username": "Instapaper gebruikersnaam",
+    "form.integration.karakeep_activate": "Artikelen opslaan in Karakeep",
+    "form.integration.karakeep_api_key": "Karakeep API-sleutel",
+    "form.integration.karakeep_url": "Karakeep URL",
     "form.integration.linkace_activate": "Artikelen opslaan in LinkAce",
     "form.integration.linkace_api_key": "LinkAce API-sleutel",
     "form.integration.linkace_check_disabled": "Koppelingcontrole uitschakelen",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen",
     "form.prefs.fieldset.reader_settings": "Lees Instellingen",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Volgorde categorieën",
     "form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
     "form.prefs.label.custom_css": "Aangepaste CSS",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Markeer artikelen automatisch als gelezen wanneer ze worden bekeken",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Markeer artikelen als gelezen wanneer ze worden bekeken. Voor audio/video, markeer als gelezen bij 90%% voltooiing",
     "form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen",
     "form.prefs.label.theme": "Thema",
     "form.prefs.label.timezone": "Tijdzone",

+ 4 - 1
internal/locale/translations/pl_PL.json

@@ -235,6 +235,9 @@
     "form.integration.instapaper_activate": "Zapisuj wpisy w Instapaper",
     "form.integration.instapaper_password": "Hasło do Instapaper",
     "form.integration.instapaper_username": "Login do Instapaper",
+    "form.integration.karakeep_activate": "Zapisuj wpisy w Karakeep",
+    "form.integration.karakeep_api_key": "Klucz API do Karakeep",
+    "form.integration.karakeep_url": "Punkt końcowy API Karakeep",
     "form.integration.linkace_activate": "Zapisuj wpisy w LinkAce",
     "form.integration.linkace_api_key": "Klucz API do LinkAce",
     "form.integration.linkace_check_disabled": "Wyłącz sprawdzanie łączy",
@@ -330,6 +333,7 @@
     "form.prefs.fieldset.global_feed_settings": "Globalne ustawienia kanałów",
     "form.prefs.fieldset.reader_settings": "Ustawienia czytnika",
     "form.prefs.help.external_font_hosts": "Lista hostów zewnętrznych czcionek, na które należy zezwolić, rozdzielona spacjami. Na przykład: „fonts.gstatic.com fonts.googleapis.com”.",
+    "form.prefs.label.always_open_external_links": "Czytaj artykuły, otwierając łącza zewnętrzne",
     "form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
     "form.prefs.label.cjk_reading_speed": "Szybkość czytania w języku chińskim, koreańskim i japońskim (znaki na minutę)",
     "form.prefs.label.custom_css": "Niestandardowy CSS",
@@ -350,7 +354,6 @@
     "form.prefs.label.mark_read_on_view": "Automatycznie oznacz wpisy jako przeczytane podczas przeglądania",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Oznacz wpisy jako przeczytane po wyświetleniu. W przypadku audio i wideo oznacz jako przeczytane po ukończeniu 90%%",
     "form.prefs.label.media_playback_rate": "Szybkość odtwarzania audio i wideo",
-    "form.prefs.label.always_open_external_links": "Czytaj artykuły, otwierając łącza zewnętrzne",
     "form.prefs.label.show_reading_time": "Pokaż szacowany czas czytania wpisów",
     "form.prefs.label.theme": "Wygląd",
     "form.prefs.label.timezone": "Strefa czasowa",

+ 4 - 1
internal/locale/translations/pt_BR.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Salvar itens no Instapaper",
     "form.integration.instapaper_password": "Senha do Instapaper",
     "form.integration.instapaper_username": "Nome do usuário do Instapaper",
+    "form.integration.karakeep_activate": "Salvar itens no Karakeep",
+    "form.integration.karakeep_api_key": "Chave de API do Karakeep",
+    "form.integration.karakeep_url": "Endpoint de API do Karakeep",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Classificação das categorias",
     "form.prefs.label.cjk_reading_speed": "Velocidade de leitura para chinês, coreano e japonês (caracteres por minuto)",
     "form.prefs.label.custom_css": "CSS customizado",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Mostrar tempo estimado de leitura de artigos",
     "form.prefs.label.theme": "Tema",
     "form.prefs.label.timezone": "Fuso horário",

+ 4 - 1
internal/locale/translations/ro_RO.json

@@ -235,6 +235,9 @@
     "form.integration.instapaper_activate": "Salvează înregistrările pe Instapaper",
     "form.integration.instapaper_password": "Parolă Instapaper",
     "form.integration.instapaper_username": "Utilizator Instapaper",
+    "form.integration.karakeep_activate": "Salvare înregistrări în Karakeep",
+    "form.integration.karakeep_api_key": "Cheie API Karakeep",
+    "form.integration.karakeep_url": "Punct acces API Karakeep",
     "form.integration.linkace_activate": "Salvează intrările în LinkAce",
     "form.integration.linkace_api_key": "Cheie API LinkAce",
     "form.integration.linkace_check_disabled": "Dezactivează verificarea link-urilor",
@@ -330,6 +333,7 @@
     "form.prefs.fieldset.global_feed_settings": "Setări Globale pt. Flux",
     "form.prefs.fieldset.reader_settings": "Setări Citire",
     "form.prefs.help.external_font_hosts": "Lista fonturilor de pe gazdă separate de virgulă care poate fi utilizate. De exemplu: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Sortare categorii",
     "form.prefs.label.cjk_reading_speed": "Viteză de citire pentru Chineză, Coreană și Japoneză (caractere pe minut)",
     "form.prefs.label.custom_css": "CSS personalizat",
@@ -350,7 +354,6 @@
     "form.prefs.label.mark_read_on_view": "Marchează intrările ca citite la vizualizare",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Marchează intrările ca citite la vizualizare. Pentru audio/video, marchează ca citit la redarea a 90%% de conținut",
     "form.prefs.label.media_playback_rate": "Viteza de rulare audio/video",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Afișare timp estimat de citire pentru înregistrări",
     "form.prefs.label.theme": "Temă",
     "form.prefs.label.timezone": "Fus orar",

+ 4 - 1
internal/locale/translations/ru_RU.json

@@ -235,6 +235,9 @@
     "form.integration.instapaper_activate": "Сохранять статьи в Instapaper",
     "form.integration.instapaper_password": "Пароль Instapaper",
     "form.integration.instapaper_username": "Имя пользователя Instapaper",
+    "form.integration.karakeep_activate": "Сохранять статьи в Karakeep",
+    "form.integration.karakeep_api_key": "API-ключ Karakeep",
+    "form.integration.karakeep_url": "Конечная точка Karakeep API",
     "form.integration.linkace_activate": "Сохранять статьи в LinkAce",
     "form.integration.linkace_api_key": "API-ключ LinkAce",
     "form.integration.linkace_check_disabled": "Отключить проверку ссылок",
@@ -330,6 +333,7 @@
     "form.prefs.fieldset.global_feed_settings": "Глобальные настройки подписок",
     "form.prefs.fieldset.reader_settings": "Настройки чтения",
     "form.prefs.help.external_font_hosts": "Список разрешённых внешних хостов для шрифтов, разделенных пробелами. Например: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Сортировка категорий",
     "form.prefs.label.cjk_reading_speed": "Скорость чтения на китайском, корейском и японском языках (знаков в минуту)",
     "form.prefs.label.custom_css": "Пользовательский CSS",
@@ -350,7 +354,6 @@
     "form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Отмечать статьи как прочитанные при просмотре. Для аудио/видео - при 90%% завершения воспроизведения",
     "form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Показать примерное время чтения статей",
     "form.prefs.label.theme": "Тема",
     "form.prefs.label.timezone": "Часовой пояс",

+ 4 - 1
internal/locale/translations/tr_TR.json

@@ -233,6 +233,9 @@
     "form.integration.instapaper_activate": "Makaleleri Instapaper'a kaydet",
     "form.integration.instapaper_password": "Instapaper Parolası",
     "form.integration.instapaper_username": "Instapaper Kullanıcı Adı",
+    "form.integration.karakeep_activate": "Makaleleri Karakeep'a kaydet",
+    "form.integration.karakeep_api_key": "Karakeep API anahtarı",
+    "form.integration.karakeep_url": "Karakeep API Uç Noktası",
     "form.integration.linkace_activate": "Makaleleri LinkAce'e kaydet",
     "form.integration.linkace_api_key": "LinkAce API anahtarı",
     "form.integration.linkace_check_disabled": "Link kontrolünü devre dışı bırak",
@@ -328,6 +331,7 @@
     "form.prefs.fieldset.global_feed_settings": "Genel Besleme Ayarları",
     "form.prefs.fieldset.reader_settings": "Okuyucu Ayarları",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Kategori sıralaması",
     "form.prefs.label.cjk_reading_speed": "Çince, Korece ve Japonca için okuma hızı (dakika başına karakter)",
     "form.prefs.label.custom_css": "Özel CSS",
@@ -348,7 +352,6 @@
     "form.prefs.label.mark_read_on_view": "Makaleler görüntülendiğinde otomatik olarak okundu olarak işaretle",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Ses/video oynatma hızı",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Makaleler için tahmini okuma süresini göster",
     "form.prefs.label.theme": "Tema",
     "form.prefs.label.timezone": "Saat Dilimi",

+ 4 - 1
internal/locale/translations/uk_UA.json

@@ -235,6 +235,9 @@
     "form.integration.instapaper_activate": "Зберігати статті до Instapaper",
     "form.integration.instapaper_password": "Пароль Instapaper",
     "form.integration.instapaper_username": "Ім’я користувача Instapaper",
+    "form.integration.karakeep_activate": "Зберігати статті до Karakeep",
+    "form.integration.karakeep_api_key": "Ключ API Karakeep",
+    "form.integration.karakeep_url": "Karakeep API Endpoint",
     "form.integration.linkace_activate": "Save entries to LinkAce",
     "form.integration.linkace_api_key": "LinkAce API key",
     "form.integration.linkace_check_disabled": "Disable link check",
@@ -330,6 +333,7 @@
     "form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
     "form.prefs.fieldset.reader_settings": "Reader Settings",
     "form.prefs.help.external_font_hosts": "Space separated list of external font hosts to allow. For example: \"fonts.gstatic.com fonts.googleapis.com\".",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "Сортування за категоріями",
     "form.prefs.label.cjk_reading_speed": "Швидкість читання для китайської, корейської та японської мови (символів на хвилину)",
     "form.prefs.label.custom_css": "Спеціальний CSS",
@@ -350,7 +354,6 @@
     "form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
     "form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
     "form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "Показувати приблизний час читання для записів",
     "form.prefs.label.theme": "Тема",
     "form.prefs.label.timezone": "Часовий пояс",

+ 4 - 1
internal/locale/translations/zh_CN.json

@@ -231,6 +231,9 @@
     "form.integration.instapaper_activate": "保存文章到 Instapaper",
     "form.integration.instapaper_password": "Instapaper 密码",
     "form.integration.instapaper_username": "Instapaper 用户名",
+    "form.integration.karakeep_activate": "保存文章到 Karakeep",
+    "form.integration.karakeep_api_key": "Karakeep API 密钥",
+    "form.integration.karakeep_url": "Karakeep API 端点",
     "form.integration.linkace_activate": "保存文章到 LinkAce",
     "form.integration.linkace_api_key": "LinkAce API 密钥",
     "form.integration.linkace_check_disabled": "关闭链接检查",
@@ -326,6 +329,7 @@
     "form.prefs.fieldset.global_feed_settings": "全局订阅源设置",
     "form.prefs.fieldset.reader_settings": "阅读器设置",
     "form.prefs.help.external_font_hosts": "允许外部字体托管的空格分隔列表。例如:\"fonts.gstatic.com fonts.googleapis.com\"。",
+    "form.prefs.label.always_open_external_links": "打开外部链接阅读文章",
     "form.prefs.label.categories_sorting_order": "分类排序",
     "form.prefs.label.cjk_reading_speed": "中文、韩文和日文的阅读速度(每分钟字符数)",
     "form.prefs.label.custom_css": "自定义 CSS",
@@ -346,7 +350,6 @@
     "form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
     "form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
     "form.prefs.label.media_playback_rate": "音频/视频的播放速度",
-    "form.prefs.label.always_open_external_links": "打开外部链接阅读文章",
     "form.prefs.label.show_reading_time": "显示文章的预计阅读时间",
     "form.prefs.label.theme": "主题",
     "form.prefs.label.timezone": "时区",

+ 4 - 1
internal/locale/translations/zh_TW.json

@@ -231,6 +231,9 @@
     "form.integration.instapaper_activate": "儲存文章到 Instapaper",
     "form.integration.instapaper_password": "Instapaper 密碼",
     "form.integration.instapaper_username": "Instapaper 使用者名稱",
+    "form.integration.karakeep_activate": "儲存文章到 Karakeep",
+    "form.integration.karakeep_api_key": "Karakeep API 金鑰",
+    "form.integration.karakeep_url": "Karakeep API 端點",
     "form.integration.linkace_activate": "儲存文章到 LinkAce",
     "form.integration.linkace_api_key": "LinkAce API 金鑰",
     "form.integration.linkace_check_disabled": "停用連結檢查",
@@ -326,6 +329,7 @@
     "form.prefs.fieldset.global_feed_settings": "全域 Feed 設定",
     "form.prefs.fieldset.reader_settings": "閱讀器設定",
     "form.prefs.help.external_font_hosts": "以空白分隔允許的外部字型來源。例如:「fonts.gstatic.com fonts.googleapis.com」。",
+    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.categories_sorting_order": "分類排序",
     "form.prefs.label.cjk_reading_speed": "中文、韓文和日文的閱讀速度(每分鐘字元數)",
     "form.prefs.label.custom_css": "自訂 CSS",
@@ -346,7 +350,6 @@
     "form.prefs.label.mark_read_on_view": "檢視時自動將文章標記為已讀",
     "form.prefs.label.mark_read_on_view_or_media_completion": "檢視文章即標記為已讀;若是音訊/視訊則在 90% 播放完成時標記",
     "form.prefs.label.media_playback_rate": "音訊/視訊播放速度",
-    "form.prefs.label.always_open_external_links": "Read articles by opening external links",
     "form.prefs.label.show_reading_time": "顯示文章的預計閱讀時間",
     "form.prefs.label.theme": "主題",
     "form.prefs.label.timezone": "時區",

+ 3 - 0
internal/model/integration.go

@@ -94,6 +94,9 @@ type Integration struct {
 	OmnivoreEnabled                  bool
 	OmnivoreAPIKey                   string
 	OmnivoreURL                      string
+	KarakeepEnabled                  bool
+	KarakeepAPIKey                   string
+	KarakeepURL                      string
 	RaindropEnabled                  bool
 	RaindropToken                    string
 	RaindropCollectionID             string

+ 16 - 3
internal/storage/integration.go

@@ -220,7 +220,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 			pushover_token,
 			pushover_device,
 			pushover_prefix,
-			rssbridge_token
+			rssbridge_token,
+			karakeep_enabled,
+			karakeep_api_key,
+			karakeep_url
 		FROM
 			integrations
 		WHERE
@@ -340,6 +343,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
 		&integration.PushoverDevice,
 		&integration.PushoverPrefix,
 		&integration.RSSBridgeToken,
+		&integration.KarakeepEnabled,
+		&integration.KarakeepAPIKey,
+		&integration.KarakeepURL,
 	)
 	switch {
 	case err == sql.ErrNoRows:
@@ -467,9 +473,12 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 			pushover_token=$108,
 			pushover_device=$109,
 			pushover_prefix=$110,
-			rssbridge_token=$111
+			rssbridge_token=$111,
+			karakeep_enabled=$112,
+			karakeep_api_key=$113,
+			karakeep_url=$114
 		WHERE
-			user_id=$112
+			user_id=$115
 	`
 	_, err := s.db.Exec(
 		query,
@@ -584,6 +593,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
 		integration.PushoverDevice,
 		integration.PushoverPrefix,
 		integration.RSSBridgeToken,
+		integration.KarakeepEnabled,
+		integration.KarakeepAPIKey,
+		integration.KarakeepURL,
 		integration.UserID,
 	)
 
@@ -622,6 +634,7 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) {
 				shaarli_enabled='t' OR
 				webhook_enabled='t' OR
 				omnivore_enabled='t' OR
+				karakeep_enabled='t' OR
 				raindrop_enabled='t' OR
 				betula_enabled='t' OR
 				cubox_enabled='t' OR

+ 19 - 0
internal/template/templates/views/integrations.html

@@ -364,6 +364,25 @@
         </div>
     </details>
 
+    <details {{ if .form.KarakeepEnabled }}open{{ end }}>
+        <summary>Karakeep</summary>
+        <div class="form-section">
+            <label>
+                <input type="checkbox" name="karakeep_enabled" value="1" {{ if .form.KarakeepEnabled }}checked{{ end }}> {{ t "form.integration.karakeep_activate" }}
+            </label>
+
+            <label for="form-karakeep-api-key">{{ t "form.integration.karakeep_api_key" }}</label>
+            <input type="text" name="karakeep_api_key" id="form-karakeep-api-key" value="{{ .form.KarakeepAPIKey }}" spellcheck="false">
+
+            <label for="form-karakeep-url">{{ t "form.integration.karakeep_url" }}</label>
+            <input type="url" name="karakeep_url" id="form-karakeep-url" value="{{ .form.KarakeepURL }}" placeholder="https://try.karakeep.app/api/v1/bookmarks" spellcheck="false">
+
+            <div class="buttons">
+                <button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
+            </div>
+        </div>
+    </details>
+
     <details {{ if .form.PinboardEnabled }}open{{ end }}>
         <summary>Pinboard</summary>
         <div class="form-section">

+ 9 - 0
internal/ui/form/integration.go

@@ -97,6 +97,9 @@ type IntegrationForm struct {
 	OmnivoreEnabled                  bool
 	OmnivoreAPIKey                   string
 	OmnivoreURL                      string
+	KarakeepEnabled                  bool
+	KarakeepAPIKey                   string
+	KarakeepURL                      string
 	RaindropEnabled                  bool
 	RaindropToken                    string
 	RaindropCollectionID             string
@@ -209,6 +212,9 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
 	integration.OmnivoreEnabled = i.OmnivoreEnabled
 	integration.OmnivoreAPIKey = i.OmnivoreAPIKey
 	integration.OmnivoreURL = i.OmnivoreURL
+	integration.KarakeepEnabled = i.KarakeepEnabled
+	integration.KarakeepAPIKey = i.KarakeepAPIKey
+	integration.KarakeepURL = i.KarakeepURL
 	integration.RaindropEnabled = i.RaindropEnabled
 	integration.RaindropToken = i.RaindropToken
 	integration.RaindropCollectionID = i.RaindropCollectionID
@@ -324,6 +330,9 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
 		OmnivoreEnabled:                  r.FormValue("omnivore_enabled") == "1",
 		OmnivoreAPIKey:                   r.FormValue("omnivore_api_key"),
 		OmnivoreURL:                      r.FormValue("omnivore_url"),
+		KarakeepEnabled:                  r.FormValue("karakeep_enabled") == "1",
+		KarakeepAPIKey:                   r.FormValue("karakeep_api_key"),
+		KarakeepURL:                      r.FormValue("karakeep_url"),
 		RaindropEnabled:                  r.FormValue("raindrop_enabled") == "1",
 		RaindropToken:                    r.FormValue("raindrop_token"),
 		RaindropCollectionID:             r.FormValue("raindrop_collection_id"),

+ 3 - 0
internal/ui/integration_show.go

@@ -111,6 +111,9 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
 		OmnivoreEnabled:                  integration.OmnivoreEnabled,
 		OmnivoreAPIKey:                   integration.OmnivoreAPIKey,
 		OmnivoreURL:                      integration.OmnivoreURL,
+		KarakeepEnabled:                  integration.KarakeepEnabled,
+		KarakeepAPIKey:                   integration.KarakeepAPIKey,
+		KarakeepURL:                      integration.KarakeepURL,
 		RaindropEnabled:                  integration.RaindropEnabled,
 		RaindropToken:                    integration.RaindropToken,
 		RaindropCollectionID:             integration.RaindropCollectionID,