Skip to content

Commit 52d9c61

Browse files
committed
feat: finish base screenshots page
1 parent 1ca3df4 commit 52d9c61

File tree

7 files changed

+293
-76
lines changed

7 files changed

+293
-76
lines changed
Lines changed: 120 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,166 @@
11
<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">
35
<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+
}"
512
>
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">
1014
<ClipboardCopyIcon/>
1115
</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 />
1418
</Button>
15-
<Button color="red" icon-only title="Delete" @click="deleteScreenshot">
19+
<Button v-tooltip="'Delete'" color="red" icon-only title="Delete" @click="deleteScreenshot">
1620
<TrashIcon/>
1721
</Button>
1822
</div>
1923

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>
2535
</div>
2636
</template>
2737

2838
<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'
3349
3450
const notifications = useNotifications()
3551
3652
const props = defineProps<{
3753
screenshot: Screenshot
54+
profilePath: string
3855
}>()
3956
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+
4069
const getFileName = (path: string | undefined) => {
4170
if (!path) return 'Untitled'
42-
return path.split('/').pop()
71+
return path.split('/').pop()!
4372
}
4473
45-
const copyImageToClipboard = async () => {
74+
onMounted(async () => {
4675
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+
})
4994
95+
const copyImageToClipboard = async () => {
96+
try {
97+
const binary = atob(imageData.value)
5098
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 })
55101
await navigator.clipboard.write([clipboardItem])
56102
57103
notifications.addNotification({
58104
title: 'Copied to clipboard',
59-
text: 'The screenshot has successfully been copied to your clipboard.',
105+
text: 'The screenshot has been copied successfully.',
60106
type: 'success',
61107
})
62-
// eslint-disable-next-line
63108
} catch (error: any) {
64109
notifications.addNotification({
65-
title: 'Failed to copy screenshot',
110+
title: 'Copy failed',
66111
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',
68137
})
69138
}
70139
}
71140
72-
const renameScreenshot = () => {
141+
const viewInFolder = () => {
142+
openProfileScreenshot(props.profilePath, props.screenshot)
73143
}
144+
</script>
74145

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;
76156
}
77157
78-
const shareScreenshot = () => {
158+
@keyframes wave {
159+
0% {
160+
background-position: -200% 0;
161+
}
162+
100% {
163+
background-position: 200% 0;
164+
}
79165
}
80-
</script>
166+
</style>
Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,45 @@
1-
import {invoke} from '@tauri-apps/api/core'
1+
import { invoke } from '@tauri-apps/api/core'
22

33
export type Screenshot = {
44
path: string
55
creation_date: string
6-
data: string
76
}
87

9-
export async function getAllProfileScreenshots(path: string): Promise<Screenshot[]> {
10-
return await invoke('plugin:screenshots|get_all_profile_screenshots', {path})
8+
export async function getAllProfileScreenshots(
9+
profilePath: string
10+
): Promise<Screenshot[]> {
11+
return await invoke<Screenshot[]>(
12+
'plugin:screenshots|get_all_profile_screenshots',
13+
{ path: profilePath }
14+
)
1115
}
1216

13-
export async function deleteScreenshotFile(screenshot: Screenshot): Promise<boolean> {
14-
return await invoke('plugin:screenshots|delete_screenshot', {path: screenshot.path})
17+
export async function deleteProfileScreenshot(
18+
profilePath: string,
19+
screenshot: Screenshot
20+
): Promise<boolean> {
21+
return await invoke<boolean>(
22+
'plugin:screenshots|delete_profile_screenshot',
23+
{ path: profilePath, screenshot }
24+
)
1525
}
1626

17-
export async function renameScreenshotFile(
18-
screenshot: Screenshot,
19-
new_filename: string,
27+
export async function openProfileScreenshot(
28+
profilePath: string,
29+
screenshot: Screenshot
2030
): Promise<boolean> {
21-
return await invoke('plugin:screenshots|rename_screenshot', {
22-
path: screenshot.path,
23-
new_filename,
24-
})
31+
return await invoke<boolean>(
32+
'plugin:screenshots|open_profile_screenshot',
33+
{ path: profilePath, screenshot }
34+
)
2535
}
36+
37+
export async function getScreenshotData(
38+
profilePath: string,
39+
screenshot: Screenshot
40+
): Promise<string | undefined> {
41+
return await invoke<string | undefined>(
42+
'plugin:screenshots|get_screenshot_data',
43+
{ path: profilePath, screenshot }
44+
)
45+
}

apps/app-frontend/src/pages/instance/Screenshots.vue

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {GameInstance} from '@/helpers/types'
33
import type ContextMenu from '@/components/ui/ContextMenu.vue'
44
import {DropdownIcon} from '@modrinth/assets'
55
import type {Version} from '@modrinth/utils'
6-
import {computed, onMounted, ref} from 'vue'
6+
import {computed, onBeforeMount, ref} from 'vue'
77
import dayjs from 'dayjs'
88
import advancedFormat from 'dayjs/plugin/advancedFormat.js'
99
import type {Screenshot} from '@/helpers/screenshots.ts'
@@ -21,11 +21,7 @@ const props = defineProps<{
2121
installed: boolean
2222
}>()
2323
24-
const screenshots = ref<Screenshot[]>([])
25-
26-
onMounted(async () => {
27-
screenshots.value = (await getAllProfileScreenshots(props.instance.path)) ?? []
28-
})
24+
const screenshots = ref<Screenshot[]>(await getAllProfileScreenshots(props.instance.path) ?? [])
2925
3026
function groupAndSortByDate(items: Screenshot[]) {
3127
const today = dayjs().startOf('day')
@@ -57,6 +53,10 @@ function groupAndSortByDate(items: Screenshot[]) {
5753
.map(([label, {items}]) => [label, items] as const)
5854
}
5955
56+
const markDeleted = (s: Screenshot) => {
57+
screenshots.value = screenshots.value.filter(shot => shot.path !== s.path)
58+
}
59+
6060
const screenshotsByDate = computed(() => groupAndSortByDate(screenshots.value))
6161
const hasToday = computed(() => screenshotsByDate.value.some(([label]) => label === 'Today'))
6262
</script>
@@ -75,7 +75,7 @@ const hasToday = computed(() => screenshotsByDate.value.some(([label]) => label
7575

7676
<div v-else class="space-y-8">
7777
<template v-if="!hasToday">
78-
<details class="space-y-2" open>
78+
<details class="group space-y-2" open>
7979
<summary class="cursor-pointer flex items-center justify-between">
8080
<h2
8181
class="text-xxl font-bold underline decoration-4 decoration-brand-green underline-offset-8"
@@ -91,7 +91,7 @@ const hasToday = computed(() => screenshotsByDate.value.some(([label]) => label
9191
</template>
9292

9393
<template v-for="[date, shots] in screenshotsByDate" :key="date">
94-
<details class="space-y-2" open>
94+
<details class="group space-y-2" open>
9595
<summary class="cursor-pointer flex items-center justify-between">
9696
<h2
9797
class="text-xxl font-bold underline decoration-4 decoration-brand-green underline-offset-8"
@@ -103,7 +103,13 @@ const hasToday = computed(() => screenshotsByDate.value.some(([label]) => label
103103
/>
104104
</summary>
105105
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pt-2">
106-
<ScreenshotCard v-for="s in shots" :key="s.path" :screenshot="s"/>
106+
<ScreenshotCard
107+
v-for="s in shots"
108+
:key="s.path"
109+
:screenshot="s"
110+
:profile-path="instance.path"
111+
@deleted="markDeleted(s)"
112+
/>
107113
</div>
108114
</details>
109115
</template>

apps/app/build.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,12 @@ fn main() {
268268
.plugin(
269269
"screenshots",
270270
InlinedPlugin::new()
271-
.commands(&["get_all_profile_screenshots"])
271+
.commands(&[
272+
"get_all_profile_screenshots",
273+
"get_screenshot_data",
274+
"delete_profile_screenshot",
275+
"open_profile_screenshot",
276+
])
272277
.default_permission(
273278
DefaultPermissionRule::AllowAllCommands,
274279
),

0 commit comments

Comments
 (0)