Parcourir la source

Add page to list and remove shared entries

Frédéric Guillot il y a 6 ans
Parent
commit
9871e4f5d0

+ 1 - 1
database/migration.go

@@ -59,7 +59,7 @@ func IsSchemaUpToDate(db *sql.DB) error {
 	var currentVersion int
 	db.QueryRow(`SELECT version FROM schema_version`).Scan(&currentVersion)
 	if currentVersion != schemaVersion {
-		return fmt.Errorf(`database schema is not up to date: current=v%d expected=v%d`, currentVersion, schemaVersion)
+		return fmt.Errorf(`the database schema is not up to date: current=v%d expected=v%d`, currentVersion, schemaVersion)
 	}
 	return nil
 }

+ 70 - 10
locale/translations.go

@@ -51,6 +51,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Artikel",
     "menu.api_keys": "API-Schlüssel",
     "menu.create_api_key": "Erstellen Sie einen neuen API-Schlüssel",
+    "menu.shared_entries": "Geteilte Einträge",
     "search.label": "Suche",
     "search.placeholder": "Suche...",
     "pagination.next": "Nächste",
@@ -78,6 +79,10 @@ var translations = map[string]string{
     "entry.comments.title": "Kommentare anzeigen",
     "entry.share.label": "Teilen",
     "entry.share.title": "Diesen Artikel teilen",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Öffnen Sie den öffentlichen Link",
+    "entry.shared_entry.label": "Teilen",
+    "page.shared_entries.title": "Geteilte Einträge",
     "page.unread.title": "Ungelesen",
     "page.starred.title": "Lesezeichen",
     "page.categories.title": "Kategorien",
@@ -191,6 +196,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Aktionen",
     "page.api_keys.never_used": "Nie benutzt",
     "page.new_api_key.title": "Neuer API-Schlüssel",
+    "alert.no_shared_entry": "Es gibt keinen gemeinsamen Eintrag.",
     "alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.",
     "alert.no_category": "Es ist keine Kategorie vorhanden.",
     "alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.",
@@ -379,6 +385,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Entries",
     "menu.api_keys": "API Keys",
     "menu.create_api_key": "Create a new API key",
+    "menu.shared_entries": "Shared entries",
     "search.label": "Search",
     "search.placeholder": "Search...",
     "pagination.next": "Next",
@@ -406,6 +413,10 @@ var translations = map[string]string{
     "entry.comments.title": "View Comments",
     "entry.share.label": "Share",
     "entry.share.title": "Share this article",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Open the public link",
+    "entry.shared_entry.label": "Share",
+    "page.shared_entries.title": "Shared Entries",
     "page.unread.title": "Unread",
     "page.starred.title": "Starred",
     "page.categories.title": "Categories",
@@ -519,6 +530,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Actions",
     "page.api_keys.never_used": "Never Used",
     "page.new_api_key.title": "New API Key",
+    "alert.no_shared_entry": "There is no shared entry.",
     "alert.no_bookmark": "There is no bookmark at the moment.",
     "alert.no_category": "There is no category.",
     "alert.no_category_entry": "There are no articles in this category.",
@@ -687,6 +699,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Artículos",
     "menu.api_keys": "Claves API",
     "menu.create_api_key": "Crear una nueva clave API",
+    "menu.shared_entries": "Entradas compartidas",
     "search.label": "Buscar",
     "search.placeholder": "Búsqueda...",
     "pagination.next": "Siguiente",
@@ -714,6 +727,10 @@ var translations = map[string]string{
     "entry.comments.title": "Ver comentarios",
     "entry.share.label": "Comparta",
     "entry.share.title": "Comparta este articulo",
+    "entry.unshare.label": "No compartir",
+    "entry.shared_entry.title": "Abrir el enlace público",
+    "entry.shared_entry.label": "Compartir",
+    "page.shared_entries.title": "Entradas compartidas",
     "page.unread.title": "No leídos",
     "page.starred.title": "Marcadores",
     "page.categories.title": "Categorias",
@@ -827,6 +844,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Acciones",
     "page.api_keys.never_used": "Nunca usado",
     "page.new_api_key.title": "Nueva clave API",
+    "alert.no_shared_entry": "No hay entrada compartida.",
     "alert.no_bookmark": "No hay marcador en este momento.",
     "alert.no_category": "No hay categoría.",
     "alert.no_category_entry": "No hay artículos en esta categoria.",
@@ -995,6 +1013,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Articles",
     "menu.api_keys": "Clés d'API",
     "menu.create_api_key": "Créer une nouvelle clé d'API",
+    "menu.shared_entries": "Articles partagés",
     "search.label": "Recherche",
     "search.placeholder": "Recherche...",
     "pagination.next": "Suivant",
@@ -1022,6 +1041,10 @@ var translations = map[string]string{
     "entry.comments.title": "Voir les commentaires",
     "entry.share.label": "Partager",
     "entry.share.title": "Partager cet article",
+    "entry.unshare.label": "Enlever le partage",
+    "entry.shared_entry.title": "Ouvrir le lien public",
+    "entry.shared_entry.label": "Partage",
+    "page.shared_entries.title": "Articles partagés",
     "page.unread.title": "Non lus",
     "page.starred.title": "Favoris",
     "page.categories.title": "Catégories",
@@ -1135,6 +1158,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Actions",
     "page.api_keys.never_used": "Jamais utilisé",
     "page.new_api_key.title": "Nouvelle clé d'API",
+    "alert.no_shared_entry": "Il n'y a pas d'article partagé.",
     "alert.no_bookmark": "Il n'y a aucun favoris pour le moment.",
     "alert.no_category": "Il n'y a aucune catégorie.",
     "alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.",
@@ -1323,6 +1347,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Articoli",
     "menu.api_keys": "Chiavi API",
     "menu.create_api_key": "Crea una nuova chiave API",
+    "menu.shared_entries": "Voci condivise",
     "search.label": "Cerca",
     "search.placeholder": "Cerca...",
     "pagination.next": "Successivo",
@@ -1350,6 +1375,10 @@ var translations = map[string]string{
     "entry.comments.title": "Mostra i commenti",
     "entry.share.label": "Condividi",
     "entry.share.title": "Condividi questo articolo",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Apri il link pubblico",
+    "entry.shared_entry.label": "Condivisione",
+    "page.shared_entries.title": "Voci condivise",
     "page.unread.title": "Da leggere",
     "page.starred.title": "Preferiti",
     "page.categories.title": "Categorie",
@@ -1463,6 +1492,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Azioni",
     "page.api_keys.never_used": "Mai usato",
     "page.new_api_key.title": "Nuova chiave API",
+    "alert.no_shared_entry": "Non ci sono voci condivise.",
     "alert.no_bookmark": "Nessun preferito disponibile.",
     "alert.no_category": "Nessuna categoria disponibile.",
     "alert.no_category_entry": "Questa categoria non contiene alcun articolo.",
@@ -1631,6 +1661,7 @@ var translations = map[string]string{
     "menu.feed_entries": "記事一覧",
     "menu.api_keys": "APIキー",
     "menu.create_api_key": "新しいAPIキーを作成する",
+    "menu.shared_entries": "共有エントリ",
     "search.label": "検索",
     "search.placeholder": "…を検索",
     "pagination.next": "次",
@@ -1658,6 +1689,10 @@ var translations = map[string]string{
     "entry.comments.title": "コメントを見る",
     "entry.share.label": "共有",
     "entry.share.title": "この記事を共有する",
+    "entry.unshare.label": "共有解除",
+    "entry.shared_entry.title": "公開リンクを開く",
+    "entry.shared_entry.label": "共有する",
+    "page.shared_entries.title": "共有エントリ",
     "page.unread.title": "未読",
     "page.starred.title": "星付き",
     "page.categories.title": "カテゴリ",
@@ -1771,6 +1806,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "アクション",
     "page.api_keys.never_used": "使われたことがない",
     "page.new_api_key.title": "新しいAPIキー",
+    "alert.no_shared_entry": "共有エントリはありません。",
     "alert.no_bookmark": "現在星付きはありません。",
     "alert.no_category": "カテゴリが存在しません。",
     "alert.no_category_entry": "このカテゴリには記事がありません。",
@@ -1939,6 +1975,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Lidwoord",
     "menu.api_keys": "API-sleutels",
     "menu.create_api_key": "Maak een nieuwe API-sleutel",
+    "menu.shared_entries": "Gedeelde vermeldingen",
     "search.label": "Zoeken",
     "search.placeholder": "Zoeken...",
     "pagination.next": "Volgende",
@@ -1966,6 +2003,10 @@ var translations = map[string]string{
     "entry.comments.title": "Bekijk de reacties",
     "entry.share.label": "Deel",
     "entry.share.title": "Deel dit artikel",
+    "entry.unshare.label": "Delen ongedaan maken",
+    "entry.shared_entry.title": "Open de openbare link",
+    "entry.shared_entry.label": "Delen",
+    "page.shared_entries.title": "Gedeelde vermeldingen",
     "page.unread.title": "Ongelezen",
     "page.starred.title": "Favorieten",
     "page.categories.title": "Categorieën",
@@ -2079,6 +2120,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Acties",
     "page.api_keys.never_used": "Nooit gebruikt",
     "page.new_api_key.title": "Nieuwe API-sleutel",
+    "alert.no_shared_entry": "Er is geen gedeelde toegang.",
     "alert.no_bookmark": "Er zijn op dit moment geen favorieten.",
     "alert.no_category": "Er zijn geen categorieën.",
     "alert.no_category_entry": "Deze categorie bevat geen feeds.",
@@ -2265,6 +2307,7 @@ var translations = map[string]string{
     "menu.feed_entries": "Artykuły",
     "menu.api_keys": "Klucze API",
     "menu.create_api_key": "Utwórz nowy klucz API",
+    "menu.shared_entries": "Udostępnione wpisy",
     "search.label": "Szukaj",
     "search.placeholder": "Szukaj...",
     "pagination.next": "Następny",
@@ -2292,6 +2335,10 @@ var translations = map[string]string{
     "entry.comments.title": "Zobacz komentarze",
     "entry.share.label": "Podzielić się",
     "entry.share.title": "Podzielić się ten artykuł",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Otwórz publiczny link",
+    "entry.shared_entry.label": "Udostępnianie",
+    "page.shared_entries.title": "Udostępnione wpisy",
     "page.unread.title": "Nieprzeczytane",
     "page.starred.title": "Oznaczone gwiazdką",
     "page.categories.title": "Kategorie",
@@ -2407,6 +2454,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Działania",
     "page.api_keys.never_used": "Nigdy nie używany",
     "page.new_api_key.title": "Nowy klucz API",
+    "alert.no_shared_entry": "Brak wspólnego wpisu.",
     "alert.no_bookmark": "Obecnie nie ma żadnych zakładek.",
     "alert.no_category": "Nie ma żadnej kategorii!",
     "alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów",
@@ -2599,6 +2647,7 @@ var translations = map[string]string{
     "menu.feed_entries": "статьи",
     "menu.api_keys": "API-ключи",
     "menu.create_api_key": "Создать новый ключ API",
+    "menu.shared_entries": "Общие записи",
     "search.label": "Поиск",
     "search.placeholder": "Поиск…",
     "pagination.next": "Следующая",
@@ -2626,6 +2675,10 @@ var translations = map[string]string{
     "entry.comments.title": "Показать комментарии",
     "entry.share.label": "поделиться",
     "entry.share.title": "поделиться эту статью",
+    "entry.unshare.label": "Удалить из открытого списка",
+    "entry.shared_entry.title": "Открыть публичную ссылку",
+    "entry.shared_entry.label": "обмен",
+    "page.shared_entries.title": "Общие записи",
     "page.unread.title": "Непрочитанное",
     "page.starred.title": "Избранное",
     "page.categories.title": "Категории",
@@ -2741,6 +2794,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "Действия",
     "page.api_keys.never_used": "Никогда не использовался",
     "page.new_api_key.title": "Новый ключ API",
+    "alert.no_shared_entry": "Там нет общей записи.",
     "alert.no_bookmark": "Нет закладок на данный момент.",
     "alert.no_category": "Категории отсутствуют.",
     "alert.no_category_entry": "В этой категории нет статей.",
@@ -2915,6 +2969,7 @@ var translations = map[string]string{
     "menu.feed_entries": "文章",
     "menu.api_keys": "API密钥",
     "menu.create_api_key": "创建一个新的API密钥",
+    "menu.shared_entries": "共享条目",
     "search.label": "搜索",
     "search.placeholder": "搜索…",
     "pagination.next": "下一页",
@@ -2942,6 +2997,10 @@ var translations = map[string]string{
     "entry.comments.title": "查看评论",
     "entry.share.label": "分享",
     "entry.share.title": "分享这篇文章",
+    "entry.unshare.label": "取消分享",
+    "entry.shared_entry.title": "打开公共链接",
+    "entry.shared_entry.label": "分享分享",
+    "page.shared_entries.title": "共享条目",
     "page.unread.title": "未读",
     "page.starred.title": "星标",
     "page.categories.title": "分类",
@@ -3053,6 +3112,7 @@ var translations = map[string]string{
     "page.api_keys.table.actions": "操作",
     "page.api_keys.never_used": "没用过",
     "page.new_api_key.title": "新的API密钥",
+    "alert.no_shared_entry": "没有共享条目。",
     "alert.no_bookmark": "目前没有书签",
     "alert.no_category": "目前没有分类",
     "alert.no_category_entry": "该分类下没有文章",
@@ -3189,14 +3249,14 @@ var translations = map[string]string{
 }
 
 var translationsChecksums = map[string]string{
-	"de_DE": "7360a69e038d71e00f64c03891401cd517779687d46a907688f4a9a7b6205146",
-	"en_US": "5d9cad74ccfd94393aa98a25f28fa4f41895d8f758461760601d04c92f898d02",
-	"es_ES": "813b8cd42907dfbc19ff51f3367e0dbb013d373b013d7854df512e846652ff21",
-	"fr_FR": "bf770b1ecfd722bbf4fa29d2ea567b064c8bcb517141073ab3d170d27f4e53e4",
-	"it_IT": "fe2c7147f3c39784f482cf922d9d3a85a94f15af8ff28ebcec64723555a3fb10",
-	"ja_JP": "508025c0c7e7f57195ae011c4499ab58a85d043c828565c1740df879fb2376c1",
-	"nl_NL": "e621a5e7408928624a060a832d9fc36b74026221bd7b07894a4cce267be3cdd1",
-	"pl_PL": "6edcefd04e453c84b03207653c54f7be9a4a8ce4699e06c82bfcca2d7946f47e",
-	"ru_RU": "d7ad59bbd7a150af9d476c4c3034eb85762de7381e2925d75e373584ed45c725",
-	"zh_CN": "e5f169a3c83c9bd7a41e9737e001e58fec243eee7aa23a71d37bfa8e05d92860",
+	"de_DE": "aa7025ff266508152d31ed22e816588c3beaf6ef0ddff20012d303ab55a584d1",
+	"en_US": "c1cd8a4c6360881299609332bc930c0d59a30aea5ae90a6c87bcc06dbba7c69d",
+	"es_ES": "94f4da8c6160ca30c59294ec520f8648496b94904b61ac47c7ca24bfa4fe793b",
+	"fr_FR": "80a2d02c7a90e90024af4f5eb3aad710f245963df99538d7d71c0efcaae79f82",
+	"it_IT": "b033ade2a67a273253d3251df97e6b2da7e2adbec60a33de5d0f41198e6783b2",
+	"ja_JP": "2136cad37933c112a6e69f28936c216c8f262b28c47b57b18482c56a3f8932fb",
+	"nl_NL": "4a3a4a117f11cf62de5cf515c5adc74c8467d05feac1844fee4f81d863173887",
+	"pl_PL": "c088a74ad9e4dca6bdbf73b0b4fce97db60b5066cb03c6884a9462573b0f93ad",
+	"ru_RU": "506a34fdfa35dacf2d86b16f44d47addf2e0f1f376d2a86a474619d6654967cc",
+	"zh_CN": "463037b8bd51bb1e940f432fd621d62d4e81697298b8058124d287b5cbaee10f",
 }

+ 6 - 0
locale/translations/de_DE.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Artikel",
     "menu.api_keys": "API-Schlüssel",
     "menu.create_api_key": "Erstellen Sie einen neuen API-Schlüssel",
+    "menu.shared_entries": "Geteilte Einträge",
     "search.label": "Suche",
     "search.placeholder": "Suche...",
     "pagination.next": "Nächste",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Kommentare anzeigen",
     "entry.share.label": "Teilen",
     "entry.share.title": "Diesen Artikel teilen",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Öffnen Sie den öffentlichen Link",
+    "entry.shared_entry.label": "Teilen",
+    "page.shared_entries.title": "Geteilte Einträge",
     "page.unread.title": "Ungelesen",
     "page.starred.title": "Lesezeichen",
     "page.categories.title": "Kategorien",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "Aktionen",
     "page.api_keys.never_used": "Nie benutzt",
     "page.new_api_key.title": "Neuer API-Schlüssel",
+    "alert.no_shared_entry": "Es gibt keinen gemeinsamen Eintrag.",
     "alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.",
     "alert.no_category": "Es ist keine Kategorie vorhanden.",
     "alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.",

+ 6 - 0
locale/translations/en_US.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Entries",
     "menu.api_keys": "API Keys",
     "menu.create_api_key": "Create a new API key",
+    "menu.shared_entries": "Shared entries",
     "search.label": "Search",
     "search.placeholder": "Search...",
     "pagination.next": "Next",
@@ -73,6 +74,10 @@
     "entry.comments.title": "View Comments",
     "entry.share.label": "Share",
     "entry.share.title": "Share this article",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Open the public link",
+    "entry.shared_entry.label": "Share",
+    "page.shared_entries.title": "Shared Entries",
     "page.unread.title": "Unread",
     "page.starred.title": "Starred",
     "page.categories.title": "Categories",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "Actions",
     "page.api_keys.never_used": "Never Used",
     "page.new_api_key.title": "New API Key",
+    "alert.no_shared_entry": "There is no shared entry.",
     "alert.no_bookmark": "There is no bookmark at the moment.",
     "alert.no_category": "There is no category.",
     "alert.no_category_entry": "There are no articles in this category.",

+ 6 - 0
locale/translations/es_ES.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Artículos",
     "menu.api_keys": "Claves API",
     "menu.create_api_key": "Crear una nueva clave API",
+    "menu.shared_entries": "Entradas compartidas",
     "search.label": "Buscar",
     "search.placeholder": "Búsqueda...",
     "pagination.next": "Siguiente",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Ver comentarios",
     "entry.share.label": "Comparta",
     "entry.share.title": "Comparta este articulo",
+    "entry.unshare.label": "No compartir",
+    "entry.shared_entry.title": "Abrir el enlace público",
+    "entry.shared_entry.label": "Compartir",
+    "page.shared_entries.title": "Entradas compartidas",
     "page.unread.title": "No leídos",
     "page.starred.title": "Marcadores",
     "page.categories.title": "Categorias",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "Acciones",
     "page.api_keys.never_used": "Nunca usado",
     "page.new_api_key.title": "Nueva clave API",
+    "alert.no_shared_entry": "No hay entrada compartida.",
     "alert.no_bookmark": "No hay marcador en este momento.",
     "alert.no_category": "No hay categoría.",
     "alert.no_category_entry": "No hay artículos en esta categoria.",

+ 6 - 0
locale/translations/fr_FR.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Articles",
     "menu.api_keys": "Clés d'API",
     "menu.create_api_key": "Créer une nouvelle clé d'API",
+    "menu.shared_entries": "Articles partagés",
     "search.label": "Recherche",
     "search.placeholder": "Recherche...",
     "pagination.next": "Suivant",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Voir les commentaires",
     "entry.share.label": "Partager",
     "entry.share.title": "Partager cet article",
+    "entry.unshare.label": "Enlever le partage",
+    "entry.shared_entry.title": "Ouvrir le lien public",
+    "entry.shared_entry.label": "Partage",
+    "page.shared_entries.title": "Articles partagés",
     "page.unread.title": "Non lus",
     "page.starred.title": "Favoris",
     "page.categories.title": "Catégories",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "Actions",
     "page.api_keys.never_used": "Jamais utilisé",
     "page.new_api_key.title": "Nouvelle clé d'API",
+    "alert.no_shared_entry": "Il n'y a pas d'article partagé.",
     "alert.no_bookmark": "Il n'y a aucun favoris pour le moment.",
     "alert.no_category": "Il n'y a aucune catégorie.",
     "alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.",

+ 6 - 0
locale/translations/it_IT.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Articoli",
     "menu.api_keys": "Chiavi API",
     "menu.create_api_key": "Crea una nuova chiave API",
+    "menu.shared_entries": "Voci condivise",
     "search.label": "Cerca",
     "search.placeholder": "Cerca...",
     "pagination.next": "Successivo",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Mostra i commenti",
     "entry.share.label": "Condividi",
     "entry.share.title": "Condividi questo articolo",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Apri il link pubblico",
+    "entry.shared_entry.label": "Condivisione",
+    "page.shared_entries.title": "Voci condivise",
     "page.unread.title": "Da leggere",
     "page.starred.title": "Preferiti",
     "page.categories.title": "Categorie",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "Azioni",
     "page.api_keys.never_used": "Mai usato",
     "page.new_api_key.title": "Nuova chiave API",
+    "alert.no_shared_entry": "Non ci sono voci condivise.",
     "alert.no_bookmark": "Nessun preferito disponibile.",
     "alert.no_category": "Nessuna categoria disponibile.",
     "alert.no_category_entry": "Questa categoria non contiene alcun articolo.",

+ 6 - 0
locale/translations/ja_JP.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "記事一覧",
     "menu.api_keys": "APIキー",
     "menu.create_api_key": "新しいAPIキーを作成する",
+    "menu.shared_entries": "共有エントリ",
     "search.label": "検索",
     "search.placeholder": "…を検索",
     "pagination.next": "次",
@@ -73,6 +74,10 @@
     "entry.comments.title": "コメントを見る",
     "entry.share.label": "共有",
     "entry.share.title": "この記事を共有する",
+    "entry.unshare.label": "共有解除",
+    "entry.shared_entry.title": "公開リンクを開く",
+    "entry.shared_entry.label": "共有する",
+    "page.shared_entries.title": "共有エントリ",
     "page.unread.title": "未読",
     "page.starred.title": "星付き",
     "page.categories.title": "カテゴリ",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "アクション",
     "page.api_keys.never_used": "使われたことがない",
     "page.new_api_key.title": "新しいAPIキー",
+    "alert.no_shared_entry": "共有エントリはありません。",
     "alert.no_bookmark": "現在星付きはありません。",
     "alert.no_category": "カテゴリが存在しません。",
     "alert.no_category_entry": "このカテゴリには記事がありません。",

+ 6 - 0
locale/translations/nl_NL.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Lidwoord",
     "menu.api_keys": "API-sleutels",
     "menu.create_api_key": "Maak een nieuwe API-sleutel",
+    "menu.shared_entries": "Gedeelde vermeldingen",
     "search.label": "Zoeken",
     "search.placeholder": "Zoeken...",
     "pagination.next": "Volgende",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Bekijk de reacties",
     "entry.share.label": "Deel",
     "entry.share.title": "Deel dit artikel",
+    "entry.unshare.label": "Delen ongedaan maken",
+    "entry.shared_entry.title": "Open de openbare link",
+    "entry.shared_entry.label": "Delen",
+    "page.shared_entries.title": "Gedeelde vermeldingen",
     "page.unread.title": "Ongelezen",
     "page.starred.title": "Favorieten",
     "page.categories.title": "Categorieën",
@@ -186,6 +191,7 @@
     "page.api_keys.table.actions": "Acties",
     "page.api_keys.never_used": "Nooit gebruikt",
     "page.new_api_key.title": "Nieuwe API-sleutel",
+    "alert.no_shared_entry": "Er is geen gedeelde toegang.",
     "alert.no_bookmark": "Er zijn op dit moment geen favorieten.",
     "alert.no_category": "Er zijn geen categorieën.",
     "alert.no_category_entry": "Deze categorie bevat geen feeds.",

+ 6 - 0
locale/translations/pl_PL.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "Artykuły",
     "menu.api_keys": "Klucze API",
     "menu.create_api_key": "Utwórz nowy klucz API",
+    "menu.shared_entries": "Udostępnione wpisy",
     "search.label": "Szukaj",
     "search.placeholder": "Szukaj...",
     "pagination.next": "Następny",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Zobacz komentarze",
     "entry.share.label": "Podzielić się",
     "entry.share.title": "Podzielić się ten artykuł",
+    "entry.unshare.label": "Unshare",
+    "entry.shared_entry.title": "Otwórz publiczny link",
+    "entry.shared_entry.label": "Udostępnianie",
+    "page.shared_entries.title": "Udostępnione wpisy",
     "page.unread.title": "Nieprzeczytane",
     "page.starred.title": "Oznaczone gwiazdką",
     "page.categories.title": "Kategorie",
@@ -188,6 +193,7 @@
     "page.api_keys.table.actions": "Działania",
     "page.api_keys.never_used": "Nigdy nie używany",
     "page.new_api_key.title": "Nowy klucz API",
+    "alert.no_shared_entry": "Brak wspólnego wpisu.",
     "alert.no_bookmark": "Obecnie nie ma żadnych zakładek.",
     "alert.no_category": "Nie ma żadnej kategorii!",
     "alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów",

+ 6 - 0
locale/translations/ru_RU.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "статьи",
     "menu.api_keys": "API-ключи",
     "menu.create_api_key": "Создать новый ключ API",
+    "menu.shared_entries": "Общие записи",
     "search.label": "Поиск",
     "search.placeholder": "Поиск…",
     "pagination.next": "Следующая",
@@ -73,6 +74,10 @@
     "entry.comments.title": "Показать комментарии",
     "entry.share.label": "поделиться",
     "entry.share.title": "поделиться эту статью",
+    "entry.unshare.label": "Удалить из открытого списка",
+    "entry.shared_entry.title": "Открыть публичную ссылку",
+    "entry.shared_entry.label": "обмен",
+    "page.shared_entries.title": "Общие записи",
     "page.unread.title": "Непрочитанное",
     "page.starred.title": "Избранное",
     "page.categories.title": "Категории",
@@ -188,6 +193,7 @@
     "page.api_keys.table.actions": "Действия",
     "page.api_keys.never_used": "Никогда не использовался",
     "page.new_api_key.title": "Новый ключ API",
+    "alert.no_shared_entry": "Там нет общей записи.",
     "alert.no_bookmark": "Нет закладок на данный момент.",
     "alert.no_category": "Категории отсутствуют.",
     "alert.no_category_entry": "В этой категории нет статей.",

+ 6 - 0
locale/translations/zh_CN.json

@@ -46,6 +46,7 @@
     "menu.feed_entries": "文章",
     "menu.api_keys": "API密钥",
     "menu.create_api_key": "创建一个新的API密钥",
+    "menu.shared_entries": "共享条目",
     "search.label": "搜索",
     "search.placeholder": "搜索…",
     "pagination.next": "下一页",
@@ -73,6 +74,10 @@
     "entry.comments.title": "查看评论",
     "entry.share.label": "分享",
     "entry.share.title": "分享这篇文章",
+    "entry.unshare.label": "取消分享",
+    "entry.shared_entry.title": "打开公共链接",
+    "entry.shared_entry.label": "分享分享",
+    "page.shared_entries.title": "共享条目",
     "page.unread.title": "未读",
     "page.starred.title": "星标",
     "page.categories.title": "分类",
@@ -184,6 +189,7 @@
     "page.api_keys.table.actions": "操作",
     "page.api_keys.never_used": "没用过",
     "page.new_api_key.title": "新的API密钥",
+    "alert.no_shared_entry": "没有共享条目。",
     "alert.no_bookmark": "目前没有书签",
     "alert.no_category": "目前没有分类",
     "alert.no_category_entry": "该分类下没有文章",

+ 29 - 19
storage/entry.go

@@ -272,7 +272,15 @@ func (s *Storage) ToggleBookmark(userID int64, entryID int64) error {
 
 // FlushHistory set all entries with the status "read" to "removed".
 func (s *Storage) FlushHistory(userID int64) error {
-	query := `UPDATE entries SET status=$1, changed_at=now() WHERE user_id=$2 AND status=$3 AND starred='f'`
+	query := `
+		UPDATE
+			entries
+		SET
+			status=$1,
+			changed_at=now()
+		WHERE
+			user_id=$2 AND status=$3 AND starred='f' AND share_code=''
+	`
 	_, err := s.db.Exec(query, model.EntryStatusRemoved, userID, model.EntryStatusRead)
 	if err != nil {
 		return fmt.Errorf(`store: unable to flush history: %v`, err)
@@ -353,34 +361,36 @@ func (s *Storage) EntryURLExists(feedID int64, entryURL string) bool {
 	return result
 }
 
-// GetEntryShareCode returns the share code of the provided entry.
+// EntryShareCode returns the share code of the provided entry.
 // It generates a new one if not already defined.
-func (s *Storage) GetEntryShareCode(userID int64, entryID int64) (shareCode string, err error) {
+func (s *Storage) EntryShareCode(userID int64, entryID int64) (shareCode string, err error) {
 	query := `SELECT share_code FROM entries WHERE user_id=$1 AND id=$2`
 	err = s.db.QueryRow(query, userID, entryID).Scan(&shareCode)
-
-	if err != nil || shareCode != "" {
+	if err != nil {
+		err = fmt.Errorf(`store: unable to get share code for entry #%d: %v`, entryID, err)
 		return
 	}
 
-	shareCode = crypto.GenerateRandomStringHex(20)
+	if shareCode == "" {
+		shareCode = crypto.GenerateRandomStringHex(20)
 
-	query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3`
-	result, err := s.db.Exec(query, shareCode, userID, entryID)
-	if err != nil {
-		err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err)
-		return
+		query = `UPDATE entries SET share_code = $1 WHERE user_id=$2 AND id=$3`
+		_, err = s.db.Exec(query, shareCode, userID, entryID)
+		if err != nil {
+			err = fmt.Errorf(`store: unable to set share code for entry #%d: %v`, entryID, err)
+			return
+		}
 	}
 
-	count, err := result.RowsAffected()
-	if err != nil {
-		err = fmt.Errorf(`store: unable to set share_code for entry #%d: %v`, entryID, err)
-		return
-	}
+	return
+}
 
-	if count == 0 {
-		err = errors.New(`store: nothing has been updated`)
+// UnshareEntry removes the share code for the given entry.
+func (s *Storage) UnshareEntry(userID int64, entryID int64) (err error) {
+	query := `UPDATE entries SET share_code='' WHERE user_id=$1 AND id=$2`
+	_, err = s.db.Exec(query, userID, entryID)
+	if err != nil {
+		err = fmt.Errorf(`store: unable to remove share code for entry #%d: %v`, entryID, err)
 	}
-
 	return
 }

+ 6 - 0
storage/entry_query_builder.go

@@ -135,6 +135,12 @@ func (e *EntryQueryBuilder) WithShareCode(shareCode string) *EntryQueryBuilder {
 	return e
 }
 
+// WithShareCodeNotEmpty adds a filter for non-empty share code.
+func (e *EntryQueryBuilder) WithShareCodeNotEmpty() *EntryQueryBuilder {
+	e.conditions = append(e.conditions, "e.share_code <> ''")
+	return e
+}
+
 // WithOrder set the sorting order.
 func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
 	e.order = order

+ 19 - 2
template/common.go

@@ -185,6 +185,16 @@ SOFTWARE.
     <line x1="10" y1="14" x2="20" y2="4" />
     <polyline points="15 4 20 4 20 9" />
 </svg>
+{{ end }}
+{{ define "icon_delete" }}
+<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+    <path stroke="none" d="M0 0h24v24H0z"/>
+    <line x1="4" y1="7" x2="20" y2="7" />
+    <line x1="10" y1="11" x2="10" y2="17" />
+    <line x1="14" y1="11" x2="14" y2="17" />
+    <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
+    <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
+</svg>
 {{ end }}`,
 	"item_meta": `{{ define "item_meta" }}
 <div class="item-meta">
@@ -197,6 +207,13 @@ SOFTWARE.
         </li>
     </ul>
     <ul class="item-meta-icons">
+        {{ if .entry.ShareCode }}
+            <li>
+                <a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
+                    title="{{ t "entry.shared_entry.title" }}"
+                    target="_blank">{{ template "icon_share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
+            </li>
+        {{ end }}
         {{ if .hasSaveEntry }}
             <li>
                 <a href="#"
@@ -455,8 +472,8 @@ var templateCommonMapChecksums = map[string]string{
 	"entry_pagination": "cdca9cf12586e41e5355190b06d9168f57f77b85924d1e63b13524bc15abcbf6",
 	"feed_list":        "46cbfc441404dc55c56a9fd7ddc43d98216762f2562f2a17e35eb508dc84246f",
 	"feed_menu":        "318d8662dda5ca9dfc75b909c8461e79c86fb5082df1428f67aaf856f19f4b50",
-	"icons":            "d9b4105a89364d913ad975b569286e6bd79f270da995856d21a10537e42b93f4",
-	"item_meta":        "be80837365e7dbcf28da44fbb2a31cccc29f29d83b88bf8da06609739a91a383",
+	"icons":            "f0d94c2cfa6655b44adaf97f0b95c52a9cff5c31f3a8829ad438e4db7114af7e",
+	"item_meta":        "a5b07cc6597e5c8f3ca849ee486acb3f16f062d8a1eaa47d2fb402ae6825b7ef",
 	"layout":           "a1f67b8908745ee4f9cee6f7bbbb0b242d4dcc101207ad4a9d67242b45683299",
 	"pagination":       "7b61288e86283c4cf0dc83bcbf8bf1c00c7cb29e60201c8c0b633b2450d2911f",
 	"settings_menu":    "e2b777630c0efdbc529800303c01d6744ed3af80ec505ac5a5b3f99c9b989156",

+ 10 - 0
template/html/common/icons.html

@@ -85,4 +85,14 @@ SOFTWARE.
     <line x1="10" y1="14" x2="20" y2="4" />
     <polyline points="15 4 20 4 20 9" />
 </svg>
+{{ end }}
+{{ define "icon_delete" }}
+<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+    <path stroke="none" d="M0 0h24v24H0z"/>
+    <line x1="4" y1="7" x2="20" y2="7" />
+    <line x1="10" y1="11" x2="10" y2="17" />
+    <line x1="14" y1="11" x2="14" y2="17" />
+    <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
+    <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
+</svg>
 {{ end }}

+ 7 - 0
template/html/common/item_meta.html

@@ -9,6 +9,13 @@
         </li>
     </ul>
     <ul class="item-meta-icons">
+        {{ if .entry.ShareCode }}
+            <li>
+                <a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
+                    title="{{ t "entry.shared_entry.title" }}"
+                    target="_blank">{{ template "icon_share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
+            </li>
+        {{ end }}
         {{ if .hasSaveEntry }}
             <li>
                 <a href="#"

+ 9 - 4
template/html/entry.html

@@ -45,10 +45,15 @@
                     </li>
                 {{ end }}
                 <li>
-                    <a href="{{ route "shareGenerate" "entryID" .entry.ID }}"
-                        title="{{ t "entry.share.title" }}"
-                        target="_blank"
-                        >{{ template "icon_share" }}<span class="icon-label">{{ t "entry.share.label" }}</span></a>
+                    {{ if .entry.ShareCode }}
+                        <a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
+                            title="{{ t "entry.shared_entry.title" }}"
+                            target="_blank">{{ template "icon_share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
+                    {{ else }}
+                        <a href="{{ route "shareEntry" "entryID" .entry.ID }}"
+                            title="{{ t "entry.share.title" }}"
+                            target="_blank">{{ template "icon_share" }}<span class="icon-label">{{ t "entry.share.label" }}</span></a>
+                    {{ end }}
                 </li>
                 <li>
                     <a href="#"

+ 9 - 0
template/html/history_entries.html

@@ -14,6 +14,15 @@
                 data-label-no="{{ t "confirm.no" }}"
                 data-label-loading="{{ t "confirm.loading" }}">{{ t "menu.flush_history" }}</a>
         </li>
+        <li>
+            <a href="{{ route "sharedEntries" }}">{{ t "menu.shared_entries" }}</a>
+        </li>
+    </ul>
+    {{ else }}
+    <ul>
+        <li>
+            <a href="{{ route "sharedEntries" }}">{{ t "menu.shared_entries" }}</a>
+        </li>
     </ul>
     {{ end }}
 </section>

+ 71 - 0
template/html/shared_entries.html

@@ -0,0 +1,71 @@
+{{ define "title"}}{{ t "page.shared_entries.title" }} ({{ .total }}){{ end }}
+
+{{ define "content"}}
+<section class="page-header">
+    <h1>{{ t "page.shared_entries.title" }} ({{ .total }})</h1>
+    {{ if .entries }}
+    <ul>
+        <li>
+            <a href="#"
+                data-confirm="true"
+                data-url="{{ route "flushHistory" }}"
+                data-label-question="{{ t "confirm.question" }}"
+                data-label-yes="{{ t "confirm.yes" }}"
+                data-label-no="{{ t "confirm.no" }}"
+                data-label-loading="{{ t "confirm.loading" }}">{{ t "menu.flush_history" }}</a>
+        </li>
+        <li>
+            <a href="{{ route "sharedEntries" }}">{{ t "menu.shared_entries" }}</a>
+        </li>
+    </ul>
+    {{ end }}
+</section>
+
+{{ if not .entries }}
+    <p class="alert alert-info">{{ t "alert.no_shared_entry" }}</p>
+{{ else }}
+    <div class="items">
+        {{ range .entries }}
+        <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
+            <div class="item-header">
+                <span class="item-title">
+                    {{ if ne .Feed.Icon.IconID 0 }}
+                        <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
+                    {{ end }}
+                    <a href="{{ route "readEntry" "entryID" .ID }}">{{ .Title }}</a>
+                    {{ if .ShareCode }}
+                        <a href="{{ route "sharedEntry" "shareCode" .ShareCode }}"
+                            title="{{ t "entry.shared_entry.title" }}"
+                            target="_blank">{{ template "icon_share" }}</a>
+                    {{ end }}
+                </span>
+                <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
+            </div>
+            <div class="item-meta">
+                <ul class="item-meta-info">
+                    <li>
+                        <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.SiteURL }}">{{ truncate .Feed.Title 35 }}</a>
+                    </li>
+                    <li>
+                        <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed $.user.Timezone .Date }}</time>
+                    </li>
+                </ul>
+                <ul class="item-meta-icons">
+                    <li>
+                        {{ template "icon_delete" }}
+                        <a href="#"
+                            data-confirm="true"
+                            data-url="{{ route "unshareEntry" "entryID" .ID }}"
+                            data-label-question="{{ t "confirm.question" }}"
+                            data-label-yes="{{ t "confirm.yes" }}"
+                            data-label-no="{{ t "confirm.no" }}"
+                            data-label-loading="{{ t "confirm.loading" }}">{{ t "entry.unshare.label" }}</a>
+                    </li>
+                </ul>
+            </div>
+        </article>
+        {{ end }}
+    </div>
+{{ end }}
+
+{{ end }}

+ 93 - 6
template/views.go

@@ -692,10 +692,15 @@ var templateViewsMap = map[string]string{
                     </li>
                 {{ end }}
                 <li>
-                    <a href="{{ route "shareGenerate" "entryID" .entry.ID }}"
-                        title="{{ t "entry.share.title" }}"
-                        target="_blank"
-                        >{{ template "icon_share" }}<span class="icon-label">{{ t "entry.share.label" }}</span></a>
+                    {{ if .entry.ShareCode }}
+                        <a href="{{ route "sharedEntry" "shareCode" .entry.ShareCode }}"
+                            title="{{ t "entry.shared_entry.title" }}"
+                            target="_blank">{{ template "icon_share" }}<span class="icon-label">{{ t "entry.shared_entry.label" }}</span></a>
+                    {{ else }}
+                        <a href="{{ route "shareEntry" "entryID" .entry.ID }}"
+                            title="{{ t "entry.share.title" }}"
+                            target="_blank">{{ template "icon_share" }}<span class="icon-label">{{ t "entry.share.label" }}</span></a>
+                    {{ end }}
                 </li>
                 <li>
                     <a href="#"
@@ -941,6 +946,15 @@ var templateViewsMap = map[string]string{
                 data-label-no="{{ t "confirm.no" }}"
                 data-label-loading="{{ t "confirm.loading" }}">{{ t "menu.flush_history" }}</a>
         </li>
+        <li>
+            <a href="{{ route "sharedEntries" }}">{{ t "menu.shared_entries" }}</a>
+        </li>
+    </ul>
+    {{ else }}
+    <ul>
+        <li>
+            <a href="{{ route "sharedEntries" }}">{{ t "menu.shared_entries" }}</a>
+        </li>
     </ul>
     {{ end }}
 </section>
@@ -1319,6 +1333,78 @@ var templateViewsMap = map[string]string{
 </div>
 {{ end }}
 
+{{ end }}
+`,
+	"shared_entries": `{{ define "title"}}{{ t "page.shared_entries.title" }} ({{ .total }}){{ end }}
+
+{{ define "content"}}
+<section class="page-header">
+    <h1>{{ t "page.shared_entries.title" }} ({{ .total }})</h1>
+    {{ if .entries }}
+    <ul>
+        <li>
+            <a href="#"
+                data-confirm="true"
+                data-url="{{ route "flushHistory" }}"
+                data-label-question="{{ t "confirm.question" }}"
+                data-label-yes="{{ t "confirm.yes" }}"
+                data-label-no="{{ t "confirm.no" }}"
+                data-label-loading="{{ t "confirm.loading" }}">{{ t "menu.flush_history" }}</a>
+        </li>
+        <li>
+            <a href="{{ route "sharedEntries" }}">{{ t "menu.shared_entries" }}</a>
+        </li>
+    </ul>
+    {{ end }}
+</section>
+
+{{ if not .entries }}
+    <p class="alert alert-info">{{ t "alert.no_shared_entry" }}</p>
+{{ else }}
+    <div class="items">
+        {{ range .entries }}
+        <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
+            <div class="item-header">
+                <span class="item-title">
+                    {{ if ne .Feed.Icon.IconID 0 }}
+                        <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="{{ .Feed.Title }}">
+                    {{ end }}
+                    <a href="{{ route "readEntry" "entryID" .ID }}">{{ .Title }}</a>
+                    {{ if .ShareCode }}
+                        <a href="{{ route "sharedEntry" "shareCode" .ShareCode }}"
+                            title="{{ t "entry.shared_entry.title" }}"
+                            target="_blank">{{ template "icon_share" }}</a>
+                    {{ end }}
+                </span>
+                <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
+            </div>
+            <div class="item-meta">
+                <ul class="item-meta-info">
+                    <li>
+                        <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.SiteURL }}">{{ truncate .Feed.Title 35 }}</a>
+                    </li>
+                    <li>
+                        <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed $.user.Timezone .Date }}</time>
+                    </li>
+                </ul>
+                <ul class="item-meta-icons">
+                    <li>
+                        {{ template "icon_delete" }}
+                        <a href="#"
+                            data-confirm="true"
+                            data-url="{{ route "unshareEntry" "entryID" .ID }}"
+                            data-label-question="{{ t "confirm.question" }}"
+                            data-label-yes="{{ t "confirm.yes" }}"
+                            data-label-no="{{ t "confirm.no" }}"
+                            data-label-loading="{{ t "confirm.loading" }}">{{ t "entry.unshare.label" }}</a>
+                    </li>
+                </ul>
+            </div>
+        </article>
+        {{ end }}
+    </div>
+{{ end }}
+
 {{ end }}
 `,
 	"unread_entries": `{{ define "title"}}{{ t "page.unread.title" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }}
@@ -1457,16 +1543,17 @@ var templateViewsMapChecksums = map[string]string{
 	"edit_category":       "b1c0b38f1b714c5d884edcd61e5b5295a5f1c8b71c469b35391e4dcc97cc6d36",
 	"edit_feed":           "cc0b5dbb73f81398410958b41771ed38246bc7ae4bd548228f0d48c49a598c2a",
 	"edit_user":           "c692db9de1a084c57b93e95a14b041d39bf489846cbb91fc982a62b72b77062a",
-	"entry":               "03814d36909f3af1a9164c407d8733bcf1f15c4aee516186d07561a81fa42eb3",
+	"entry":               "0e405b2370aaefaaa122c955f58fd395b9f19612e4cc7b209fc79e7b00c5561b",
 	"feed_entries":        "9c70b82f55e4b311eff20be1641733612e3c1b406ce8010861e4c417d97b6dcc",
 	"feeds":               "ec7d3fa96735bd8422ba69ef0927dcccddc1cc51327e0271f0312d3f881c64fd",
-	"history_entries":     "87e17d39de70eb3fdbc4000326283be610928758eae7924e4b08dcb446f3b6a9",
+	"history_entries":     "93c0c4cc541eec7f07f5c2634f250ea82ac64024939179276b6f636b72c189bf",
 	"import":              "1b59b3bd55c59fcbc6fbb346b414dcdd26d1b4e0c307e437bb58b3f92ef01ad1",
 	"integrations":        "30329452743b35c668278f519245fd9be05c1726856e0384ba542f7c307f2788",
 	"login":               "79ff2ca488c0a19b37c8fa227a21f73e94472eb357a51a077197c852f7713f11",
 	"search_entries":      "274950d03298c24f3942e209c0faed580a6d57be9cf76a6c236175a7e766ac6a",
 	"sessions":            "5d5c677bddbd027e0b0c9f7a0dd95b66d9d95b4e130959f31fb955b926c2201c",
 	"settings":            "d949ecdd28a33eadafaa3a727e548b3466c5aa44b0a4bbf86cc49784704ff7f6",
+	"shared_entries":      "19caea053664220bb9519df295eb2a17cf5836eaa9104b7ee24c60b88bb524e9",
 	"unread_entries":      "e38f7ffce17dfad3151b08cd33771a2cefe8ca9db42df04fc98bd1d675dd6075",
 	"users":               "d7ff52efc582bbad10504f4a04fa3adcc12d15890e45dff51cac281e0c446e45",
 }

+ 1 - 1
ui/middleware.go

@@ -135,7 +135,7 @@ func (m *middleware) isPublicRoute(r *http.Request) bool {
 		"favicon",
 		"webManifest",
 		"robots",
-		"share",
+		"sharedEntry",
 		"healthcheck":
 		return true
 	default:

+ 15 - 5
ui/share.go

@@ -1,4 +1,4 @@
-// Copyright 2017 Frédéric Guillot. All rights reserved.
+// Copyright 2020 Frédéric Guillot. All rights reserved.
 // Use of this source code is governed by the Apache 2.0
 // license that can be found in the LICENSE file.
 
@@ -17,18 +17,28 @@ import (
 	"miniflux.app/ui/view"
 )
 
-func (h *handler) shareGenerate(w http.ResponseWriter, r *http.Request) {
+func (h *handler) createSharedEntry(w http.ResponseWriter, r *http.Request) {
 	entryID := request.RouteInt64Param(r, "entryID")
-	shareCode, err := h.store.GetEntryShareCode(request.UserID(r), entryID)
+	shareCode, err := h.store.EntryShareCode(request.UserID(r), entryID)
 	if err != nil {
 		html.ServerError(w, r, err)
 		return
 	}
 
-	html.Redirect(w, r, route.Path(h.router, "share", "shareCode", shareCode))
+	html.Redirect(w, r, route.Path(h.router, "sharedEntry", "shareCode", shareCode))
 }
 
-func (h *handler) sharePage(w http.ResponseWriter, r *http.Request) {
+func (h *handler) unshareEntry(w http.ResponseWriter, r *http.Request) {
+	entryID := request.RouteInt64Param(r, "entryID")
+	if err := h.store.UnshareEntry(request.UserID(r), entryID); err != nil {
+		html.ServerError(w, r, err)
+		return
+	}
+
+	html.Redirect(w, r, route.Path(h.router, "sharedEntries"))
+}
+
+func (h *handler) sharedEntry(w http.ResponseWriter, r *http.Request) {
 	shareCode := request.RouteStringParam(r, "shareCode")
 	if shareCode == "" {
 		html.NotFound(w, r)

+ 52 - 0
ui/shared_entries.go

@@ -0,0 +1,52 @@
+// Copyright 2020 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui // import "miniflux.app/ui"
+
+import (
+	"net/http"
+
+	"miniflux.app/http/request"
+	"miniflux.app/http/response/html"
+	"miniflux.app/model"
+	"miniflux.app/ui/session"
+	"miniflux.app/ui/view"
+)
+
+func (h *handler) sharedEntries(w http.ResponseWriter, r *http.Request) {
+	user, err := h.store.UserByID(request.UserID(r))
+	if err != nil {
+		html.ServerError(w, r, err)
+		return
+	}
+
+	builder := h.store.NewEntryQueryBuilder(user.ID)
+	builder.WithShareCodeNotEmpty()
+	builder.WithOrder(model.DefaultSortingOrder)
+	builder.WithDirection(user.EntryDirection)
+
+	entries, err := builder.GetEntries()
+	if err != nil {
+		html.ServerError(w, r, err)
+		return
+	}
+
+	count, err := builder.CountEntries()
+	if err != nil {
+		html.ServerError(w, r, err)
+		return
+	}
+
+	sess := session.New(h.store, request.SessionID(r))
+	view := view.New(h.tpl, r, sess)
+	view.Set("entries", entries)
+	view.Set("total", count)
+	view.Set("menu", "history")
+	view.Set("user", user)
+	view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+	view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
+	view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
+
+	html.OK(w, r, view.Render("shared_entries"))
+}

+ 4 - 2
ui/ui.go

@@ -89,8 +89,10 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
 	uiRouter.HandleFunc("/entry/bookmark/{entryID}", handler.toggleBookmark).Name("toggleBookmark").Methods("POST")
 
 	// Share pages.
-	uiRouter.HandleFunc("/entry/share/{entryID}", handler.shareGenerate).Name("shareGenerate").Methods("GET")
-	uiRouter.HandleFunc("/share/{shareCode}", handler.sharePage).Name("share").Methods("GET")
+	uiRouter.HandleFunc("/entry/share/{entryID}", handler.createSharedEntry).Name("shareEntry").Methods("GET")
+	uiRouter.HandleFunc("/entry/unshare/{entryID}", handler.unshareEntry).Name("unshareEntry").Methods("POST")
+	uiRouter.HandleFunc("/share/{shareCode}", handler.sharedEntry).Name("sharedEntry").Methods("GET")
+	uiRouter.HandleFunc("/shares", handler.sharedEntries).Name("sharedEntries").Methods("GET")
 
 	// User pages.
 	uiRouter.HandleFunc("/users", handler.showUsersPage).Name("users").Methods("GET")