Folder button
Automatically add stuff added to the mods folder to the next-ran cluster if in a shared directory Add a txt explanation as to why certain folders are empty in shared directory
This commit is contained in:
parent
a645388a40
commit
0f04462863
10 changed files with 143 additions and 7 deletions
|
|
@ -422,4 +422,15 @@ pub fn link_button() -> Button {
|
|||
.primary()
|
||||
.child(Icon::new(IconType::LinkExternal01).size(14.).color(colors::fg_primary()))
|
||||
.child(label().text("Open"))
|
||||
}
|
||||
|
||||
pub fn open_folder_button(folder: std::path::PathBuf) -> Button {
|
||||
Button::new()
|
||||
.secondary()
|
||||
.icon()
|
||||
.on_press(move |_| {
|
||||
std::fs::create_dir_all(&folder).ok();
|
||||
crate::platform::open_url(&folder.to_string_lossy());
|
||||
})
|
||||
.child(Icon::new(IconType::Folder).size(16.))
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ mod toggle;
|
|||
|
||||
pub use active_cluster_panel::ActiveClusterPanel;
|
||||
pub use avatar::Avatar;
|
||||
pub use button::{Button, ButtonSize, ButtonVariant, link_button};
|
||||
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
|
||||
pub use charts::{BarChart, ValueUnit};
|
||||
pub use cluster_landscape_art::ClusterLandscapeArt;
|
||||
pub use context_menu::ContextMenu;
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ impl Component for ClusterShell {
|
|||
dispatch,
|
||||
launch_state,
|
||||
game.is_running(cluster_id),
|
||||
cluster.game_dir().ok(),
|
||||
)
|
||||
.into_element()
|
||||
});
|
||||
|
|
@ -235,6 +236,7 @@ fn cluster_header(
|
|||
dispatch: crate::BridgeDispatch,
|
||||
launch_state: (&'static str, bool),
|
||||
running: bool,
|
||||
folder: Option<std::path::PathBuf>,
|
||||
) -> impl IntoElement {
|
||||
let (launch_label, launch_enabled) = launch_state;
|
||||
let kill_dispatch = dispatch.clone();
|
||||
|
|
@ -272,6 +274,7 @@ fn cluster_header(
|
|||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(10.)
|
||||
.maybe_child(folder.map(crate::components::open_folder_button))
|
||||
.maybe(running, |el| {
|
||||
el.child(
|
||||
Button::new()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use oneclient_core::{LogFileInfo, LogKind, LogLevel};
|
|||
|
||||
use crate::components::{
|
||||
Button, Icon, IconType, LogViewer, OverlayPopup, ScrollArea, Segment, SegmentedControl,
|
||||
TextInput,
|
||||
TextInput, open_folder_button,
|
||||
};
|
||||
use crate::hooks::{
|
||||
LogAction, UploadLogKeys, UseLogAction, UseUploadLog, invalidate_logs_queries,
|
||||
|
|
@ -159,9 +159,10 @@ impl Component for ClusterLogs {
|
|||
None,
|
||||
);
|
||||
|
||||
let Some(_cluster) = load_cluster(cluster_id) else {
|
||||
let Some(cluster) = load_cluster(cluster_id) else {
|
||||
return cluster_not_found();
|
||||
};
|
||||
let folder = cluster.game_dir().ok().map(|d| d.join("logs"));
|
||||
|
||||
let files = try_cluster_logs(&logs_query).unwrap_or_default();
|
||||
let has_log = !files.is_empty();
|
||||
|
|
@ -190,6 +191,7 @@ impl Component for ClusterLogs {
|
|||
selected_info.clone(),
|
||||
size_text,
|
||||
has_log,
|
||||
folder,
|
||||
selected,
|
||||
confirm,
|
||||
))
|
||||
|
|
@ -214,6 +216,7 @@ fn top_bar(
|
|||
selected_info: Option<LogFileInfo>,
|
||||
size_text: String,
|
||||
has_log: bool,
|
||||
folder: Option<PathBuf>,
|
||||
selected: State<PathBuf>,
|
||||
mut confirm: State<Option<Confirm>>,
|
||||
) -> impl IntoElement {
|
||||
|
|
@ -249,6 +252,7 @@ fn top_bar(
|
|||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(rect().width(Size::flex(1.0)))
|
||||
.maybe_child(folder.map(open_folder_button))
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
|
|
|
|||
|
|
@ -281,6 +281,10 @@ impl Component for PackageManager {
|
|||
let content_type = self.content_type;
|
||||
let dispatch = use_dispatch();
|
||||
|
||||
let folder = super::load_cluster(cluster_id)
|
||||
.and_then(|c| c.game_dir().ok())
|
||||
.map(|dir| dir.join(content_type.folder_name()));
|
||||
|
||||
let total = items.len();
|
||||
let enabled = items.iter().filter(|i| i.enabled).count();
|
||||
|
||||
|
|
@ -325,6 +329,7 @@ impl Component for PackageManager {
|
|||
content_type,
|
||||
cluster_id,
|
||||
dispatch,
|
||||
folder,
|
||||
))
|
||||
.child(toolbar(search, sort, sort_filter, layout))
|
||||
.child(tab_bar(&tabs, &items, active_idx, active))
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ pub(super) fn header(
|
|||
content_type: ContentType,
|
||||
cluster_id: i64,
|
||||
dispatch: BridgeDispatch,
|
||||
folder: Option<std::path::PathBuf>,
|
||||
) -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
|
|
@ -159,6 +160,7 @@ pub(super) fn header(
|
|||
.main_align(Alignment::End)
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.maybe_child(folder.map(crate::components::open_folder_button))
|
||||
.child(add_from_file_button(cluster_id, content_type, dispatch))
|
||||
.child(browse_button(cluster_id, package_type)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use oneclient_core::settings::ViewLayout;
|
|||
|
||||
use crate::components::{
|
||||
Button, ContextMenu, Icon, IconType, LocalImage, OverlayPopup, ScreenshotViewer, ScrollArea,
|
||||
open_folder_button,
|
||||
Segment, SegmentedControl,
|
||||
};
|
||||
use crate::hooks::{
|
||||
|
|
@ -36,9 +37,10 @@ pub struct ClusterScreenshots {
|
|||
|
||||
impl Component for ClusterScreenshots {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let Some(_cluster) = load_cluster(self.cluster_id) else {
|
||||
let Some(cluster) = load_cluster(self.cluster_id) else {
|
||||
return cluster_not_found();
|
||||
};
|
||||
let folder = cluster.game_dir().ok().map(|d| d.join("screenshots"));
|
||||
|
||||
let query = use_cluster_screenshots(self.cluster_id);
|
||||
let action = use_screenshot_action();
|
||||
|
|
@ -55,7 +57,7 @@ impl Component for ClusterScreenshots {
|
|||
let grid_width = use_state(|| 0f32);
|
||||
let mut menu = use_state(|| None::<(f32, f32, PathBuf)>);
|
||||
|
||||
let toolbar = toolbar_row(&shots, view_mode, edit_mode, selected, confirm_delete);
|
||||
let toolbar = toolbar_row(&shots, folder, view_mode, edit_mode, selected, confirm_delete);
|
||||
|
||||
let content: Element = if shots.is_empty() {
|
||||
empty_state(matches!(
|
||||
|
|
@ -194,6 +196,7 @@ impl Component for ClusterScreenshots {
|
|||
|
||||
fn toolbar_row(
|
||||
shots: &[ScreenshotInfo],
|
||||
folder: Option<PathBuf>,
|
||||
view_mode: State<ViewLayout>,
|
||||
mut edit_mode: State<bool>,
|
||||
mut selected: State<HashSet<PathBuf>>,
|
||||
|
|
@ -206,7 +209,8 @@ fn toolbar_row(
|
|||
let mut right = rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(10.);
|
||||
.spacing(10.)
|
||||
.maybe_child((!editing).then_some(folder).flatten().map(open_folder_button));
|
||||
|
||||
if editing {
|
||||
let select_all_paths = all_paths.clone();
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ pub async fn launch_cluster(
|
|||
let post_hook = profile.hook_post.clone();
|
||||
let cluster_name = cluster.name.clone();
|
||||
tokio::spawn(async move {
|
||||
let cluster = cluster;
|
||||
let started = Instant::now();
|
||||
let status = tokio::select! {
|
||||
status = child.wait() => status,
|
||||
|
|
@ -342,6 +343,13 @@ pub async fn launch_cluster(
|
|||
|
||||
run_hook(post_hook.as_deref(), &cwd).await;
|
||||
|
||||
if !dedicated {
|
||||
crate::game::import_manual_content(&state.services, &cluster, &cwd).await;
|
||||
if let Err(err) = crate::game::clear_shared_content(&cwd).await {
|
||||
tracing::warn!(cluster_id, error = %err, "failed to clear shared content on exit");
|
||||
}
|
||||
}
|
||||
|
||||
match status {
|
||||
Ok(status) if status.success() => state
|
||||
.services
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ pub use metadata::{
|
|||
resolve_minecraft_version,
|
||||
};
|
||||
pub use rules::{validate_rule, validate_rules};
|
||||
pub use shared_dir::sync_shared_content;
|
||||
pub use shared_dir::{clear_shared_content, import_manual_content, sync_shared_content};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use oneclient_db::dao::artifact as artifact_dao;
|
||||
|
|
@ -22,6 +23,8 @@ pub async fn sync_shared_content(
|
|||
) -> LauncherResult<()> {
|
||||
tokio::fs::create_dir_all(game_dir).await.ok();
|
||||
|
||||
import_manual_content(services, cluster, game_dir).await;
|
||||
|
||||
for content_type in SWAP_TYPES {
|
||||
let dir = game_dir.join(content_type.folder_name());
|
||||
if dir.exists() {
|
||||
|
|
@ -66,6 +69,102 @@ pub async fn sync_shared_content(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn import_manual_content(services: &LauncherServices, cluster: &Cluster, game_dir: &Path) {
|
||||
let linked = match PackageStore::list_linked_artifacts(cluster.id, services).await {
|
||||
Ok(linked) => linked,
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to list links; skipping manual-content import");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for content_type in SWAP_TYPES {
|
||||
let dir = game_dir.join(content_type.folder_name());
|
||||
let Ok(mut entries) = tokio::fs::read_dir(&dir).await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let known: HashSet<&str> = linked
|
||||
.iter()
|
||||
.filter(|link| link.content_type == content_type)
|
||||
.map(|link| link.cluster_file_name.as_str())
|
||||
.collect();
|
||||
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let Ok(file_type) = entry.file_type().await else {
|
||||
continue;
|
||||
};
|
||||
if !file_type.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if name.starts_with('.') || !has_content_extension(content_type, name) {
|
||||
continue;
|
||||
}
|
||||
if known.contains(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match PackageStore::import_local_file(&path, content_type, cluster.id, services).await {
|
||||
Ok(_) => {
|
||||
tracing::info!(file = name, "registered manually-added shared content")
|
||||
}
|
||||
Err(err) => tracing::warn!(
|
||||
file = name,
|
||||
error = %err,
|
||||
"failed to register manually-added shared content"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_content_extension(content_type: ContentType, name: &str) -> bool {
|
||||
let lower = name.to_lowercase();
|
||||
match content_type {
|
||||
ContentType::Mod => lower.ends_with(".jar"),
|
||||
ContentType::ResourcePack | ContentType::Shader => lower.ends_with(".zip"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
const EMPTY_NOTE_NAME: &str = "WHY_IS_THIS_EMPTY.txt";
|
||||
|
||||
fn empty_note_body(content_type: ContentType) -> String {
|
||||
let noun = match content_type {
|
||||
ContentType::ResourcePack => "resource packs",
|
||||
ContentType::Shader => "shader packs",
|
||||
_ => "mods",
|
||||
};
|
||||
format!(
|
||||
"It's empty here, but nothing is broken!\n\
|
||||
\n\
|
||||
OneClient keeps your {noun} safe somewhere else and only puts them here \
|
||||
while you play. When you close the game, it tidies them away again.\n\
|
||||
\n\
|
||||
Want to add {noun}? The easy way is right inside OneClient. Or you can drop \
|
||||
files in this folder, and OneClient will pick them up the next time you play.\n"
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn clear_shared_content(game_dir: &Path) -> LauncherResult<()> {
|
||||
for content_type in SWAP_TYPES {
|
||||
let dir = game_dir.join(content_type.folder_name());
|
||||
if dir.exists() {
|
||||
tokio::fs::remove_dir_all(&dir).await.ok();
|
||||
}
|
||||
tokio::fs::create_dir_all(&dir).await.ok();
|
||||
tokio::fs::write(dir.join(EMPTY_NOTE_NAME), empty_note_body(content_type))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_fabric_dep_overrides(cluster: &Cluster, game_dir: &Path) -> LauncherResult<()> {
|
||||
let src = cluster.dir()?.join(FABRIC_DEP_OVERRIDES);
|
||||
let dest = game_dir.join(FABRIC_DEP_OVERRIDES);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue