|
1 | 1 | <template>
|
2 |
| - <div class="group rounded-lg relative overflow-hidden shadow-md w-full text-contrast"> |
| 2 | + <div |
| 3 | +class="group rounded-lg relative overflow-hidden shadow-md w-full text-contrast" @mouseenter="isHovered = true" |
| 4 | + @mouseleave="isHovered = false"> |
3 | 5 | <div
|
4 |
| - class="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10" |
| 6 | + v-if="loaded" |
| 7 | + class="absolute top-2 right-2 flex gap-1 transition-opacity duration-200 z-10" |
| 8 | + :class="{ |
| 9 | + 'opacity-0': !isHovered, |
| 10 | + 'opacity-100': isHovered |
| 11 | + }" |
5 | 12 | >
|
6 |
| - <Button icon-only title="Rename" @click="renameScreenshot"> |
7 |
| - <EditIcon/> |
8 |
| - </Button> |
9 |
| - <Button icon-only title="Copy" @click="copyImageToClipboard"> |
| 13 | + <Button v-tooltip="'Copy'" icon-only title="Copy" @click="copyImageToClipboard"> |
10 | 14 | <ClipboardCopyIcon/>
|
11 | 15 | </Button>
|
12 |
| - <Button icon-only title="Share" @click="shareScreenshot"> |
13 |
| - <ShareIcon/> |
| 16 | + <Button v-tooltip="'View in folder'" icon-only title="View in folder" @click="viewInFolder"> |
| 17 | + <ExternalIcon /> |
14 | 18 | </Button>
|
15 |
| - <Button color="red" icon-only title="Delete" @click="deleteScreenshot"> |
| 19 | + <Button v-tooltip="'Delete'" color="red" icon-only title="Delete" @click="deleteScreenshot"> |
16 | 20 | <TrashIcon/>
|
17 | 21 | </Button>
|
18 | 22 | </div>
|
19 | 23 |
|
20 |
| - <img |
21 |
| - :alt="getFileName(screenshot.path)" |
22 |
| - :src="`data:image/png;base64,${screenshot.data}`" |
23 |
| - class="w-full h-full object-cover" |
24 |
| - /> |
| 24 | + <div class="aspect-video bg-bg-raised overflow-hidden"> |
| 25 | + <div v-if="!loaded" class="absolute inset-0 skeleton"></div> |
| 26 | + <img |
| 27 | + v-else |
| 28 | + :alt="getFileName(screenshot.path)" |
| 29 | + :src="`data:image/png;base64,${imageData}`" |
| 30 | + class="w-full h-full object-cover transition-opacity duration-700" |
| 31 | + :class="{ 'opacity-0': !loaded, 'opacity-100': loaded }" |
| 32 | + @load="onLoad" |
| 33 | + /> |
| 34 | + </div> |
25 | 35 | </div>
|
26 | 36 | </template>
|
27 | 37 |
|
28 | 38 | <script lang="ts" setup>
|
29 |
| -import {ClipboardCopyIcon, EditIcon, ShareIcon, TrashIcon} from '@modrinth/assets' |
30 |
| -import {Button} from '@modrinth/ui' |
31 |
| -import type {Screenshot} from '@/helpers/screenshots.ts' |
32 |
| -import {useNotifications} from '@/store/state' |
| 39 | +import { ref, onMounted } from 'vue' |
| 40 | +import { ClipboardCopyIcon, TrashIcon, ExternalIcon } from '@modrinth/assets' |
| 41 | +import { Button } from '@modrinth/ui' |
| 42 | +import { |
| 43 | + type Screenshot, |
| 44 | + deleteProfileScreenshot, |
| 45 | + openProfileScreenshot, |
| 46 | + getScreenshotData |
| 47 | +} from '@/helpers/screenshots' |
| 48 | +import { useNotifications } from '@/store/state' |
33 | 49 |
|
34 | 50 | const notifications = useNotifications()
|
35 | 51 |
|
36 | 52 | const props = defineProps<{
|
37 | 53 | screenshot: Screenshot
|
| 54 | + profilePath: string |
38 | 55 | }>()
|
39 | 56 |
|
| 57 | +const emit = defineEmits(['deleted']) |
| 58 | +
|
| 59 | +const loaded = ref(false) |
| 60 | +const imageData = ref<string>('') |
| 61 | +
|
| 62 | +// Note: cant use tailwind group because it's being used in the parent component |
| 63 | +const isHovered = ref(false) |
| 64 | +
|
| 65 | +const onLoad = () => { |
| 66 | + loaded.value = true |
| 67 | +} |
| 68 | +
|
40 | 69 | const getFileName = (path: string | undefined) => {
|
41 | 70 | if (!path) return 'Untitled'
|
42 |
| - return path.split('/').pop() |
| 71 | + return path.split('/').pop()! |
43 | 72 | }
|
44 | 73 |
|
45 |
| -const copyImageToClipboard = async () => { |
| 74 | +onMounted(async () => { |
46 | 75 | try {
|
47 |
| - const base64 = props.screenshot.data |
48 |
| - const binary = atob(base64) |
| 76 | + const result = await getScreenshotData(props.profilePath, props.screenshot) |
| 77 | + if (result) { |
| 78 | + imageData.value = result |
| 79 | + loaded.value = true; |
| 80 | + } else { |
| 81 | + notifications.addNotification({ |
| 82 | + title: 'Failed to load image', |
| 83 | + type: 'error', |
| 84 | + }) |
| 85 | + } |
| 86 | + } catch (err: any) { |
| 87 | + notifications.addNotification({ |
| 88 | + title: 'Error fetching screenshot', |
| 89 | + text: err.message, |
| 90 | + type: 'error', |
| 91 | + }) |
| 92 | + } |
| 93 | +}) |
49 | 94 |
|
| 95 | +const copyImageToClipboard = async () => { |
| 96 | + try { |
| 97 | + const binary = atob(imageData.value) |
50 | 98 | const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
|
51 |
| -
|
52 |
| - const blob = new Blob([bytes], {type: `data:image/png`}) |
53 |
| - const clipboardItem = new ClipboardItem({'image/png': blob}) |
54 |
| -
|
| 99 | + const blob = new Blob([bytes], { type: 'image/png' }) |
| 100 | + const clipboardItem = new ClipboardItem({ 'image/png': blob }) |
55 | 101 | await navigator.clipboard.write([clipboardItem])
|
56 | 102 |
|
57 | 103 | notifications.addNotification({
|
58 | 104 | title: 'Copied to clipboard',
|
59 |
| - text: 'The screenshot has successfully been copied to your clipboard.', |
| 105 | + text: 'The screenshot has been copied successfully.', |
60 | 106 | type: 'success',
|
61 | 107 | })
|
62 |
| - // eslint-disable-next-line |
63 | 108 | } catch (error: any) {
|
64 | 109 | notifications.addNotification({
|
65 |
| - title: 'Failed to copy screenshot', |
| 110 | + title: 'Copy failed', |
66 | 111 | text: error.message,
|
67 |
| - type: 'warn', |
| 112 | + type: 'error', |
| 113 | + }) |
| 114 | + } |
| 115 | +} |
| 116 | +
|
| 117 | +const deleteScreenshot = async () => { |
| 118 | + try { |
| 119 | + const result = await deleteProfileScreenshot(props.profilePath, props.screenshot) |
| 120 | + if (!result) { |
| 121 | + notifications.addNotification({ |
| 122 | + title: 'Unable to delete screenshot', |
| 123 | + type: 'error', |
| 124 | + }) |
| 125 | + } else { |
| 126 | + notifications.addNotification({ |
| 127 | + title: 'Successfully deleted screenshot', |
| 128 | + type: 'success', |
| 129 | + }) |
| 130 | + emit('deleted') |
| 131 | + } |
| 132 | + } catch (err: any) { |
| 133 | + notifications.addNotification({ |
| 134 | + title: 'Error deleting screenshot', |
| 135 | + text: err.message, |
| 136 | + type: 'error', |
68 | 137 | })
|
69 | 138 | }
|
70 | 139 | }
|
71 | 140 |
|
72 |
| -const renameScreenshot = () => { |
| 141 | +const viewInFolder = () => { |
| 142 | + openProfileScreenshot(props.profilePath, props.screenshot) |
73 | 143 | }
|
| 144 | +</script> |
74 | 145 |
|
75 |
| -const deleteScreenshot = () => { |
| 146 | +<style scoped> |
| 147 | +.skeleton { |
| 148 | + background: linear-gradient( |
| 149 | + 90deg, |
| 150 | + var(--color-bg) 25%, |
| 151 | + var(--color-raised-bg) 50%, |
| 152 | + var(--color-bg) 75% |
| 153 | + ); |
| 154 | + background-size: 200% 100%; |
| 155 | + animation: wave 1500ms infinite linear; |
76 | 156 | }
|
77 | 157 |
|
78 |
| -const shareScreenshot = () => { |
| 158 | +@keyframes wave { |
| 159 | + 0% { |
| 160 | + background-position: -200% 0; |
| 161 | + } |
| 162 | + 100% { |
| 163 | + background-position: 200% 0; |
| 164 | + } |
79 | 165 | }
|
80 |
| -</script> |
| 166 | +</style> |
0 commit comments