SongThumbnail.vue 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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 onLoadError = () => {
  43. // Error codes
  44. // -1 - Error occured, fallback disabled
  45. // 0 - No errors
  46. // 1 - Error occured with thumbnail, fallback to YouTube
  47. // 2 - Error occured with thumbnail, fallback to notes
  48. if (!props.fallback) loadError.value = -1;
  49. else if (
  50. loadError.value === 0 &&
  51. !(isNotesThumbnail.value || isYoutubeThumbnail.value) &&
  52. ((props.song.mediaSource &&
  53. props.song.mediaSource.startsWith("youtube:")) ||
  54. props.song.youtubeId)
  55. )
  56. loadError.value = 1;
  57. else loadError.value = 2;
  58. emit("loadError", loadError.value);
  59. };
  60. const onLoad = () => {
  61. loaded.value = true;
  62. };
  63. watch(
  64. () => props.song,
  65. () => {
  66. loadError.value = 0;
  67. emit("loadError", loadError.value);
  68. }
  69. );
  70. </script>
  71. <template>
  72. <div class="thumbnail">
  73. <slot name="icon" />
  74. <div
  75. v-if="
  76. loaded &&
  77. !isNotesThumbnail &&
  78. thumbnail !== '/assets/notes-transparent.png'
  79. "
  80. class="thumbnail-bg"
  81. :style="{
  82. 'background-image': `url('${thumbnail}')`
  83. }"
  84. ></div>
  85. <img
  86. loading="lazy"
  87. :src="thumbnail"
  88. @error="onLoadError"
  89. @load="onLoad"
  90. />
  91. </div>
  92. </template>
  93. <style lang="less">
  94. .thumbnail {
  95. min-width: 130px;
  96. height: 130px;
  97. position: relative;
  98. margin-top: -15px;
  99. margin-bottom: -15px;
  100. margin-left: -10px;
  101. overflow: hidden;
  102. img {
  103. width: 100%;
  104. margin-top: auto;
  105. margin-bottom: auto;
  106. z-index: 1;
  107. position: absolute;
  108. top: 0;
  109. bottom: 0;
  110. left: 0;
  111. right: 0;
  112. }
  113. .thumbnail-bg {
  114. height: 100%;
  115. width: 100%;
  116. display: block;
  117. position: absolute;
  118. top: 0;
  119. filter: blur(1px);
  120. background: url("/assets/notes-transparent.png") no-repeat center center;
  121. background-size: cover;
  122. }
  123. }
  124. </style>