mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 +1,2 @@
|
||||
pub mod deep_link;
|
||||
pub mod menu;
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user