Skip to content

Commit 18c1464

Browse files
committed
feat: Improve layout to make it closer to the draft design.
1 parent c18cef2 commit 18c1464

File tree

7 files changed

+221
-38
lines changed

7 files changed

+221
-38
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<template>
2+
<div class="rounded-lg overflow-hidden shadow-md w-full text-contrast bg-bg-raised">
3+
<div class="relative">
4+
<img
5+
:alt="getFileName(screenshot.path)"
6+
:src="`data:image/png;base64,${screenshot.data}`"
7+
class="w-full h-auto object-contain"
8+
/>
9+
</div>
10+
<div class="p-4">
11+
<div class="flex items-center gap-2">
12+
<div class="font-medium truncate max-w-[calc(100%-120px)]">
13+
{{ getFileName(screenshot.path) }}
14+
</div>
15+
<div class="flex gap-1 ml-auto">
16+
<Button icon-only title="Rename" @click="renameScreenshot">
17+
<EditIcon/>
18+
</Button>
19+
<Button
20+
icon-only
21+
title="Copy"
22+
@click="copyImageToClipboard"
23+
>
24+
<ClipboardCopyIcon/>
25+
</Button>
26+
<Button icon-only title="Share" @click="shareScreenshot">
27+
<ShareIcon/>
28+
</Button>
29+
<Button color="red" icon-only title="Delete" @click="deleteScreenshot">
30+
<TrashIcon/>
31+
</Button>
32+
</div>
33+
</div>
34+
</div>
35+
</div>
36+
</template>
37+
38+
<script lang="ts" setup>
39+
import {ClipboardCopyIcon, EditIcon, ShareIcon, TrashIcon} from "@modrinth/assets";
40+
import {Button} from "@modrinth/ui";
41+
import type {Screenshot} from "@/helpers/screenshots.ts";
42+
import {useNotifications} from "@/store/state";
43+
44+
const notifications = useNotifications();
45+
46+
const props = defineProps<{
47+
screenshot: Screenshot
48+
}>();
49+
50+
const getFileName = (path: string | undefined) => {
51+
if (!path) return 'Untitled'
52+
return path.split('/').pop()
53+
}
54+
55+
const copyImageToClipboard = async () => {
56+
try {
57+
const base64 = props.screenshot.data;
58+
const binary = atob(base64);
59+
60+
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
61+
62+
const blob = new Blob([bytes], {type: `data:image/png`});
63+
const clipboardItem = new ClipboardItem({"image/png": blob});
64+
65+
await navigator.clipboard.write([clipboardItem]);
66+
67+
notifications.addNotification({
68+
title: "Copied to clipboard",
69+
text: "The screenshot has successfully been copied to your clipboard.",
70+
type: 'success'
71+
})
72+
} catch (error: any) {
73+
notifications.addNotification({
74+
title: 'Failed to copy screenshot',
75+
text: error.message,
76+
type: 'warn'
77+
})
78+
}
79+
}
80+
81+
const renameScreenshot = () => {
82+
}
83+
84+
const deleteScreenshot = () => {
85+
}
86+
87+
const shareScreenshot = () => {
88+
}
89+
</script>
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import {invoke} from "@tauri-apps/api/core";
22

3-
type Screenshot = {
4-
filename: string;
3+
export type Screenshot = {
4+
path: string;
55
creation_date: string;
6+
data: string;
67
}
78

89
export async function getAllProfileScreenshots(path: string): Promise<Screenshot[]> {
910
return await invoke('plugin:screenshots|get_all_profile_screenshots', { path })
11+
}
12+
13+
export async function deleteScreenshotFile(screenshot: Screenshot): Promise<boolean> {
14+
return await invoke('plugin:screenshots|delete_screenshot', {path: screenshot.path})
15+
}
16+
17+
export async function renameScreenshotFile(screenshot: Screenshot, new_filename: string): Promise<boolean> {
18+
return await invoke('plugin:screenshots|rename_screenshot', {path: screenshot.path, new_filename})
1019
}
Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,103 @@
11
<script setup lang="ts">
22
import type {GameInstance} from "@/helpers/types";
33
import type ContextMenu from "@/components/ui/ContextMenu.vue";
4+
import {DropdownIcon} from "@modrinth/assets";
45
import type {Version} from "@modrinth/utils";
6+
import {computed, onMounted, ref} from 'vue';
7+
import dayjs from "dayjs";
8+
import advancedFormat from 'dayjs/plugin/advancedFormat.js';
9+
import type {Screenshot} from "@/helpers/screenshots.ts";
510
import {getAllProfileScreenshots} from "@/helpers/screenshots.ts";
11+
import ScreenshotCard from "@/components/ui/ScreenshotCard.vue";
12+
13+
dayjs.extend(advancedFormat);
614
715
const props = defineProps<{
8-
instance: GameInstance
9-
options: InstanceType<typeof ContextMenu> | null
10-
offline: boolean
11-
playing: boolean
12-
versions: Version[]
13-
installed: boolean
14-
}>()
15-
16-
const screenshots = await getAllProfileScreenshots(props.instance.path);
16+
instance: GameInstance;
17+
options: InstanceType<typeof ContextMenu> | null;
18+
offline: boolean;
19+
playing: boolean;
20+
versions: Version[];
21+
installed: boolean;
22+
}>();
23+
24+
const screenshots = ref<Screenshot[]>([]);
25+
26+
onMounted(async () => {
27+
screenshots.value = (await getAllProfileScreenshots(props.instance.path)) ?? [];
28+
});
29+
30+
function groupAndSortByDate(items: Screenshot[]) {
31+
const today = dayjs().startOf('day');
32+
const yesterday = today.subtract(1, 'day');
33+
const map = new Map<string, { labelDate: dayjs.Dayjs; items: any[] }>();
34+
35+
for (const shot of items) {
36+
const d = dayjs(shot.creation_date).startOf('day');
37+
let label: string;
38+
if (d.isSame(today)) label = 'Today';
39+
else if (d.isSame(yesterday)) label = 'Yesterday';
40+
else label = dayjs(shot.creation_date).format("MMMM Do, YYYY");
41+
42+
if (!map.has(label)) {
43+
map.set(label, {labelDate: d, items: []});
44+
}
45+
46+
map.get(label)!.items.push(shot);
47+
}
48+
49+
return Array.from(map.entries())
50+
.sort(([a, aData], [b, bData]) => {
51+
if (a === 'Today') return -1;
52+
if (b === 'Today') return 1;
53+
if (a === 'Yesterday') return -1;
54+
if (b === 'Yesterday') return 1;
55+
return bData.labelDate.unix() - aData.labelDate.unix();
56+
})
57+
.map(([label, {items}]) => [label, items] as const);
58+
}
59+
60+
const screenshotsByDate = computed(() => groupAndSortByDate(screenshots.value));
61+
const hasToday = computed(() => screenshotsByDate.value.some(([label]) => label === 'Today'));
1762
</script>
1863

1964
<template>
20-
<div class="card">
21-
{{ screenshots }}
65+
<div class="w-full p-5">
66+
<div v-if="!screenshots.length" class="flex flex-col items-center justify-center py-12 text-center">
67+
<div class="text-lg font-medium mb-2">No screenshots yet</div>
68+
<div class="text-sm text-gray-500 dark:text-gray-400">
69+
Screenshots taken in-game will appear here
70+
</div>
71+
</div>
72+
73+
<div v-else class="space-y-8">
74+
<template v-if="!hasToday">
75+
<details class="group space-y-2" open>
76+
<summary class="cursor-pointer flex items-center justify-between">
77+
<h2 class="text-xxl font-bold underline decoration-4 decoration-brand-green underline-offset-8">Today</h2>
78+
<DropdownIcon class="w-5 h-5 transform transition-transform duration-200 group-open:rotate-180"/>
79+
</summary>
80+
<p class="text-lg font-medium mb-2">You haven't taken any screenshots today.</p>
81+
</details>
82+
</template>
83+
84+
<template v-for="([date, shots]) in screenshotsByDate" :key="date">
85+
<details class="group space-y-2" open>
86+
<summary class="cursor-pointer flex items-center justify-between">
87+
<h2 class="text-xxl font-bold underline decoration-4 decoration-brand-green underline-offset-8">{{
88+
date
89+
}}</h2>
90+
<DropdownIcon class="w-5 h-5 transform transition-transform duration-200 group-open:rotate-180"/>
91+
</summary>
92+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pt-2">
93+
<ScreenshotCard
94+
v-for="s in shots"
95+
:key="s.path"
96+
:screenshot="s"
97+
/>
98+
</div>
99+
</details>
100+
</template>
101+
</div>
22102
</div>
23103
</template>

apps/app/src/api/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ pub mod utils;
1919
pub mod ads;
2020
pub mod cache;
2121
pub mod friends;
22-
pub mod worlds;
2322
pub mod screenshots;
23+
pub mod worlds;
2424

2525
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
2626

apps/app/src/api/screenshots.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
use tauri::{AppHandle, Runtime};
2-
use theseus::{screenshots};
1+
use tauri::Runtime;
2+
use theseus::screenshots;
33

44
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
55
tauri::plugin::Builder::new("screenshots")
6-
.invoke_handler(tauri::generate_handler![
7-
get_all_profile_screenshots
8-
])
6+
.invoke_handler(tauri::generate_handler![get_all_profile_screenshots])
97
.build()
108
}
119

1210
#[tauri::command]
13-
pub async fn get_all_profile_screenshots<R: Runtime>(
14-
app_handle: AppHandle<R>,
11+
pub async fn get_all_profile_screenshots(
1512
path: &str,
1613
) -> crate::api::Result<Vec<screenshots::Screenshot>> {
1714
Ok(screenshots::get_all_profile_screenshots(path).await?)
18-
}
15+
}

packages/app-lib/src/api/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ pub mod mr_auth;
1010
pub mod pack;
1111
pub mod process;
1212
pub mod profile;
13+
pub mod screenshots;
1314
pub mod settings;
1415
pub mod tags;
1516
pub mod worlds;
16-
pub mod screenshots;
1717

1818
pub mod data {
1919
pub use crate::state::{
@@ -28,12 +28,12 @@ pub mod data {
2828

2929
pub mod prelude {
3030
pub use crate::{
31-
State,
3231
data::*,
3332
event::CommandPayload,
34-
jre, metadata, minecraft_auth, mr_auth, pack, process,
35-
profile::{self, Profile, create},
33+
jre,
34+
metadata, minecraft_auth, mr_auth, pack, process, profile::{self, create, Profile},
3635
settings,
37-
util::io::{IOError, canonicalize},
36+
util::io::{canonicalize, IOError},
37+
State,
3838
};
3939
}

packages/app-lib/src/api/screenshots.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
use std::ffi::OsStr;
2-
use std::path::Path;
3-
use chrono::{DateTime, Utc};
4-
use serde::{Deserialize, Serialize};
5-
use tokio::fs::canonicalize;
61
use crate::profile::get_full_path;
72
use crate::util::io::{metadata, read_dir};
3+
use base64::{engine::general_purpose::STANDARD, Engine};
4+
use chrono::{DateTime, Utc};
5+
use serde::{Deserialize, Serialize};
6+
use std::ffi::OsStr;
7+
use std::path::Path;
8+
use tokio::fs::{canonicalize, read};
89

910
#[derive(Deserialize, Serialize, Debug, Clone)]
1011
pub struct Screenshot {
1112
pub path: String,
12-
pub creation_date: DateTime<Utc>
13+
pub creation_date: DateTime<Utc>,
14+
pub data: String,
1315
}
1416

1517
pub async fn get_all_profile_screenshots(
1618
profile_path: &str,
1719
) -> crate::Result<Vec<Screenshot>> {
18-
get_all_screenshots_in_profile(&get_full_path(profile_path).await?)
19-
.await
20+
get_all_screenshots_in_profile(&get_full_path(profile_path).await?).await
2021
}
2122

2223
async fn get_all_screenshots_in_profile(
@@ -52,10 +53,17 @@ async fn get_all_screenshots_in_profile(
5253
let created_time = meta.created().unwrap_or(meta.modified()?);
5354
let creation_date = DateTime::<Utc>::from(created_time);
5455

55-
screenshots.push(Screenshot { path: full_path, creation_date });
56+
let bytes = read(&abs_path).await?;
57+
let data = Engine::encode(&STANDARD, &bytes);
58+
59+
screenshots.push(Screenshot {
60+
path: full_path,
61+
creation_date,
62+
data,
63+
});
5664
}
5765

5866
screenshots.sort_by_key(|s| s.creation_date);
59-
67+
6068
Ok(screenshots)
61-
}
69+
}

0 commit comments

Comments
 (0)