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
+27
View File
@@ -218,6 +218,32 @@ fn main() {
"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_list",
"instance_list_groups",
"instance_create_group",
@@ -398,6 +424,7 @@ fn main() {
"add_server_to_instance",
"edit_server_in_instance",
"remove_server_from_instance",
"desync_server",
"get_instance_protocol_version",
"get_server_status",
"start_join_singleplayer_world",
+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,
+10
View File
@@ -25,6 +25,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
add_server_to_instance,
edit_server_in_instance,
remove_server_from_instance,
desync_server,
get_instance_protocol_version,
get_server_status,
start_join_singleplayer_world,
@@ -193,6 +194,15 @@ pub async fn remove_server_from_instance(
Ok(())
}
#[tauri::command]
pub async fn desync_server(
instance_id: &str,
server_id: &str,
mode: theseus::instance::DesyncServerMode,
) -> Result<()> {
Ok(theseus::instance::desync_server(instance_id, server_id, mode).await?)
}
#[tauri::command]
pub async fn get_instance_protocol_version(
instance_id: &str,
+88
View File
@@ -0,0 +1,88 @@
use tauri::{
AppHandle, Emitter, Runtime,
menu::{
AboutMetadata, HELP_SUBMENU_ID, Menu, MenuBuilder, MenuEvent,
MenuItemBuilder, SubmenuBuilder, WINDOW_SUBMENU_ID,
},
};
// Default macos undo/redo doesn't get passed through into the webkit window
const UNDO_MENU_ITEM_ID: &str = "edit-menu-undo";
const REDO_MENU_ITEM_ID: &str = "edit-menu-redo";
const UNDO_EVENT: &str = "edit-menu://undo";
const REDO_EVENT: &str = "edit-menu://redo";
pub fn create<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let package_info = app.package_info();
let config = app.config();
let about_metadata = AboutMetadata {
name: Some(package_info.name.clone()),
version: Some(package_info.version.to_string()),
copyright: config.bundle.copyright.clone(),
authors: config
.bundle
.publisher
.clone()
.map(|publisher| vec![publisher]),
..Default::default()
};
let app_menu = SubmenuBuilder::new(app, package_info.name.clone())
.about(Some(about_metadata))
.separator()
.services()
.separator()
.hide()
.hide_others()
.separator()
.quit()
.build()?;
let file_menu = SubmenuBuilder::new(app, "File").close_window().build()?;
let undo = MenuItemBuilder::with_id(UNDO_MENU_ITEM_ID, "Undo")
.accelerator("CmdOrCtrl+Z")
.build(app)?;
let redo = MenuItemBuilder::with_id(REDO_MENU_ITEM_ID, "Redo")
.accelerator("CmdOrCtrl+Shift+Z")
.build(app)?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.item(&undo)
.item(&redo)
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()?;
let view_menu = SubmenuBuilder::new(app, "View").fullscreen().build()?;
let window_menu = SubmenuBuilder::with_id(app, WINDOW_SUBMENU_ID, "Window")
.minimize()
.maximize()
.separator()
.close_window()
.build()?;
let help_menu =
SubmenuBuilder::with_id(app, HELP_SUBMENU_ID, "Help").build()?;
MenuBuilder::new(app)
.items(&[
&app_menu,
&file_menu,
&edit_menu,
&view_menu,
&window_menu,
&help_menu,
])
.build()
}
pub fn handle_event<R: Runtime>(app: &AppHandle<R>, event: MenuEvent) {
let event_name = match event.id().as_ref() {
UNDO_MENU_ITEM_ID => UNDO_EVENT,
REDO_MENU_ITEM_ID => REDO_EVENT,
_ => return,
};
if let Err(error) = app.emit_to("main", event_name, ()) {
tracing::warn!(?error, "Failed to emit edit menu event");
}
}
+1
View File
@@ -1 +1,2 @@
pub mod deep_link;
pub mod menu;
+7
View File
@@ -144,6 +144,13 @@ fn main() {
let mut builder = tauri::Builder::default();
#[cfg(target_os = "macos")]
{
builder = builder
.menu(|app| macos::menu::create(app))
.on_menu_event(macos::menu::handle_event);
}
#[cfg(feature = "updater")]
{
use tauri_plugin_http::reqwest::header::{HeaderValue, USER_AGENT};