Initial bug fixes (#127)

* Initial bug fixes

* fix compile error on non-mac

* Fix even more bugs

* Fix more

* fix more

* fix build

* fix build

* address review comments
This commit is contained in:
Geometrically
2023-06-02 07:09:46 -07:00
committed by GitHub
parent 9ea548cfe3
commit ee61951698
57 changed files with 3823 additions and 2813 deletions

View File

@@ -13,19 +13,30 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Sync node version and setup cache
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
cache-dependency-path: "theseus_gui"
node-version: 18.x
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: yarn install --immutable --immutable-cache --check-cache
run: pnpm install
- name: Run Lint
run: yarn run lint
run: pnpm lint
- name: Build
run: yarn run build
run: pnpm build

View File

@@ -36,12 +36,27 @@ jobs:
with:
workspaces: './src-tauri -> target'
- name: Sync node version and setup cache
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
cache-dependency-path: "theseus_gui"
node-version: 18.x
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: install dependencies (ubuntu only)
if: startsWith(matrix.platform, 'ubuntu')
@@ -49,8 +64,8 @@ jobs:
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install frontend dependencies
run: yarn install --immutable --immutable-cache --check-cache
- name: Install frontend dependencies
run: pnpm install
- name: build app (macos)
uses: tauri-apps/tauri-action@v0

838
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ use crate::{launcher::auth as inner, State};
use futures::prelude::*;
use tokio::sync::oneshot;
use crate::state::AuthTask;
pub use inner::Credentials;
/// Authenticate a user with Hydra - part 1
@@ -11,8 +12,7 @@ pub use inner::Credentials;
/// to call authenticate and call the flow from the frontend.
/// Visit the URL in a browser, then call and await 'authenticate_await_complete_flow'.
pub async fn authenticate_begin_flow() -> crate::Result<url::Url> {
let st = State::get().await?.clone();
let url = st.auth_flow.write().await.begin_auth().await?;
let url = AuthTask::begin_auth().await?;
Ok(url)
}
@@ -21,12 +21,15 @@ pub async fn authenticate_begin_flow() -> crate::Result<url::Url> {
/// This can be used in conjunction with 'authenticate_begin_flow'
/// to call authenticate and call the flow from the frontend.
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
let st = State::get().await?.clone();
let credentials =
st.auth_flow.write().await.await_auth_completion().await?;
let credentials = AuthTask::await_auth_completion().await?;
Ok(credentials)
}
/// Cancels the active authentication flow
pub async fn cancel_flow() -> crate::Result<()> {
AuthTask::cancel().await
}
/// Authenticate a user with Hydra
/// To run this, you need to first spawn this function as a task, then
/// open a browser to the given URL and finally wait on the spawned future
@@ -38,7 +41,6 @@ pub async fn authenticate(
) -> crate::Result<Credentials> {
let mut flow = inner::HydraAuthFlow::new().await?;
let state = State::get().await?;
let mut users = state.users.write().await;
let url = flow.prepare_login_url().await?;
browser_url.send(url).map_err(|url| {
@@ -48,7 +50,10 @@ pub async fn authenticate(
})?;
let credentials = flow.extract_credentials(&state.fetch_semaphore).await?;
users.insert(&credentials).await?;
{
let mut users = state.users.write().await;
users.insert(&credentials).await?;
}
if state.settings.read().await.default_user.is_none() {
let mut settings = state.settings.write().await;
@@ -69,7 +74,7 @@ pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
let fetch_semaphore = &state.fetch_semaphore;
futures::future::ready(users.get(user).ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to refresh nonexistent user with ID {user}"
"You are not logged in with a Minecraft account!"
))
.as_error()
}))

View File

@@ -79,6 +79,7 @@ enum PackDependency {
#[tracing::instrument]
#[theseus_macros::debug_pin]
pub async fn install_pack_from_version_id(
project_id: String,
version_id: String,
title: String,
icon_url: Option<String>,
@@ -91,7 +92,10 @@ pub async fn install_pack_from_version_id(
None,
None,
icon_url.clone(),
None,
Some(LinkedData {
project_id: Some(project_id),
version_id: Some(version_id.clone()),
}),
Some(true),
)
.await?;
@@ -240,261 +244,277 @@ async fn install_pack(
) -> crate::Result<PathBuf> {
let state = &State::get().await?;
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
let result = async {
let reader: Cursor<&bytes::Bytes> = Cursor::new(&file);
// Create zip reader around file
let mut zip_reader = ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Create zip reader around file
let mut zip_reader =
ZipFileReader::new(reader).await.map_err(|_| {
crate::Error::from(crate::ErrorKind::InputError(
"Failed to read input modpack zip".to_string(),
))
})?;
// Extract index of modrinth.index.json
let zip_index_option = zip_reader
.file()
.entries()
.iter()
.position(|f| f.entry().filename() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let entry = zip_reader
// Extract index of modrinth.index.json
let zip_index_option = zip_reader
.file()
.entries()
.get(zip_index)
.unwrap()
.entry()
.clone();
let mut reader = zip_reader.entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest, &entry).await?;
.iter()
.position(|f| f.entry().filename() == "modrinth.index.json");
if let Some(zip_index) = zip_index_option {
let mut manifest = String::new();
let entry = zip_reader
.file()
.entries()
.get(zip_index)
.unwrap()
.entry()
.clone();
let mut reader = zip_reader.entry(zip_index).await?;
reader.read_to_string_checked(&mut manifest, &entry).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(),
)
.into());
}
let mut game_version = None;
let mut mod_loader = None;
let mut loader_version = None;
for (key, value) in &pack.dependencies {
match key {
PackDependency::Forge => {
mod_loader = Some(ModLoader::Forge);
loader_version = Some(value);
}
PackDependency::FabricLoader => {
mod_loader = Some(ModLoader::Fabric);
loader_version = Some(value);
}
PackDependency::QuiltLoader => {
mod_loader = Some(ModLoader::Quilt);
loader_version = Some(value);
}
PackDependency::Minecraft => game_version = Some(value),
if &*pack.game != "minecraft" {
return Err(crate::ErrorKind::InputError(
"Pack does not support Minecraft".to_string(),
)
.into());
}
}
let game_version = if let Some(game_version) = game_version {
game_version
} else {
return Err(crate::ErrorKind::InputError(
"Pack did not specify Minecraft version".to_string(),
)
.into());
};
let loader_version =
crate::profile_create::get_loader_version_from_loader(
game_version.clone(),
mod_loader.unwrap_or(ModLoader::Vanilla),
loader_version.cloned(),
)
.await?;
crate::api::profile::edit(&profile, |prof| {
prof.metadata.name =
override_title.clone().unwrap_or_else(|| pack.name.clone());
prof.install_stage = ProfileInstallStage::PackInstalling;
prof.metadata.linked_data = Some(LinkedData {
project_id: project_id.clone(),
version_id: version_id.clone(),
});
prof.metadata.icon = icon.clone();
prof.metadata.game_version = game_version.clone();
prof.metadata.loader_version = loader_version.clone();
prof.metadata.loader = mod_loader.unwrap_or(ModLoader::Vanilla);
async { Ok(()) }
})
.await?;
State::sync().await?;
let profile = profile.clone();
let result = async {
let loading_bar = init_or_edit_loading(
existing_loading_bar,
LoadingBarType::PackDownload {
profile_path: profile.clone(),
pack_name: pack.name.clone(),
icon,
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack",
)
.await?;
let num_files = pack.files.len();
use futures::StreamExt;
loading_try_for_each_concurrent(
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>),
None,
Some(&loading_bar),
70.0,
num_files,
None,
|project| {
let profile = profile.clone();
async move {
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
let file = fetch_mirrors(
&project
.downloads
.iter()
.map(|x| &**x)
.collect::<Vec<&str>>(),
project
.hashes
.get(&PackFileHash::Sha1)
.map(|x| &**x),
&state.fetch_semaphore,
)
.await?;
let path = std::path::Path::new(&project.path)
.components()
.next();
if let Some(path) = path {
match path {
Component::CurDir | Component::Normal(_) => {
let path = profile.join(project.path);
write(&path, &file, &state.io_semaphore)
.await?;
}
_ => {}
};
}
Ok(())
let mut game_version = None;
let mut mod_loader = None;
let mut loader_version = None;
for (key, value) in &pack.dependencies {
match key {
PackDependency::Forge => {
mod_loader = Some(ModLoader::Forge);
loader_version = Some(value);
}
},
)
.await?;
PackDependency::FabricLoader => {
mod_loader = Some(ModLoader::Fabric);
loader_version = Some(value);
}
PackDependency::QuiltLoader => {
mod_loader = Some(ModLoader::Quilt);
loader_version = Some(value);
}
PackDependency::Minecraft => game_version = Some(value),
}
}
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))
let game_version = if let Some(game_version) = game_version {
game_version
} else {
return Err(crate::ErrorKind::InputError(
"Pack did not specify Minecraft version".to_string(),
)
.into());
};
let loader_version =
crate::profile_create::get_loader_version_from_loader(
game_version.clone(),
mod_loader.unwrap_or(ModLoader::Vanilla),
loader_version.cloned(),
)
.await?;
crate::api::profile::edit(&profile, |prof| {
prof.metadata.name =
override_title.clone().unwrap_or_else(|| pack.name.clone());
prof.install_stage = ProfileInstallStage::PackInstalling;
prof.metadata.linked_data = Some(LinkedData {
project_id: project_id.clone(),
version_id: version_id.clone(),
});
prof.metadata.icon = icon.clone();
prof.metadata.game_version = game_version.clone();
prof.metadata.loader_version = loader_version.clone();
prof.metadata.loader = mod_loader.unwrap_or(ModLoader::Vanilla);
async { Ok(()) }
})
.await?;
State::sync().await?;
let profile = profile.clone();
let result = async {
let loading_bar = init_or_edit_loading(
existing_loading_bar,
LoadingBarType::PackDownload {
profile_path: profile.clone(),
pack_name: pack.name.clone(),
icon,
pack_id: project_id,
pack_version: version_id,
},
100.0,
"Downloading modpack",
)
.await?;
let mut total_len = 0;
let num_files = pack.files.len();
use futures::StreamExt;
loading_try_for_each_concurrent(
futures::stream::iter(pack.files.into_iter())
.map(Ok::<PackFile, crate::Error>),
None,
Some(&loading_bar),
70.0,
num_files,
None,
|project| {
let profile = profile.clone();
async move {
//TODO: Future update: prompt user for optional files in a modpack
if let Some(env) = project.env {
if env
.get(&EnvType::Client)
.map(|x| x == &SideType::Unsupported)
.unwrap_or(false)
{
return Ok(());
}
}
for index in 0..zip_reader.file().entries().len() {
let file =
zip_reader.file().entries().get(index).unwrap().entry();
let file = fetch_mirrors(
&project
.downloads
.iter()
.map(|x| &**x)
.collect::<Vec<&str>>(),
project
.hashes
.get(&PackFileHash::Sha1)
.map(|x| &**x),
&state.fetch_semaphore,
)
.await?;
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
total_len += 1;
}
}
let path = std::path::Path::new(&project.path)
.components()
.next();
if let Some(path) = path {
match path {
Component::CurDir
| Component::Normal(_) => {
let path = profile.join(project.path);
write(
&path,
&file,
&state.io_semaphore,
)
.await?;
}
_ => {}
};
}
Ok(())
}
},
)
.await?;
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
emit_loading(&loading_bar, 0.0, Some("Extracting overrides"))
.await?;
let file_path = PathBuf::from(file.filename());
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = zip_reader.entry(index).await?;
reader.read_to_end_checked(&mut content, &file).await?;
let mut total_len = 0;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for index in 0..zip_reader.file().entries().len() {
let file =
zip_reader.file().entries().get(index).unwrap().entry();
for component in components {
new_path.push(component);
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
total_len += 1;
}
}
if new_path.file_name().is_some() {
write(
&profile.join(new_path),
&content,
&state.io_semaphore,
for index in 0..zip_reader.file().entries().len() {
let file = zip_reader
.file()
.entries()
.get(index)
.unwrap()
.entry()
.clone();
let file_path = PathBuf::from(file.filename());
if (file.filename().starts_with("overrides")
|| file.filename().starts_with("client_overrides"))
&& !file.filename().ends_with('/')
{
// Reads the file into the 'content' variable
let mut content = Vec::new();
let mut reader = zip_reader.entry(index).await?;
reader.read_to_end_checked(&mut content, &file).await?;
let mut new_path = PathBuf::new();
let components = file_path.components().skip(1);
for component in components {
new_path.push(component);
}
if new_path.file_name().is_some() {
write(
&profile.join(new_path),
&content,
&state.io_semaphore,
)
.await?;
}
emit_loading(
&loading_bar,
30.0 / total_len as f64,
Some(&format!(
"Extracting override {}/{}",
index, total_len
)),
)
.await?;
}
}
emit_loading(
&loading_bar,
30.0 / total_len as f64,
Some(&format!(
"Extracting override {}/{}",
index, total_len
)),
if let Some(profile_val) =
crate::api::profile::get(&profile, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
)
.await?;
}
}
if let Some(profile_val) =
crate::api::profile::get(&profile, None).await?
{
crate::launcher::install_minecraft(
&profile_val,
Some(loading_bar),
)
.await?;
Ok::<PathBuf, crate::Error>(profile.clone())
}
.await;
Ok::<PathBuf, crate::Error>(profile.clone())
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile).await;
Err(err)
}
}
} else {
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
}
.await;
}
.await;
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile).await;
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile).await;
Err(err)
}
Err(err)
}
} else {
let _ = crate::api::profile::remove(&profile).await;
Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)))
}
}

View File

@@ -71,6 +71,8 @@ where
match profiles.0.get_mut(path) {
Some(ref mut profile) => {
action(profile).await?;
emit_profile(
profile.uuid,
profile.path.clone(),
@@ -79,7 +81,7 @@ where
)
.await?;
action(profile).await
Ok(())
}
None => Err(crate::ErrorKind::UnmanagedProfileError(
path.display().to_string(),
@@ -102,6 +104,15 @@ pub async fn edit_icon(
match profiles.0.get_mut(path) {
Some(ref mut profile) => {
profile
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
emit_profile(
profile.uuid,
profile.path.clone(),
@@ -110,14 +121,7 @@ pub async fn edit_icon(
)
.await?;
profile
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await
Ok(())
}
None => Err(crate::ErrorKind::UnmanagedProfileError(
path.display().to_string(),
@@ -265,7 +269,8 @@ pub async fn update_all(
async move {
let new_path =
update_project(profile_path, &project).await?;
update_project(profile_path, &project, Some(true))
.await?;
map.write().await.insert(project, new_path);
@@ -276,6 +281,14 @@ pub async fn update_all(
)
.await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
Ok(Arc::try_unwrap(map).unwrap().into_inner())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
@@ -290,6 +303,7 @@ pub async fn update_all(
pub async fn update_project(
profile_path: &Path,
project_path: &Path,
skip_send_event: Option<bool>,
) -> crate::Result<PathBuf> {
if let Some(profile) = get(profile_path, None).await? {
if let Some(project) = profile.projects.get(project_path) {
@@ -322,6 +336,16 @@ pub async fn update_project(
}
}
if !skip_send_event.unwrap_or(false) {
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
}
return Ok(path);
}
}
@@ -347,6 +371,14 @@ pub async fn add_project_from_version(
if let Some(profile) = get(profile_path, None).await? {
let (path, _) = profile.add_project_version(version_id).await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
Ok(path)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
@@ -379,6 +411,14 @@ pub async fn add_project_from_path(
)
.await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
Ok(path)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
@@ -395,7 +435,17 @@ pub async fn toggle_disable_project(
project: &Path,
) -> crate::Result<PathBuf> {
if let Some(profile) = get(profile, None).await? {
Ok(profile.toggle_disable_project(project).await?)
let res = profile.toggle_disable_project(project).await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
Ok(res)
} else {
Err(crate::ErrorKind::UnmanagedProfileError(
profile.display().to_string(),
@@ -413,6 +463,14 @@ pub async fn remove_project(
if let Some(profile) = get(profile, None).await? {
profile.remove_project(project, None).await?;
emit_profile(
profile.uuid,
profile.path,
&profile.metadata.name,
ProfilePayloadType::Edited,
)
.await?;
Ok(())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(

View File

@@ -96,44 +96,57 @@ pub async fn profile_create(
let path = canonicalize(&path)?;
let mut profile =
Profile::new(uuid, name, game_version, path.clone()).await?;
if let Some(ref icon) = icon {
let bytes = tokio::fs::read(icon).await?;
profile
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
let result = async {
if let Some(ref icon) = icon {
let bytes = tokio::fs::read(icon).await?;
profile
.set_icon(
&state.directories.caches_dir(),
&state.io_semaphore,
bytes::Bytes::from(bytes),
&icon.to_string_lossy(),
)
.await?;
}
profile.metadata.icon_url = icon_url;
if let Some(loader_version) = loader {
profile.metadata.loader = modloader;
profile.metadata.loader_version = Some(loader_version);
}
profile.metadata.linked_data = linked_data;
emit_profile(
uuid,
path.clone(),
&profile.metadata.name,
ProfilePayloadType::Created,
)
.await?;
{
let mut profiles = state.profiles.write().await;
profiles.insert(profile.clone()).await?;
}
if !skip_install_profile.unwrap_or(false) {
crate::launcher::install_minecraft(&profile, None).await?;
}
State::sync().await?;
Ok(path)
}
profile.metadata.icon_url = icon_url;
if let Some(loader_version) = loader {
profile.metadata.loader = modloader;
profile.metadata.loader_version = Some(loader_version);
.await;
match result {
Ok(profile) => Ok(profile),
Err(err) => {
let _ = crate::api::profile::remove(&profile.path).await;
Err(err)
}
}
profile.metadata.linked_data = linked_data;
emit_profile(
uuid,
path.clone(),
&profile.metadata.name,
ProfilePayloadType::Created,
)
.await?;
{
let mut profiles = state.profiles.write().await;
profiles.insert(profile.clone()).await?;
}
if !skip_install_profile.unwrap_or(false) {
crate::launcher::install_minecraft(&profile, None).await?;
}
State::sync().await?;
Ok(path)
}
#[tracing::instrument]

View File

@@ -137,10 +137,6 @@ pub fn get_jvm_arguments(
parsed_arguments.push("-cp".to_string());
parsed_arguments.push(class_paths.to_string());
}
if let Some(minimum) = memory.minimum {
parsed_arguments.push(format!("-Xms{minimum}M"));
}
parsed_arguments.push(format!("-Xmx{}M", memory.maximum));
for arg in custom_args {
if !arg.is_empty() {

View File

@@ -34,7 +34,11 @@ pub fn parse_rule(rule: &d::minecraft::Rule, java_version: &str) -> bool {
Rule {
features: Some(ref features),
..
} => features.has_demo_resolution.unwrap_or(false),
} => {
features.has_demo_resolution.unwrap_or(false)
|| (features.has_demo_resolution.is_none()
&& features.is_demo_user.is_none())
}
_ => false,
};

View File

@@ -13,7 +13,9 @@ impl AuthTask {
AuthTask(None)
}
pub async fn begin_auth(&mut self) -> crate::Result<url::Url> {
pub async fn begin_auth() -> crate::Result<url::Url> {
let state = crate::State::get().await?;
// Creates a channel to receive the URL
let (tx, rx) = tokio::sync::oneshot::channel::<url::Url>();
let task = tokio::spawn(crate::auth::authenticate(tx));
@@ -29,16 +31,20 @@ impl AuthTask {
};
// Flow is going, store in state and return
self.0 = Some(task);
let mut write = state.auth_flow.write().await;
write.0 = Some(task);
Ok(url)
}
pub async fn await_auth_completion(
&mut self,
) -> crate::Result<Credentials> {
pub async fn await_auth_completion() -> crate::Result<Credentials> {
// Gets the task handle from the state, replacing with None
let task = mem::replace(&mut self.0, None);
let task = {
let state = crate::State::get().await?;
let mut write = state.auth_flow.write().await;
mem::replace(&mut write.0, None)
};
// Waits for the task to complete, and returns the credentials
let credentials = task
@@ -49,13 +55,20 @@ impl AuthTask {
Ok(credentials)
}
pub async fn cancel(&mut self) {
pub async fn cancel() -> crate::Result<()> {
// Gets the task handle from the state, replacing with None
let task = mem::replace(&mut self.0, None);
let task = {
let state = crate::State::get().await?;
let mut write = state.auth_flow.write().await;
mem::replace(&mut write.0, None)
};
if let Some(task) = task {
// Cancels the task
task.abort();
}
Ok(())
}
}

View File

@@ -403,14 +403,6 @@ impl Profile {
}
}
emit_profile(
self.uuid,
self.path.clone(),
&self.metadata.name,
ProfilePayloadType::Synced,
)
.await?;
Ok(path)
}

View File

@@ -196,6 +196,7 @@ pub enum ProjectMetadata {
authors: Vec<String>,
version: Option<String>,
icon: Option<PathBuf>,
project_type: Option<String>,
},
Unknown,
}
@@ -484,6 +485,7 @@ pub async fn infer_data_from_files(
.unwrap_or_default(),
version: pack.version.clone(),
icon,
project_type: Some("mod".to_string()),
},
},
);
@@ -544,6 +546,7 @@ pub async fn infer_data_from_files(
authors: pack.author_list.unwrap_or_default(),
version: pack.version,
icon,
project_type: Some("mod".to_string()),
},
},
);
@@ -612,6 +615,7 @@ pub async fn infer_data_from_files(
.collect(),
version: Some(pack.version),
icon,
project_type: Some("mod".to_string()),
},
},
);
@@ -687,6 +691,7 @@ pub async fn infer_data_from_files(
.unwrap_or_default(),
version: Some(pack.version),
icon,
project_type: Some("mod".to_string()),
},
},
);
@@ -735,6 +740,7 @@ pub async fn infer_data_from_files(
authors: Vec::new(),
version: None,
icon,
project_type: None,
},
},
);

View File

@@ -118,17 +118,12 @@ pub enum Theme {
/// Minecraft memory settings
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct MemorySettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<u32>,
pub maximum: u32,
}
impl Default for MemorySettings {
fn default() -> Self {
Self {
minimum: None,
maximum: 2048,
}
Self { maximum: 2048 }
}
}

View File

@@ -33,9 +33,17 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
// Add JRES directly on PATH
jre_paths.extend(get_all_jre_path().await?);
jre_paths.extend(get_all_autoinstalled_jre_path().await?);
if let Ok(java_home) = env::var("JAVA_HOME") {
jre_paths.insert(PathBuf::from(java_home));
}
// Hard paths for locations for commonly installed .exes
let java_paths = [r"C:/Program Files/Java", r"C:/Program Files (x86)/Java"];
let java_paths = [
r"C:/Program Files/Java",
r"C:/Program Files (x86)/Java",
r"C:\Program Files\Eclipse Adoptium",
r"C:\Program Files (x86)\Eclipse Adoptium",
];
for java_path in java_paths {
let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue };
for java_subpath in java_subpaths {
@@ -201,7 +209,6 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
let contents = std::fs::read_to_string(file_path)?;
let entry = entry.path().join(contents);
println!("{:?}", entry);
jre_paths.insert(entry);
}
}

View File

@@ -9,30 +9,32 @@
"preview": "vite preview",
"tauri": "tauri",
"lint:js": "eslint --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue .",
"lint": "npm run lint:js && prettier --check .",
"lint": "pnpm run lint:js && prettier --check .",
"fix": "eslint --fix --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue . && prettier --write ."
},
"dependencies": {
"@tauri-apps/api": "^1.2.0",
"@tauri-apps/api": "^1.3.0",
"dayjs": "^1.11.7",
"floating-vue": "^2.0.0-beta.20",
"ofetch": "^1.0.1",
"omorphia": "^0.4.17",
"pinia": "^2.0.33",
"omorphia": "^0.4.22",
"pinia": "^2.1.3",
"vite-svg-loader": "^4.0.0",
"vue": "^3.2.45",
"vue-multiselect": "^3.0.0-alpha.2",
"vue-router": "4"
"vue": "^3.3.4",
"vue-multiselect": "^3.0.0-beta.2",
"vue-router": "4.2.1"
},
"devDependencies": {
"@rollup/plugin-alias": "^4.0.3",
"@tauri-apps/cli": "^1.2.2",
"@vitejs/plugin-vue": "^4.0.0",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-vue": "^9.9.0",
"prettier": "^2.8.7",
"sass": "^1.58.3",
"vite": "^4.0.0",
"@rollup/plugin-alias": "^4.0.4",
"@tauri-apps/cli": "^1.3.1",
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^9.14.1",
"prettier": "^2.8.8",
"sass": "^1.62.1",
"vite": "^4.3.9",
"vite-plugin-eslint": "^1.8.1"
}
},
"packageManager": "pnpm@8.5.1"
}

1782
theseus_gui/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.2", features = [] }
tauri-build = { version = "1.3", features = [] }
regex = "1.5"
[dependencies]
@@ -19,7 +19,9 @@ theseus = { path = "../../theseus", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["devtools", "dialog", "dialog-open", "protocol-asset", "shell-open", "updater", "window-close", "window-create"] }
tauri = { version = "1.3", features = ["devtools", "dialog", "dialog-open", "protocol-asset", "shell-open", "updater", "window-close", "window-create"] }
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
tokio-stream = { version = "0.1", features = ["fs"] }

View File

@@ -16,6 +16,11 @@ pub async fn auth_authenticate_await_completion() -> Result<Credentials> {
Ok(auth::authenticate_await_complete_flow().await?)
}
#[tauri::command]
pub async fn auth_cancel_flow() -> Result<()> {
Ok(auth::cancel_flow().await?)
}
/// Refresh some credentials using Hydra, if needed
// invoke('auth_refresh',user)
#[tauri::command]

View File

@@ -18,11 +18,7 @@ pub async fn logs_get_logs(
profile_uuid: Uuid,
clear_contents: Option<bool>,
) -> Result<Vec<Logs>> {
use std::time::Instant;
let now = Instant::now();
let val = logs::get_logs(profile_uuid, clear_contents).await?;
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
Ok(val)
}

View File

@@ -12,6 +12,7 @@ pub mod profile;
pub mod profile_create;
pub mod settings;
pub mod tags;
pub mod utils;
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;

View File

@@ -4,13 +4,15 @@ use theseus::prelude::*;
#[tauri::command]
pub async fn pack_install_version_id(
project_id: String,
version_id: String,
pack_title: String,
pack_icon: Option<String>,
) -> Result<PathBuf> {
let res =
pack::install_pack_from_version_id(version_id, pack_title, pack_icon)
.await?;
let res = pack::install_pack_from_version_id(
project_id, version_id, pack_title, pack_icon,
)
.await?;
Ok(res)
}

View File

@@ -88,7 +88,7 @@ pub async fn profile_update_project(
path: &Path,
project_path: &Path,
) -> Result<PathBuf> {
Ok(profile::update_project(path, project_path).await?)
Ok(profile::update_project(path, project_path, None).await?)
}
// Adds a project to a profile from a version ID

View File

@@ -0,0 +1,76 @@
use crate::api::Result;
use std::process::Command;
// cfg only on mac os
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
#[cfg(target_os = "macos")]
#[tauri::command]
pub async fn should_disable_mouseover() -> bool {
// We try to match version to 12.2 or higher. If unrecognizable to pattern or lower, we default to the css with disabled mouseover for safety
let os = os_info::get();
if let os_info::Version::Semantic(major, minor, _) = os.version() {
if *major >= 12 && *minor >= 3 {
// Mac os version is 12.3 or higher, we allow mouseover
return false;
}
}
true
}
#[cfg(not(target_os = "macos"))]
#[tauri::command]
pub async fn should_disable_mouseover() -> bool {
false
}
#[tauri::command]
pub fn show_in_folder(path: String) -> Result<()> {
{
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()?;
}
#[cfg(target_os = "linux")]
{
use std::fs;
use std::fs::metadata;
use std::path::PathBuf;
if path.contains(",") {
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
let new_path = match metadata(&path)?.is_dir() {
true => path.clone(),
false => {
let mut path2 = PathBuf::from(path.clone());
path2.pop();
path2.to_string_lossy().to_string()
}
};
Command::new("xdg-open").arg(&new_path).spawn()?;
} else {
Command::new("dbus-send")
.args([
"--session",
"--dest=org.freedesktop.FileManager1",
"--type=method_call",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
format!("array:string:\"file://{path}\"").as_str(),
"string:\"\"",
])
.spawn()?;
}
}
#[cfg(target_os = "macos")]
{
Command::new("open").args(["-R", &path]).spawn()?;
}
Ok::<(), theseus::Error>(())
}?;
Ok(())
}

View File

@@ -5,6 +5,7 @@
use theseus::prelude::*;
use tauri::Manager;
use tracing_error::ErrorLayer;
use tracing_subscriber::EnvFilter;
@@ -20,29 +21,14 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
Ok(())
}
// cfg only on mac os
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
#[cfg(target_os = "macos")]
#[tauri::command]
async fn should_disable_mouseover() -> bool {
// We try to match version to 12.2 or higher. If unrecognizable to pattern or lower, we default to the css with disabled mouseover for safety
let os = os_info::get();
if let os_info::Version::Semantic(major, minor, _) = os.version() {
if *major >= 12 && *minor >= 3 {
// Mac os version is 12.3 or higher, we allow mouseover
return false;
}
}
true
}
#[cfg(not(target_os = "macos"))]
#[tauri::command]
async fn should_disable_mouseover() -> bool {
false
}
use tracing_subscriber::prelude::*;
#[derive(Clone, serde::Serialize)]
struct Payload {
args: Vec<String>,
cwd: String,
}
fn main() {
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
@@ -70,9 +56,13 @@ fn main() {
.expect("setting default subscriber failed");
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
app.emit_all("single-instance", Payload { args: argv, cwd })
.unwrap();
}))
.plugin(tauri_plugin_window_state::Builder::default().build())
.invoke_handler(tauri::generate_handler![
initialize_state,
should_disable_mouseover,
api::progress_bars_list,
api::profile_create::profile_create_empty,
api::profile_create::profile_create,
@@ -98,6 +88,7 @@ fn main() {
api::pack::pack_install_file,
api::auth::auth_authenticate_begin_flow,
api::auth::auth_authenticate_await_completion,
api::auth::auth_cancel_flow,
api::auth::auth_refresh,
api::auth::auth_remove_user,
api::auth::auth_has_user,
@@ -141,6 +132,8 @@ fn main() {
api::logs::logs_get_stderr_by_datetime,
api::logs::logs_delete_logs,
api::logs::logs_delete_logs_by_datetime,
api::utils::show_in_folder,
api::utils::should_disable_mouseover,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -1,7 +1,7 @@
{
"build": {
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false

View File

@@ -53,6 +53,33 @@ const notificationsWrapper = ref(null)
watch(notificationsWrapper, () => {
notifications.setNotifs(notificationsWrapper.value)
})
// Link handler
document.querySelector('body').addEventListener('click', function (e) {
let target = e.target
while (target != null) {
if (target.matches('a')) {
if (
target.href &&
['http://', 'https://', 'mailto:', 'tel:'].some((v) => target.href.startsWith(v)) &&
!target.classList.contains('router-link-active') &&
!target.href.startsWith('http://localhost') &&
!target.href.startsWith('https://tauri.localhost')
) {
window.__TAURI_INVOKE__('tauri', {
__tauriModule: 'Shell',
message: {
cmd: 'open',
path: target.href,
},
})
e.preventDefault()
}
break
}
target = target.parentElement
}
})
</script>
<template>

View File

@@ -1,5 +1,3 @@
@import 'inter.scss';
:root {
font-family: var(--font-standard);
color-scheme: dark;
@@ -7,10 +5,15 @@
--expanded-view-width: calc(100% - 13rem);
}
body {
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.card-divider {
@@ -41,130 +44,7 @@ a {
}
input {
border: none;
}
.multiselect {
color: var(--color-base) !important;
outline: 2px solid transparent;
max-width: 15rem;
width: 100% !important;
.multiselect__input:focus-visible {
outline: none !important;
box-shadow: none !important;
padding: 0 !important;
min-height: 0 !important;
font-weight: normal !important;
margin-left: 0.5rem;
margin-bottom: 10px;
}
input {
background: transparent;
box-shadow: none;
border: none !important;
&:focus {
box-shadow: none;
}
}
input::placeholder {
color: var(--color-base);
}
.multiselect__tags {
border-radius: var(--radius-md);
background: var(--color-button-bg);
box-shadow: var(--shadow-inset-sm);
border: none;
cursor: pointer;
padding-left: 0.5rem;
font-size: 1rem;
transition: background-color 0.1s ease-in-out;
&:active {
filter: brightness(1.25);
.multiselect__spinner {
filter: brightness(1.25);
}
}
.multiselect__single {
background: transparent;
}
.multiselect__tag {
border-radius: var(--radius-md);
color: var(--color-base);
background: transparent;
border: 2px solid var(--color-brand);
}
.multiselect__tag-icon {
background: transparent;
&:after {
color: var(--color-contrast);
}
}
.multiselect__placeholder {
color: var(--color-base);
margin-left: 0.5rem;
opacity: 0.6;
font-size: 1rem;
line-height: 1.25rem;
}
}
.multiselect__content-wrapper {
background: var(--color-button-bg);
border: none;
overflow-x: hidden;
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
width: 100%;
.multiselect__element {
.multiselect__option--highlight {
background: var(--color-button-bg);
filter: brightness(1.25);
color: var(--color-contrast);
}
.multiselect__option--selected {
background: var(--color-brand);
font-weight: bold;
color: var(--color-accent-contrast);
}
}
}
.multiselect__spinner {
background: var(--color-button-bg);
&:active {
filter: brightness(1.25);
}
}
&.multiselect--disabled {
background: none;
.multiselect__current,
.multiselect__select {
background: none;
}
}
}
.multiselect--above .multiselect__content-wrapper {
border-top: none !important;
border-top-left-radius: var(--radius-md) !important;
border-top-right-radius: var(--radius-md) !important;
border: none !important;
}
.mod-text {
@@ -173,7 +53,3 @@ input {
gap: 1rem;
color: var(--color-contrast);
}
input {
border: none !important;
}

View File

@@ -1,41 +0,0 @@
// TODO: move to omorphia
@font-face {
font-family: inter;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff2?v=3.19') format('woff2'),
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Regular.woff?v=3.19') format('woff');
}
@font-face {
font-family: inter;
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff2?v=3.19') format('woff2'),
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Medium.woff?v=3.19') format('woff');
}
@font-face {
font-family: inter;
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff2?v=3.19') format('woff2'),
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-SemiBold.woff?v=3.19') format('woff');
}
@font-face {
font-family: inter;
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff2?v=3.19') format('woff2'),
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-Bold.woff?v=3.19') format('woff');
}
@font-face {
font-family: inter;
font-style: normal;
font-weight: 800;
font-display: swap;
src: url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff2?v=3.19') format('woff2'),
url('https://cdn-raw.modrinth.com/fonts/inter/Inter-ExtraBold.woff?v=3.19') format('woff');
}

View File

@@ -1,6 +1,5 @@
<script setup>
import Instance from '@/components/ui/Instance.vue'
import { ref } from 'vue'
const props = defineProps({
instances: {
@@ -9,19 +8,11 @@ const props = defineProps({
return []
},
},
news: {
type: Array,
default() {
return []
},
},
label: {
type: String,
default: '',
},
canPaginate: Boolean,
})
const modsRow = ref(null)
</script>
<template>
<div class="row">
@@ -29,7 +20,7 @@ const modsRow = ref(null)
<p>{{ props.label }}</p>
<hr />
</div>
<section ref="modsRow" class="instances">
<section class="instances">
<Instance
v-for="instance in props.instances"
:key="instance.id"
@@ -56,6 +47,7 @@ const modsRow = ref(null)
gap: 1rem;
p {
margin: 0;
font-size: 1rem;
white-space: nowrap;
color: var(--color-contrast);
@@ -101,7 +93,6 @@ const modsRow = ref(null)
width: 100%;
gap: 1rem;
margin-right: auto;
margin-top: 0.8rem;
scroll-behavior: smooth;
overflow-y: auto;
}

View File

@@ -42,16 +42,10 @@ onMounted(() => {
handlePaginationDisplay()
})
onUnmounted(() => {
if (props.canPaginate) window.removeEventListener('resize', handlePaginationDisplay)
})
const handleLeftPage = () => {
modsRow.value.scrollLeft -= 170
}
const handleRightPage = () => {
modsRow.value.scrollLeft += 170
}
</script>
<template>
<div v-if="props.instances.length > 0" class="row">
@@ -59,8 +53,8 @@ const handleRightPage = () => {
<p>{{ props.label }}</p>
<hr aria-hidden="true" />
<div v-if="allowPagination" class="pagination">
<ChevronLeftIcon role="button" @click="handleLeftPage" />
<ChevronRightIcon role="button" @click="handleRightPage" />
<ChevronLeftIcon role="button" @click="modsRow.value.scrollLeft -= 170" />
<ChevronRightIcon role="button" @click="modsRow.value.scrollLeft += 170" />
</div>
</div>
<section ref="modsRow" class="instances">
@@ -95,6 +89,7 @@ const handleRightPage = () => {
gap: 1rem;
p {
margin: 0;
font-size: 1rem;
white-space: nowrap;
color: var(--color-contrast);
@@ -140,11 +135,14 @@ const handleRightPage = () => {
width: 100%;
gap: 1rem;
margin-right: auto;
margin-top: 0.8rem;
scroll-behavior: smooth;
overflow-x: scroll;
overflow-y: hidden;
:deep(.instance-card-item) {
margin-bottom: 0.1rem;
}
&::-webkit-scrollbar {
width: 0px;
background: transparent;

View File

@@ -10,7 +10,7 @@
<div class="text no-select">
{{ selectedAccount ? selectedAccount.username : 'Offline' }}
</div>
<p class="no-select">
<p class="accounts-text no-select">
<UsersIcon />
Accounts
</p>
@@ -24,13 +24,13 @@
<h4>{{ selectedAccount.username }}</h4>
<p>Selected</p>
</div>
<Button icon-only color="raised" @click="logout(selectedAccount.id)">
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
<XIcon />
</Button>
</div>
<div v-else class="logged-out account">
<h4>Not signed in</h4>
<Button icon-only color="primary" @click="login()">
<Button v-tooltip="'Log in'" icon-only color="primary" @click="login()">
<LogInIcon />
</Button>
</div>
@@ -40,7 +40,7 @@
<Avatar :src="account.profile_picture" class="icon" />
<p>{{ account.username }}</p>
</Button>
<Button icon-only @click="logout(account.id)">
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
<XIcon />
</Button>
</div>
@@ -79,7 +79,7 @@ const appendProfiles = (accounts) => {
return accounts.map((account) => {
return {
...account,
profile_picture: `https://crafthead.net/helm/${account.id.replace(/-/g, '')}/128`,
profile_picture: `https://mc-heads.net/avatar/${account.id}/128`,
}
})
}
@@ -187,6 +187,11 @@ onBeforeUnmount(() => {
align-items: center;
text-align: left;
padding: 0.5rem 1rem;
h4,
p {
margin: 0;
}
}
.account-card {
@@ -287,4 +292,11 @@ onBeforeUnmount(() => {
overflow: hidden;
text-overflow: ellipsis;
}
.accounts-text {
display: flex;
align-items: center;
gap: 0.25rem;
margin: 0;
}
</style>

View File

@@ -5,13 +5,15 @@ import { ref } from 'vue'
const version = ref('')
const title = ref('')
const projectId = ref('')
const icon = ref('')
const confirmModal = ref(null)
const installing = ref(false)
defineExpose({
show: (id, projectTitle, projectIcon) => {
show: (id, projectId, projectTitle, projectIcon) => {
version.value = id
projectId.value = projectId
title.value = projectTitle
icon.value = projectIcon
confirmModal.value.show()
@@ -20,7 +22,7 @@ defineExpose({
async function install() {
installing.value = true
await pack_install(version.value, title.value, icon.value ? icon.value : null)
await pack_install(projectId.value, version.value, title.value, icon.value ? icon.value : null)
confirmModal.value.hide()
}
</script>
@@ -28,10 +30,8 @@ async function install() {
<template>
<Modal ref="confirmModal" header="Are you sure?">
<div class="modal-body">
<p>
This project is already installed on your system. Are you sure you want to install it again?
</p>
<div class="button-group">
<p>You already have this modpack installed. Are you sure you want to install it again?</p>
<div class="input-group push-right">
<Button @click="() => $refs.confirmModal.hide()"><XIcon />Cancel</Button>
<Button color="primary" :disabled="installing" @click="install()"
><DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}</Button
@@ -48,11 +48,4 @@ async function install() {
gap: 1rem;
padding: 1rem;
}
.button-group {
display: flex;
flex-direction: row;
gap: 0.5rem;
justify-content: flex-end;
}
</style>

View File

@@ -1,9 +1,8 @@
<script setup>
import { onUnmounted, ref, useSlots } from 'vue'
import { onUnmounted, ref, useSlots, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Card, DownloadIcon, XIcon, Avatar, AnimatedLogo, PlayIcon } from 'omorphia'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import { install as pack_install } from '@/helpers/pack'
import { run, list } from '@/helpers/profile'
import {
@@ -14,6 +13,8 @@ import {
import { process_listener } from '@/helpers/events'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/state.js'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
const props = defineProps({
instance: {
@@ -29,10 +30,20 @@ const props = defineProps({
})
const confirmModal = ref(null)
const modInstallModal = ref(null)
const playing = ref(false)
const uuid = ref(null)
const modLoading = ref(false)
const modLoading = ref(
props.instance.install_stage ? props.instance.install_stage !== 'installed' : false
)
watch(props.instance, () => {
modLoading.value = props.instance.install_stage
? props.instance.install_stage !== 'installed'
: false
})
const slots = useSlots()
const router = useRouter()
@@ -74,21 +85,26 @@ const install = async (e) => {
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === props.instance.project_id)
) {
try {
modLoading.value = true
await pack_install(versions[0].id, props.instance.title, props.instance.icon_url).catch(
handleError
)
modLoading.value = false
} catch (err) {
console.error(err)
modLoading.value = false
}
} else confirmModal.value.show(versions[0].id, props.instance.title, props.instance.icon_url)
modLoading.value = true
await pack_install(
props.instance.project_id,
versions[0].id,
props.instance.title,
props.instance.icon_url
).catch(handleError)
modLoading.value = false
} else
confirmModal.value.show(
props.instance.project_id,
versions[0].id,
props.instance.title,
props.instance.icon_url
)
} else {
modInstallModal.value.show(props.instance.project_id, versions)
}
modLoading.value = false
// TODO: Add condition for installing a mod
}
const play = async (e) => {
@@ -103,21 +119,14 @@ const stop = async (e) => {
e.stopPropagation()
playing.value = false
try {
// If we lost the uuid for some reason, such as a user navigating
// from-then-back to this page, we will get all uuids by the instance path.
// For-each uuid, kill the process.
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(props.instance.path).catch(handleError)
uuid.value = uuids[0]
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError) // If we still have the uuid, just kill it
} catch (err) {
// Theseus currently throws:
// "Error launching Minecraft: Minecraft exited with non-zero code 1" error
// For now, we will catch and just warn
console.warn(err)
}
// If we lost the uuid for some reason, such as a user navigating
// from-then-back to this page, we will get all uuids by the instance path.
// For-each uuid, kill the process.
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(props.instance.path).catch(handleError)
uuid.value = uuids[0]
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError) // If we still have the uuid, just kill it
uuid.value = null
}
@@ -206,9 +215,10 @@ onUnmounted(() => unlisten())
>
<XIcon />
</div>
<div v-else class="install cta buttonbase" @click="install"><DownloadIcon /></div>
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
</template>
<InstallConfirmModal ref="confirmModal" />
<InstanceInstallModal ref="modInstallModal" />
</div>
</template>
@@ -276,7 +286,7 @@ onUnmounted(() => unlisten())
&:hover {
.cta {
opacity: 1;
bottom: 4.5rem;
bottom: 5.5rem;
}
.instance-card-item {
@@ -312,11 +322,12 @@ onUnmounted(() => unlisten())
z-index: 1;
width: 3rem;
height: 3rem;
right: 1rem;
bottom: 3.5rem;
right: 1.25rem;
bottom: 5rem;
opacity: 0;
transition: 0.3s ease-in-out bottom, 0.1s ease-in-out opacity !important;
transition: 0.2s ease-in-out bottom, 0.1s ease-in-out opacity, 0.1s ease-in-out filter !important;
cursor: pointer;
box-shadow: var(--shadow-floating);
svg {
color: var(--color-accent-contrast);
@@ -324,11 +335,6 @@ onUnmounted(() => unlisten())
height: 1.5rem !important;
}
&:hover {
filter: none !important; /* overrides button-base class */
box-shadow: var(--shadow-floating);
}
&.install {
background: var(--color-brand);
display: flex;
@@ -400,6 +406,8 @@ onUnmounted(() => unlisten())
line-height: 125%;
margin: 0.25rem 0 0;
text-transform: capitalize;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

View File

@@ -1,6 +1,6 @@
<template>
<Modal ref="modal" header="Create instance">
<div v-if="showContent" class="modal-body">
<div class="modal-body">
<div class="image-upload">
<Avatar :src="display_icon" size="md" :rounded="true" />
<div class="image-input">
@@ -16,7 +16,7 @@
</div>
<div class="input-row">
<p class="input-label">Name</p>
<input v-model="profile_name" class="text-input" type="text" />
<input v-model="profile_name" autocomplete="off" class="text-input" type="text" />
</div>
<div class="input-row">
<p class="input-label">Loader</p>
@@ -25,7 +25,16 @@
<div class="input-row">
<p class="input-label">Game version</p>
<div class="versions">
<DropdownSelect v-model="game_version" :options="game_versions" render-up />
<multiselect
v-model="game_version"
class="selector"
:options="game_versions"
:multiple="false"
:searchable="true"
placeholder="Select game version"
open-direction="top"
:show-labels="false"
/>
<Checkbox
v-if="showAdvanced"
v-model="showSnapshots"
@@ -41,17 +50,21 @@
<div v-if="showAdvanced && loader_version === 'other' && loader !== 'vanilla'">
<div v-if="game_version" class="input-row">
<p class="input-label">Select version</p>
<DropdownSelect
<multiselect
v-model="specified_loader_version"
class="selector"
:options="selectable_versions"
render-up
:searchable="true"
placeholder="Select loader version"
open-direction="top"
:show-labels="false"
/>
</div>
<div v-else class="input-row">
<p class="warning">Select a game version before you select a loader version</p>
</div>
</div>
<div class="button-group">
<div class="input-group push-right">
<Button @click="toggle_advanced">
<CodeIcon />
{{ showAdvanced ? 'Hide advanced' : 'Show advanced' }}
@@ -74,7 +87,6 @@ import {
Avatar,
Button,
Chips,
DropdownSelect,
Modal,
PlusIcon,
UploadIcon,
@@ -83,19 +95,24 @@ import {
Checkbox,
} from 'omorphia'
import { computed, ref, shallowRef } from 'vue'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile'
import { open } from '@tauri-apps/api/dialog'
import { tauri } from '@tauri-apps/api'
import { get_fabric_versions, get_forge_versions, get_quilt_versions } from '@/helpers/metadata'
import {
get_game_versions,
get_fabric_versions,
get_forge_versions,
get_quilt_versions,
} from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect'
const profile_name = ref('')
const game_version = ref('')
const loader = ref('vanilla')
const loader_version = ref('stable')
const specified_loader_version = ref('')
const showContent = ref(false)
const icon = ref(null)
const display_icon = ref(null)
const showAdvanced = ref(false)
@@ -104,22 +121,17 @@ const showSnapshots = ref(false)
defineExpose({
show: () => {
showContent.value = false
modal.value.show()
game_version.value = ''
specified_loader_version.value = ''
profile_name.value = ''
creating.value = false
showAdvanced.value = false
showSnapshots.value = false
loader.value = ''
loader.value = 'vanilla'
loader_version.value = 'stable'
icon.value = null
display_icon.value = null
setTimeout(() => {
showContent.value = true
}, 100)
modal.value.show()
},
})
@@ -138,53 +150,50 @@ const [fabric_versions, forge_versions, quilt_versions, all_game_versions, loade
.then(ref)
.catch(handleError),
])
loaders.value.push('vanilla')
loaders.value.unshift('vanilla')
const game_versions = computed(() => {
return all_game_versions.value
return all_game_versions.value.versions
.filter((item) => {
let defaultVal = item.version_type === 'release' || showSnapshots.value
let defaultVal = item.type === 'release' || showSnapshots.value
if (loader.value === 'fabric') {
defaultVal &= fabric_versions.value.gameVersions.some((x) => item.version === x.id)
defaultVal &= fabric_versions.value.gameVersions.some((x) => item.id === x.id)
} else if (loader.value === 'forge') {
defaultVal &= forge_versions.value.gameVersions.some((x) => item.version === x.id)
defaultVal &= forge_versions.value.gameVersions.some((x) => item.id === x.id)
} else if (loader.value === 'quilt') {
defaultVal &= quilt_versions.value.gameVersions.some((x) => item.version === x.id)
defaultVal &= quilt_versions.value.gameVersions.some((x) => item.id === x.id)
}
return defaultVal
})
.map((item) => item.version)
.map((item) => item.id)
})
const modal = ref(null)
const check_valid = computed(() => {
return (
profile_name.value && game_version.value && game_versions.value.includes(game_version.value)
profile_name.value.trim() &&
game_version.value &&
game_versions.value.includes(game_version.value)
)
})
const create_instance = async () => {
try {
creating.value = true
const loader_version_value =
loader_version.value === 'other' ? specified_loader_version.value : loader_version.value
creating.value = true
const loader_version_value =
loader_version.value === 'other' ? specified_loader_version.value : loader_version.value
await create(
profile_name.value,
game_version.value,
loader.value,
loader.value === 'vanilla' ? null : loader_version_value ?? 'stable',
icon.value
).catch(handleError)
create(
profile_name.value,
game_version.value,
loader.value,
loader.value === 'vanilla' ? null : loader_version_value ?? 'stable',
icon.value
).catch(handleError)
modal.value.hide()
creating.value = false
} catch (e) {
console.error(e)
creating.value = false
}
modal.value.hide()
creating.value = false
}
const upload_icon = async () => {
@@ -246,12 +255,6 @@ const toggle_advanced = () => {
width: 20rem;
}
.button-group {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.image-upload {
display: flex;
gap: 1rem;
@@ -277,4 +280,8 @@ const toggle_advanced = () => {
:deep(button.checkbox) {
border: none;
}
.selector {
max-width: 20rem;
}
</style>

View File

@@ -143,9 +143,15 @@ const check_valid = computed(() => {
</script>
<template>
<Modal ref="installModal" header="Install mod to instance">
<Modal ref="installModal" header="Install project to instance">
<div class="modal-body">
<input v-model="searchFilter" type="text" class="search" placeholder="Search for a profile" />
<input
v-model="searchFilter"
autocomplete="off"
type="text"
class="search"
placeholder="Search for an instance"
/>
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
<div v-for="profile in profiles" :key="profile.metadata.name" class="option">
<Button
@@ -181,7 +187,13 @@ const check_valid = computed(() => {
</div>
</div>
<div class="creation-settings">
<input v-model="name" type="text" placeholder="Name" class="creation-input" />
<input
v-model="name"
autocomplete="off"
type="text"
placeholder="Name"
class="creation-input"
/>
<Button :disabled="creatingInstance === true || !check_valid" @click="createInstance()">
<RightArrowIcon />
{{ creatingInstance ? 'Creating...' : 'Create' }}
@@ -189,7 +201,7 @@ const check_valid = computed(() => {
</div>
</div>
</Card>
<div class="footer">
<div class="input-group push-right">
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
<PlusIcon />
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
@@ -302,12 +314,4 @@ const check_valid = computed(() => {
.profile-image {
--size: 2rem !important;
}
.footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 0.5rem;
margin-left: auto;
}
</style>

View File

@@ -11,7 +11,7 @@
<div class="table-cell table-text">
<span>{{ javaInstall.version }}</span>
</div>
<div class="table-cell table-text">
<div v-tooltip="javaInstall.path" class="table-cell table-text">
<span>{{ javaInstall.path }}</span>
</div>
<div class="table-cell table-text manage">
@@ -22,10 +22,10 @@
</div>
</div>
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
<div class="table-cell table-text">No JARS Found!</div>
<div class="table-cell table-text">No java installations found!</div>
</div>
</div>
<div class="button-group">
<div class="input-group push-right">
<Button @click="$refs.detectJavaModal.hide()">
<XIcon />
Cancel
@@ -96,13 +96,6 @@ function setJavaInstall(javaInstall) {
}
}
.button-group {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.manage {
display: flex;
gap: 0.5rem;

View File

@@ -2,6 +2,7 @@
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<div class="toggle-setting">
<input
autocomplete="off"
:disabled="props.disabled"
:value="props.modelValue ? props.modelValue.path : ''"
type="text"
@@ -28,30 +29,25 @@
<FolderSearchIcon />
Browse
</Button>
<Button :disabled="props.disabled" @click="testJava">
<Button v-if="testingJava" disabled> Testing... </Button>
<Button v-else-if="testingJavaSuccess === true">
<CheckIcon class="test-success" />
Success
</Button>
<Button v-else-if="testingJavaSuccess === false">
<XIcon class="test-fail" />
Failed
</Button>
<Button v-else :disabled="props.disabled" @click="testJava">
<PlayIcon />
Test
</Button>
<AnimatedLogo v-if="testingJava === true" class="testing-loader" />
<CheckIcon
v-else-if="testingJavaSuccess === true && testingJava === false"
class="test-success"
/>
<XIcon v-else-if="testingJavaSuccess === false && testingJava === false" class="test-fail" />
</span>
</div>
</template>
<script setup>
import {
Button,
SearchIcon,
PlayIcon,
CheckIcon,
XIcon,
AnimatedLogo,
FolderSearchIcon,
} from 'omorphia'
import { Button, SearchIcon, PlayIcon, CheckIcon, XIcon, FolderSearchIcon } from 'omorphia'
import { get_jre } from '@/helpers/jre.js'
import { ref } from 'vue'
import { open } from '@tauri-apps/api/dialog'
@@ -143,14 +139,3 @@ async function handleJavaFileInput() {
color: var(--color-red);
}
</style>
<style lang="scss">
.testing-loader {
height: 1rem !important;
width: 1rem !important;
svg {
height: inherit !important;
width: inherit !important;
}
}
</style>

View File

@@ -4,10 +4,10 @@
<span class="running-text">
{{ currentProcesses[0].metadata.name }}
</span>
<Button icon-only class="icon-button stop" @click="stop()">
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop()">
<StopCircleIcon />
</Button>
<Button icon-only class="icon-button" @click="goToTerminal()">
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
<TerminalSquareIcon />
</Button>
<Button
@@ -22,7 +22,7 @@
</div>
<div v-else class="status">
<span class="circle stopped" />
<span class="running-text"> No running profiles </span>
<span class="running-text"> No running instances </span>
<Button
v-if="currentLoadingBars.length > 0"
ref="infoButton"

View File

@@ -140,7 +140,7 @@ async function install() {
queuedVersionData = versions.find(
(v) =>
v.game_versions.includes(props.instance.metadata.game_version) &&
v.loaders.includes(props.instance.metadata.loader)
(props.project.project_type !== 'mod' || v.loaders.includes(props.instance.metadata.loader))
)
}
@@ -152,11 +152,19 @@ async function install() {
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === props.project.project_id)
) {
await packInstall(queuedVersionData.id, props.project.title, props.project.icon_url).catch(
handleError
)
await packInstall(
props.project.project_id,
queuedVersionData.id,
props.project.title,
props.project.icon_url
).catch(handleError)
} else {
props.confirmModal.show(queuedVersionData.id)
props.confirmModal.show(
props.project.project_id,
queuedVersionData.id,
props.project.title,
props.project.icon_url
)
}
} else {
if (props.instance) {

View File

@@ -35,6 +35,8 @@ defineProps({
svg {
width: 12rem;
height: 12rem;
fill: var(--color-brand);
color: var(--color-brand);
}
}
</style>

View File

@@ -28,6 +28,10 @@ export async function authenticate_await_completion() {
return await invoke('auth_authenticate_await_completion')
}
export async function cancel_flow() {
return await invoke('auth_cancel_flow')
}
/// Refresh some credentials using Hydra, if needed
/// user is UUID
/// update_name is bool

View File

@@ -6,5 +6,6 @@ export const useFetch = async (url, item) => {
return await ofetch(url)
} catch (err) {
handleError({ message: `Error fetching ${item}` })
console.error(err)
}
}

View File

@@ -6,8 +6,8 @@
import { invoke } from '@tauri-apps/api/tauri'
// Installs pack from a version ID
export async function install(versionId, packTitle, packIcon) {
return await invoke('pack_install_version_id', { versionId, packTitle, packIcon })
export async function install(projectId, versionId, packTitle, packIcon) {
return await invoke('pack_install_version_id', { projectId, versionId, packTitle, packIcon })
}
// Installs pack from a path

View File

@@ -1,6 +1,11 @@
import { add_project_from_version as installMod, check_installed } from '@/helpers/profile'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import { invoke } from '@tauri-apps/api/tauri'
export async function showInFolder(path) {
return await invoke('show_in_folder', { path })
}
export const releaseColor = (releaseType) => {
switch (releaseType) {
@@ -17,11 +22,20 @@ export const releaseColor = (releaseType) => {
export const installVersionDependencies = async (profile, version) => {
for (const dep of version.dependencies) {
if (dep.dependency_type !== 'required') continue
if (dep.version_id) {
if (await check_installed(profile.path, dep.project_id).catch(handleError)) continue
if (
dep.project_id &&
(await check_installed(profile.path, dep.project_id).catch(handleError))
)
continue
await installMod(profile.path, dep.version_id)
} else {
if (await check_installed(profile.path, dep.project_id).catch(handleError)) continue
if (
dep.project_id &&
(await check_installed(profile.path, dep.project_id).catch(handleError))
)
continue
const depVersions = await useFetch(
`https://api.modrinth.com/v2/project/${dep.project_id}/version`,
'dependency versions'

View File

@@ -4,6 +4,7 @@ import App from '@/App.vue'
import { createPinia } from 'pinia'
import 'omorphia/dist/style.css'
import '@/assets/stylesheets/global.scss'
import 'floating-vue/dist/style.css'
import FloatingVue from 'floating-vue'
import { initialize_state } from '@/helpers/state'
import loadCssMixin from './mixins/macCssFix.js'

View File

@@ -150,6 +150,25 @@ const handleInstanceSwitch = async (value) => {
searchStore.ignoreInstance = value
await switchPage(1)
}
const selectableProjectTypes = computed(() => {
const values = [
{ label: 'Data Packs', href: `/browse/datapack` },
{ label: 'Shaders', href: `/browse/shader` },
{ label: 'Resource Packs', href: `/browse/resourcepack` },
]
if (searchStore.instanceContext) {
if (searchStore.instanceContext.metadata.loader !== 'vanilla') {
values.unshift({ label: 'Mods', href: '/browse/mod' })
}
} else {
values.unshift({ label: 'Mods', href: '/browse/mod' })
values.unshift({ label: 'Modpacks', href: '/browse/modpack' })
}
return values
})
</script>
<template>
@@ -160,7 +179,7 @@ const handleInstanceSwitch = async (value) => {
<Checkbox
:model-value="searchStore.ignoreInstance"
:checked="searchStore.ignoreInstance"
label="Unfilter loader & version"
label="Show unsupported content"
class="filter-checkbox"
@update:model-value="(value) => handleInstanceSwitch(value)"
/>
@@ -277,30 +296,14 @@ const handleInstanceSwitch = async (value) => {
<div class="search">
<Promotion class="promotion" />
<Card class="project-type-container">
<NavRow
:links="
searchStore.instanceContext
? [
{ label: 'Mods', href: `/browse/mod` },
{ label: 'Datapacks', href: `/browse/datapack` },
{ label: 'Shaders', href: `/browse/shader` },
{ label: 'Resource Packs', href: `/browse/resourcepack` },
]
: [
{ label: 'Modpacks', href: '/browse/modpack' },
{ label: 'Mods', href: '/browse/mod' },
{ label: 'Datapacks', href: '/browse/datapack' },
{ label: 'Shaders', href: '/browse/shader' },
{ label: 'Resource Packs', href: '/browse/resourcepack' },
]
"
/>
<NavRow :links="selectableProjectTypes" />
</Card>
<Card class="search-panel-container">
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="searchStore.searchInput"
autocomplete="off"
type="text"
:placeholder="`Search ${searchStore.projectType}s...`"
@input="getSearchResults"

View File

@@ -20,18 +20,20 @@ breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const recentInstances = shallowRef([])
const getInstances = async () => {
filter.value = ''
const profiles = await list(true).catch(handleError)
recentInstances.value = Object.values(profiles)
const excludeIds = recentInstances.value.map((i) => i.metadata?.linked_data?.project_id)
excludeIds.forEach((id, index) => {
filter.value += `NOT"project_id"="${id}"`
if (index < excludeIds.length - 1) filter.value += ' AND '
})
let filters = []
for (const instance of recentInstances.value) {
if (instance.metadata.linked_data && instance.metadata.linked_data.project_id) {
filters.push(`NOT"project_id"="${instance.metadata.linked_data.project_id}"`)
}
}
filter.value = filters.join(' AND ')
}
const getFeaturedModpacks = async () => {
console.log(filter.value)
const response = await useFetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
'featured modpacks'
@@ -40,7 +42,7 @@ const getFeaturedModpacks = async () => {
}
const getFeaturedMods = async () => {
const response = await useFetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows&filters=${filter.value}`,
'https://api.modrinth.com/v2/search?facets=[["project_type:mod"]]&limit=10&index=follows',
'featured mods'
)
featuredMods.value = response.hits

View File

@@ -53,15 +53,20 @@ watch(settings.value, async (oldSettings, newSettings) => {
</script>
<template>
<div>
<Card class="theming">
<h2>Display</h2>
<div class="toggle-setting">
<div class="description">
<h3>Color theme</h3>
<p>Change the global launcher color theme.</p>
</div>
<div class="settings-page">
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Display</span>
</h3>
</div>
<div class="adjacent-input">
<label for="theme">
<span class="label__title">Color theme</span>
<span class="label__description">Change the global launcher color theme.</span>
</label>
<DropdownSelect
id="theme"
name="Theme dropdown"
:options="themeStore.themeOptions"
:default-value="settings.theme"
@@ -75,12 +80,15 @@ watch(settings.value, async (oldSettings, newSettings) => {
"
/>
</div>
<div class="toggle-setting">
<div class="description">
<h3>Collapsed navigation mode</h3>
<p>Change the style of the side navigation bar</p>
</div>
<div class="adjacent-input">
<label for="collapsed-nav">
<span class="label__title">Collapsed navigation mode</span>
<span class="label__description"
>Change the style of the side navigation bar to a compact version.</span
>
</label>
<Toggle
id="collapsed-nav"
:model-value="themeStore.collapsedNavigation"
:checked="themeStore.collapsedNavigation"
@update:model-value="
@@ -92,116 +100,187 @@ watch(settings.value, async (oldSettings, newSettings) => {
/>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Launcher settings</h2>
<div class="settings-group">
<h3>Resource management</h3>
<div class="toggle-setting">
<span>Maximum concurrent downloads</span>
<Slider
v-model="settings.max_concurrent_downloads"
class="concurrent-downloads"
:min="1"
:max="100"
:step="1"
/>
</div>
<div class="toggle-setting">
<span>Maximum concurrent writes</span>
<Slider
v-model="settings.max_concurrent_writes"
class="concurrent-downloads"
:min="1"
:max="100"
:step="1"
/>
</div>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Resource management</span>
</h3>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Java</h2>
<div class="settings-group">
<h3>Java 17 location</h3>
<JavaSelector v-model="settings.java_globals.JAVA_17" :version="17" />
</div>
<div class="settings-group">
<h3>Java 8 location</h3>
<JavaSelector v-model="settings.java_globals.JAVA_8" :version="8" />
</div>
<hr class="card-divider" />
<div class="settings-group">
<h3>Java arguments</h3>
<input
v-model="settings.javaArgs"
type="text"
class="input installation-input"
placeholder="Enter java arguments..."
<div class="adjacent-input">
<label for="max-downloads">
<span class="label__title">Maximum concurrent downloads</span>
<span class="label__description"
>The maximum amount of files the launcher can download at the same time. Set this to a
lower value if you have a poor internet connection.</span
>
</label>
<Slider
id="max-downloads"
v-model="settings.max_concurrent_downloads"
:min="1"
:max="100"
:step="1"
/>
</div>
<div class="settings-group">
<h3>Environment variables</h3>
<input
v-model="settings.envArgs"
type="text"
class="input installation-input"
placeholder="Enter environment variables..."
<div class="adjacent-input">
<label for="max-writes">
<span class="label__title">Maximum concurrent writes</span>
<span class="label__description"
>The maximum amount of files the launcher can write to the disk at once. Set this to a
lower value if you are frequently getting I/O errors.</span
>
</label>
<Slider
id="max-writes"
v-model="settings.max_concurrent_writes"
:min="1"
:max="100"
:step="1"
/>
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Java settings</span>
</h3>
</div>
<label for="java-17">
<span class="label__title">Java 17 location</span>
</label>
<JavaSelector id="java-17" v-model="settings.java_globals.JAVA_17" :version="17" />
<label for="java-8">
<span class="label__title">Java 8 location</span>
</label>
<JavaSelector id="java-8" v-model="settings.java_globals.JAVA_8" :version="8" />
<hr class="card-divider" />
<div class="settings-group">
<div class="sliders">
<span class="slider">
Minimum memory
<Slider v-model="settings.memory.minimum" :min="256" :max="maxMemory" :step="10" />
<label for="java-args">
<span class="label__title">Java arguments</span>
</label>
<input
id="java-args"
v-model="settings.javaArgs"
autocomplete="off"
type="text"
class="installation-input"
placeholder="Enter java arguments..."
/>
<label for="env-vars">
<span class="label__title">Environmental variables</span>
</label>
<input
id="env-vars"
v-model="settings.envArgs"
autocomplete="off"
type="text"
class="installation-input"
placeholder="Enter environmental variables..."
/>
<hr class="card-divider" />
<div class="adjacent-input">
<label for="max-memory">
<span class="label__title">Java memory</span>
<span class="label__description">
The memory allocated to each instance when it is ran.
</span>
<span class="slider">
Maximum memory
<Slider v-model="settings.memory.maximum" :min="256" :max="maxMemory" :step="10" />
</span>
</div>
</label>
<Slider
id="max-memory"
v-model="settings.memory.maximum"
:min="256"
:max="maxMemory"
:step="1"
/>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Hooks</h2>
<div class="settings-group">
<div class="toggle-setting">
Pre launch
<input v-model="settings.hooks.pre_launch" type="text" class="input" />
</div>
<div class="toggle-setting">
Wrapper
<input v-model="settings.hooks.wrapper" type="text" class="input" />
</div>
<div class="toggle-setting">
Post exit
<input v-model="settings.hooks.post_exit" type="text" class="input" />
</div>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Hooks</span>
</h3>
</div>
<div class="adjacent-input">
<label for="pre-launch">
<span class="label__title">Pre launch</span>
<span class="label__description"> Ran before the instance is launched. </span>
</label>
<input
id="pre-launch"
v-model="settings.hooks.pre_launch"
autocomplete="off"
type="text"
placeholder="Enter pre-launch command..."
/>
</div>
<div class="adjacent-input">
<label for="wrapper">
<span class="label__title">Wrapper</span>
<span class="label__description"> Wrapper command for launching Minecraft. </span>
</label>
<input
id="wrapper"
v-model="settings.hooks.wrapper"
autocomplete="off"
type="text"
placeholder="Enter wrapper command..."
/>
</div>
<div class="adjacent-input">
<label for="post-exit">
<span class="label__title">Post exit</span>
<span class="label__description"> Ran after the game closes. </span>
</label>
<input
id="post-exit"
v-model="settings.hooks.post_exit"
autocomplete="off"
type="text"
placeholder="Enter post-exit command..."
/>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Window Size</h2>
<div class="settings-group">
<div class="toggle-setting">
Width
<input v-model="settings.game_resolution[0]" type="number" class="input" />
</div>
<div class="toggle-setting">
Height
<input v-model="settings.game_resolution[1]" type="number" class="input" />
</div>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Window size</span>
</h3>
</div>
<div class="adjacent-input">
<label for="width">
<span class="label__title">Width</span>
<span class="label__description"> The width of the game window when launched. </span>
</label>
<input
id="width"
v-model="settings.game_resolution[0]"
autocomplete="off"
type="number"
placeholder="Enter width..."
/>
</div>
<div class="adjacent-input">
<label for="height">
<span class="label__title">Height</span>
<span class="label__description"> The height of the game window when launched. </span>
</label>
<input
id="height"
v-model="settings.game_resolution[1]"
autocomplete="off"
type="number"
class="input"
placeholder="Enter height..."
/>
</div>
</Card>
</div>
</template>
<style lang="scss" scoped>
.concurrent-downloads {
width: 80% !important;
}
.slider-input {
width: 5rem !important;
flex-basis: 5rem !important;
.settings-page {
margin: 1rem 1rem 1rem 0;
}
.installation-input {
@@ -209,54 +288,11 @@ watch(settings.value, async (oldSettings, newSettings) => {
flex-grow: 1;
}
.theming,
.settings-card {
margin: 1rem;
}
.theming {
.toggle-setting {
display: flex;
}
}
.theme-dropdown {
text-transform: capitalize;
}
.settings-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.settings-title {
color: var(--color-contrast);
}
.settings-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toggle-setting {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.sliders {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 1rem;
width: 100%;
.slider {
flex-grow: 1;
}
.card-divider {
margin: 1rem 0;
}
</style>

View File

@@ -48,9 +48,13 @@
>
Loading...
</Button>
<!--TODO: https://github.com/tauri-apps/tauri/issues/4062 -->
<Button class="instance-button" icon-only @click="open({ defaultPath: instance.path })">
<Button
v-tooltip="'Open instance folder'"
class="instance-button"
@click="showInFolder(instance.path)"
>
<FolderOpenIcon />
Folder
</Button>
</span>
</Card>
@@ -104,8 +108,8 @@ import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute } from 'vue-router'
import { ref, onUnmounted } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import { handleError, useBreadcrumbs, useLoading, useSearch } from '@/store/state'
import { showInFolder } from '@/helpers/utils.js'
const route = useRoute()
const searchStore = useSearch()
@@ -148,24 +152,17 @@ await checkProcess()
const stopInstance = async () => {
playing.value = false
try {
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(instance.value.path).catch(handleError)
uuid.value = uuids[0] // populate Uuid to listen for in the process_listener
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError)
} catch (err) {
// Theseus currently throws:
// "Error launching Minecraft: Minecraft exited with non-zero code 1" error
// For now, we will catch and just warn
console.warn(err)
}
if (!uuid.value) {
const uuids = await get_uuids_by_profile_path(instance.value.path).catch(handleError)
uuid.value = uuids[0] // populate Uuid to listen for in the process_listener
uuids.forEach(async (u) => await kill_by_uuid(u).catch(handleError))
} else await kill_by_uuid(uuid.value).catch(handleError)
}
const unlistenProfiles = await profile_listener(async (event) => {
if (event.path === route.params.id) {
instance.value = await get(route.params.id).catch(handleError)
searchStore.instanceContext = instance.value
}
})

View File

@@ -30,10 +30,9 @@
</div>
</div>
<div ref="logContainer" class="log-text">
<!-- {{ logs[1] }}-->
<div v-for="line in logs[selectedLogIndex]?.stdout.split('\n')" :key="line" class="no-wrap">
{{ line }}
</div>
<span v-for="line in logs[selectedLogIndex]?.stdout.split('\n')" :key="line" class="no-wrap">
{{ line }} <br />
</span>
</div>
</Card>
</template>
@@ -210,5 +209,9 @@ onUnmounted(() => {
overflow: auto;
white-space: normal;
color-scheme: dark;
.no-wrap {
white-space: pre;
}
}
</style>

View File

@@ -3,29 +3,55 @@
<div class="card-row">
<div class="iconified-input">
<SearchIcon />
<input v-model="searchFilter" type="text" placeholder="Search Mods" class="text-input" />
<input
v-model="searchFilter"
type="text"
:placeholder="`Search ${search.length} ${(['All', 'Other'].includes(selectedProjectType)
? 'projects'
: selectedProjectType.toLowerCase()
).slice(0, search.length === 1 ? -1 : 64)}...`"
class="text-input"
autocomplete="off"
/>
</div>
<span class="manage">
<span class="text-combo">
<span class="no-wrap sort"> Sort By </span>
<span class="no-wrap sort"> Sort by </span>
<DropdownSelect
v-model="sortFilter"
name="sort-by"
:options="['Name', 'Version', 'Author']"
:options="['Name', 'Version', 'Author', 'Enabled']"
default-value="Name"
class="dropdown"
/>
</span>
<Button color="primary" @click="router.push({ path: '/browse/mod' })">
<Button
color="primary"
@click="
router.push({
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
})
"
>
<PlusIcon />
<span class="no-wrap"> Add Content </span>
<span class="no-wrap"> Add content </span>
</Button>
</span>
</div>
<Chips
v-if="Object.keys(selectableProjectTypes).length > 1"
v-model="selectedProjectType"
:items="Object.keys(selectableProjectTypes)"
/>
<div class="table">
<div class="table-row table-head">
<div class="table-cell table-text">
<Button icon-only :disabled="!projects.some((x) => x.outdated)" @click="updateAll">
<Button
v-tooltip="'Update all projects'"
icon-only
:disabled="!projects.some((x) => x.outdated)"
@click="updateAll"
>
<UpdatedIcon />
</Button>
</div>
@@ -37,7 +63,13 @@
<div v-for="mod in search" :key="mod.file_name" class="table-row">
<div class="table-cell table-text">
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
<Button v-else :disabled="!mod.outdated" icon-only @click="updateProject(mod)">
<Button
v-else
v-tooltip="'Update project'"
:disabled="!mod.outdated"
icon-only
@click="updateProject(mod)"
>
<UpdatedIcon v-if="mod.outdated" />
<CheckIcon v-else />
</Button>
@@ -55,11 +87,12 @@
<div class="table-cell table-text">{{ mod.version }}</div>
<div class="table-cell table-text">{{ mod.author }}</div>
<div class="table-cell table-text manage">
<Button icon-only @click="removeMod(mod)">
<Button v-tooltip="'Remove project'" icon-only @click="removeMod(mod)">
<TrashIcon />
</Button>
<input
id="switch-1"
autocomplete="off"
type="checkbox"
class="switch stylized-toggle"
:checked="!mod.disabled"
@@ -82,6 +115,8 @@ import {
UpdatedIcon,
DropdownSelect,
AnimatedLogo,
Chips,
formatProjectType,
} from 'omorphia'
import { computed, ref } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
@@ -120,6 +155,7 @@ for (const [path, project] of Object.entries(props.instance.projects)) {
disabled: project.disabled,
updateVersion: project.metadata.update_version,
outdated: !!project.metadata.update_version,
project_type: project.metadata.project.project_type,
})
} else if (project.metadata.type === 'inferred') {
projects.value.push({
@@ -131,6 +167,7 @@ for (const [path, project] of Object.entries(props.instance.projects)) {
icon: project.metadata.icon ? convertFileSrc(project.metadata.icon) : null,
disabled: project.disabled,
outdated: false,
project_type: project.metadata.project_type,
})
} else {
projects.value.push({
@@ -142,16 +179,33 @@ for (const [path, project] of Object.entries(props.instance.projects)) {
icon: null,
disabled: project.disabled,
outdated: false,
project_type: null,
})
}
}
const searchFilter = ref('')
const sortFilter = ref('')
const selectedProjectType = ref('All')
const selectableProjectTypes = computed(() => {
const obj = { All: 'all' }
for (const project of projects.value) {
obj[project.project_type ? formatProjectType(project.project_type) + 's' : 'Other'] =
project.project_type
}
return obj
})
const search = computed(() => {
const projectType = selectableProjectTypes.value[selectedProjectType.value]
const filtered = projects.value.filter((mod) => {
return mod.name.toLowerCase().includes(searchFilter.value.toLowerCase())
return (
mod.name.toLowerCase().includes(searchFilter.value.toLowerCase()) &&
(projectType === 'all' || mod.project_type === projectType)
)
})
return updateSort(filtered, sortFilter.value)
@@ -179,6 +233,16 @@ function updateSort(projects, sort) {
}
return 0
})
case 'Enabled':
return projects.slice().sort((a, b) => {
if (a.disabled && !b.disabled) {
return 1
}
if (!a.disabled && b.disabled) {
return -1
}
return 0
})
default:
return projects.slice().sort((a, b) => {
if (a.name < b.name) {
@@ -230,6 +294,8 @@ async function updateProject(mod) {
async function toggleDisableMod(mod) {
mod.path = await toggle_disable_project(props.instance.path, mod.path).catch(handleError)
console.log(mod.disabled)
mod.disabled = !mod.disabled
}
async function removeMod(mod) {
@@ -248,6 +314,10 @@ async function removeMod(mod) {
gap: 0.5rem;
}
.table {
margin-block-start: 0;
}
.table-row {
grid-template-columns: min-content 2fr 1fr 1fr 8rem;
}
@@ -284,14 +354,8 @@ async function removeMod(mod) {
width: 7rem !important;
}
.no-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.sort {
padding-left: 0.5rem;
}
.sort {
padding-left: 0.5rem;
}
</style>
<style lang="scss">
@@ -300,4 +364,8 @@ async function removeMod(mod) {
margin-left: 0.5rem !important;
}
}
.v-popper--theme-tooltip .v-popper__inner {
background: #fff !important;
}
</style>

View File

@@ -3,7 +3,7 @@
<div class="change-versions-modal universal-body">
<div class="input-row">
<p class="input-label">Loader</p>
<Chips v-model="loader" :items="loaders" />
<Chips v-model="loader" :items="loaders" :never-empty="false" />
</div>
<div class="input-row">
<p class="input-label">Game Version</p>
@@ -22,7 +22,7 @@
@change="(value) => (loaderVersionIndex = value.index)"
/>
</div>
<div class="button-group">
<div class="push-right input-group">
<button class="btn" @click="$refs.changeVersionsModal.hide()">
<XIcon />
Cancel
@@ -41,7 +41,7 @@
<section class="card">
<div class="label">
<h3>
<span class="label__title size-card-header">Profile</span>
<span class="label__title size-card-header">Instance</span>
</h3>
</div>
<label for="instance-icon">
@@ -68,13 +68,13 @@
<label for="project-name">
<span class="label__title">Name</span>
</label>
<input id="profile-name" v-model="title" maxlength="80" type="text" />
<input id="profile-name" v-model="title" autocomplete="off" maxlength="80" type="text" />
<div class="adjacent-input">
<label for="edit-versions">
<span class="label__title">Edit mod loader/game versions</span>
<span class="label__description">
Allows you to change the mod loader, loader version, or game version of the profile.
Allows you to change the mod loader, loader version, or game version of the instance.
</span>
</label>
<button id="edit-versions" class="btn" @click="$refs.changeVersionsModal.show()">
@@ -83,8 +83,12 @@
</button>
</div>
</section>
<Card class="settings-card">
<h2 class="settings-title">Java</h2>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Java</span>
</h3>
</div>
<div class="settings-group">
<h3>Installation</h3>
<Checkbox v-model="overrideJavaInstall" label="Override global java installations" />
@@ -95,10 +99,12 @@
<h3>Java arguments</h3>
<Checkbox v-model="overrideJavaArgs" label="Override global java arguments" />
<input
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
type="text"
class="input installation-input"
class="installation-input"
placeholder="Enter java arguments..."
/>
</div>
@@ -107,98 +113,156 @@
<Checkbox v-model="overrideEnvVars" label="Override global environment variables" />
<input
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
type="text"
class="input installation-input"
class="installation-input"
placeholder="Enter environment variables..."
/>
</div>
<hr class="card-divider" />
<div class="settings-group">
<h3>Java memory</h3>
<Checkbox v-model="overrideMemorySettings" label="Override global memory settings" />
<div class="sliders">
<span class="slider">
Minimum memory
<Slider
v-model="memory.minimum"
:disabled="!overrideMemorySettings"
:min="256"
:max="maxMemory"
:step="10"
/>
<Slider
v-model="memory.maximum"
:disabled="!overrideMemorySettings"
:min="256"
:max="maxMemory"
:step="1"
/>
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Window</span>
</h3>
</div>
<div class="adjacent-input">
<Checkbox v-model="overrideWindowSettings" label="Override global window settings" />
</div>
<div class="adjacent-input">
<label for="width">
<span class="label__title">Width</span>
<span class="label__description"> The width of the game window when launched. </span>
</label>
<input
id="width"
v-model="resolution[0]"
autocomplete="off"
:disabled="!overrideWindowSettings"
type="number"
placeholder="Enter width..."
/>
</div>
<div class="adjacent-input">
<label for="height">
<span class="label__title">Height</span>
<span class="label__description"> The height of the game window when launched. </span>
</label>
<input
id="height"
v-model="resolution[1]"
autocomplete="off"
:disabled="!overrideWindowSettings"
type="number"
class="input"
placeholder="Enter height..."
/>
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Hooks</span>
</h3>
</div>
<div class="adjacent-input">
<Checkbox v-model="overrideHooks" label="Override global hooks" />
</div>
<div class="adjacent-input">
<label for="pre-launch">
<span class="label__title">Pre launch</span>
<span class="label__description"> Ran before the instance is launched. </span>
</label>
<input
id="pre-launch"
v-model="hooks.pre_launch"
autocomplete="off"
:disabled="!overrideHooks"
type="text"
placeholder="Enter pre-launch command..."
/>
</div>
<div class="adjacent-input">
<label for="wrapper">
<span class="label__title">Wrapper</span>
<span class="label__description"> Wrapper command for launching Minecraft. </span>
</label>
<input
id="wrapper"
v-model="hooks.wrapper"
autocomplete="off"
:disabled="!overrideHooks"
type="text"
placeholder="Enter wrapper command..."
/>
</div>
<div class="adjacent-input">
<label for="post-exit">
<span class="label__title">Post exit</span>
<span class="label__description"> Ran after the game closes. </span>
</label>
<input
id="post-exit"
v-model="hooks.post_exit"
autocomplete="off"
:disabled="!overrideHooks"
type="text"
placeholder="Enter post-exit command..."
/>
</div>
</Card>
<Card>
<div class="label">
<h3>
<span class="label__title size-card-header">Instance management</span>
</h3>
</div>
<div class="adjacent-input">
<label for="repair-profile">
<span class="label__title">Repair instance</span>
<span class="label__description">
Reinstalls the instance and checks for corruption. Use this if your game is not launching
due to launcher-related errors.
</span>
<span class="slider">
Maximum memory
<Slider
v-model="memory.maximum"
:disabled="!overrideMemorySettings"
:min="256"
:max="maxMemory"
:step="10"
/>
</label>
<button
id="repair-profile"
class="btn btn-highlight"
:disabled="repairing"
@click="repairProfile"
>
<HammerIcon /> Repair
</button>
</div>
<div class="adjacent-input">
<label for="delete-profile">
<span class="label__title">Delete instance</span>
<span class="label__description">
Fully removes a instance from the disk. Be careful, as once you delete a instance there is
no way to recover it.
</span>
</div>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Window</h2>
<Checkbox v-model="overrideWindowSettings" label="Override global window settings" />
<div class="settings-group">
<div class="toggle-setting">
Width
<input
v-model="resolution[0]"
:disabled="!overrideWindowSettings"
type="number"
class="input"
@change="updateProfile"
/>
</div>
<div class="toggle-setting">
Height
<input
v-model="resolution[1]"
:disabled="!overrideWindowSettings"
type="number"
class="input"
@change="updateProfile"
/>
</div>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Hooks</h2>
<Checkbox v-model="overrideHooks" label="Override global hooks" />
<div class="settings-group">
<div class="toggle-setting">
Pre launch
<input v-model="hooks.pre_launch" :disabled="!overrideHooks" type="text" />
</div>
<div class="toggle-setting">
Wrapper
<input v-model="hooks.wrapper" :disabled="!overrideHooks" type="text" />
</div>
<div class="toggle-setting">
Post exit
<input v-model="hooks.post_exit" :disabled="!overrideHooks" type="text" />
</div>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title">Profile management</h2>
<div class="settings-group">
<div class="toggle-setting">
Repair profile
<button class="btn btn-highlight" :disabled="repairing" @click="repairProfile">
<HammerIcon /> Repair
</button>
</div>
<div class="toggle-setting">
Delete profile
<button class="btn btn-danger" :disabled="removing" @click="removeProfile">
<TrashIcon /> Delete
</button>
</div>
</label>
<button
id="delete-profile"
class="btn btn-danger"
:disabled="removing"
@click="removeProfile"
>
<TrashIcon /> Delete
</button>
</div>
</Card>
</template>
@@ -221,7 +285,7 @@ import {
} from 'omorphia'
import { useRouter } from 'vue-router'
import { edit, edit_icon, get_optimal_jre_key, install, remove } from '@/helpers/profile.js'
import { computed, readonly, ref, shallowRef, watch } from 'vue'
import { computed, onMounted, readonly, ref, shallowRef, watch } from 'vue'
import { get_max_memory } from '@/helpers/jre.js'
import { get } from '@/helpers/settings.js'
import JavaSelector from '@/components/ui/JavaSelector.vue'
@@ -388,10 +452,9 @@ const [fabric_versions, forge_versions, quilt_versions, all_game_versions, loade
.then(ref)
.catch(handleError),
])
loaders.value.push('vanilla')
loaders.value.unshift('vanilla')
const loader = ref(props.instance.metadata.loader)
const gameVersion = ref(props.instance.metadata.game_version)
const selectableGameVersions = computed(() => {
return all_game_versions.value
@@ -457,6 +520,8 @@ async function saveGvLoaderEdits() {
editing.value = false
changeVersionsModal.value.hide()
}
onMounted(() => console.log(loader.value))
</script>
<style scoped lang="scss">
@@ -478,73 +543,23 @@ async function saveGvLoaderEdits() {
flex-direction: row;
gap: 1rem;
}
.button-group {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
}
.settings-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.input-stack {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.settings-title {
color: var(--color-contrast);
}
.settings-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1rem 0;
h3 {
margin: 0;
}
}
.installation-input {
width: 100%;
}
.sliders {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 1rem;
width: 100%;
.slider {
flex-grow: 1;
}
}
.toggle-setting {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.card-divider {
background-color: var(--color-button-bg);
border: none;
color: var(--color-button-bg);
height: 1px;
margin: var(--gap-sm) 0;
}
:deep(button.checkbox) {
border: none;
}

View File

@@ -50,7 +50,6 @@
</Button>
<a
class="open btn icon-only"
target="_blank"
:href="
expandedGalleryItem.url
? expandedGalleryItem.url

View File

@@ -43,7 +43,6 @@
<a
:href="`https://modrinth.com/${data.project_type}/${data.slug}`"
rel="external"
target="_blank"
class="btn"
>
<ExternalIcon />
@@ -97,7 +96,6 @@
:href="data.issues_url"
class="title"
rel="noopener nofollow ugc external"
target="_blank"
>
<IssuesIcon aria-hidden="true" />
<span>Issues</span>
@@ -106,7 +104,6 @@
v-if="data.source_url"
:href="data.source_url"
class="title"
target="_blank"
rel="noopener nofollow ugc external"
>
<CodeIcon aria-hidden="true" />
@@ -117,7 +114,6 @@
:href="data.wiki_url"
class="title"
rel="noopener nofollow ugc external"
target="_blank"
>
<WikiIcon aria-hidden="true" />
<span>Wiki</span>
@@ -127,7 +123,6 @@
:href="data.wiki_url"
class="title"
rel="noopener nofollow ugc external"
target="_blank"
>
<DiscordIcon aria-hidden="true" />
<span>Discord</span>
@@ -136,7 +131,6 @@
v-for="(donation, index) in data.donation_urls"
:key="index"
:href="donation.url"
target="_blank"
rel="noopener nofollow ugc external"
>
<BuyMeACoffeeIcon v-if="donation.id === 'bmac'" aria-hidden="true" />
@@ -322,11 +316,19 @@ async function install(version) {
.map((value) => value.metadata)
.find((pack) => pack.linked_data?.project_id === data.value.id)
) {
await packInstall(queuedVersionData.id, data.value.title, data.value.icon_url).catch(
handleError
)
await packInstall(
data.value.id,
queuedVersionData.id,
data.value.title,
data.value.icon_url
).catch(handleError)
} else {
confirmModal.value.show(queuedVersionData.id, data.value.title, data.value.icon_url)
confirmModal.value.show(
data.value.id,
queuedVersionData.id,
data.value.title,
data.value.icon_url
)
}
} else {
if (instance.value) {

View File

@@ -22,7 +22,6 @@
<a
:href="`https://modrinth.com/mod/${route.params.id}/version/${route.params.version}`"
rel="external"
target="_blank"
class="btn"
>
<ExternalIcon />
@@ -145,7 +144,6 @@
<a
:href="`https://modrinth.com/user/${author.user.username}`"
rel="external"
target="_blank"
class="metadata-value btn author"
>
<Avatar size="sm" :src="author.user.avatar_url" circle />

View File

@@ -105,6 +105,15 @@ export default new createRouter({
breadcrumb: [{ name: '?Instance' }],
},
},
{
path: 'projects/:type',
name: 'ModsFilter',
component: Instance.Mods,
meta: {
useRootContext: true,
breadcrumb: [{ name: '?Instance' }],
},
},
{
path: 'options',
name: 'Options',
@@ -128,4 +137,8 @@ export default new createRouter({
],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active',
scrollBehavior() {
// always scroll to top
return { top: 0 }
},
})

File diff suppressed because it is too large Load Diff