feat: add screenshots and worlds page

This commit is contained in:
emirsassan 2025-07-10 11:27:30 +03:00
parent 72157f689b
commit 18554785ae
6 changed files with 176 additions and 14 deletions

View file

@ -0,0 +1,37 @@
import DefaultWorld from '@/assets/images/default_world.png';
import { convertFileSrc } from '@tauri-apps/api/core';
import { dataDir, join } from '@tauri-apps/api/path';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface WorldIconProps {
world_name: string;
cluster_name: string;
className?: string;
}
export default function WorldIcon(props: WorldIconProps) {
const { world_name, cluster_name, className } = props;
const [imagePath, setImagePath] = useState<string>('');
useEffect(() => {
async function loadImagePath() {
const iconPath = await join(await dataDir(), 'OneLauncher', 'clusters', cluster_name, 'saves', world_name, 'icon.png');
setImagePath(convertFileSrc(iconPath));
}
loadImagePath().catch(console.error);
}, [cluster_name, world_name]);
return (
<img
{...props}
alt={`${world_name} icon`}
className={twMerge('rounded-md size-16', className)}
onError={(e) => {
e.currentTarget.src = DefaultWorld;
}}
src={imagePath}
/>
);
}

View file

@ -1,6 +1,11 @@
import ScrollableContainer from '@/components/ScrollableContainer';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { convertFileSrc } from '@tauri-apps/api/core';
import { dataDir, join } from '@tauri-apps/api/path';
import { useEffect, useState } from 'react';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/screenshots')({
@ -8,7 +13,10 @@ export const Route = createFileRoute('/app/cluster/screenshots')({
});
function RouteComponent() {
const arr = [1, 2, 3, 4, 5, 6];
const { id } = Route.useSearch();
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
const result = useCommand('getScreenshots', () => bindings.core.getScreenshots(Number(id.toString()) as unknown as bigint));
return (
<Sidebar.Page>
<h1>Screenshots</h1>
@ -16,11 +24,12 @@ function RouteComponent() {
<div className="h-full">
<Show
fallback={<div className="text-border/400">No screenshots found. Press F2 in game to take a screenshot!</div>}
when
// @ts-ignore idk
when={result.data?.length > 0}
>
<div className="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full transform-gpu gap-2">
{arr.map(data => (
<ScreenshotEntry cluster_path="cluster" key={data} path={`screenshot-${data}.png`} />
{result.data?.map(data => (
<ScreenshotEntry cluster_path={cluster.data?.folder_name || ''} key={data} path={data} />
))}
</div>
</Show>
@ -32,20 +41,34 @@ function RouteComponent() {
function ScreenshotEntry({
path,
cluster_path,
}: {
path: string;
cluster_path: string;
}) {
function onClick() {
// screen shot viewer overlay
}
const [imagePath, setImagePath] = useState<string>('');
useEffect(() => {
async function loadImagePath() {
const screenshotsDir = await join(await dataDir(), 'OneLauncher', 'clusters', cluster_path, 'screenshots');
const fullPath = await join(screenshotsDir, path);
setImagePath(convertFileSrc(fullPath));
}
loadImagePath().catch(console.error);
}, [cluster_path, path]);
return (
<div
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80"
onClick={onClick}
>
<img alt={path} className="aspect-video w-full rounded-lg" src="https://github.com/emirsassan.png" />
</div>
<>
<div
className="flex flex-col items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover hover:opacity-80 cursor-pointer"
>
<img
alt={path}
className="aspect-video w-full rounded-lg"
src={imagePath}
/>
</div>
</>
);
}

View file

@ -1,5 +1,10 @@
import WorldIcon from '@/components/content/WorldIcon';
import ScrollableContainer from '@/components/ScrollableContainer';
import { bindings } from '@/main';
import { useCommand } from '@onelauncher/common';
import { Button, Show } from '@onelauncher/common/components';
import { createFileRoute } from '@tanstack/react-router';
import { Trash01Icon } from '@untitled-theme/icons-react';
import Sidebar from '../settings/route';
export const Route = createFileRoute('/app/cluster/worlds')({
@ -7,14 +12,52 @@ export const Route = createFileRoute('/app/cluster/worlds')({
});
function RouteComponent() {
const { id } = Route.useSearch();
const cluster = useCommand('getClusterById', () => bindings.core.getClusterById(Number(id.toString()) as unknown as bigint));
const list = useCommand('getWorlds', () => bindings.core.getWorlds(Number(id.toString()) as unknown as bigint));
return (
<Sidebar.Page>
<h1>Worlds</h1>
<ScrollableContainer>
<div className="h-full">
<Show
fallback={<div className="text-border/400">No worlds found.</div>}
when={list.data !== undefined && list.data.length > 0}
>
<div className="flex flex-col gap-2">
{list.data?.map(data => (
<WorldEntry cluster_path={cluster.data?.folder_name || ''} key={data} name={data} />
))}
</div>
</Show>
</div>
</ScrollableContainer>
</Sidebar.Page>
);
}
function WorldEntry(props: { name: string; cluster_path: string }) {
const { name, cluster_path } = props;
return (
<div
className="flex flex-row items-center justify-between gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover"
>
<div className="flex flex-row items-center gap-x-3">
<WorldIcon className="aspect-ratio-square h-16 w-16" cluster_name={cluster_path} world_name={name} />
<div className="flex flex-col gap-y-2">
<h3>{name}</h3>
<p>Todo</p>
</div>
</div>
<div className="flex flex-row items-center justify-end gap-x-3">
<Button
children={<Trash01Icon />}
color="danger"
/>
</div>
</div>
);
}

View file

@ -0,0 +1,39 @@
use onelauncher_entity::clusters;
use crate::{error::LauncherResult, store::Dirs, utils::io};
pub async fn get_screenshots(cluster: &clusters::Model) -> LauncherResult<Vec<String>> {
let dir = cluster.folder_name.clone();
let path = Dirs::get_clusters_dir().await?.join(dir).join("screenshots");
if !path.exists() {
io::create_dir(&path).await?;
return Ok(Vec::new());
}
let mut list = vec![];
let mut files = io::read_dir(path).await?;
while let Ok(Some(entry)) = files.next_entry().await {
list.push(entry.file_name().to_string_lossy().to_string());
}
Ok(list)
}
pub async fn get_worlds(cluster: &clusters::Model) -> LauncherResult<Vec<String>> {
let dir = cluster.folder_name.clone();
let path = Dirs::get_clusters_dir().await?.join(dir).join("saves");
if !path.exists() {
io::create_dir(&path).await?;
return Ok(Vec::new());
}
let mut list = vec![];
let mut files = io::read_dir(path).await?;
while let Ok(Some(entry)) = files.next_entry().await {
list.push(entry.file_name().to_string_lossy().to_string());
}
Ok(list)
}

View file

@ -18,6 +18,7 @@ use crate::store::{Dirs, State};
use crate::utils::io::{self, IOError};
pub mod dao;
pub mod content;
mod sync;
pub use sync::*;

View file

@ -31,6 +31,12 @@ pub trait TauriLauncherApi {
#[taurpc(alias = "updateClusterById")]
async fn update_cluster_by_id(id: ClusterId, request: ClusterUpdate) -> LauncherResult<()>;
#[taurpc(alias = "getScreenshots")]
async fn get_screenshots(id: ClusterId) -> LauncherResult<Vec<String>>;
#[taurpc(alias = "getWorlds")]
async fn get_worlds(id: ClusterId) -> LauncherResult<Vec<String>>;
// Setting Profiles
#[taurpc(alias = "getProfileOrDefault")]
@ -184,6 +190,19 @@ impl TauriLauncherApi for TauriLauncherApiImpl {
Ok(())
}
async fn get_screenshots(self, id: ClusterId) -> LauncherResult<Vec<String>> {
let cluster = api::cluster::dao::get_cluster_by_id(id).await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?;
api::cluster::content::get_screenshots(&cluster).await
}
async fn get_worlds(self, id: ClusterId) -> LauncherResult<Vec<String>> {
let cluster = api::cluster::dao::get_cluster_by_id(id).await?
.ok_or_else(|| anyhow::anyhow!("cluster with id {} not found", id))?;
api::cluster::content::get_worlds(&cluster).await
}
// Setting Profiles
async fn get_global_profile(self) -> SettingsProfile {