MediaItem.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <script lang="ts" setup>
  2. import { defineAsyncComponent, ref } from "vue";
  3. import { storeToRefs } from "pinia";
  4. import dayjs from "@/dayjs";
  5. import { useModalsStore } from "@/stores/modals";
  6. import { useUserAuthStore } from "@/stores/userAuth";
  7. import DropdownListItem from "@/pages/NewStation/Components/DropdownListItem.vue";
  8. const AddToPlaylistDropdown = defineAsyncComponent(
  9. () => import("@/pages/NewStation/Components/AddToPlaylistDropdown.vue")
  10. );
  11. const Button = defineAsyncComponent(
  12. () => import("@/pages/NewStation/Components/Button.vue")
  13. );
  14. const DropdownList = defineAsyncComponent(
  15. () => import("@/pages/NewStation/Components/DropdownList.vue")
  16. );
  17. const SongThumbnail = defineAsyncComponent(
  18. () => import("@/components/SongThumbnail.vue")
  19. );
  20. const UserLink = defineAsyncComponent(
  21. () => import("@/components/UserLink.vue")
  22. );
  23. // TODO: Experimental: soundcloud
  24. const props = withDefaults(
  25. defineProps<{
  26. media: any;
  27. showRequested?: boolean;
  28. }>(),
  29. {
  30. showRequested: false
  31. }
  32. );
  33. const { openModal } = useModalsStore();
  34. const userAuthStore = useUserAuthStore();
  35. const { hasPermission } = userAuthStore;
  36. const { loggedIn } = storeToRefs(userAuthStore);
  37. const actions = ref();
  38. const expandActions = () => {
  39. actions.value.expand();
  40. };
  41. const collapseActions = () => {
  42. actions.value.collapse();
  43. };
  44. const edit = () => {
  45. collapseActions();
  46. openModal({
  47. modal: "editSong",
  48. props: { song: props.media }
  49. });
  50. };
  51. const report = () => {
  52. collapseActions();
  53. openModal({
  54. modal: "report",
  55. props: { song: props.media }
  56. });
  57. };
  58. const view = () => {
  59. collapseActions();
  60. openModal({
  61. modal: "viewMedia",
  62. props: { mediaSource: props.media.mediaSource }
  63. });
  64. };
  65. defineExpose({
  66. expandActions,
  67. collapseActions
  68. });
  69. </script>
  70. <template>
  71. <div class="media-item">
  72. <SongThumbnail :song="media" />
  73. <div class="media-item__content">
  74. <p class="media-item__title" :title="media.title">
  75. {{ media.title }}
  76. <i
  77. v-if="media.verified"
  78. class="material-icons media-item__verified"
  79. title="Verified media"
  80. >
  81. check_circle
  82. </i>
  83. </p>
  84. <p class="media-item__artists" :title="media.artists?.join(', ')">
  85. {{ media.artists?.join(", ") }}
  86. </p>
  87. <p class="media-item__details">
  88. <strong>
  89. {{ dayjs.duration(media.duration, "s").formatDuration() }}
  90. </strong>
  91. <template
  92. v-if="
  93. showRequested &&
  94. (media.requestedBy || media.requestedType)
  95. "
  96. >
  97. <span class="media-item__divider">&middot;</span>
  98. <UserLink
  99. v-if="media.requestedBy"
  100. :key="media.mediaSource"
  101. :user-id="media.requestedBy"
  102. />
  103. <span v-else>Station</span>
  104. <span>
  105. <template v-if="media.requestedType === 'autofill'">
  106. requested automatically
  107. </template>
  108. <template
  109. v-else-if="media.requestedType === 'autorequest'"
  110. >
  111. autorequested
  112. </template>
  113. <template v-else>requested</template>
  114. </span>
  115. <span :title="dayjs(media.requestedAt).format()">{{
  116. dayjs(media.requestedAt).fromNow()
  117. }}</span>
  118. </template>
  119. </p>
  120. </div>
  121. <slot name="featuredAction" />
  122. <DropdownList ref="actions">
  123. <Button icon="more_horiz" square inverse title="Actions" />
  124. <template #options>
  125. <slot name="actions" />
  126. <template v-if="loggedIn">
  127. <DropdownListItem>
  128. <AddToPlaylistDropdown
  129. :media-source="media.mediaSource"
  130. >
  131. <button class="dropdown-list-item__action">
  132. <span
  133. class="material-icons dropdown-list-item__icon"
  134. aria-hidden="true"
  135. >
  136. playlist_add
  137. </span>
  138. Add to playlist
  139. </button>
  140. </AddToPlaylistDropdown>
  141. </DropdownListItem>
  142. <DropdownListItem
  143. icon="play_arrow"
  144. label="View media"
  145. @click="view"
  146. />
  147. <DropdownListItem
  148. icon="flag"
  149. label="Report song"
  150. @click="report"
  151. />
  152. <DropdownListItem
  153. v-if="media._id && hasPermission('songs.update')"
  154. icon="edit"
  155. label="Edit song"
  156. @click="edit"
  157. />
  158. </template>
  159. <DropdownListItem
  160. v-else
  161. icon="play_arrow"
  162. label="View on YouTube"
  163. :href="`https://www.youtube.com/watch?v=${media.mediaSource.split(':')[1]}`"
  164. target="_blank"
  165. />
  166. </template>
  167. </DropdownList>
  168. </div>
  169. </template>
  170. <style lang="less" scoped>
  171. .media-item {
  172. display: flex;
  173. align-items: center;
  174. flex-shrink: 0;
  175. height: 48px;
  176. background-color: var(--white);
  177. border-radius: 5px;
  178. border: solid 1px var(--light-grey-1);
  179. gap: 5px;
  180. overflow: hidden;
  181. :deep(.thumbnail) {
  182. height: 48px;
  183. min-width: 48px;
  184. flex-shrink: 0;
  185. margin: 0;
  186. }
  187. &__content {
  188. display: flex;
  189. flex-direction: column;
  190. flex-grow: 1;
  191. min-width: 0;
  192. justify-content: center;
  193. }
  194. &__title {
  195. display: inline-flex;
  196. align-items: center;
  197. gap: 2px;
  198. font-size: 12px !important;
  199. line-height: 16px;
  200. overflow: hidden;
  201. text-overflow: ellipsis;
  202. white-space: nowrap;
  203. }
  204. &__verified {
  205. color: var(--primary-color);
  206. font-size: 12px !important;
  207. }
  208. &__artists {
  209. display: inline-flex;
  210. align-items: center;
  211. font-size: 11px !important;
  212. font-weight: 500 !important;
  213. line-height: 12px;
  214. color: var(--dark-grey-1);
  215. overflow: hidden;
  216. text-overflow: ellipsis;
  217. white-space: nowrap;
  218. }
  219. &__details {
  220. display: inline-flex;
  221. align-items: center;
  222. gap: 2px;
  223. font-size: 10px !important;
  224. font-weight: 500 !important;
  225. line-height: 12px;
  226. color: var(--dark-grey-1);
  227. overflow: hidden;
  228. text-overflow: ellipsis;
  229. white-space: nowrap;
  230. }
  231. &__divider {
  232. font-size: 25px;
  233. }
  234. & > :deep(.dropdown-list__reference) {
  235. display: flex;
  236. flex-direction: column;
  237. justify-content: center;
  238. padding-right: 5px;
  239. }
  240. :deep(.dropdown-list-item > .dropdown-list__reference) {
  241. display: flex;
  242. flex-grow: 1;
  243. }
  244. }
  245. </style>