feat(instance-sync): screenshots sync (#7218)

* pnpm prepr

* feat(instance-sync): screenshots sync

* fix: prepr + fmt

* fix: qa

* feat: screenshit editor

* feat: screenshot* editor qa

* fix: qa

* fix: lint

* fix: aecsocket rev

* qa: a11y tab navigation focus is cut off

* qa: Change “Edited” badge to be bg-highlight-green

* qa: friends list input style wrong

* qa: toolbar width + labels

* qa: multiselect changes

* qa: anim changes

* qa: card hover colors

* qa: copy feedback

* qa: hide date

* qa: instance icon in overflow/contextmenu

* qa: spacing

* qa: group empty state text

* qa: drag preview badge

* qa: group deletion skip warning if no screenshots

* qa: redesign editor

* qa: remove screenshot parent/edited badge stuff

* qa: control font size consistency

* qa: viewer copy control

* qa: editor fixes

* qa: redirect on disable screenshot sync

* qa: margin police

* fix: crop

* fix: qa

* feat: initial start on basic instance file syncing (#7220)

* qa: final

* qa: final 2

* fix: lint + prepr

* fix: copy

* fix: screenshot editing outside of app causing ghost files in db + sync fail rollback impl

* chore: split up

* fix: fmt

---------

Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
Calum H.
2026-08-27 16:49:46 +00:00
committed by GitHub
co-authored by tdgao
parent 2bd108c278
commit 7c67cca7a7
217 changed files with 17139 additions and 1887 deletions
+306
View File
@@ -4,6 +4,9 @@ use path_util::SafeRelativeUtf8UnixPathBuf;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tauri::{AppHandle, Manager, Runtime};
use tauri_plugin_fs::FsExt;
use tauri_plugin_opener::OpenerExt;
use theseus::DownloadReason;
use theseus::data::{
AppliedContentSetPatch, ContentItem, Dependency,
@@ -43,6 +46,32 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
instance_get_optimal_jre_key,
instance_get_full_path,
instance_get_mod_full_path,
instance_list_screenshots,
instance_list_all_screenshots,
instance_list_synced_screenshots,
instance_save_edited_screenshot,
instance_list_screenshot_groups,
instance_create_screenshot_group,
instance_rename_screenshot_group,
instance_delete_screenshot_group,
instance_set_screenshot_group_memberships,
instance_import_screenshot_groups,
instance_delete_screenshots,
instance_export_screenshots,
instance_move_screenshots,
instance_open_screenshot,
instance_set_synced_option,
instance_get_synced_option_join_preview,
instance_get_synced_options_overview,
instance_get_global_synced_options,
instance_set_global_synced_option,
instance_get_command_history,
instance_set_command_history,
instance_open_synced_options_folder,
instance_list_synced_servers,
instance_update_synced_server,
instance_remove_synced_server,
instance_rebuild_synced_options,
instance_check_installed,
instance_update_all,
instance_update_project,
@@ -94,6 +123,7 @@ pub struct Instance {
pub loader: ModLoader,
pub loader_version: Option<String>,
pub group_ids: Vec<String>,
pub synced_options: InstanceSyncedOptions,
pub link: Option<InstanceLink>,
pub shared_instance: Option<SharedInstanceAttachment>,
pub quarantined: bool,
@@ -112,6 +142,19 @@ pub struct Instance {
pub hooks: Hooks,
}
#[derive(Serialize, Debug, Clone)]
pub struct InstanceScreenshot {
pub id: String,
pub instance_id: String,
pub instance_name: String,
pub file_name: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub modified_at: i64,
pub group_id: Option<String>,
pub path: PathBuf,
pub url: url::Url,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InstanceLink {
@@ -258,6 +301,7 @@ impl From<InstanceMetadata> for Instance {
loader: metadata.applied_content_set.loader,
loader_version: metadata.applied_content_set.loader_version,
group_ids: metadata.group_ids,
synced_options: metadata.synced_options,
link: InstanceLink::from_core(metadata.link),
shared_instance: metadata.shared_instance.map(Into::into),
quarantined: metadata.quarantined,
@@ -632,6 +676,268 @@ pub async fn instance_get_mod_full_path(
Ok(theseus::instance::get_mod_full_path(instance_id, project_path).await?)
}
#[tauri::command]
pub async fn instance_list_screenshots<R: Runtime>(
app_handle: AppHandle<R>,
instance_id: &str,
) -> Result<Vec<InstanceScreenshot>> {
let screenshots = theseus::instance::list_screenshots(instance_id).await?;
serialize_screenshots(&app_handle, screenshots)
}
#[tauri::command]
pub async fn instance_list_all_screenshots<R: Runtime>(
app_handle: AppHandle<R>,
) -> Result<Vec<InstanceScreenshot>> {
let screenshots = theseus::instance::list_all_screenshots().await?;
serialize_screenshots(&app_handle, screenshots)
}
#[tauri::command]
pub async fn instance_list_synced_screenshots<R: Runtime>(
app_handle: AppHandle<R>,
) -> Result<Vec<InstanceScreenshot>> {
let screenshots = theseus::instance::list_synced_screenshots().await?;
serialize_screenshots(&app_handle, screenshots)
}
#[tauri::command]
pub async fn instance_save_edited_screenshot<R: Runtime>(
app_handle: AppHandle<R>,
key: theseus::instance::ScreenshotKey,
png_bytes: Vec<u8>,
mode: theseus::instance::ScreenshotEditSaveMode,
) -> Result<InstanceScreenshot> {
let screenshot =
theseus::instance::save_edited_screenshot(key, png_bytes, mode).await?;
serialize_screenshot(&app_handle, screenshot)
}
#[tauri::command]
pub async fn instance_list_screenshot_groups()
-> Result<Vec<theseus::instance::ScreenshotGroup>> {
Ok(theseus::instance::list_screenshot_groups().await?)
}
#[tauri::command]
pub async fn instance_create_screenshot_group(
name: String,
screenshot_ids: Vec<String>,
) -> Result<theseus::instance::ScreenshotGroup> {
Ok(
theseus::instance::create_screenshot_group(name, screenshot_ids)
.await?,
)
}
#[tauri::command]
pub async fn instance_rename_screenshot_group(
id: String,
new_name: String,
) -> Result<theseus::instance::ScreenshotGroup> {
Ok(theseus::instance::rename_screenshot_group(id, new_name).await?)
}
#[tauri::command]
pub async fn instance_delete_screenshot_group(id: String) -> Result<()> {
Ok(theseus::instance::delete_screenshot_group(id).await?)
}
#[tauri::command]
pub async fn instance_set_screenshot_group_memberships(
updates: Vec<theseus::instance::ScreenshotGroupMembershipUpdate>,
) -> Result<()> {
Ok(theseus::instance::set_screenshot_group_memberships(updates).await?)
}
#[tauri::command]
pub async fn instance_import_screenshot_groups(
groups: Vec<theseus::instance::ScreenshotGroupImport>,
) -> Result<()> {
Ok(theseus::instance::import_screenshot_groups(groups).await?)
}
#[tauri::command]
pub async fn instance_delete_screenshots(
keys: Vec<theseus::instance::ScreenshotKey>,
) -> Result<()> {
Ok(theseus::instance::delete_screenshots(&keys).await?)
}
#[tauri::command]
pub async fn instance_export_screenshots(
keys: Vec<theseus::instance::ScreenshotKey>,
export_path: PathBuf,
) -> Result<()> {
Ok(theseus::instance::export_screenshots(&keys, export_path).await?)
}
#[tauri::command]
pub async fn instance_move_screenshots(
keys: Vec<theseus::instance::ScreenshotKey>,
target_instance_id: &str,
) -> Result<Vec<theseus::instance::ScreenshotKey>> {
Ok(theseus::instance::move_screenshots(&keys, target_instance_id).await?)
}
#[tauri::command]
pub async fn instance_open_screenshot<R: Runtime>(
app_handle: AppHandle<R>,
key: theseus::instance::ScreenshotKey,
) -> Result<()> {
let path = theseus::instance::get_screenshot_path(&key).await?;
app_handle
.opener()
.reveal_item_in_dir(path)
.map_err(|error| std::io::Error::other(error.to_string()))?;
Ok(())
}
#[tauri::command]
pub async fn instance_set_synced_option(
instance_id: &str,
option: InstanceSyncedOption,
enabled: bool,
resolution: Option<theseus::instance::SyncedOptionJoinResolution>,
) -> Result<Instance> {
Ok(Instance::from(
theseus::instance::set_synced_option(
instance_id,
option,
enabled,
resolution,
)
.await?,
))
}
#[tauri::command]
pub async fn instance_get_synced_option_join_preview(
instance_id: &str,
option: InstanceSyncedOption,
) -> Result<theseus::instance::SyncedOptionJoinPreview> {
Ok(
theseus::instance::get_synced_option_join_preview(instance_id, option)
.await?,
)
}
#[tauri::command]
pub async fn instance_get_synced_options_overview(
instance_id: &str,
) -> Result<theseus::instance::SyncedOptionsOverview> {
Ok(theseus::instance::get_synced_options_overview(instance_id).await?)
}
#[tauri::command]
pub async fn instance_get_global_synced_options()
-> Result<theseus::instance::GlobalSyncedOptions> {
Ok(theseus::instance::get_global_synced_options().await?)
}
#[tauri::command]
pub async fn instance_set_global_synced_option(
option: InstanceSyncedOption,
enabled: bool,
) -> Result<theseus::instance::GlobalSyncedOptions> {
Ok(theseus::instance::set_global_synced_option(option, enabled).await?)
}
#[tauri::command]
pub async fn instance_get_command_history() -> Result<String> {
Ok(theseus::instance::get_command_history().await?)
}
#[tauri::command]
pub async fn instance_set_command_history(contents: &str) -> Result<String> {
Ok(theseus::instance::set_command_history(contents).await?)
}
#[tauri::command]
pub async fn instance_open_synced_options_folder<R: Runtime>(
app_handle: AppHandle<R>,
) -> Result<()> {
let path = theseus::instance::get_synced_options_folder().await?;
app_handle
.opener()
.open_path(path.to_string_lossy(), None::<&str>)
.map_err(|error| std::io::Error::other(error.to_string()))?;
Ok(())
}
#[tauri::command]
pub async fn instance_list_synced_servers()
-> Result<Vec<theseus::instance::SyncedServer>> {
Ok(theseus::instance::list_synced_servers().await?)
}
#[tauri::command]
pub async fn instance_update_synced_server(
server: theseus::instance::SyncedServer,
) -> Result<()> {
Ok(theseus::instance::update_synced_server(server).await?)
}
#[tauri::command]
pub async fn instance_remove_synced_server(server_id: &str) -> Result<()> {
Ok(theseus::instance::remove_synced_server(server_id).await?)
}
#[tauri::command]
pub async fn instance_rebuild_synced_options(
instance_id: Option<&str>,
) -> Result<()> {
if let Some(instance_id) = instance_id {
theseus::instance::reconcile_instance_synced_options(instance_id)
.await?;
} else {
theseus::instance::reconcile_all_synced_options().await?;
}
Ok(())
}
fn serialize_screenshots<R: Runtime>(
app_handle: &AppHandle<R>,
screenshots: Vec<theseus::instance::InstanceScreenshot>,
) -> Result<Vec<InstanceScreenshot>> {
let mut result = Vec::with_capacity(screenshots.len());
for screenshot in screenshots {
result.push(serialize_screenshot(app_handle, screenshot)?);
}
Ok(result)
}
fn serialize_screenshot<R: Runtime>(
app_handle: &AppHandle<R>,
screenshot: theseus::instance::InstanceScreenshot,
) -> Result<InstanceScreenshot> {
app_handle
.asset_protocol_scope()
.allow_file(&screenshot.path)
.map_err(|error| std::io::Error::other(error.to_string()))?;
app_handle
.fs_scope()
.allow_file(&screenshot.path)
.map_err(|error| std::io::Error::other(error.to_string()))?;
let mut url = super::utils::tauri_convert_file_src(&screenshot.path)?;
url.query_pairs_mut()
.append_pair("revision", &screenshot.modified_at.to_string());
Ok(InstanceScreenshot {
id: screenshot.id,
instance_id: screenshot.instance_id,
instance_name: screenshot.instance_name,
file_name: screenshot.file_name,
created_at: screenshot.created_at,
modified_at: screenshot.modified_at,
group_id: screenshot.group_id,
path: screenshot.path,
url,
})
}
#[tauri::command]
pub async fn instance_get_optimal_jre_key(
instance_id: &str,