Merge pull request #56 from modrinth/mod-management

Profile mod management
This commit is contained in:
Wyatt Verchere
2023-03-30 16:32:48 -07:00
committed by GitHub
65 changed files with 1906 additions and 1263 deletions

1
.envrc
View File

@@ -1 +0,0 @@
use flake

42
.github/workflows/cli-build.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: CLI Build + Lint
on:
push:
branches: [ master ]
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./theseus_cli
steps:
- uses: actions/checkout@v2
- name: install dependencies (ubuntu only)
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Get build cache
id: cache-build
uses: actions/cache@v2
with:
path: ../target/**
key: ${{ runner.os }}-theseus
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- uses: actions-rs/cargo@v1
name: Build program
with:
command: build
args: --bin theseus_cli
- name: Run Lint
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --bin theseus_cli

34
.github/workflows/gui-build.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: GUI Build + Lint
on:
push:
branches: [master]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./theseus_gui
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --immutable --immutable-cache --check-cache
- name: Run Lint
run: npm run lint
- name: Build
run: npm run build

59
.github/workflows/tauri-build.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: 'Tauri GUI Build'
on:
push:
branches: [ master ]
pull_request:
jobs:
test-tauri:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
defaults:
run:
working-directory: ./theseus_gui
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Get build cache
id: cache-build
uses: actions/cache@v2
with:
path: ../target/**
key: ${{ runner.os }}-theseus
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: install frontend dependencies
run: yarn install --immutable --immutable-cache --check-cache
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Lint
if: matrix.platform == 'ubuntu-20.04'
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --bin theseus_cli

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "theseus_gui/locales"]
path = theseus_gui/locales
url = git@github.com:modrinth/translations.git

13
COPYING.md Normal file
View File

@@ -0,0 +1,13 @@
# Copying
The source code of the knossos repository is licensed under the GNU Affero General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
## Modrinth logo
Any files depicting the Modrinth branding, including the wrench-in-labyrinth logo, the landing image, and variations thereof, are licensed as follows:
> All rights reserved. © 2020-2023 Rinth, Inc.
This includes, but may not be limited to, the following files:
- theseus_gui/src-tauri/icons

1465
Cargo.lock generated

File diff suppressed because it is too large Load Diff

103
flake.lock generated
View File

@@ -1,103 +0,0 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1655706580,
"narHash": "sha256-7DshIT1Ya5W9NAW7UdnYCHsGmXfOXJZCEHbbB/cCX7g=",
"owner": "nix-community",
"repo": "fenix",
"rev": "d895003d8e03ac2fc8ffe2aa898299cbef1a7048",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1655042882,
"narHash": "sha256-9BX8Fuez5YJlN7cdPO63InoyBy7dm3VlJkkmTt6fS1A=",
"owner": "nix-community",
"repo": "naersk",
"rev": "cddffb5aa211f50c4b8750adbec0bbbdfb26bb9f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1655624069,
"narHash": "sha256-7g1zwTdp35GMTERnSzZMWJ7PG3QdDE8VOX3WsnOkAtM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "0d68d7c857fe301d49cdcd56130e0beea4ecd5aa",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1655654433,
"narHash": "sha256-auHQ0XPCiaTPSn+R3Yu4J7oZ5Zq/FS5/Da1ivvdYb/Y=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "427061da19723f2206fe4dcb175c9c43b9a6193d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1653893745,
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,72 +0,0 @@
{
description = "The official Modrinth launcher";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
naersk = {
url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{self, ...}:
inputs.utils.lib.eachDefaultSystem (system: let
pkgs = import inputs.nixpkgs { inherit system; };
fenix = inputs.fenix.packages.${system};
utils = inputs.utils.lib;
toolchain = with fenix;
combine [
minimal.rustc minimal.cargo
];
naersk = inputs.naersk.lib.${system}.override {
rustc = toolchain;
cargo = toolchain;
};
deps = with pkgs; {
global = [
openssl pkg-config gcc
];
gui = [
gtk4 gdk-pixbuf atk webkitgtk dbus
];
shell = [
(with fenix; combine [toolchain default.clippy complete.rust-src rust-analyzer])
git
jdk17 jdk8
];
};
in {
packages = {
theseus-cli = naersk.buildPackage {
pname = "theseus_cli";
src = ./.;
buildInputs = deps.global;
cargoBuildOptions = x: x ++ ["-p" "theseus_cli"];
};
};
apps = {
cli = utils.mkApp {
drv = self.packages.${system}.theseus-cli;
};
cli-dev = utils.mkApp {
drv = self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
release = false;
});
};
};
devShell = pkgs.mkShell {
buildInputs = with deps;
global ++ gui ++ shell;
};
});
}

View File

@@ -11,7 +11,9 @@ bytes = "1"
bincode = { version = "2.0.0-rc.1", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha1 = { version = "0.6.0", features = ["std"]}
toml = "0.7.3"
sha1 = { version = "0.6.1", features = ["std"]}
sha2 = "0.9.9"
sled = { version = "0.34.7", features = ["compression"] }
url = "2.2"
uuid = { version = "1.1", features = ["serde", "v4"] }
@@ -29,7 +31,7 @@ tracing = "0.1"
tracing-error = "0.2"
async-tungstenite = { version = "0.17", features = ["tokio-runtime", "tokio-native-tls"] }
async-tungstenite = { version = "0.20.0", features = ["tokio-runtime", "tokio-native-tls"] }
futures = "0.3"
once_cell = "1.9.0"
reqwest = { version = "0.11", features = ["json"] }

View File

@@ -88,7 +88,7 @@ pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
let state = State::get().await?;
let users = state.users.read().await;
Ok(users.contains(user)?)
users.contains(user)
}
/// Get a copy of the list of all user credentials

View File

@@ -68,12 +68,7 @@ pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
let state = State::get().await?;
let profiles = state.profiles.read().await;
Ok(profiles
.0
.get(profile)
.map(Option::as_ref)
.flatten()
.is_some())
Ok(profiles.0.get(profile).and_then(Option::as_ref).is_some())
}
/// Edit a profile using a given asynchronous closure
@@ -138,8 +133,8 @@ pub async fn run(
})?;
let version_info = d::minecraft::fetch_version_info(version).await?;
let ref pre_launch_hooks =
profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
let pre_launch_hooks =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
for hook in pre_launch_hooks.iter() {
// TODO: hook parameters
let mut cmd = hook.split(' ');
@@ -190,7 +185,7 @@ pub async fn run(
.as_error());
}
let ref java_args = profile
let java_args = profile
.java
.as_ref()
.and_then(|it| it.extra_arguments.as_ref())
@@ -201,18 +196,18 @@ pub async fn run(
.as_ref()
.map_or(&settings.hooks.wrapper, |it| &it.wrapper);
let ref memory = profile.memory.unwrap_or(settings.memory);
let ref resolution = profile.resolution.unwrap_or(settings.game_resolution);
let memory = profile.memory.unwrap_or(settings.memory);
let resolution = profile.resolution.unwrap_or(settings.game_resolution);
crate::launcher::launch_minecraft(
&profile.metadata.game_version,
&profile.metadata.loader_version,
&profile.path,
&java_install,
&java_args,
&wrapper,
memory,
resolution,
java_install,
java_args,
wrapper,
&memory,
&resolution,
credentials,
)
.await

View File

@@ -17,6 +17,8 @@ pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
.unwrap()
});
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
pub fn sled_config() -> sled::Config {
sled::Config::default().use_compression(true)
}

View File

@@ -68,7 +68,7 @@ pub fn get_class_paths_jar<T: AsRef<str>>(
pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
let mut path = libraries_path.to_path_buf();
path.push(get_path_from_artifact(lib.as_ref())?);
path.push(get_path_from_artifact(lib)?);
let path = &path.canonicalize().map_err(|_| {
crate::ErrorKind::LauncherError(format!(
@@ -164,8 +164,7 @@ fn parse_jvm_argument(
))
.as_error()
})?
.to_string_lossy()
.to_string(),
.to_string_lossy(),
)
.replace("${classpath_separator}", classpath_separator())
.replace("${launcher_name}", "theseus")
@@ -219,7 +218,6 @@ pub fn get_minecraft_arguments(
resolution,
)?
.split(' ')
.into_iter()
.map(|x| x.to_string())
.collect())
} else {
@@ -260,8 +258,7 @@ fn parse_minecraft_argument(
))
.as_error()
})?
.to_string_lossy()
.to_owned(),
.to_string_lossy(),
)
.replace(
"${assets_root}",
@@ -274,8 +271,7 @@ fn parse_minecraft_argument(
))
.as_error()
})?
.to_string_lossy()
.to_owned(),
.to_string_lossy(),
)
.replace(
"${game_assets}",
@@ -288,8 +284,7 @@ fn parse_minecraft_argument(
))
.as_error()
})?
.to_string_lossy()
.to_owned(),
.to_string_lossy(),
)
.replace("${version_type}", version_type.as_str())
.replace("${resolution_width}", &resolution.0.to_string())
@@ -366,7 +361,7 @@ pub fn get_processor_arguments<T: AsRef<str>>(
pub async fn get_processor_main_class(
path: String,
) -> crate::Result<Option<String>> {
Ok(tokio::task::spawn_blocking(move || {
tokio::task::spawn_blocking(move || {
let zipfile = std::fs::File::open(&path)?;
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
crate::ErrorKind::LauncherError(format!(
@@ -400,5 +395,5 @@ pub async fn get_processor_main_class(
Ok::<Option<String>, crate::Error>(None)
})
.await
.unwrap()?)
.unwrap()
}

View File

@@ -3,12 +3,14 @@ use async_tungstenite as ws;
use bincode::{Decode, Encode};
use chrono::{prelude::*, Duration};
use futures::prelude::*;
use once_cell::sync::*;
use lazy_static::lazy_static;
use serde::Deserialize;
use url::Url;
pub const HYDRA_URL: Lazy<Url> =
Lazy::new(|| Url::parse("https://hydra.modrinth.com").unwrap());
lazy_static! {
static ref HYDRA_URL: Url =
Url::parse("https://hydra.modrinth.com").unwrap();
}
// Socket messages
#[derive(Deserialize)]
@@ -65,7 +67,7 @@ pub struct HydraAuthFlow<S: AsyncRead + AsyncWrite + Unpin> {
impl HydraAuthFlow<ws::tokio::ConnectStream> {
pub async fn new() -> crate::Result<Self> {
let sock_url = wrap_ref_builder!(
it = HYDRA_URL =>
it = HYDRA_URL.clone() =>
{ it.set_scheme("wss").ok() }
);
let (socket, _) = ws::tokio::connect_async(sock_url.clone()).await?;

View File

@@ -75,7 +75,7 @@ pub async fn download_client(
st: &State,
version_info: &GameVersionInfo,
) -> crate::Result<()> {
let ref version = version_info.id;
let version = &version_info.id;
log::debug!("Locating client for version {version}");
let client_download = version_info
.downloads
@@ -143,7 +143,7 @@ pub async fn download_assets(
stream::iter(index.objects.iter())
.map(Ok::<(&String, &Asset), crate::Error>)
.try_for_each_concurrent(None, |(name, asset)| async move {
let ref hash = asset.hash;
let hash = &asset.hash;
let resource_path = st.directories.object_dir(hash);
let url = format!(
"https://resources.download.minecraft.net/{sub_hash}/{hash}",
@@ -158,7 +158,7 @@ pub async fn download_assets(
let resource = fetch_cell
.get_or_try_init(|| fetch(&url, Some(hash), &permit))
.await?;
write(&resource_path, &resource, &permit).await?;
write(&resource_path, resource, &permit).await?;
log::info!("Fetched asset with hash {hash}");
}
Ok::<_, crate::Error>(())
@@ -172,7 +172,7 @@ pub async fn download_assets(
let resource_path = st.directories.legacy_assets_dir().join(
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
);
write(&resource_path, &resource, &permit).await?;
write(&resource_path, resource, &permit).await?;
log::info!("Fetched legacy asset with hash {hash}");
}
Ok::<_, crate::Error>(())

View File

@@ -44,6 +44,7 @@ macro_rules! processor_rules {
}
}
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip_all, fields(path = ?instance_path))]
pub async fn launch_minecraft(
game_version: &str,
@@ -75,7 +76,7 @@ pub async fn launch_minecraft(
let mut version_info = download::download_version_info(
&state,
&version,
version,
loader_version.as_ref(),
)
.await?;

View File

@@ -122,6 +122,12 @@ impl DirectoryInfo {
self.config_dir.join("settings.json")
}
/// Get the cache directory for Theseus
#[inline]
pub fn caches_dir(&self) -> PathBuf {
self.config_dir.join("caches")
}
/// Get path from environment variable
#[inline]
fn env_path(name: &str) -> Option<PathBuf> {

View File

@@ -57,7 +57,7 @@ impl Metadata {
if let Some(ref meta_bin) = db.get(METADATA_DB_FIELD)? {
match bincode::decode_from_slice::<Self, _>(
&meta_bin,
meta_bin,
*BINCODE_CONFIG,
) {
Ok((meta, _)) => metadata = Some(meta),

View File

@@ -16,6 +16,9 @@ pub use self::profiles::*;
mod settings;
pub use self::settings::*;
mod projects;
pub use self::projects::*;
mod users;
pub use self::users::*;
@@ -62,7 +65,7 @@ impl State {
// Launcher data
let (metadata, profiles) = tokio::try_join! {
Metadata::init(&database),
Profiles::init(&database),
Profiles::init(&database, &directories),
}?;
let users = Users::init(&database)?;

View File

@@ -1,5 +1,7 @@
use super::settings::{Hooks, MemorySettings, WindowSize};
use crate::config::BINCODE_CONFIG;
use crate::data::DirectoryInfo;
use crate::state::projects::Project;
use daedalus::modded::LoaderVersion;
use futures::prelude::*;
use serde::{Deserialize, Serialize};
@@ -16,7 +18,7 @@ pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
// TODO: possibly add defaults to some of these values
pub const CURRENT_FORMAT_VERSION: u32 = 1;
pub const SUPPORTED_ICON_FORMATS: &[&'static str] = &[
pub const SUPPORTED_ICON_FORMATS: &[&str] = &[
"bmp", "gif", "jpeg", "jpg", "jpe", "png", "svg", "svgz", "webp", "rgb",
"mp4",
];
@@ -27,6 +29,7 @@ pub struct Profile {
#[serde(skip)]
pub path: PathBuf,
pub metadata: ProfileMetadata,
pub projects: HashMap<PathBuf, Project>,
#[serde(skip_serializing_if = "Option::is_none")]
pub java: Option<JavaSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -51,26 +54,23 @@ pub struct ProfileMetadata {
}
// TODO: Quilt?
#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
#[derive(
Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize, Default,
)]
#[serde(rename_all = "lowercase")]
pub enum ModLoader {
#[default]
Vanilla,
Forge,
Fabric,
}
impl Default for ModLoader {
fn default() -> Self {
ModLoader::Vanilla
}
}
impl std::fmt::Display for ModLoader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
&Self::Vanilla => "Vanilla",
&Self::Forge => "Forge",
&Self::Fabric => "Fabric",
f.write_str(match *self {
Self::Vanilla => "Vanilla",
Self::Forge => "Forge",
Self::Fabric => "Fabric",
})
}
}
@@ -107,6 +107,7 @@ impl Profile {
loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
},
projects: HashMap::new(),
java: None,
memory: None,
resolution: None,
@@ -200,7 +201,10 @@ impl Profile {
impl Profiles {
#[tracing::instrument(skip(db))]
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
pub async fn init(
db: &sled::Db,
dirs: &DirectoryInfo,
) -> crate::Result<Self> {
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
Ok(Default::default()),
|bytes| {
@@ -212,7 +216,7 @@ impl Profiles {
},
)?;
let profiles = stream::iter(profile_db.iter())
let mut profiles = stream::iter(profile_db.iter())
.then(|it| async move {
let path = PathBuf::from(it);
let prof = match Self::read_profile_from_dir(&path).await {
@@ -227,6 +231,37 @@ impl Profiles {
.collect::<HashMap<PathBuf, Option<Profile>>>()
.await;
// project path, parent profile path
let mut files: HashMap<PathBuf, PathBuf> = HashMap::new();
{
for (profile_path, _profile_opt) in profiles.iter() {
let mut read_paths = |path: &str| {
for path in std::fs::read_dir(profile_path.join(path))? {
files.insert(path?.path(), profile_path.clone());
}
Ok::<(), crate::Error>(())
};
read_paths("mods")?;
read_paths("shaders")?;
read_paths("resourcepacks")?;
read_paths("datapacks")?;
}
}
let inferred = super::projects::infer_data_from_files(
files.keys().cloned().collect(),
dirs.caches_dir(),
)
.await?;
for (key, value) in inferred {
if let Some(profile_path) = files.get(&key) {
if let Some(Some(profile)) = profiles.get_mut(profile_path) {
profile.projects.insert(key, value);
}
}
}
Ok(Self(profiles))
}
@@ -296,63 +331,3 @@ impl Profiles {
Ok(profile)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::{assert_eq, assert_str_eq};
use std::collections::HashSet;
#[test]
fn profile_test() -> Result<(), serde_json::Error> {
let profile = Profile {
path: PathBuf::new(),
metadata: ProfileMetadata {
name: String::from("Example Pack"),
icon: None,
game_version: String::from("1.18.2"),
loader: ModLoader::Vanilla,
loader_version: None,
format_version: CURRENT_FORMAT_VERSION,
},
java: Some(JavaSettings {
install: Some(PathBuf::from("/usr/bin/java")),
extra_arguments: Some(Vec::new()),
}),
memory: Some(MemorySettings {
minimum: None,
maximum: 8192,
}),
resolution: Some(WindowSize(1920, 1080)),
hooks: Some(Hooks {
pre_launch: HashSet::new(),
wrapper: None,
post_exit: HashSet::new(),
}),
};
let json = serde_json::json!({
"metadata": {
"name": "Example Pack",
"game_version": "1.18.2",
"format_version": 1u32,
"loader": "vanilla",
},
"java": {
"extra_arguments": [],
"install": "/usr/bin/java",
},
"memory": {
"maximum": 8192u32,
},
"resolution": (1920u16, 1080u16),
"hooks": {},
});
assert_eq!(serde_json::to_value(profile.clone())?, json.clone());
assert_str_eq!(
format!("{:?}", serde_json::from_value::<Profile>(json)?),
format!("{:?}", profile),
);
Ok(())
}
}

View File

@@ -0,0 +1,428 @@
//! Project management + inference
use crate::config::{MODRINTH_API_URL, REQWEST_CLIENT};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Digest;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tokio::io::AsyncReadExt;
use zip::ZipArchive;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Project {
pub sha512: String,
pub disabled: bool,
pub metadata: ProjectMetadata,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModrinthProject {
pub id: String,
pub slug: Option<String>,
pub project_type: String,
pub team: String,
pub title: String,
pub description: String,
pub body: String,
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub client_side: String,
pub server_side: String,
pub downloads: u32,
pub followers: u32,
pub categories: Vec<String>,
pub additional_categories: Vec<String>,
pub game_versions: Vec<String>,
pub loaders: Vec<String>,
pub versions: Vec<String>,
pub icon_url: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProjectMetadata {
Modrinth(Box<ModrinthProject>),
Inferred {
title: Option<String>,
description: Option<String>,
authors: Vec<String>,
version: Option<String>,
icon: Option<PathBuf>,
},
Unknown,
}
pub async fn infer_data_from_files(
paths: Vec<PathBuf>,
cache_dir: PathBuf,
) -> crate::Result<HashMap<PathBuf, Project>> {
let mut file_path_hashes = HashMap::new();
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
for path in paths.clone() {
let mut file = tokio::fs::File::open(path.clone()).await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
let hash = format!("{:x}", sha2::Sha512::digest(&buffer));
file_path_hashes.insert(hash, path.clone());
}
// TODO: add disabled mods
// TODO: add retrying
#[derive(Deserialize)]
pub struct ModrinthVersion {
pub project_id: String,
}
let files: HashMap<String, ModrinthVersion> = REQWEST_CLIENT
.post(format!("{}version_files", MODRINTH_API_URL))
.json(&json!({
"hashes": file_path_hashes.keys().collect::<Vec<_>>(),
"algorithm": "sha512",
}))
.send()
.await?
.json()
.await?;
let projects: Vec<ModrinthProject> = REQWEST_CLIENT
.get(format!(
"{}projects?ids={}",
MODRINTH_API_URL,
serde_json::to_string(
&files
.values()
.map(|x| x.project_id.clone())
.collect::<Vec<_>>()
)?
))
.send()
.await?
.json()
.await?;
let mut return_projects = HashMap::new();
let mut further_analyze_projects: Vec<(String, PathBuf)> = Vec::new();
for (hash, path) in file_path_hashes {
if let Some(file) = files.get(&hash) {
if let Some(project) =
projects.iter().find(|x| file.project_id == x.id)
{
return_projects.insert(
path,
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Modrinth(Box::new(
project.clone(),
)),
},
);
continue;
}
}
further_analyze_projects.push((hash, path));
}
for (hash, path) in further_analyze_projects {
let file = File::open(path.clone())?;
// TODO: get rid of below unwrap
let mut zip = ZipArchive::new(file).unwrap();
let read_icon_from_file =
|icon_path: Option<String>| -> crate::Result<Option<PathBuf>> {
if let Some(icon_path) = icon_path {
// we have to repoen the zip twice here :(
let zip_file = File::open(path.clone())?;
if let Ok(mut zip) = ZipArchive::new(zip_file) {
if let Ok(mut file) = zip.by_name(&icon_path) {
let mut bytes = Vec::new();
if file.read_to_end(&mut bytes).is_ok() {
let extension = Path::new(&icon_path)
.extension()
.and_then(OsStr::to_str);
let hash = sha1::Sha1::from(&bytes).hexdigest();
let path = cache_dir.join("icons").join(
if let Some(ext) = extension {
format!("{hash}.{ext}")
} else {
hash
},
);
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(path.clone())?;
file.write_all(&bytes)?;
}
return Ok(Some(path));
}
};
}
}
Ok(None)
};
if let Ok(mut file) = zip.by_name("META-INF/mods.toml") {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeModInfo {
pub mods: Vec<ForgeMod>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeMod {
mod_id: String,
version: Option<String>,
display_name: Option<String>,
description: Option<String>,
logo_file: Option<String>,
authors: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) =
serde_json::from_str::<ForgeModInfo>(&file_str)
{
if let Some(pack) = pack.mods.first() {
let icon = read_icon_from_file(pack.logo_file.clone())?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(
pack.display_name
.clone()
.unwrap_or(pack.mod_id.clone()),
),
description: pack.description.clone(),
authors: pack
.authors
.clone()
.map(|x| vec![x])
.unwrap_or_default(),
version: pack.version.clone(),
icon,
},
},
);
continue;
}
}
}
}
if let Ok(mut file) = zip.by_name("mcmod.info") {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ForgeMod {
modid: String,
name: String,
description: Option<String>,
version: Option<String>,
author_list: Option<Vec<String>>,
logo_file: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<ForgeMod>(&file_str) {
let icon = read_icon_from_file(pack.logo_file)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(if pack.name.is_empty() {
pack.modid
} else {
pack.name
}),
description: pack.description,
authors: pack.author_list.unwrap_or_default(),
version: pack.version,
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("fabric.mod.json") {
#[derive(Deserialize)]
#[serde(untagged)]
enum FabricAuthor {
String(String),
Object { name: String },
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FabricMod {
id: String,
version: String,
name: Option<String>,
description: Option<String>,
authors: Vec<FabricAuthor>,
icon: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<FabricMod>(&file_str) {
let icon = read_icon_from_file(pack.icon)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(pack.name.unwrap_or(pack.id)),
description: pack.description,
authors: pack
.authors
.into_iter()
.map(|x| match x {
FabricAuthor::String(name) => name,
FabricAuthor::Object { name } => name,
})
.collect(),
version: Some(pack.version),
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("quilt.mod.json") {
#[derive(Deserialize)]
struct QuiltMetadata {
pub name: Option<String>,
pub description: Option<String>,
pub contributors: Option<HashMap<String, String>>,
pub icon: Option<String>,
}
#[derive(Deserialize)]
struct QuiltMod {
id: String,
version: String,
metadata: Option<QuiltMetadata>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<QuiltMod>(&file_str) {
let icon = read_icon_from_file(
pack.metadata.as_ref().and_then(|x| x.icon.clone()),
)?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: Some(
pack.metadata
.as_ref()
.and_then(|x| x.name.clone())
.unwrap_or(pack.id),
),
description: pack
.metadata
.as_ref()
.and_then(|x| x.description.clone()),
authors: pack
.metadata
.map(|x| {
x.contributors
.unwrap_or_default()
.keys()
.cloned()
.collect()
})
.unwrap_or_default(),
version: Some(pack.version),
icon,
},
},
);
continue;
}
}
}
if let Ok(mut file) = zip.by_name("pack.mcmeta") {
#[derive(Deserialize)]
struct Pack {
description: Option<String>,
}
let mut file_str = String::new();
if file.read_to_string(&mut file_str).is_ok() {
if let Ok(pack) = serde_json::from_str::<Pack>(&file_str) {
let icon =
read_icon_from_file(Some("pack.png".to_string()))?;
return_projects.insert(
path.clone(),
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Inferred {
title: None,
description: pack.description,
authors: Vec::new(),
version: None,
icon,
},
},
);
continue;
}
}
}
return_projects.insert(
path,
Project {
sha512: hash,
disabled: false,
metadata: ProjectMetadata::Unknown,
},
);
}
Ok(return_projects)
}

View File

@@ -103,7 +103,7 @@ impl Default for WindowSize {
}
/// Game initialization hooks
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct Hooks {
#[serde(skip_serializing_if = "HashSet::is_empty")]
@@ -113,13 +113,3 @@ pub struct Hooks {
#[serde(skip_serializing_if = "HashSet::is_empty")]
pub post_exit: HashSet<String>,
}
impl Default for Hooks {
fn default() -> Self {
Self {
pre_launch: HashSet::<String>::new(),
wrapper: None,
post_exit: HashSet::<String>::new(),
}
}
}

View File

@@ -57,7 +57,7 @@ pub async fn fetch_mirrors(
let _permits = sem.acquire_many(permits).await.unwrap();
let sem = Arc::new(Semaphore::new(permits.try_into().unwrap()));
future::select_ok(urls.into_iter().map(|url| {
future::select_ok(urls.iter().map(|url| {
let sha1 = sha1.map(String::from);
let url = String::from(*url);
let sem = Arc::clone(&sem);

View File

@@ -185,11 +185,11 @@ pub fn get_all_jre_path() -> Result<HashSet<JavaVersion>, JREError> {
#[cfg(target_os = "windows")]
#[allow(dead_code)]
const JAVA_BIN: &'static str = "java.exe";
const JAVA_BIN: &str = "java.exe";
#[cfg(not(target_os = "windows"))]
#[allow(dead_code)]
const JAVA_BIN: &'static str = "java";
const JAVA_BIN: &str = "java";
// For example filepath 'path', attempt to resolve it and get a Java version at this path
// If no such path exists, or no such valid java at this path exists, returns None

View File

@@ -31,11 +31,7 @@ pub fn os_rule(rule: &OsRule) -> bool {
let mut rule_match = true;
if let Some(ref arch) = rule.arch {
rule_match &= match arch.as_str() {
"x86" => cfg!(any(target_arch = "x86", target_arch = "x86_64")),
"arm" => cfg!(target_arch = "arm"),
_ => true,
};
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
}
if let Some(name) = &rule.name {

View File

@@ -202,7 +202,7 @@ impl ProfileInit {
let filter = |it: &LoaderVersion| match version.as_str() {
"latest" => true,
"stable" => it.stable,
id => it.id == String::from(id),
id => it.id == *id,
};
let loader_data = match loader {
@@ -211,7 +211,7 @@ impl ProfileInit {
_ => eyre::bail!("Could not get manifest for loader {loader}. This is a bug in the CLI!"),
};
let ref loaders = loader_data.game_versions
let loaders = &loader_data.game_versions
.iter()
.find(|it| it.id == game_version)
.ok_or_else(|| eyre::eyre!("Modloader {loader} unsupported for Minecraft version {game_version}"))?

13
theseus_gui/.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
[*.md]
trim_trailing_whitespace = false

View File

@@ -4,29 +4,18 @@
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"extends": ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],
"parserOptions": {
"ecmaVersion": "latest",
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": [
"vue",
"@typescript-eslint"
],
"plugins": ["vue"],
"rules": {
"no-console": "off",
"vue/no-v-html": "off",
"comma-dangle": [
"error",
"only-multiline"
],
"comma-dangle": ["error", "only-multiline"],
"vue/comment-directive": "off",
"vue/multi-word-component-names": "off",
"import/no-named-as-default": "off"
}
}
}

View File

@@ -3,4 +3,4 @@
"semi": false,
"singleQuote": true,
"endOfLine": "auto"
}
}

View File

@@ -1,7 +1,3 @@
{
"recommendations": [
"Vue.volar",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
"recommendations": ["Vue.volar", "tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

View File

@@ -7,7 +7,10 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri"
"tauri": "tauri",
"lint:js": "eslint --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue .",
"lint": "npm run lint:js && prettier --check .",
"fix": "eslint --fix --ext .js,.vue,.ts,.jsx,.tsx,.html,.vue ."
},
"dependencies": {
"@tauri-apps/api": "^1.2.0",
@@ -25,7 +28,7 @@
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-vue": "^9.9.0",
"prettier": "^2.8.4",
"prettier": "^2.8.7",
"sass": "^1.58.3",
"vite": "^4.0.0",
"vite-plugin-eslint": "^1.8.1"

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -27,9 +27,13 @@
},
"externalBin": [],
"icon": [
"icons/favicon.ico"
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tauri.dev",
"identifier": "com.modrinth.theseus",
"longDescription": "",
"macOS": {
"entitlements": null,
@@ -65,4 +69,4 @@
}
]
}
}
}

View File

@@ -9,12 +9,10 @@ import {
ClientIcon,
PlusIcon,
SettingsIcon,
Button,
Avatar,
} from 'omorphia'
import { useTheming, useInstances } from '@/store/state'
import { toggleTheme } from '@/helpers/theme'
import Instance from '@/components/ui/Instance.vue'
const route = useRoute()
const router = useRouter()
@@ -39,7 +37,9 @@ watch(theme, (newState) => {
<RouterLink to="/" class="button-base nav-button"><ClientIcon /></RouterLink>
<RouterLink to="/browse" class="button-base nav-button"> <SearchIcon /></RouterLink>
<RouterLink to="/library" class="button-base nav-button"> <BookIcon /></RouterLink>
<button color="primary" class="button-base primary nav-button" icon-only><PlusIcon /></button>
<button color="primary" class="button-base primary nav-button" icon-only>
<PlusIcon />
</button>
</div>
</div>
<div class="settings pages-list">
@@ -194,7 +194,7 @@ watch(theme, (newState) => {
.nav-button {
height: 3rem;
width: 3rem;
padding: .75rem;
padding: 0.75rem;
border-radius: var(--radius-md);
svg {

View File

@@ -1,3 +1,3 @@
export { default as PlayIcon } from './play.svg'
export { default as OpenFolderIcon } from './folder-open.svg'
export { default as BrowseIcon } from './folder-search.svg'
export { default as BrowseIcon } from './folder-search.svg'

View File

@@ -1,13 +1,25 @@
<script setup>
import {ChevronLeftIcon, ChevronRightIcon} from 'omorphia'
import { ChevronLeftIcon, ChevronRightIcon } from 'omorphia'
import Instance from '@/components/ui/Instance.vue'
import News from '@/components/ui/News.vue'
import {onMounted, onUnmounted, ref} from 'vue'
import { ref } from 'vue'
const props = defineProps({
instances: Array,
news: Array,
label: String,
instances: {
type: Array,
default() {
return []
},
},
news: {
type: Array,
default() {
return []
},
},
label: {
type: String,
default: '',
},
canPaginate: Boolean,
})
const allowPagination = ref(false)
@@ -16,9 +28,6 @@ const newsRow = ref(null)
// Remove after state is populated with real data
const shouldRenderNormalInstances = props.instances && props.instances?.length !== 0
const shouldRenderNews = props.news && props.news?.length !== 0
onUnmounted(() => {
if (props.canPaginate) window.removeEventListener('resize', handlePaginationDisplay)
})
const handleLeftPage = () => {
if (shouldRenderNormalInstances) modsRow.value.scrollLeft -= 170
else if (shouldRenderNews) newsRow.value.scrollLeft -= 170
@@ -32,10 +41,10 @@ const handleRightPage = () => {
<div class="row">
<div class="header">
<p>{{ props.label }}</p>
<hr>
<hr />
<div v-if="allowPagination" class="pagination">
<ChevronLeftIcon @click="handleLeftPage"/>
<ChevronRightIcon @click="handleRightPage"/>
<ChevronLeftIcon @click="handleLeftPage" />
<ChevronRightIcon @click="handleRightPage" />
</div>
</div>
<section ref="modsRow" class="instances">
@@ -129,4 +138,4 @@ const handleRightPage = () => {
}
}
}
</style>
</style>

View File

@@ -1,13 +1,26 @@
<script setup>
import {ChevronLeftIcon, ChevronRightIcon} from 'omorphia'
import { ChevronLeftIcon, ChevronRightIcon } from 'omorphia'
import Instance from '@/components/ui/Instance.vue'
import News from '@/components/ui/News.vue'
import {onMounted, onUnmounted, ref} from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
const props = defineProps({
instances: Array,
news: Array,
label: String,
instances: {
type: Array,
default() {
return []
},
},
news: {
type: Array,
default() {
return []
},
},
label: {
type: String,
default: '',
},
canPaginate: Boolean,
})
const allowPagination = ref(false)
@@ -48,23 +61,23 @@ const handleRightPage = () => {
<div class="row">
<div class="header">
<p>{{ props.label }}</p>
<hr aria-hidden="true"/>
<hr aria-hidden="true" />
<div v-if="allowPagination" class="pagination">
<ChevronLeftIcon @click="handleLeftPage"/>
<ChevronRightIcon @click="handleRightPage"/>
<ChevronLeftIcon @click="handleLeftPage" />
<ChevronRightIcon @click="handleRightPage" />
</div>
</div>
<section ref="modsRow" class="instances" v-if="shouldRenderNormalInstances">
<section v-if="shouldRenderNormalInstances" ref="modsRow" class="instances">
<Instance
v-for="instance in props.instances"
:key="instance.id"
display="card"
:instance="instance"
class="row-instance"
v-for="instance in props.instances"
:key="instance.id"
display="card"
:instance="instance"
class="row-instance"
/>
</section>
<section ref="newsRow" class="news" v-else-if="shouldRenderNews">
<News v-for="news in props.news" :key="news.id" :news="news"/>
<section v-else-if="shouldRenderNews" ref="newsRow" class="news">
<News v-for="actualNews in props.news" :key="actualNews.id" :news="actualNews" />
</section>
</div>
</template>
@@ -171,4 +184,4 @@ const handleRightPage = () => {
min-width: 12rem;
max-width: 12rem;
}
</style>
</style>

View File

@@ -3,17 +3,32 @@ import { RouterLink } from 'vue-router'
import { Card } from 'omorphia'
import { PlayIcon } from '@/assets/icons'
const props = defineProps({
display: String,
instance: Object,
display: {
type: String,
default: '',
},
instance: {
type: Object,
default() {
return {}
},
},
})
</script>
<template>
<div>
<RouterLink v-if="display === 'list'" class="instance-list-item" :to="`/instance/${props.instance.id}`">{{
props.instance.name
}}</RouterLink>
<Card class="instance-card-item" v-else-if="display === 'card'" @click="this.$router.push(`/instance/${props.instance.id}`)">
<RouterLink
v-if="display === 'list'"
class="instance-list-item"
:to="`/instance/${props.instance.id}`"
>{{ props.instance.name }}</RouterLink
>
<Card
v-else-if="display === 'card'"
class="instance-card-item"
@click="$router.push(`/instance/${props.instance.id}`)"
>
<img :src="props.instance.img" alt="Trending mod card" />
<div class="project-info">
<p class="title">{{ props.instance.name }}</p>
@@ -113,4 +128,4 @@ const props = defineProps({
color: #000;
}
}
</style>
</style>

View File

@@ -1,7 +1,12 @@
<script setup>
import { Card, ChevronRightIcon } from 'omorphia'
const props = defineProps({
news: Object,
news: {
type: Object,
default() {
return {}
},
},
})
</script>
@@ -83,4 +88,4 @@ const props = defineProps({
}
}
}
</style>
</style>

View File

@@ -14,9 +14,9 @@ const popularInstances = instances.instances.filter((i) => i.downloads > 50 || i
<template>
<div class="page-container">
<RowDisplay label="Jump back in" :instances="recentInstances" :canPaginate="false" />
<RowDisplay label="Popular packs" :instances="popularInstances" :canPaginate="true" />
<RowDisplay label="News & updates" :news="news.news" :canPaginate="true" />
<RowDisplay label="Jump back in" :instances="recentInstances" :can-paginate="false" />
<RowDisplay label="Popular packs" :instances="popularInstances" :can-paginate="true" />
<RowDisplay label="News & updates" :news="news.news" :can-paginate="true" />
</div>
</template>

View File

@@ -2,9 +2,9 @@
<div class="instance-container">
<div class="side-cards">
<Card class="instance-card">
<Avatar size="lg" :src="getInstance(instances).img"/>
<Avatar size="lg" :src="getInstance(instances).img" />
<div class="instance-info">
<h2 class="name">{{getInstance(instances).name}}</h2>
<h2 class="name">{{ getInstance(instances).name }}</h2>
Fabric {{ getInstance(instances).version }}
</div>
<span class="button-group">
@@ -18,40 +18,40 @@
</span>
</Card>
<div class="pages-list">
<RouterLink :to="`/instance/${this.$route.params.id}/`" class="btn">
<BoxIcon/>
<RouterLink :to="`/instance/${$route.params.id}/`" class="btn">
<BoxIcon />
Mods
</RouterLink>
<RouterLink :to="`/instance/${this.$route.params.id}/options`" class="btn">
<SettingsIcon/>
<RouterLink :to="`/instance/${$route.params.id}/options`" class="btn">
<SettingsIcon />
Options
</RouterLink>
<RouterLink :to="`/instance/${this.$route.params.id}/logs`" class="btn">
<FileIcon/>
<RouterLink :to="`/instance/${$route.params.id}/logs`" class="btn">
<FileIcon />
Logs
</RouterLink>
</div>
</div>
<div class="content">
<Promotion />
<router-view/>
<router-view />
</div>
</div>
</template>
<script setup>
import {BoxIcon, SettingsIcon, FileIcon, Button, Avatar, Card, Promotion} from 'omorphia'
import {PlayIcon, OpenFolderIcon} from "@/assets/icons";
import {useInstances} from '@/store/state'
import { BoxIcon, SettingsIcon, FileIcon, Button, Avatar, Card, Promotion } from 'omorphia'
import { PlayIcon, OpenFolderIcon } from '@/assets/icons'
import { useInstances } from '@/store/state'
const instances = useInstances();
instances.fetchInstances();
const instances = useInstances()
instances.fetchInstances()
</script>
<script>
export default {
methods: {
getInstance(instances) {
return instances.instances.find((i) => i.id === parseInt(this.$route.params.id));
}
return instances.instances.find((i) => i.id === parseInt(this.$route.params.id))
},
},
}
</script>
@@ -266,4 +266,4 @@ Button {
height: 1px;
margin: var(--gap-xl) 0;
}
</style>
</style>

View File

@@ -4,21 +4,21 @@
<DropdownSelect :options="['logs/latest.log']" />
<div class="button-group">
<Button>
<ClipboardCopyIcon/>
<ClipboardCopyIcon />
Copy
</Button>
<Button color="primary">
<SendIcon/>
<SendIcon />
Share
</Button>
<Button color="danger">
<TrashIcon/>
<TrashIcon />
Delete
</Button>
</div>
</div>
<div class="log-text">
<div v-for="line in fileContents.value.split('\n')"> {{ line }} </div>
<div v-for="(line, index) in fileContents.value.split('\n')" :key="index">{{ line }}</div>
</div>
</Card>
</template>
@@ -27,66 +27,66 @@
import { Card, Button, TrashIcon, SendIcon, ClipboardCopyIcon, DropdownSelect } from 'omorphia'
</script>
<script>
export default {
data() {
return {
fileContents: {
value: "'ServerLevel[New World]'/minecraft:the_end\n" +
"[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n" +
"[22:13:02] [Server thread/INFO]: venashial left the game\n" +
"[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n" +
"[22:13:02] [Server thread/INFO]: Stopping server\n" +
"[22:13:02] [Server thread/INFO]: Saving players\n" +
"[22:13:02] [Server thread/INFO]: Saving worlds\n" +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n" +
"[22:13:06] [Render thread/INFO]: Stopping worker threads\n" +
"[22:13:07] [Render thread/INFO]: Stopping!\n" +
"[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n" +
"'ServerLevel[New World]'/minecraft:the_end\n" +
"[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n" +
"[22:13:02] [Server thread/INFO]: venashial left the game\n" +
"[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n" +
"[22:13:02] [Server thread/INFO]: Stopping server\n" +
"[22:13:02] [Server thread/INFO]: Saving players\n" +
"[22:13:02] [Server thread/INFO]: Saving worlds\n" +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n" +
"[22:13:06] [Render thread/INFO]: Stopping worker threads\n" +
"[22:13:07] [Render thread/INFO]: Stopping!\n" +
"[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n" +
"'ServerLevel[New World]'/minecraft:the_end\n" +
"[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n" +
"[22:13:02] [Server thread/INFO]: venashial left the game\n" +
"[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n" +
"[22:13:02] [Server thread/INFO]: Stopping server\n" +
"[22:13:02] [Server thread/INFO]: Saving players\n" +
"[22:13:02] [Server thread/INFO]: Saving worlds\n" +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n" +
"[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n" +
"[22:13:06] [Render thread/INFO]: Stopping worker threads\n" +
"[22:13:07] [Render thread/INFO]: Stopping!\n" +
"[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence..."
}
};
export default {
data() {
return {
fileContents: {
value:
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' +
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' +
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...',
},
}
};
},
}
</script>
<style scoped lang="scss">
@@ -118,4 +118,4 @@ import { Card, Button, TrashIcon, SendIcon, ClipboardCopyIcon, DropdownSelect }
overflow: auto;
white-space: normal;
}
</style>
</style>

View File

@@ -2,63 +2,59 @@
<Card class="mod-card">
<div class="card-row">
<div class="iconified-input">
<SearchIcon/>
<input
type="text"
placeholder="Search Mods"
v-model="searchFilter"
/>
<SearchIcon />
<input v-model="searchFilter" type="text" placeholder="Search Mods" />
</div>
<span class="manage">
<span class="text-combo">
Sort By
<DropdownSelect :options="['Name', 'Version', 'Author']" v-model="sortFilter" default-value="Name" class="dropdown"/>
<DropdownSelect
v-model="sortFilter"
:options="['Name', 'Version', 'Author']"
default-value="Name"
class="dropdown"
/>
</span>
<Button color="primary">
<PlusIcon />
Add Mods
</Button>
<PlusIcon />
Add Mods
</Button>
</span>
</div>
<div class="table-container">
<div class="table-row table-head">
<div class="table-cell table-text">
<Button color="success" iconOnly>
<Button color="success" icon-only>
<UpdatedIcon />
</Button>
</div>
<div class="table-cell table-text name-cell"> Name </div>
<div class="table-cell table-text"> Version </div>
<div class="table-cell table-text"> Author </div>
<div class="table-cell table-text"> Actions </div>
<div class="table-cell table-text name-cell">Name</div>
<div class="table-cell table-text">Version</div>
<div class="table-cell table-text">Author</div>
<div class="table-cell table-text">Actions</div>
</div>
<div class="table-row" v-for="mod in search" :key="mod.name">
<div v-for="mod in search" :key="mod.name" class="table-row">
<div class="table-cell table-text">
<Button v-if="mod.outdated" iconOnly>
<Button v-if="mod.outdated" icon-only>
<UpdatedIcon />
</Button>
<Button v-else disabled iconOnly>
<CheckCircleIcon/>
<Button v-else disabled icon-only>
<CheckCircleIcon />
</Button>
</div>
<div class="table-cell table-text name-cell">
<span class="mod-text">
<Avatar :src="mod.icon"/>
<Avatar :src="mod.icon" />
{{ mod.name }}
</span>
</div>
<div class="table-cell table-text"> {{ mod.version }} </div>
<div class="table-cell table-text"> {{ mod.author }} </div>
<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 iconOnly>
<Button icon-only>
<TrashIcon />
</Button>
<input
type="checkbox"
class="switch stylized-toggle"
id="switch-1"
checked
/>
<input id="switch-1" type="checkbox" class="switch stylized-toggle" checked />
</div>
</div>
</div>
@@ -67,55 +63,66 @@
<script>
export default {
name: "Mods",
name: 'Mods',
data() {
return {
searchFilter: "",
sortFilter: "",
searchFilter: '',
sortFilter: '',
mods: [
{
name: "Fabric API",
icon: "https://cdn.modrinth.com/data/P7dR8mSH/icon.png",
version: "0.76.0+1.19.4",
author: "modmuss50",
description: "Lightweight and modular API providing common hooks and intercompatibility measures utilized by mods using the Fabric toolchain.",
outdated: true
name: 'Fabric API',
icon: 'https://cdn.modrinth.com/data/P7dR8mSH/icon.png',
version: '0.76.0+1.19.4',
author: 'modmuss50',
description:
'Lightweight and modular API providing common hooks and intercompatibility measures utilized by mods using the Fabric toolchain.',
outdated: true,
},
{
name: "Spirit",
icon: "https://cdn.modrinth.com/data/b1LdOZlE/465598dc5d89f67fb8f8de6def21240fa35e3a54.png",
version: "2.2.4",
author: "CodexAdrian",
description: "Create your own configurable mob spawner!",
outdated: true
name: 'Spirit',
icon: 'https://cdn.modrinth.com/data/b1LdOZlE/465598dc5d89f67fb8f8de6def21240fa35e3a54.png',
version: '2.2.4',
author: 'CodexAdrian',
description: 'Create your own configurable mob spawner!',
outdated: true,
},
{
name: "Botarium",
icon: "https://cdn.modrinth.com/data/2u6LRnMa/98b286b0d541ad4f9409e0af3df82ad09403f179.gif",
version: "2.0.5",
author: "CodexAdrian",
description: "A crossplatform API for devs that makes transfer and storage of items, fluids and energy easier, as well as some other helpful things",
outdated: true
name: 'Botarium',
icon: 'https://cdn.modrinth.com/data/2u6LRnMa/98b286b0d541ad4f9409e0af3df82ad09403f179.gif',
version: '2.0.5',
author: 'CodexAdrian',
description:
'A crossplatform API for devs that makes transfer and storage of items, fluids and energy easier, as well as some other helpful things',
outdated: true,
},
{
name: "Tempad",
icon: "https://cdn.modrinth.com/data/gKNwt7xu/icon.gif",
version: "2.2.4",
author: "CodexAdrian",
description: "Create a portal to anywhere from anywhere",
outdated: false
name: 'Tempad',
icon: 'https://cdn.modrinth.com/data/gKNwt7xu/icon.gif',
version: '2.2.4',
author: 'CodexAdrian',
description: 'Create a portal to anywhere from anywhere',
outdated: false,
},
{
name: "Sodium",
icon: "https://cdn.modrinth.com/data/AANobbMI/icon.png",
version: "0.4.10",
author: "jellysquid3",
description: "Modern rendering engine and client-side optimization mod for Minecraft",
outdated: false
}
]
name: 'Sodium',
icon: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
version: '0.4.10',
author: 'jellysquid3',
description: 'Modern rendering engine and client-side optimization mod for Minecraft',
outdated: false,
},
],
}
},
computed: {
search() {
const filtered = this.mods.filter((mod) => {
return mod.name.toLowerCase().includes(this.searchFilter.toLowerCase())
})
return this.updateSort(filtered, this.sortFilter)
},
},
methods: {
updateSort(projects, sort) {
switch (sort) {
@@ -148,23 +155,24 @@ export default {
return 1
}
return 0
})
})
}
},
},
computed: {
search() {
const filtered = this.mods.filter((mod) => {
return mod.name.toLowerCase().includes(this.searchFilter.toLowerCase())
})
return this.updateSort(filtered, this.sortFilter);
}
}
}
</script>
<script setup>
import { Avatar, Button, TrashIcon, PlusIcon, Card, CheckCircleIcon, SearchIcon, UpdatedIcon, DropdownSelect } from 'omorphia'
import {
Avatar,
Button,
TrashIcon,
PlusIcon,
Card,
CheckCircleIcon,
SearchIcon,
UpdatedIcon,
DropdownSelect,
} from 'omorphia'
</script>
<style scoped lang="scss">
@@ -244,4 +252,4 @@ import { Avatar, Button, TrashIcon, PlusIcon, Card, CheckCircleIcon, SearchIcon,
.dropdown {
width: 7rem !important;
}
</style>
</style>

View File

@@ -1,172 +1,132 @@
<template>
<Card class="settings-card">
<h2 class="settings-title"> Java </h2>
<h2 class="settings-title">Java</h2>
<div class="settings-group">
<h3>Installation</h3>
<input
type="text"
class="input installation-input"
placeholder="/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
ref="javaPath"
v-model="javaPath"
ref="javaPath"
v-model="javaPath"
type="text"
class="input installation-input"
placeholder="/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
/>
<span class="installation-buttons">
<Button @click="saveJavaPath">
<SearchIcon/>
Auto Detect
</Button>
<Button @click="saveJavaPath">
<BrowseIcon/>
Browse
</Button>
<Button @click="saveJavaPath">
<PlayIcon/>
Test
</Button>
</span>
<Button @click="saveJavaPath">
<SearchIcon />
Auto Detect
</Button>
<Button @click="saveJavaPath">
<BrowseIcon />
Browse
</Button>
<Button @click="saveJavaPath">
<PlayIcon />
Test
</Button>
</span>
</div>
<hr class="card-divider">
<hr class="card-divider" />
<div class="settings-group">
<h3>Arguments</h3>
<input
type="text"
class="input installation-input"
ref="javaArgs"
v-model="javaArgs"
/>
<input ref="javaArgs" v-model="javaArgs" type="text" class="input installation-input" />
</div>
<hr class="card-divider">
<hr class="card-divider" />
<div class="settings-group">
<div class="sliders">
<span class="slider">
Minimum Memory
<Slider
v-model="javaMemory"
:min="1024"
:max="8192"
:step="1024"
/>
</span>
<span class="slider">
Maximum Memory
<Slider
v-model="javaMemory"
:min="1024"
:max="8192"
:step="1024"
/>
</span>
Minimum Memory
<Slider v-model="javaMemory" :min="1024" :max="8192" :step="1024" />
</span>
<span class="slider">
Maximum Memory
<Slider v-model="javaMemory" :min="1024" :max="8192" :step="1024" />
</span>
</div>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title"> Window </h2>
<h2 class="settings-title">Window</h2>
<div class="settings-group">
<div class="settings-group">
<div class="sliders">
<span class="slider">
Width
<Slider
v-model="javaMemory"
:min="1024"
:max="8192"
:step="1024"
/>
<Slider v-model="javaMemory" :min="1024" :max="8192" :step="1024" />
</span>
<span class="slider">
Height
<Slider
v-model="javaMemory"
:min="1024"
:max="8192"
:step="1024"
/>
<Slider v-model="javaMemory" :min="1024" :max="8192" :step="1024" />
</span>
</div>
<div class="toggle-setting">
Start in Fullscreen
<input
type="checkbox"
id="fullscreen"
name="fullscreen"
v-model="fullscreen"
class="switch stylized-toggle"
id="fullscreen"
v-model="fullscreen"
type="checkbox"
name="fullscreen"
class="switch stylized-toggle"
/>
</div>
</div>
<hr class="card-divider">
<hr class="card-divider" />
<div class="settings-group">
<h3>Console</h3>
<div class="toggle-setting">
Show console while game is running
<input
type="checkbox"
id="fullscreen"
name="fullscreen"
v-model="fullscreen"
class="switch stylized-toggle"
id="fullscreen"
v-model="fullscreen"
type="checkbox"
name="fullscreen"
class="switch stylized-toggle"
/>
</div>
<div class="toggle-setting">
Close console when game quits
<input
type="checkbox"
id="fullscreen"
name="fullscreen"
v-model="fullscreen"
class="switch stylized-toggle"
id="fullscreen"
v-model="fullscreen"
type="checkbox"
name="fullscreen"
class="switch stylized-toggle"
/>
</div>
<div class="toggle-setting">
Show console when game crashes
<input
type="checkbox"
id="fullscreen"
name="fullscreen"
v-model="fullscreen"
class="switch stylized-toggle"
id="fullscreen"
v-model="fullscreen"
type="checkbox"
name="fullscreen"
class="switch stylized-toggle"
/>
</div>
</div>
</div>
</Card>
<Card class="settings-card">
<h2 class="settings-title"> Commands </h2>
<h2 class="settings-title">Commands</h2>
<div class="settings-group">
<div class="toggle-setting">
Pre Launch
<input
type="text"
class="input"
ref="javaArgs"
v-model="javaArgs"
/>
<input ref="javaArgs" v-model="javaArgs" type="text" class="input" />
</div>
<div class="toggle-setting">
Wrapper
<input
type="text"
class="input"
ref="javaArgs"
v-model="javaArgs"
/>
<input ref="javaArgs" v-model="javaArgs" type="text" class="input" />
</div>
<div class="toggle-setting">
Post Launch
<input
type="text"
class="input"
ref="javaArgs"
v-model="javaArgs"
/>
<input ref="javaArgs" v-model="javaArgs" type="text" class="input" />
</div>
</div>
</Card>
</template>
<script setup>
import {Card, Button, SearchIcon, Slider } from 'omorphia'
import {BrowseIcon, PlayIcon} from "@/assets/icons";
import { Card, Button, SearchIcon, Slider } from 'omorphia'
import { BrowseIcon, PlayIcon } from '@/assets/icons'
</script>
<style scoped lang="scss">
@@ -177,7 +137,7 @@ import {BrowseIcon, PlayIcon} from "@/assets/icons";
}
.settings-title {
color: var(--color-contrast)
color: var(--color-contrast);
}
.settings-group {
@@ -195,9 +155,8 @@ import {BrowseIcon, PlayIcon} from "@/assets/icons";
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: .5rem;
gap: 0.5rem;
margin: 0;
}
.sliders {

View File

@@ -1,6 +1,6 @@
import Index from './Index.vue'
import Mods from "./Mods.vue";
import Options from "./Options.vue";
import Logs from "./Logs.vue";
import Mods from './Mods.vue'
import Options from './Options.vue'
import Logs from './Logs.vue'
export { Index, Mods, Options, Logs }
export { Index, Mods, Options, Logs }

View File

@@ -45,9 +45,9 @@ export default new createRouter({
props: true,
children: [
{
path: '',
name: 'Mods',
component: Instance.Mods,
path: '',
name: 'Mods',
component: Instance.Mods,
},
{
path: 'options',
@@ -58,9 +58,9 @@ export default new createRouter({
path: 'logs',
name: 'Logs',
component: Instance.Logs,
}
]
}
},
],
},
],
linkActiveClass: 'router-link-active',
linkExactActiveClass: 'router-link-exact-active',

View File

@@ -2,7 +2,8 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import alias from '@rollup/plugin-alias'
import { resolve } from 'path'
import svgLoader from "vite-svg-loader";
import eslint from 'vite-plugin-eslint'
import svgLoader from 'vite-svg-loader'
const projectRootDir = resolve(__dirname)
@@ -18,6 +19,7 @@ export default defineConfig({
},
],
}),
eslint(),
svgLoader({
svgoConfig: {
plugins: [

View File

@@ -1315,7 +1315,7 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^2.8.4:
prettier@^2.8.7:
version "2.8.7"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450"
integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==