From 6c05fcb6992530f5e98f300b68b3c0b070334688 Mon Sep 17 00:00:00 2001
From: Deluan <deluan@navidrome.org>
Date: Mon, 23 Jan 2023 20:44:21 -0500
Subject: [PATCH] Create contents label for group of shared mediafiles

---
 core/share.go              | 55 +++++++++++++++++++++++++++-----------
 server/subsonic/sharing.go |  3 +++
 ui/src/share/ShareList.js  | 43 +++++++++++++++++++++++++++--
 3 files changed, 84 insertions(+), 17 deletions(-)

diff --git a/core/share.go b/core/share.go
index 83d9f7118..c6b97b245 100644
--- a/core/share.go
+++ b/core/share.go
@@ -102,16 +102,20 @@ func (r *shareRepositoryWrapper) Save(entity interface{}) (string, error) {
 	switch v.(type) {
 	case *model.Album:
 		s.ResourceType = "album"
-		s.Contents = r.shareContentsFromAlbums(s.ID, s.ResourceIDs)
+		s.Contents = r.contentsLabelFromAlbums(s.ID, s.ResourceIDs)
 	case *model.Playlist:
 		s.ResourceType = "playlist"
-		s.Contents = r.shareContentsFromPlaylist(s.ID, s.ResourceIDs)
+		s.Contents = r.contentsLabelFromPlaylist(s.ID, s.ResourceIDs)
 	case *model.MediaFile:
 		s.ResourceType = "media_file"
+		s.Contents = r.contentsLabelFromMediaFiles(s.ID, s.ResourceIDs)
 	default:
 		log.Error(r.ctx, "Invalid Resource ID", "id", firstId)
 		return "", model.ErrNotFound
 	}
+	if len(s.Contents) > 30 {
+		s.Contents = s.Contents[:26] + "..."
+	}
 
 	id, err = r.Persistable.Save(s)
 	return id, err
@@ -127,28 +131,49 @@ func (r *shareRepositoryWrapper) Update(id string, entity interface{}, _ ...stri
 	return r.Persistable.Update(id, entity, cols...)
 }
 
-func (r *shareRepositoryWrapper) shareContentsFromAlbums(shareID string, ids string) string {
-	all, err := r.ds.Album(r.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"id": ids}})
+func (r *shareRepositoryWrapper) contentsLabelFromAlbums(shareID string, ids string) string {
+	idList := strings.Split(ids, ",")
+	all, err := r.ds.Album(r.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"id": idList}})
 	if err != nil {
 		log.Error(r.ctx, "Error retrieving album names for share", "share", shareID, err)
 		return ""
 	}
 	names := slice.Map(all, func(a model.Album) string { return a.Name })
-	content := strings.Join(names, ", ")
-	if len(content) > 30 {
-		content = content[:26] + "..."
-	}
-	return content
+	return strings.Join(names, ", ")
 }
-func (r *shareRepositoryWrapper) shareContentsFromPlaylist(shareID string, id string) string {
+func (r *shareRepositoryWrapper) contentsLabelFromPlaylist(shareID string, id string) string {
 	pls, err := r.ds.Playlist(r.ctx).Get(id)
 	if err != nil {
 		log.Error(r.ctx, "Error retrieving album names for share", "share", shareID, err)
 		return ""
 	}
-	content := pls.Name
-	if len(content) > 30 {
-		content = content[:26] + "..."
-	}
-	return content
+	return pls.Name
+}
+
+func (r *shareRepositoryWrapper) contentsLabelFromMediaFiles(shareID string, ids string) string {
+	idList := strings.Split(ids, ",")
+	mfs, err := r.ds.MediaFile(r.ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"id": idList}})
+	if err != nil {
+		log.Error(r.ctx, "Error retrieving media files for share", "share", shareID, err)
+		return ""
+	}
+
+	albums := slice.Group(mfs, func(mf model.MediaFile) string {
+		return mf.Album
+	})
+	if len(albums) == 1 {
+		for name := range albums {
+			return name
+		}
+	}
+	artists := slice.Group(mfs, func(mf model.MediaFile) string {
+		return mf.AlbumArtist
+	})
+	if len(artists) == 1 {
+		for name := range artists {
+			return name
+		}
+	}
+
+	return mfs[0].Title
 }
diff --git a/server/subsonic/sharing.go b/server/subsonic/sharing.go
index 4f19dec90..d52549b20 100644
--- a/server/subsonic/sharing.go
+++ b/server/subsonic/sharing.go
@@ -38,6 +38,9 @@ func (api *Router) buildShare(r *http.Request, share model.Share) responses.Shar
 		LastVisited: share.LastVisitedAt,
 		VisitCount:  share.VisitCount,
 	}
+	if resp.Description == "" {
+		resp.Description = share.Contents
+	}
 	if len(share.Albums) > 0 {
 		resp.Entry = childrenFromAlbums(r.Context(), share.Albums)
 	} else {
diff --git a/ui/src/share/ShareList.js b/ui/src/share/ShareList.js
index ab90bfa69..35272e29f 100644
--- a/ui/src/share/ShareList.js
+++ b/ui/src/share/ShareList.js
@@ -5,12 +5,14 @@ import {
   NumberField,
   SimpleList,
   TextField,
+  useNotify,
   useTranslate,
 } from 'react-admin'
 import React from 'react'
+import { IconButton, Link, useMediaQuery } from '@material-ui/core'
+import ShareIcon from '@material-ui/icons/Share'
 import { DateField, QualityInfo } from '../common'
 import { shareUrl } from '../utils'
-import { Link, useMediaQuery } from '@material-ui/core'
 import config from '../config'
 
 export const FormatInfo = ({ record, size }) => {
@@ -23,6 +25,33 @@ export const FormatInfo = ({ record, size }) => {
 const ShareList = (props) => {
   const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
   const translate = useTranslate()
+  const notify = useNotify()
+
+  const handleShare = (r) => (e) => {
+    const url = shareUrl(r?.id)
+    navigator.clipboard
+      .writeText(url)
+      .then(() => {
+        notify(translate('message.shareSuccess', { url }), {
+          type: 'info',
+          multiLine: true,
+          duration: 0,
+        })
+      })
+      .catch((err) => {
+        notify(
+          translate('message.shareFailure', { url }) + ': ' + err.message,
+          {
+            type: 'warning',
+            multiLine: true,
+            duration: 0,
+          }
+        )
+      })
+    e.preventDefault()
+    e.stopPropagation()
+  }
+
   return (
     <List
       {...props}
@@ -31,13 +60,23 @@ const ShareList = (props) => {
     >
       {isXsmall ? (
         <SimpleList
+          leftIcon={(r) => (
+            <IconButton onClick={handleShare(r)}>
+              <ShareIcon />
+            </IconButton>
+          )}
           primaryText={(r) => r.description || r.contents || r.id}
           secondaryText={(r) => (
             <>
               {translate('resources.share.fields.expiresAt')}:{' '}
-              <DateField record={r} source={'expiresAt'} showTime />
+              <DateField record={r} source={'expiresAt'} />
             </>
           )}
+          tertiaryText={(r) =>
+            `${translate('resources.share.fields.visitCount')}: ${
+              r.visitCount || '0'
+            }`
+          }
         />
       ) : (
         <Datagrid rowClick="edit">