SongThumbnail.vue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. <script setup lang="ts">
  2. import { ref, computed, watch } from "vue";
  3. const props = defineProps({
  4. song: { type: Object, default: () => {} },
  5. fallback: { type: Boolean, default: true }
  6. });
  7. const emit = defineEmits(["loadError"]);
  8. const loadError = ref(0);
  9. const loaded = ref(false);
  10. const isYoutubeThumbnail = computed(
  11. () =>
  12. props.song.thumbnail &&
  13. (props.song.thumbnail.lastIndexOf("i.ytimg.com") !== -1 ||
  14. props.song.thumbnail.lastIndexOf("img.youtube.com") !== -1)
  15. );
  16. const isNotesThumbnail = computed(
  17. () =>
  18. !props.song.thumbnail ||
  19. (props.song.thumbnail &&
  20. (props.song.thumbnail.lastIndexOf("notes-transparent") !== -1 ||
  21. props.song.thumbnail.lastIndexOf("/assets/notes.png") !== -1 ||
  22. props.song.thumbnail === "empty"))
  23. );
  24. const thumbnail = computed(() => {
  25. if (
  26. (loadError.value === 0 &&
  27. !(isNotesThumbnail.value || isYoutubeThumbnail.value)) ||
  28. !props.fallback ||
  29. loadError.value === -1
  30. )
  31. return props.song.thumbnail;
  32. const { mediaSource, youtubeId } = props.song;
  33. if (
  34. loadError.value < 2 &&
  35. ((mediaSource && mediaSource.startsWith("youtube:")) || youtubeId)
  36. )
  37. return `https://img.youtube.com/vi/${
  38. mediaSource ? mediaSource.split(":")[1] : youtubeId
  39. }/mqdefault.jpg`;
  40. return "/assets/notes-transparent.png";
  41. });
  42. const backgroundVisible = computed(
  43. () =>
  44. loaded.value &&
  45. thumbnail.value !== "/assets/notes-transparent.png" &&
  46. !((!props.fallback || loadError.value === -1) && isNotesThumbnail.value)
  47. );
  48. const onLoadError = () => {
  49. // Error codes
  50. // -1 - Error occured, fallback disabled
  51. // 0 - No errors
  52. // 1 - Error occured with thumbnail, fallback to YouTube
  53. // 2 - Error occured with thumbnail, fallback to notes
  54. if (!props.fallback) loadError.value = -1;
  55. else if (
  56. loadError.value === 0 &&
  57. !(isNotesThumbnail.value || isYoutubeThumbnail.value) &&
  58. ((props.song.mediaSource &&
  59. props.song.mediaSource.startsWith("youtube:")) ||
  60. props.song.youtubeId)
  61. )
  62. loadError.value = 1;
  63. else loadError.value = 2;
  64. emit("loadError", loadError.value);
  65. };
  66. const onLoad = () => {
  67. loaded.value = true;
  68. };
  69. watch(
  70. () => props.song,
  71. () => {
  72. loadError.value = 0;
  73. emit("loadError", loadError.value);
  74. }
  75. );
  76. </script>
  77. <template>
  78. <div class="thumbnail">
  79. <slot name="icon" />
  80. <div
  81. v-if="backgroundVisible"
  82. class="thumbnail-bg"
  83. :style="{
  84. 'background-image': `url('${thumbnail}')`
  85. }"
  86. ></div>
  87. <img
  88. loading="lazy"
  89. :src="thumbnail"
  90. @error="onLoadError"
  91. @load="onLoad"
  92. />
  93. </div>
  94. </template>
  95. <style lang="less">
  96. .thumbnail {
  97. min-width: 130px;
  98. height: 130px;
  99. position: relative;
  100. margin-top: -15px;
  101. margin-bottom: -15px;
  102. margin-left: -10px;
  103. overflow: hidden;
  104. img {
  105. width: 100%;
  106. margin-top: auto;
  107. margin-bottom: auto;
  108. z-index: 1;
  109. position: absolute;
  110. top: 0;
  111. bottom: 0;
  112. left: 0;
  113. right: 0;
  114. }
  115. .thumbnail-bg {
  116. height: 100%;
  117. width: 100%;
  118. display: block;
  119. position: absolute;
  120. top: 0;
  121. filter: blur(1px);
  122. background: url("/assets/notes-transparent.png") no-repeat center center;
  123. background-size: cover;
  124. }
  125. }
  126. </style>