Feature: Package API (#75) (i'm just going to merge this because the other branches are inactive)

* feat: Start work on package api

* feat: Browser Context Controller

* feat: Add packages to cluster metadata
feat: Change structure of Package

* 🐛 fix: format

* 🔨 feat: Add more functions wrapping around Modrinth

* 🔨 feat: Start working on browser frontend

* feat: BrowserPackage work + markdown support

* 🐛 fix: fmt

Signed-off-by: Pauline <git@ethanlibs.co>

* 🚧 wip: h

Signed-off-by: Pauline <git@ethanlibs.co>

* feat: Download button for packages for suitable clusters + tweaks

* 🚧 wip: markdown and stuff

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: some patches

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: remove react bloat

Signed-off-by: Pauline <git@ethanlibs.co>

* 🔨 chore(pkg/infer): try to reduce reused code

Signed-off-by: Pauline <git@ethanlibs.co>

* 🔨 chore: bump deps

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: markdown innerhtml

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: h

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: tweak

Signed-off-by: Pauline <git@ethanlibs.co>

* 🚧 wip: idk

Signed-off-by: Pauline <git@ethanlibs.co>

* fix: Gamemode compile error
chore: Add missing and remove unused libraries
feat: usePreventLeave hook
feat: ClusterOverview edit mode continues with navigation (assuming there was a navigation attempt) after pressing Yes or No for save

* feat: Add reload clusters button on home page

* feat: More information provided to BrowserPackage

* fix: Minor fixes (Navbar isActive, Markdown click prevention, Markdown list styling)

* 🐛 fix: h

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: highlighting

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: forge modern

Signed-off-by: Pauline <git@ethanlibs.co>

* 🔨 chore: bump deps

Signed-off-by: Pauline <git@ethanlibs.co>

* fix: Markdown-it issue + Added promptOpener hook for Markdown links

* feat: Made BrowserPackage sidebar sticky

* refactor: Package system (not yet in a working state)

* 🚧 wip: bump deps temp

Signed-off-by: Pauline <git@ethanlibs.co>

* feat: Added a package manager

* ✨ feature: start prisma work, but it doesnt break anything so we can keep it until its done. also fix some stuff

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: pushing

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: linting

Signed-off-by: Pauline <git@ethanlibs.co>

* ✨ feature: start ingress integration

Signed-off-by: Pauline <git@ethanlibs.co>

* feat: Package Manager should work now 👍

* fix: Package manager tracing spam

* feat: Added tauri commands for cluster packages
format: Auto formatted frontend + backend

* 🐛 fix: tmp

Signed-off-by: Pauline <git@ethanlibs.co>

* 🔨 chore: format

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: specta extension

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: github actions

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: alias

Signed-off-by: Pauline <git@ethanlibs.co>

* chore: Some tweaks and fixes

* chore: Rebase + gitignore bindings.ts

* chore: Remove bindings.ts

* 🐛 fix: migrate

Signed-off-by: Pauline <git@ethanlibs.co>

* 🚧 wip: not sure

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: finalize prisma init schema

Signed-off-by: Pauline <git@ethanlibs.co>

* 🔨 chore: format

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: bump deps

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: system deps

Signed-off-by: Pauline <git@ethanlibs.co>

* fix: solid-router navigation issue

* feat: Added new BrowserMain page

* feat: Tweaks + new package list view

* 🐛 fix: bump + format (todo: tauri2, etc)

Signed-off-by: Pauline <git@ethanlibs.co>

* 🐛 fix: devx (fixes #126)

Signed-off-by: Pauline <git@ethanlibs.co>

* 🔨 chore: migrate to consola

Signed-off-by: Pauline <git@ethanlibs.co>

---------

Signed-off-by: Pauline <git@ethanlibs.co>
Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
Pauline 2024-08-31 16:23:24 -04:00 • committed by GitHub
parent d7faee2066
commit 28b1f9f0e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 12554 additions and 4512 deletions

View file

@ -29,10 +29,9 @@ rustflags = ["-C", "link-args=-fuse-ld={{{linker}}}"]
[target.aarch64-unknown-linux-gnu]
{{#hasLLD}}
linker = "clang"
rustflags = ["-C", "link-args=-fus-ld={{{linker}}}"]
rustflags = ["-C", "link-args=-fuse-ld={{{linker}}}"]
{{/hasLLD}}
{{/isLinux}}
{{#hasAlias}}
prisma = "run -p prisma-cli --bin prisma --"
{{/hasAlias}}
[alias]
prisma = "run -p onelauncher_prisma_cli --bin prisma --"

223
.github/actions/clippy/dist/LICENSE vendored Normal file
View file

@ -0,0 +1,223 @@
@actions/core
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/exec
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/http-client
MIT
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/io
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/tool-cache
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@fastify/busboy
MIT
Copyright Brian White. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
pathe
MIT
MIT License
Copyright (c) Pooya Parsa <pooya@pi0.io> - Daniel Roe <daniel@roe.dev>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
Copyright Joyent, Inc. and other Node contributors.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
semver
ISC
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
tunnel
MIT
The MIT License (MIT)
Copyright (c) 2012 Koichi Kobayashi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
undici
MIT
MIT License
Copyright (c) Matteo Collina and Undici contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
uuid
MIT
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because one or more lines are too long

View file

@ -12,14 +12,14 @@
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {
"build": "ncc build src/index.ts --minify"
"build": "rimraf dist && ncc build src/index.ts -m -q --license ../../LICENSE"
},
"dependencies": {
"@actions/artifact": "^2.1.9",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/glob": "^0.5.0",
"@actions/http-client": "^2.2.2",
"@actions/http-client": "^2.2.3",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "^2.0.1",
"pathe": "^1.1.2"

View file

@ -8,10 +8,11 @@ import { exec } from '@actions/exec';
import { getInput as getCoreInput, group } from '@actions/core';
import { installReviewdog } from './installer';
const getInput = <T = string>(name: string): T => getCoreInput(name) as T;
async function run(): Promise<void> {
const runnerTmpdir = env.RUNNER_TEMP || getTmpdir();
const tmpdir = await fs.mkdtemp(join(runnerTmpdir, 'reviewdog-'));
const getInput = <T = string>(name: string): T => getCoreInput(name) as T;
const reviewdogVersion = getInput('reviewdog_version') || 'latest';
const toolName = getInput('tool_name') || 'clippy';

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,7 @@
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {
"build": "ncc build src/index.ts --minify"
"build": "rimraf dist && ncc build src/index.ts -m -q --license ../../LICENSE"
},
"dependencies": {
"@actions/artifact": "^2.1.9",

View file

@ -40,3 +40,47 @@ runs:
- name: Prepare Cargo
shell: bash
run: echo '{}' | npx -y mustache - .cargo/config.toml.mustache .cargo/config.toml
- name: Prisma Cache
id: prisma-cache
uses: actions/cache/restore@v4
with:
key: prisma-v1-${{ runner.os }}-${{ hashFiles('./packages/core/prisma/*', './Cargo.*') }}
path: packages/prisma/src/**/*.rs
- name: Generate Prisma
if: ${{ steps.prisma-cache.outputs.cache-hit != 'true' }}
shell: bash
run: |
set -euxo pipefail
npx -y prisma generate --schema packages/core/prisma/schema.prisma
if ! [ -f packages/prisma/src/prisma/mod.rs ]; then
echo "prisma generation failed" >&2
exit 1
fi
npx -y prisma migrate dev --schema packages/core/prisma/schema.prisma -n test --create-only --skip-generate
_new_migrations="$(
git ls-files --others --exclude-standard \
| { grep '^prisma/migrations/' || true; } \
| xargs sh -euxc '[ "$#" -lt 2 ] || grep -L "$@" || true' sh 'empty migration' \
| wc -l \
| awk '{$1=$1};1'
)"
if [ "$_new_migrations" -gt 0 ]; then
echo "::error file=packages/core/prisma/schema.prisma,title=missing migration::new migration should be generated due to changes in prisma schema"
case "$GITHUB_REF" in
"refs/heads/main" | "refs/heads/gh-readonly-queue/main/"* | "refs/tags/"*)
exit 1
;;
esac
fi
- name: Save Prisma Cache
id: prisma-cache-save
if: ${{ steps.prisma-cache.outputs.cache-hit != 'true' && (github.ref == 'refs/heads/main' || inputs.save-cache == 'true') }}
uses: actions/cache/save@v4
with:
key: ${{ steps.cache-prisma-restore.outputs.cache-primary-key }}
path: packages/prisma/src/**/*.rs

View file

@ -93,11 +93,11 @@ jobs:
- '.github/actions/setup-rust/**'
- '.github/actions/setup-system/**'
- name: Setup System
- name: Setup Rust
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'
uses: ./.github/actions/setup-system
uses: ./.github/actions/setup-rust
with:
token: ${{ secrets.GITHUB_TOKEN }}
restore-cache: false
- name: Run Rustfmt
if: steps.filter.outcome != 'success' || steps.filter.outputs.changes == 'true'

150
.gitignore vendored
View file

@ -2,9 +2,10 @@ crates/prisma/src/prisma.rs
dev.db
dev.db-journal
.cargo/config.toml
.deps/*
# Created by https://www.toptal.com/developers/gitignore/api/git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all
# Created by https://www.toptal.com/developers/gitignore/api/node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git
# Edit at https://www.toptal.com/developers/gitignore?templates=node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git
### Git ###
# Created by git for backups. To disable backups in Git:
@ -21,7 +22,7 @@ dev.db-journal
*_LOCAL_*.txt
*_REMOTE_*.txt
### Intellij+all ###
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
@ -100,14 +101,151 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Intellij+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### Intellij+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### Linux ###
*~
@ -378,4 +516,4 @@ $RECYCLE.BIN/
# and uncomment the following lines
# .pnp.*
# End of https://www.toptal.com/developers/gitignore/api/git,node,rust,yarn,linux,macos,windows,rust-analyzer,visualstudiocode,intellij+all
# End of https://www.toptal.com/developers/gitignore/api/node,rust,yarn,linux,macos,windows,intellij+all,rust-analyzer,visualstudiocode,intellij,intellij+iml,git

View file

@ -2,6 +2,9 @@
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"files.readonlyInclude": {
"**/node_modules/**": true
},
"files.watcherExclude": {
"**/_build": true,
"**/.classpath": true,
@ -94,7 +97,7 @@
"flags": ["unstable_ts_config"]
},
// updated 2024-08-03 17:32
// updated 2024-08-26 15:46
// https://github.com/antfu/vscode-file-nesting-config
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
@ -127,7 +130,7 @@
"mix.exs": ".credo.exs, .dialyzer_ignore.exs, .formatter.exs, .iex.exs, .tool-versions, mix.lock",
"next.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, next-env.d.ts, next-i18next.config.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"nuxt.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .nuxtignore, .nuxtrc, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"package.json": ".browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lockb, bunfig.toml, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, ruff.toml, simple-git-hooks*, sonar-project.properties, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.toml, xo.config.*, yarn*",
"package.json": "*.code-workspace, .browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lockb, bunfig.toml, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, ruff.toml, simple-git-hooks*, sonar-project.properties, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.toml, xo.config.*, yarn*",
"Pipfile": ".editorconfig, .flake8, .isort.cfg, .python-version, Pipfile, Pipfile.lock, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
"pubspec.yaml": ".metadata, .packages, all_lint_rules.yaml, analysis_options.yaml, build.yaml, pubspec.lock, pubspec_overrides.yaml",
"pyproject.toml": ".commitlint*, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .flake8, .flowconfig, .isort.cfg, .jslint*, .lintstagedrc*, .markdownlint*, .pdm-python, .pdm.toml, .prettier*, .pylintrc, .python-version, .ruff.toml, .stylelint*, .textlint*, .xo-config*, .yamllint*, MANIFEST.in, Pipfile, Pipfile.lock, biome.json*, commitlint*, dangerfile*, dlint.json, dprint.json*, eslint*, hatch.toml, lint-staged*, pdm.lock, phpcs.xml, poetry.lock, poetry.toml, prettier*, pyproject.toml, pyrightconfig.json, requirements*.in, requirements*.pip, requirements*.txt, ruff.toml, setup.cfg, setup.py, stylelint*, tox.ini, tslint*, xo.config.*",
@ -137,7 +140,7 @@
"README*": "AUTHORS, Authors, BACKERS*, Backers*, CHANGELOG*, CITATION*, CODEOWNERS, CODE_OF_CONDUCT*, CONTRIBUTING*, CONTRIBUTORS, COPYING*, CREDITS, Changelog*, Citation*, Code_Of_Conduct*, Codeowners, Contributing*, Contributors, Copying*, Credits, GOVERNANCE.MD, Governance.md, HISTORY.MD, History.md, LICENSE*, License*, MAINTAINERS, Maintainers, README-*, README_*, RELEASE_NOTES*, Readme-*, Readme_*, Release_Notes*, SECURITY.MD, SPONSORS*, Security.md, Sponsors*, authors, backers*, changelog*, citation*, code_of_conduct*, codeowners, contributing*, contributors, copying*, credits, governance.md, history.md, license*, maintainers, readme-*, readme_*, release_notes*, security.md, sponsors*",
"remix.config.*": "*.env, .babelrc*, .codecov, .cssnanorc*, .env.*, .envrc, .htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, capacitor.config.*, contentlayer.config.*, cssnano.config.*, cypress.*, env.d.ts, formkit.config.*, formulate.config.*, histoire.config.*, htmlnanorc.*, i18n.config.*, ionic.config.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, panda.config.*, playwright.config.*, postcss.config.*, puppeteer.config.*, remix.*, rspack.config.*, sst.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, uno.config.*, unocss.config.*, vitest.config.*, vuetify.config.*, webpack.config.*, windi.config.*",
"requirements.txt": ".editorconfig, .flake8, .isort.cfg, .python-version, requirements*.in, requirements*.pip, requirements*.txt, tox.ini",
"rush.json": ".browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lockb, bunfig.toml, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, ruff.toml, simple-git-hooks*, sonar-project.properties, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.toml, xo.config.*, yarn*",
"rush.json": "*.code-workspace, .browserslist*, .circleci*, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json*, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitmojirc.json, .gitpod*, .huskyrc*, .jslint*, .knip.*, .lintstagedrc*, .markdownlint*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .pylintrc, .release-please*.json, .releaserc*, .ruff.toml, .sentry*, .simple-git-hooks*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, apollo.config.*, appveyor*, azure-pipelines*, biome.json*, bower.json, build.config.*, bun.lockb, bunfig.toml, commitlint*, crowdin*, dangerfile*, dlint.json, dprint.json*, electron-builder.*, eslint*, firebase.json, grunt*, gulp*, jenkins*, knip.*, lerna*, lint-staged*, nest-cli.*, netlify*, nodemon*, npm-shrinkwrap.json, nx.*, package-lock.json, package.nls*.json, phpcs.xml, pm2.*, pnpm*, prettier*, pullapprove*, pyrightconfig.json, release-please*.json, release-tasks.sh, release.config.*, renovate*, rollup.config.*, rspack*, ruff.toml, simple-git-hooks*, sonar-project.properties, stylelint*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, webpack*, workspace.json, wrangler.toml, xo.config.*, yarn*",
"sanity.config.*": "sanity.cli.*, sanity.types.ts, schema.json",
"setup.cfg": ".editorconfig, .flake8, .isort.cfg, .python-version, MANIFEST.in, requirements*.in, requirements*.pip, requirements*.txt, setup.cfg, tox.ini",
"setup.py": ".editorconfig, .flake8, .isort.cfg, .python-version, MANIFEST.in, requirements*.in, requirements*.pip, requirements*.txt, setup.cfg, setup.py, tox.ini",

2314
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,9 @@ members = [
"apps/desktop", # onelauncher_gui -- the rust code for our tauri app
"packages/core", # onelauncher -- the rust core for onelauncher
"packages/macros", # onelauncher_macros -- utility procedural macros for onelauncher
"packages/gamemode" # onelauncher_gamemode -- rust wrapper for feral gamemode
"packages/gamemode", # onelauncher_gamemode -- rust wrapper for feral gamemode
"packages/prisma-cli", # onelauncher_prisma_cli -- rust wrapper for prisma cli
"packages/prisma" # onelauncher_prisma -- rust client for prisma
]
[workspace.package] # sets default [package] settings for workspace members
@ -27,6 +29,8 @@ onelauncher_macros = { path = "./packages/macros" }
onelauncher_gui = { path = "./apps/desktop" }
onelauncher_test = { path = "./apps/testing" }
onelauncher_gamemode = { path = "./packages/gamemode" }
onelauncher_prisma = { path = "./packages/prisma" }
onelauncher_prisma_cli = { path = "./packages/prisma-cli" }
# # ============= TAURI ============= # #
# tauri uses latest crates.io rc channel
@ -90,7 +94,7 @@ specta-typescript = { version = "=0.0.7" }
# rspc-specta2 is the specta v2 (rc) adapter for rspc.
rspc-tauri2 = { version = "=0.0.1" }
# core utilities for minecraft and mod management with pulseflow apis
interpulse = { version = "1", features = [ "specta" ] }
interpulse = { version = "1.1.0", features = [ "specta" ] }
# # ============= SECURITY ============= # #
thiserror = { version = "1.0" }
@ -115,6 +119,8 @@ byteorder = { version = "1.5.0" }
base64 = { version = "0.22" }
p256 = { version = "0.13.2", features = [ "ecdsa" ] }
uuid = { version = "1.8", features = [ "serde", "v4" ] }
toml = { version = "0.8" }
nom = { version = "7" }
# # ============= NETWORK ============= # #
reqwest = { version = "0.12", default-features = false, features = [
@ -183,7 +189,7 @@ features = [
]
default-features = false
[workspace.dependencies.prisma-client-rust-cli]
[workspace.dependencies.prisma-client-rust-generator]
git = "https://github.com/brendonovich/prisma-client-rust"
rev = "4f9ef9d38ca732162accff72b2eb684d2f120bab"
features = [

31
TODO.md
View file

@ -1,6 +1,11 @@
# todo list
- [ ] **remove clusterpath, just switch to pathbuf or string in prisma, and refactor package api**
- [ ] **splash screen and reduce startup times**
- [ ] fix missing jars and libraries when launching clusters
- [ ] migrate launcher backend storage to `prisma-client-rust` (see `prisma` branch).
- maybe use `sqlx` instead to avoid unnecessary bloat (but also i don't like sqlx lol)
- todo: also add caching to the prisma schema
- [ ] migrate api commands that currently use `tauri-specta` but don't actually rely on `tauri` to `rspc`
- waiting on `rspc` to update to the latest version (hopefully once tauri comes out of rc)
- [ ] bump `tauri` to stable v2 when it releases.
@ -9,6 +14,7 @@
- legacy fabric, babric, nilloader, java agent support
- nilloader <https://github.com/modrinth/labrinth/issues/903> <https://github.com/orgs/modrinth/discussions/45>
- [ ] shared cluster resources and options (<https://github.com/enjarai/shared-resources>)
- [ ] utils testing, dom testing, client testing, e2e testing
- [ ] ftb, technic, and tlauncher importing
- [ ] better debug logging and mc logging, for the launcher and spawn
- [ ] implement forgewrapper instead of processing ourselves
@ -23,10 +29,10 @@
- [x] modal stacking
- [x] popup visibility toggled
- [x] smooth animations/animation accessibility
- [ ] cluster grouping
- [x] cluster grouping
- [ ] cluster reorganizing (drag n drop)
- [ ] different cluster views
- [ ] settings tsx
- [x] different cluster views
- [x] settings tsx
- [ ] packages api
- [ ] browser tsx
- [ ] importing tsx
@ -35,10 +41,25 @@
- [x] self updater
- [x] authentication
- [x] manifest fetching
- [ ] tauri updater v1 -> v2
- [ ] fix old intel gpus <https://github.com/MultiMC/Launcher/issues/1276#issuecomment-774541617>
- [ ] fix cmd+a on macos not selecting all text
- [ ] fix cmd+c on macos not pasting in oauth and entire launcher
- [ ] hotfix some webkitgtk problems if possible
- [ ] fetching from screenshots
- [x] fetching from screenshots
- [ ] inferring metadata from zip
- [ ] inferring metadata from modrinth api
- [ ] feature parity with prism:
- [ ] h
- [ ]
- [ ] feature parity with modrinth app:
- [ ]
- [ ] reduce bundle size (<https://bundlephobia.com>):
- [ ] markdown-it (157kb) - lots of alternatives
- [ ] highlight.js (143kb) - shiki is a good alternative i think
- [ ] uFuzzy (24kb) - lots of alternatives, but its one of the best fuzzy searching libs
- [ ] overlayscrollbars (60kb) - not sure
- [ ] assets (63kb in icons and a lot more in .pngs) - we need to optimize our own assets, the ones from libs are negligable (default_instance_cover_ and header jpgs specifically)
- [ ] reduce app size (release binary) including in appimage (10mb right now, aiming for less than 14mb total release)
- [ ] drag and drop files into the launcher window (mods and modpack files)
- [ ] <https://github.com/gorilla-devs/GDLauncher-Carbon> and <https://github.com/gorilla-devs/libium>
- [ ] <https://github.com/gorilla-devs/furse> and <https://github.com/gorilla-devs/ferinth>

View file

@ -15,7 +15,7 @@ use uuid::Uuid;
#[specta::specta]
#[tauri::command]
pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
match onelauncher::cluster::get_by_uuid(uuid, None).await? {
match onelauncher::cluster::get_by_uuid(uuid).await? {
Some(cluster) => Ok(cluster),
None => Err("Cluster does not exist".into()),
}
@ -24,13 +24,13 @@ pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
#[specta::specta]
#[tauri::command]
pub async fn get_clusters() -> Result<Vec<Cluster>, String> {
Ok(onelauncher::cluster::list(None).await?)
Ok(onelauncher::cluster::list().await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_clusters_grouped() -> Result<HashMap<String, Vec<Cluster>>, String> {
Ok(onelauncher::cluster::list_grouped(None).await?)
Ok(onelauncher::cluster::list_grouped().await?)
}
#[derive(Serialize, Deserialize, Type)]
@ -62,7 +62,7 @@ pub async fn create_cluster(props: CreateCluster) -> Result<Uuid, String> {
)
.await?;
if let Some(cluster) = cluster::get(&path, None).await? {
if let Some(cluster) = cluster::get(&path).await? {
Ok(cluster.uuid)
} else {
Err("Cluster does not exist".to_string())
@ -103,7 +103,7 @@ pub async fn edit_cluster_meta(
name: Option<String>,
icon_path: Option<String>,
) -> Result<(), String> {
let cluster = cluster::get_by_uuid(uuid, None)
let cluster = cluster::get_by_uuid(uuid)
.await?
.ok_or("cluster does not exist")?;
@ -144,7 +144,7 @@ pub async fn run_cluster(uuid: Uuid) -> Result<DetailedProcess, String> {
#[specta::specta]
#[tauri::command]
pub async fn get_cluster_logs(uuid: Uuid) -> Result<Vec<String>, String> {
let cluster = onelauncher::cluster::get_by_uuid(uuid, None)
let cluster = onelauncher::cluster::get_by_uuid(uuid)
.await?
.ok_or("cluster not found")?;
let logs = logger::get_logs(&cluster.cluster_path(), None)
@ -158,7 +158,7 @@ pub async fn get_cluster_logs(uuid: Uuid) -> Result<Vec<String>, String> {
#[specta::specta]
#[tauri::command]
pub async fn get_cluster_log(uuid: Uuid, log_name: String) -> Result<String, String> {
let cluster = onelauncher::cluster::get_by_uuid(uuid, None)
let cluster = onelauncher::cluster::get_by_uuid(uuid)
.await?
.ok_or("cluster not found")?;
let log = logger::get_output_by_file(&cluster.cluster_path(), logger::LogType::Info, &log_name)
@ -169,7 +169,7 @@ pub async fn get_cluster_log(uuid: Uuid, log_name: String) -> Result<String, Str
#[specta::specta]
#[tauri::command]
pub async fn upload_log(uuid: Uuid, log_name: String) -> Result<String, String> {
let cluster = onelauncher::cluster::get_by_uuid(uuid, None)
let cluster = onelauncher::cluster::get_by_uuid(uuid)
.await?
.ok_or("cluster not found")?;
let log = logger::get_output_by_file(&cluster.cluster_path(), logger::LogType::Info, &log_name)
@ -182,7 +182,7 @@ pub async fn upload_log(uuid: Uuid, log_name: String) -> Result<String, String>
#[specta::specta]
#[tauri::command]
pub async fn get_screenshots(uuid: Uuid) -> Result<Vec<String>, String> {
let cluster = onelauncher::cluster::get_by_uuid(uuid, None)
let cluster = onelauncher::cluster::get_by_uuid(uuid)
.await?
.ok_or("cluster not found")?;
@ -195,7 +195,7 @@ pub async fn get_screenshots(uuid: Uuid) -> Result<Vec<String>, String> {
#[specta::specta]
#[tauri::command]
pub async fn get_worlds(uuid: Uuid) -> Result<Vec<String>, String> {
let cluster = onelauncher::cluster::get_by_uuid(uuid, None)
let cluster = onelauncher::cluster::get_by_uuid(uuid)
.await?
.ok_or("cluster not found")?;

View file

@ -56,10 +56,18 @@ macro_rules! collect_commands {
set_settings,
// Metadata
get_minecraft_versions,
// Package
random_mods,
get_mod,
download_mod,
// Provider Packages
get_provider_package,
get_provider_packages,
search_provider_packages,
get_provider_authors,
download_provider_package,
// Cluster Packages
get_cluster_package,
get_cluster_packages,
add_cluster_package,
remove_cluster_package,
sync_cluster_packages,
// Updater
check_for_update,
install_update

View file

@ -1,38 +1,125 @@
use onelauncher::data::ManagedPackage;
use onelauncher::package::content;
use onelauncher::cluster::content::package;
use onelauncher::data::{Loader, ManagedPackage, ManagedUser, PackageType};
use onelauncher::package::content::Providers;
use onelauncher::store::{Author, ClusterPath, Package, PackagePath, ProviderSearchResults};
use uuid::Uuid;
#[specta::specta]
#[tauri::command]
pub async fn random_mods() -> Result<Vec<ManagedPackage>, String> {
let provider = content::Providers::Modrinth;
Ok(provider.list().await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_mod(project_id: String) -> Result<ManagedPackage, String> {
let provider = content::Providers::Modrinth;
pub async fn get_provider_package(
provider: Providers,
project_id: String,
) -> Result<ManagedPackage, String> {
Ok(provider.get(&project_id).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn download_mod(cluster_id: Uuid, version_id: String) -> Result<(), String> {
let cluster = onelauncher::cluster::get_by_uuid(cluster_id, None)
pub async fn get_provider_packages(
provider: Providers,
project_ids: Vec<String>,
) -> Result<Vec<ManagedPackage>, String> {
Ok(provider.get_multiple(&project_ids).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn search_provider_packages(
provider: Providers,
query: Option<String>,
limit: Option<u8>,
game_versions: Option<Vec<String>>,
categories: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
open_source: Option<bool>,
) -> Result<ProviderSearchResults, String> {
Ok(provider
.search(
query,
limit,
game_versions,
categories,
loaders,
open_source,
)
.await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_provider_authors(
provider: Providers,
author: Author,
) -> Result<Vec<ManagedUser>, String> {
Ok(provider.get_authors(&author).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn download_provider_package(
provider: Providers,
package_id: String,
cluster_id: Uuid,
game_version: Option<String>,
loader: Option<Loader>,
package_version: Option<String>,
) -> Result<(), String> {
let mut cluster = onelauncher::cluster::get_by_uuid(cluster_id)
.await?
.ok_or("cluster not found")?;
let provider = content::Providers::Modrinth;
let game_version = cluster.meta.mc_version.clone();
provider
.get_version_for_game_version(&version_id, &game_version)
.await?
.files
.first()
.ok_or("no files found")?
.download_to_cluster(&cluster)
.await?;
let pkg = provider.get(&package_id).await?;
package::download_package(&pkg, &mut cluster, game_version, loader, package_version).await?;
Ok(())
}
#[specta::specta]
#[tauri::command]
pub async fn get_cluster_package(
cluster_path: ClusterPath,
package_path: PackagePath,
package_type: PackageType,
) -> Result<Package, String> {
Ok(package::get_package(&cluster_path, &package_path, package_type).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_cluster_packages(
cluster_path: ClusterPath,
package_type: PackageType,
) -> Result<Vec<Package>, String> {
Ok(package::get_packages(&cluster_path, package_type).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn add_cluster_package(
cluster_path: ClusterPath,
package_path: PackagePath,
pkg: Package,
package_type: Option<PackageType>,
) -> Result<(), String> {
package::add_package(&cluster_path, package_path, pkg, package_type).await?;
Ok(())
}
#[specta::specta]
#[tauri::command]
pub async fn remove_cluster_package(
cluster_path: ClusterPath,
package_path: PackagePath,
package_type: PackageType,
) -> Result<(), String> {
package::remove_package(&cluster_path, &package_path, package_type).await?;
Ok(())
}
#[specta::specta]
#[tauri::command]
pub async fn sync_cluster_packages(cluster_path: ClusterPath) -> Result<(), String> {
package::sync_packages(&cluster_path).await?;
Ok(())
}

View file

@ -18,6 +18,7 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
.build()
}
// TODO: fix the error system/rspc
pub type Result<T> = std::result::Result<T, OneLauncherSerializableError>;
#[derive(thiserror::Error, Debug, Serialize, specta::Type)]

View file

@ -5,6 +5,8 @@ use std::process::Command;
pub fn formatter(file: &Path) -> io::Result<()> {
Command::new("pnpm")
.arg("eslint")
.arg("--flag")
.arg("unstable_ts_config")
.arg("--fix")
.arg(file)
.output()

View file

@ -20,10 +20,10 @@ async fn initialize_state(handle: &tauri::AppHandle) -> api::Result<()> {
Ok(())
}
/// initializes the logger and runs the app. if the logger fails to initialize
/// we panic because nothing else can be debugged once the logger fails.
/// the only thing that can fail before the logger should be our [`tokio::main`] loop.
pub async fn run() {
// initializes the logger and runs the app. if the logger fails to initialize
// we panic because nothing else can be debugged once the logger fails.
// the only thing that can fail before the logger should be our `tokio::main` loop.
let _log_guard = onelauncher::start_logger();
tracing::info!("initialized logger. loading onelauncher/tauri...");
@ -50,7 +50,7 @@ pub async fn run_app<F: FnOnce(&tauri::AppHandle<tauri::Wry>) + Send + 'static>(
specta_typescript::Typescript::default()
.bigint(specta_typescript::BigIntExportBehavior::BigInt)
.formatter(crate::ext::specta::formatter),
"../frontend/src/bindings.ts",
"../../packages/client/src/bindings.ts",
)
.expect("failed to export debug bindings!");

View file

@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/core/tauri-config-schema/schema.json",
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
"productName": "OneLauncher",
"identifier": "org.polyfrost.launcher",
"app": {
@ -18,7 +18,9 @@
"https://polyfrost.org",
"https://*.polyfrost.org",
"https://*.cloudflare.com",
"https://api.mclo.gs"
"https://api.mclo.gs",
"ipc:",
"http://ipc.localhost"
],
"img-src": [
"'self'",
@ -41,9 +43,11 @@
"https://www.youtube-nocookie.com",
"https://discord.com",
"https://*.githubusercontent.com",
"'self'"
"'self'",
"'unsafe-inline'"
],
"style-src": [
"'unsafe-inline'",
"unsafe-inline",
"'self'"
]
@ -107,7 +111,7 @@
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org/",
"longDescription": "Next-generation open source Minecraft launcher",
"createUpdaterArtifacts": true,
"createUpdaterArtifacts": "v1Compatible",
"resources": [],
"externalBin": [],
"linux": {

View file

@ -20,39 +20,36 @@
"dependencies": {
"@fontsource/poppins": "^5.0.15",
"@formatjs/intl-durationformat": "^0.2.4",
"@iconify-json/logos": "^1.1.44",
"@leeoniya/ufuzzy": "^1.0.14",
"@onelauncher/client": "workspace:*",
"@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/refs": "^1.0.8",
"@solidjs/router": "^0.14.3",
"@t3-oss/env-core": "^0.11.0",
"@tanstack/solid-query": "^5.51.24",
"@tanstack/solid-query": "^5.53.1",
"@tauri-apps/api": "=2.0.0-rc.1",
"@tauri-apps/plugin-clipboard-manager": "=2.0.0-rc.0",
"@tauri-apps/plugin-dialog": "=2.0.0-rc.0",
"@tauri-apps/plugin-shell": "=2.0.0-rc.0",
"@thisbeyond/solid-dnd": "^0.7.5",
"@thisbeyond/solid-select": "^0.15.0",
"@unocss/reset": "^0.62.2",
"@unocss/reset": "^0.62.3",
"@untitled-theme/icons-solid": "^0.10.5",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-solid": "^0.5.5",
"pathe": "^1.1.2",
"solid-js": "^1.8.21",
"solid-js": "^1.8.22",
"solid-transition-group": "^0.2.3",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@sentry/vite-plugin": "^2.22.2",
"@sentry/vite-plugin": "^2.22.3",
"@solidjs/testing-library": "^0.8.9",
"@tauri-apps/cli": "=2.0.0-rc.4",
"@testing-library/jest-dom": "^6.4.8",
"@types/uuid": "^10.0.0",
"jsdom": "^24.1.1",
"@testing-library/jest-dom": "^6.5.0",
"jsdom": "^25.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.8",
"unocss": "^0.62.2",
"unocss": "^0.62.3",
"vite": "^5.4.2",
"vite-plugin-solid": "^2.10.2",
"vite-plugin-solid-svg": "^0.8.1",
"vite-tsconfig-paths": "^5.0.1"

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.5066 12.693C30.5528 12.7766 30.6013 12.8641 30.6389 12.9553C31.4253 14.606 31.3934 16.9885 30.4923 18.6027L27.2136 24.2779C26.2441 22.7962 24.886 21.5249 23.2535 20.5646C23.2947 20.5007 23.3312 20.4366 23.3632 20.3725C26.2579 14.5146 23.2535 6.77718 16.0008 6.89151C8.62014 6.76804 5.70242 14.6472 8.72531 20.5829C7.10177 21.5433 5.75289 22.8098 4.78784 24.2871C4.76529 24.239 4.4442 23.6872 4.00388 22.9301C2.93343 21.0898 1.15761 18.0374 1.2485 18.0632C0.640255 16.5907 0.649417 14.5785 1.28511 13.1244C1.32329 13.0218 1.37063 12.9229 1.42043 12.8278C1.4481 12.7746 1.47669 12.7226 1.5046 12.6717L6.17803 4.56842C6.18495 4.55916 6.19077 4.54867 6.19656 4.53828C6.20238 4.52812 6.20795 4.51807 6.21465 4.50903C7.17947 2.90392 9.46148 1.60059 11.3226 1.60059H20.6697C22.7915 1.60059 24.7534 2.73468 25.8098 4.56842C25.8205 4.58193 27.4458 7.39536 28.7785 9.70205C29.7019 11.3007 30.4847 12.6558 30.4923 12.6671L30.5066 12.693Z" fill="currentColor"/>
<path d="M11.7015 23.5876C14.157 24.9777 17.8794 24.9731 20.2801 23.5784C22.3289 24.5935 23.8197 26.3222 24.2816 28.3526C23.2709 29.0979 22.027 29.6055 20.9021 29.6604C20.829 29.6696 20.7467 29.6696 20.6735 29.6696C20.6095 29.6679 19.3378 29.6687 17.7459 29.6697H17.6627C14.9349 29.6714 11.3248 29.6736 11.2076 29.665C10.0552 29.6376 8.76098 29.1253 7.72292 28.3526C8.17575 26.3268 9.66196 24.6027 11.7015 23.5876Z" fill="currentColor"/>
<path d="M20.6189 15.6397C20.7788 22.0189 11.2214 22.0143 11.3815 15.6397C11.2946 9.22384 20.7011 9.22384 20.6189 15.6397Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,970 +0,0 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands */
/** tauri-specta globals */
import {
invoke as TAURI_INVOKE,
} from '@tauri-apps/api/core';
import * as TAURI_API_EVENT from '@tauri-apps/api/event';
import type { WebviewWindow as __WebviewWindow__ } from '@tauri-apps/api/webviewWindow';
export const commands = {
async authLogin(): Promise<Result<MinecraftCredentials | null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('auth_login') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getUsers(): Promise<Result<MinecraftCredentials[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_users') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getUser(uuid: string): Promise<Result<MinecraftCredentials, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_user', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getDefaultUser(fallback: boolean | null): Promise<Result<MinecraftCredentials | null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_default_user', { fallback }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async setDefaultUser(uuid: string | null): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('set_default_user', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async removeUser(uuid: string): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('remove_user', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async createCluster(props: CreateCluster): Promise<Result<string, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('create_cluster', { props }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
/**
* Updates the cluster with the given UUID. The cluster only updates game setting fields
*/
async editGameSettings(uuid: string, newCluster: Cluster): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('edit_game_settings', { uuid, newCluster }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async editClusterMeta(uuid: string, name: string | null, iconPath: string | null): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('edit_cluster_meta', { uuid, name, iconPath }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async removeCluster(uuid: string): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('remove_cluster', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getCluster(uuid: string): Promise<Result<Cluster, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_cluster', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getClusters(): Promise<Result<Cluster[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_clusters') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getClustersGrouped(): Promise<Result<{ [key in string]: Cluster[] }, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_clusters_grouped') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async runCluster(uuid: string): Promise<Result<DetailedProcess, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('run_cluster', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getClusterLogs(uuid: string): Promise<Result<string[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_cluster_logs', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getClusterLog(uuid: string, logName: string): Promise<Result<string, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_cluster_log', { uuid, logName }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async uploadLog(uuid: string, logName: string): Promise<Result<string, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('upload_log', { uuid, logName }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getScreenshots(uuid: string): Promise<Result<string[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_screenshots', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getWorlds(uuid: string): Promise<Result<string[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_worlds', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getRunningClusters(): Promise<Result<Cluster[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_running_clusters') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getProcessesByPath(path: ClusterPath): Promise<Result<string[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_processes_by_path', { path }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async killProcess(uuid: string): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('kill_process', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async isClusterRunning(uuid: string): Promise<Result<boolean, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('is_cluster_running', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getPidByUuid(uuid: string): Promise<Result<number, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_pid_by_uuid', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getUserByProcess(uuid: string): Promise<Result<string | null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_user_by_process', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getProcessStartedAt(uuid: string): Promise<Result<string, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_process_started_at', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getProcessesDetailedByPath(path: ClusterPath): Promise<Result<DetailedProcess[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_processes_detailed_by_path', { path }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getProcessDetailedById(uuid: string): Promise<Result<DetailedProcess, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_process_detailed_by_id', { uuid }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getSettings(): Promise<Result<Settings, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_settings') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async setSettings(settings: Settings): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('set_settings', { settings }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getMinecraftVersions(): Promise<Result<Version[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_minecraft_versions') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async randomMods(): Promise<Result<ManagedPackage[], string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('random_mods') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async getMod(projectId: string): Promise<Result<ManagedPackage, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('get_mod', { projectId }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async downloadMod(clusterId: string, versionId: string): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('download_mod', { clusterId, versionId }) };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async checkForUpdate(): Promise<Result<Update | null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('check_for_update') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
async installUpdate(): Promise<Result<null, string>> {
try {
return { status: 'ok', data: await TAURI_INVOKE('install_update') };
}
catch (e) {
if (e instanceof Error)
throw e;
else return { status: 'error', error: e as any };
}
},
};
/** user-defined events */
export const events = __makeEvents__<{
clusterPayload: ClusterPayload;
ingressPayload: IngressPayload;
internetPayload: InternetPayload;
messagePayload: MessagePayload;
offlinePayload: OfflinePayload;
processPayload: ProcessPayload;
}>({
clusterPayload: 'cluster-payload',
ingressPayload: 'ingress-payload',
internetPayload: 'internet-payload',
messagePayload: 'message-payload',
offlinePayload: 'offline-payload',
processPayload: 'process-payload',
});
/** user-defined constants */
export const PROGRAM_INFO = { arch: '64', dev_build: true, launcher_version: '1.0.0-alpha.1', platform: 'linux', tauri_version: '2.0.0-rc.3', webview_version: '2.44.2' } as const;
/** user-defined types */
/**
* Represents a single Instance and installation of Minecraft
* Contains settings and identifiers on a per-Cluster basis, falling back to default settings for Options<>
*/
export interface Cluster {
/**
* The Uuid of a specific cluster.
*/
uuid: string;
/**
* The download stage that of a cluster.
*/
stage?: ClusterStage;
/**
* The core path of the cluster.
*/
path?: string;
/**
* The associated cluster metadata.
*/
meta: ClusterMeta;
/**
* The per-cluster JVM memory allocation options.
*/
memory?: Memory | null;
/**
* The per-cluster JVM options.
*/
java?: JavaOptions | null;
/**
* The per-cluster Minecraft window default resolution.
*/
resolution?: Resolution | null;
/**
* The per-cluster Minecraft window fullscreen status.
*/
force_fullscreen: boolean | null;
init_hooks?: InitHooks | null;
packages: { [key in PackagePath]: Package };
update?: string | null;
};
/**
* Represents core Cluster metadata ([`Cluster#meta`]).
*/
export interface ClusterMeta {
/**
* The user-facing name of the cluster stored as a string.
*/
name: string;
/**
* The user-facing group of the cluster stored as a string.
*/
group?: string | null;
/**
* The associated Minecraft version of the cluster as last updated.
*/
mc_version: string;
/**
* The associated mod [`Loader`] as specified in the cluster.
*/
loader?: Loader;
/**
* The associated mod [`LoaderVersion`] if available.
*/
loader_version?: LoaderVersion | null;
/**
* The time that the cluster was created in [`DateTime<Utc>`].
*/
created_at?: string;
/**
* The time the cluster was last modified in [`DateTime<Utc>`].
*/
modified_at?: string;
/**
* The last time the cluster was played in [`DateTime<Utc>`].
* (Defaults to None if the cluster has never been played)
*/
played_at?: string | null;
/**
* The overall time played in seconds stored as a [`u64`].
*/
overall_played?: bigint;
/**
* The recent time played stored as a [`u64`].
*/
recently_played?: bigint;
/**
* The associated [`PackageData`] and modpack data for the cluster.
*/
package_data?: PackageData | null;
/**
* The user-facing cluster icon as a [`PathBuf`].
*/
icon?: string | null;
/**
* The user-facing cluster icon as a URL string.
*/
icon_url?: string | null;
};
/**
* Relative Path wrapper to be used as an identifer for a cluster path.
*/
export type ClusterPath = string;
export interface ClusterPayload { uuid: string; cluster_path: ClusterPath; path: string; name: string; event: ClusterPayloadType };
export type ClusterPayloadType = 'created' | 'inserted' | 'synced' | 'edited' | 'deleted';
/**
* Core Cluster stages used in package logic.
*/
export type ClusterStage =
/**
* Installed with no downloads left.
*/
'installed' |
/**
* Downloading any sort of core metadata or mod file.
*/
'downloading' |
/**
* Downloading a full pack file, which is granted lower priority due to the high network demand.
*/
'pack_downloading' |
/**
* Not installed at all.
*/
'not_installed';
export interface CreateCluster { name: string; mc_version: string; mod_loader: Loader; loader_version: string | null; icon: string | null; icon_url: string | null; package_data: PackageData | null; skip: boolean | null; skip_watch: boolean | null };
export interface DetailedProcess { uuid: string; user: string | null; started_at: string; pid: number };
export interface IngressPayload { event: IngressType; ingress_uuid: string; fraction: number | null; message: string };
export type IngressType = { type: 'initialize' } | { type: 'download_java'; version: number } | { type: 'download_loader'; cluster_path: string; cluster_name: string } | { type: 'sync_cluster'; cluster_path: string; cluster_name: string } | { type: 'copy_cluster'; import: string; cluster_name: string } | { type: 'sync_config'; new_path: string } | { type: 'archival'; cluster_path: string; cluster_name: string } | { type: 'download_package'; cluster_path: string; package_name: string; icon: string | null; package_id: string | null; package_version: string | null } | { type: 'download_pack'; cluster_path: string; package_name: string; icon: string | null; package_version: string };
/**
* Global initialization hooks for all Minecraft clusters.
*/
export interface InitHooks {
/**
* Pre-launch hook.
*/
pre?: string | null;
/**
* Wrapper hook for the runtime of the game instance.
*/
wrapper?: string | null;
/**
* Post-launch hook.
*/
post?: string | null;
};
export type InternetPayload = { event: 'InstallPackage'; id: string } | { event: 'InstallPack'; id: string } | { event: 'InstallPath'; path: string };
/**
* Custom Java arguments on a per-cluster basis.
*/
export interface JavaOptions {
/**
* A custom java version from the global java store, if specified.
*/
custom_version?: JavaVersion | null;
/**
* Custom runtime arguments when running the cluster.
*/
custom_arguments?: string[] | null;
/**
* Custom environment variables when running the cluster.
*/
custom_env_arguments?: ([string, string])[] | null;
};
/**
* A struct representing a single version of the Java Runtime Environment.
* Use [`locate_java`] to get an array of all located java instances on the machine.
*
* common java paths from https://github.com/PrismLauncher/PrismLauncher/blob/develop/launcher/java/JavaUtils.cpp under GPL 3.0
*
* java check functionality from https://github.com/modrinth/theseus/blob/master/theseus/src/util/jre.rs under MIT
*/
export interface JavaVersion { version: string; arch: string; path: string };
/**
* A HashMap of all located and installed available Java versions
*/
export type JavaVersions = { [key in string]: JavaVersion };
/**
* Available mod loaders to be used for a cluster.
*/
export type Loader =
/**
* The default vanilla loader, no modding supported.
*/
'vanilla' |
/**
* The MinecraftForge Minecraft mod loader.
*/
'forge' |
/**
* The FabircMC Minecraft mod loader.
*/
'fabric' |
/**
* The NeoForge Minecraft mod loader.
*/
'neoforge' |
/**
* The Quilt Minecraft mod loader.
*/
'quilt' |
/**
* The Legacy Fabric port mod loader.
*/
'legacyfabric';
/**
* A version of a Minecraft mod loader
*/
export interface LoaderVersion {
/**
* The version ID of the loader
*/
id: string;
/**
* The URL of the version's manifest
*/
url: string;
/**
* Whether the loader is stable or not
*/
stable: boolean;
};
/**
* Universal interface for managed package dependencies.
*/
export interface ManagedDependency { version_id: string | null; package_id: string | null; file_name: string | null; dependency_type: PackageDependency };
/**
* Universal metadata for any managed package from a Mod distribution platform.
*/
export interface ManagedPackage { id: string; uid: string | null; package_type: PackageType; title: string; description: string; main: string; versions: string[]; game_versions: string[]; loaders: string[]; icon_url: string | null; created: string; updated: string; client: PackageSide; server: PackageSide; downloads: number; followers: number; categories: string[]; optional_categories: string[] | null };
/**
* Universal interface for managed package authors and users.
*/
export interface ManagedUser { id: string; role: string; username: string; name: string | null; avatar: string | null; description: string | null; created: string };
/**
* Universal managed package version of a package.
*/
export interface ManagedVersion { id: string; package_id: string; author: string; name: string; featured: boolean; version_id: string; changelog: string; changelog_url: string | null; published: string; downloads: number; version_type: string; files: ManagedVersionFile[]; deps: ManagedDependency[]; game_versions: string[]; loaders: string[] };
/**
* Universal interface for managed package files.
*/
export interface ManagedVersionFile { url: string; file_name: string; primary: boolean; size: number; file_type: PackageFile | null; hashes: { [key in string]: string } };
/**
* Global memory settings across all clusters.
*/
export interface Memory {
/**
* Maximum amount of Java memory available globally.
*/
maximum: number;
/**
* Minimum amount of Java memory available globally.
*/
minimum: number;
};
export interface MessagePayload { message: string };
/**
* A structure of all needed Minecraft credentials for logging in and account management.
*/
export interface MinecraftCredentials {
/**
* The uuid of the credentials.
*/
id: string;
/**
* The username of the Minecraft account.
*/
username: string;
/**
* The access token as a String.
*/
access_token: string;
/**
* The refresh token as a string for [`MinecraftState#refresh`].
*/
refresh_token: string;
/**
* The time that the access token expires as a [`DateTime<Utc>`].
*/
expires: string;
};
export interface OfflinePayload { offline: boolean };
/**
* A struct that represents a Package.
*/
export interface Package { sha512: string; meta: PackageMetadata; file_name: string; disabled: boolean };
/**
* Optional data used to link a specific cluster to a package project.
*/
export interface PackageData {
/**
* The package ID as a String.
*/
package_id: string | null;
/**
* The version of the package as a String.
*/
version_id: string | null;
/**
* Whether or not the current package is locked (for legacy modpack support).
*/
locked?: boolean | null;
};
/**
* The type of a [`ManagedDependency`].
*/
export type PackageDependency = 'Required' | 'Optional' | 'Incompatible' | 'Embedded';
/**
* The file type of a [`Package`].
*/
export type PackageFile = 'required_pack' | 'optional_pack' | 'unknown';
/**
* Metadata that represents a [`Package`].
*/
export type PackageMetadata = { type: 'managed'; package: ManagedPackage; version: ManagedVersion; users: ManagedUser; update: ManagedVersion | null; incompatible: boolean } | { type: 'mapped'; title: string | null; description: string | null; authors: string[]; version: string | null; icon: string | null; package_type: string | null } | { type: 'unknown' };
/**
* Relative [`PathBuf`] for a specific [`Package`] of a [`Cluster`].
*/
export type PackagePath = string;
/**
* The Client/Server side type of a [`Package`].
*/
export type PackageSide = 'required' | 'optional' | 'unsupported' | 'unknown';
/**
* Represents types of packages handled by the launcher.
*/
export type PackageType =
/**
* represents a mod jarfile
*/
'mod' |
/**
* represents a datapack file
*/
'datapack' |
/**
* represents a resourcepack file
*/
'resource' |
/**
* represents a shaderpack file
*/
'shader';
export interface ProcessPayload { uuid: string; pid: number; event: ProcessPayloadType; message: string };
export type ProcessPayloadType = 'started' | 'modified' | 'finished' | 'logging';
/**
* Global Minecraft resolution.
*/
export type Resolution = [number, number];
/**
* A global settings state for the launcher.
*/
export interface Settings {
/**
* A OneLauncher [`Theme`] managed by the core GUI.
*/
theme: Theme;
/**
* Does not ask for confirmation when closing the OneLauncher GUI
*/
hide_close_prompt?: boolean;
/**
* Disables animations in the OneLauncher GUI
*/
disable_animations?: boolean;
/**
* A global fullscreen Minecraft state.
*/
force_fullscreen?: boolean;
/**
* Whether to allow launching the same cluster under the same account.
*/
allow_parallel_running_clusters?: boolean;
/**
* Whether to launch Feral Gamemode on Linux systems.
*/
enable_gamemode?: boolean;
/**
* A global default [`Resolution`] for Minecraft.
*/
resolution: Resolution;
/**
* A global [`JavaVersions`] list and default version.
*/
java_versions: JavaVersions;
/**
* A global [`Memory`] settings store for Java memory settings.
*/
memory: Memory;
/**
* Global and default initialization hooks .
*/
init_hooks: InitHooks;
/**
* Global and default custom Java arguments.
*/
custom_java_args: string[];
/**
* Global and default custom environment variables.
*/
custom_env_args: ([string, string])[];
/**
* Global and default maximum [`Semaphore`] I/O operations.
*/
max_async_io_operations: bigint;
/**
* Global and default maximum [`Semaphore`] HTTP operations.
*/
max_async_fetches: bigint;
/**
* The [`CURRENT_FORMAT_VERSION`] of this settings file.
*/
version: number;
/**
* Whether or not to disable Discord IPC.
*/
disable_discord?: boolean;
/**
* Whether or not to enable a debug mode in the launcher.
*/
debug_mode?: boolean;
/**
* Whether or not to disable Plausible and Sentry analytics.
*/
disable_analytics?: boolean;
/**
* The core global config directory stored as a [`PathBuf`].
*/
config_dir?: string | null;
/**
* Whether or not to minimize the launcher upon a game launch.
*/
hide_on_launch?: boolean;
/**
* Enable/disable advanced rendering and window decorations.
*/
rendering?: boolean;
};
/**
* A OneLauncher theme managed by the GUI.
*/
export type Theme =
/**
* A default Dark theme.
*/
'dark' |
/**
* A default Light theme.
*/
'light' |
/**
* OLED Dark Theme.
*/
'contrast' |
/**
* Cute and colorful theme.
*/
'cat';
export interface Update { version: string };
/**
* A game version of Minecraft
*/
export interface Version {
/**
* A unique identifier of the version
*/
id: string;
/**
* The release type of the version
*/
type: VersionType;
/**
* A link to additional information about the version
*/
url: string;
/**
* The latest time a file in this version was updated
*/
time: string;
/**
* The time this version was released
*/
releaseTime: string;
/**
* The SHA1 hash of the additional information about the version
*/
sha1: string;
/**
* Whether the version supports the latest player safety features
*/
complianceLevel: number;
/**
* The SHA1 hash of the original unmodified Minecraft version's JSON
* This is only available when using the Pulseflow mirror
*/
originalSha1?: string | null;
};
/**
* The version type
*/
export type VersionType =
/**
* A major version, which is stable for all players to use
*/
'release' |
/**
* An experimental version, which is unstable and used for feature previews and beta testing
*/
'snapshot' |
/**
* The oldest versions before the game was released
*/
'old_alpha' |
/**
* Early versions of the game
*/
'old_beta';
interface __EventObj__<T> {
listen: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.listen<T>>;
once: (
cb: TAURI_API_EVENT.EventCallback<T>,
) => ReturnType<typeof TAURI_API_EVENT.once<T>>;
emit: T extends null
? (payload?: T) => ReturnType<typeof TAURI_API_EVENT.emit>
: (payload: T) => ReturnType<typeof TAURI_API_EVENT.emit>;
}
export type Result<T, E> =
| { status: 'ok'; data: T }
| { status: 'error'; error: E };
function __makeEvents__<T extends Record<string, any>>(
mappings: Record<keyof T, string>,
) {
return new Proxy(
{} as unknown as {
[K in keyof T]: __EventObj__<T[K]> & {
(handle: __WebviewWindow__): __EventObj__<T[K]>;
};
},
{
get: (_, event) => {
const name = mappings[event as keyof T];
return new Proxy((() => {}) as any, {
apply: (_, __, [window]: [__WebviewWindow__]) => ({
listen: (arg: any) => window.listen(name, arg),
once: (arg: any) => window.once(name, arg),
emit: (arg: any) => window.emit(name, arg),
}),
get: (_, command: keyof __EventObj__<any>) => {
switch (command) {
case 'listen':
return (arg: any) => TAURI_API_EVENT.listen(name, arg);
case 'once':
return (arg: any) => TAURI_API_EVENT.once(name, arg);
case 'emit':
return (arg: any) => TAURI_API_EVENT.emit(name, arg);
}
},
});
},
},
);
}

View file

@ -5,7 +5,8 @@ import '@fontsource/poppins/600.css';
import 'overlayscrollbars/overlayscrollbars.css';
import 'virtual:uno.css';
import './globals.css';
import '~styles/globals.css';
import '~styles/highlightjs.css';
import '~utils/patches';
export * as bridge from './bindings';
export * as bridge from '@onelauncher/client/bindings';

View file

@ -36,6 +36,12 @@ h1 { @apply text-2xl font-semibold; line-height: 1.5 !important; }
h2 { @apply text-xxl font-semibold; line-height: 1.25 !important; }
h3 { @apply text-xl font-medium; }
h4 { @apply text-lg font-medium; }
h5 { @apply text-md text-fg-secondary font-bold uppercase; }
h6 { @apply text-sm text-fg-secondary font-medium uppercase; }
code {
@apply rounded-md bg-component-bg p-2 text-left;
}
.os-scrollbar {
--os-size: 10px;

View file

@ -0,0 +1,126 @@
// TODO: make this support light theme too idk how :3
/*!
Theme: GitHub Dark Dimmed
Description: Dark dimmed theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Colors taken from GitHub's CSS
*/
.hljs {
color: #adbac7;
background: #22272e;
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #f47067;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #dcbdfb;
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #6cb6ff;
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #96d0ff;
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #f69d50;
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #768390;
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #8ddb8c;
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #adbac7;
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #316dca;
font-weight: bold;
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #eac55f;
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #adbac7;
font-style: italic;
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #adbac7;
font-weight: bold;
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #b4f1b4;
background-color: #1b4721;
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #ffd8d3;
background-color: #78191b;
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}

View file

@ -1,5 +1,6 @@
import type { ParentProps } from 'solid-js';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import { PROGRAM_INFO } from '@onelauncher/client/bindings';
import WindowFrame from './components/WindowFrame';
import Navbar from './components/Navbar';
import ErrorBoundary from './components/ErrorBoundary';
@ -10,7 +11,7 @@ import { ClusterModalControllerProvider } from './components/overlay/cluster/Clu
import { MultiProvider } from './components/MultiProvider';
import { AccountControllerProvider } from './components/overlay/account/AddAccountModal';
import { ModalProvider, ModalRenderer } from './components/overlay/Modal';
import { PROGRAM_INFO } from '~bindings';
import { BrowserProvider } from './hooks/useBrowser';
function App(props: ParentProps) {
if (PROGRAM_INFO.dev_build !== true)
@ -28,9 +29,11 @@ function App(props: ParentProps) {
<div class="absolute left-0 top-0 h-full w-full flex flex-col overflow-x-hidden">
<ErrorBoundary>
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute left-0 top-0 h-full w-full flex flex-col overflow-x-hidden overflow-y-auto px-8 pb-8">
<AnimatedRoutes>
{props.children}
</AnimatedRoutes>
<div class="h-full flex-1">
<AnimatedRoutes>
{props.children}
</AnimatedRoutes>
</div>
</OverlayScrollbarsComponent>
</ErrorBoundary>
</div>
@ -53,6 +56,7 @@ function GlobalContexts(props: ParentProps) {
SettingsProvider,
AccountControllerProvider,
ClusterModalControllerProvider,
BrowserProvider,
]}
>
{props.children}

View file

@ -1,9 +1,9 @@
import { type ParentProps, Show } from 'solid-js';
import { Transition, type TransitionProps } from 'solid-transition-group';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
function AnimatedRoutes(props: TransitionProps & ParentProps) {
const { settings } = useSettingsContext();
const { settings } = useSettings();
const before: Keyframe = {
transform: 'translateX(-85px)',

View file

@ -1,47 +1,24 @@
import { type ParentProps, createSignal, onCleanup, onMount } from 'solid-js';
import { formatAsRelative } from '~utils';
type TimeProps = ParentProps & {
timestamp: number;
timestamp: Date | number;
};
export function TimeAgo(props: TimeProps) {
const formatter = new Intl.RelativeTimeFormat(navigator.language, { numeric: 'auto' });
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: 24 * 60 * 60 * 1000 * 365 / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
export const TimeAgo = (props: TimeProps) => <InternalTime {...props} format={formatAsRelative} />;
const getRelativeTime = (timestamp: number): string => {
const elapsed = timestamp - Date.now();
for (const [unit, ms] of Object.entries(units))
if (Math.abs(elapsed) > ms || unit === 'second')
return formatter.format(Math.round(elapsed / ms), unit as Intl.RelativeTimeFormatUnit);
return 'now';
};
return <InternalTime {...props} format={getRelativeTime} />;
}
type InternalTimeProps = TimeProps & {
format: (time: number) => string;
};
type InternalTimeProps = TimeProps & { format: (time: number) => string };
function InternalTime(props: InternalTimeProps) {
const [time, setTime] = createSignal('');
const [intervalId, setIntervalId] = createSignal<NodeJS.Timeout | undefined>(undefined);
onMount(() => {
const loop = () => {
setTime(props.format(props.timestamp));
};
const timestamp = typeof props.timestamp === 'number' ? props.timestamp : props.timestamp.getTime();
const loop = () => setTime(props.format(timestamp));
loop();
setIntervalId(setInterval(loop, 1000));
});

View file

@ -1,19 +1,22 @@
import type { ParentProps } from 'solid-js';
import { ErrorBoundary as Boundary } from 'solid-js';
import { useDebugState } from '@onelauncher/client';
import Button from './base/Button';
import Popup from './overlay/Popup';
// TODO: Uncomment code
function ErrorBoundary(props: ParentProps) {
return (
<>{props.children}</>
// <Capturer
// fallback={err => (
// <div class="p-2 bg-danger/20 rounded-md border-danger border">
// <h2>An unexpected error has occurred</h2>
// <p>{err.toString()}</p>
// </div>
// )}
// >
// {props.children}
// </Capturer>
<Boundary
fallback={err => (
<div class="border border-danger rounded-md bg-danger/20 p-2">
<h2>An unexpected error has occurred</h2>
<p>{err.toString()}</p>
</div>
)}
>
{props.children}
</Boundary>
);
}

View file

@ -23,7 +23,7 @@ function NavbarLink(props: NavbarLinkProps) {
class="rounded-lg px-4 py-2 text-lg hover:bg-component-bg-hover hover:text-fg-primary-hover"
inactiveClass="text-fg-secondary"
activeClass="text-fg-primary"
end
end={props.path === '/'}
>
{props.label}
</A>

View file

@ -3,7 +3,7 @@ import type { ParentProps } from 'solid-js';
function ScrollableContainer(props: ParentProps) {
return (
<div class="w-[calc(100%+14px)] flex flex-1 flex-col overflow-hidden">
<div class="h-full h-screen w-[calc(100%+14px)] flex flex-1 flex-col overflow-hidden">
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar relative flex-1">
<div class="absolute h-full w-[calc(100%-14px)] flex flex-col gap-2">
{props.children}

View file

@ -4,7 +4,7 @@ import type { JSX } from 'solid-js';
import { onMount } from 'solid-js';
import Modal, { createModal } from './overlay/Modal';
import Button from './base/Button';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
interface TitlebarButtonProps {
icon: (any: any) => JSX.Element;
@ -22,13 +22,13 @@ function TitlebarButton(props: TitlebarButtonProps) {
);
}
function WindowFrame() {
const { settings } = useSettingsContext();
const maximize = () => Window.getCurrent().toggleMaximize();
const minimize = () => Window.getCurrent().minimize();
const quit = () => Window.getCurrent().close();
const kill = () => Window.getCurrent().destroy();
const maximize = () => Window.getCurrent().toggleMaximize();
const minimize = () => Window.getCurrent().minimize();
const quit = () => Window.getCurrent().close();
const kill = () => Window.getCurrent().destroy();
function WindowFrame() {
const { settings } = useSettings();
const modal = createModal(props => (
<Modal.Simple

View file

@ -66,5 +66,5 @@
}
.icon > * {
@apply w-4;
@apply w-4 min-w-4;
}

View file

@ -43,8 +43,8 @@ Button.Toggle = (props: ButtonToggleProps) => {
<Button
onClick={(e) => {
toggle();
// @ts-expect-error -- JSX doesn't seem to use the same type
split.onClick?.(e);
if (typeof split.onClick === 'function')
split.onClick(e);
}}
aria-checked={checked()}
{...rest}

View file

@ -1,4 +1,4 @@
import { type Accessor, Index, type JSX, Match, type ParentProps, type ResolvedJSXElement, type Setter, Show, Switch, children, createSignal, splitProps } from 'solid-js';
import { type Accessor, Index, type JSX, Match, type ParentProps, type ResolvedJSXElement, type Setter, Show, Switch, children, createEffect, createSignal, splitProps } from 'solid-js';
import { ChevronDownIcon, ChevronUpIcon } from '@untitled-theme/icons-solid';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import Popup from '../overlay/Popup';
@ -8,7 +8,7 @@ import styles from './Dropdown.module.scss';
export type DropdownProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
onChange?: (selected: number) => any;
text?: string;
selected?: number;
selected?: Accessor<number>;
dropdownClass?: string;
listToolRow?: () => JSX.Element;
component?: (
@ -23,9 +23,12 @@ export type DropdownProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange'>
function Dropdown(props: DropdownProps) {
const [split, rest] = splitProps(props, ['disabled', 'children', 'class', 'text', 'onChange', 'selected', 'component', 'dropdownClass', 'listToolRow']);
const [visible, setVisible] = createSignal(false);
const [selected, setSelected] = createSignal(0);
// eslint-disable-next-line solid/reactivity -- todo
const [selected, setSelected] = createSignal<number>(split.selected || 0);
createEffect(() => {
if (split.selected)
setSelected(split.selected());
});
let ref!: HTMLDivElement;
@ -36,7 +39,7 @@ function Dropdown(props: DropdownProps) {
setVisible(false);
if (split.onChange)
split.onChange(selected());
split.onChange(index);
}
const Component = () => (

View file

@ -0,0 +1,3 @@
.link {
@apply text-link hover:text-link-hover active:text-link-pressed underline text-left;
}

View file

@ -0,0 +1,36 @@
import { type JSX, splitProps } from 'solid-js';
import { LinkExternal01Icon } from '@untitled-theme/icons-solid';
import styles from './Link.module.scss';
import usePromptOpener from '~ui/hooks/usePromptOpener';
export type LinkProps = JSX.IntrinsicElements['a'] & {
prompt?: boolean;
includeIcon?: boolean;
};
function Link(props: LinkProps) {
const [split, rest] = splitProps(props, ['prompt', 'class', 'href', 'onClick', 'children']);
const open = usePromptOpener();
return (
<button
{...rest as JSX.ButtonHTMLAttributes<HTMLButtonElement>}
class={`${styles.link} ${split.class || ''}`}
onClick={(e) => {
open(split.href, !!split.prompt);
if (typeof split.onClick === 'function')
split.onClick(e as any);
}}
children={(
<div class="flex flex-row gap-x-1">
{props.children}
{props.includeIcon === true && (
<LinkExternal01Icon class="h-3 min-h-3 min-w-3 w-3" />
)}
</div>
)}
/>
);
}
export default Link;

View file

@ -60,8 +60,8 @@ SelectList.Row = (props: SelectListRowProps) => {
if (props.index !== undefined)
setSelected(props.index);
// @ts-expect-error -- JSX doesn't seem to use the same type
split.onClick?.(e);
if (typeof split.onClick === 'function')
split.onClick(e);
}}
class={`${styles.row} ${props.index !== undefined && selected() === props.index ? styles.selected : ''} ${split.class || ''}`}
>

View file

@ -10,11 +10,10 @@ function Toggle(props: ToggleProps) {
const [checked, setChecked] = createSignal(props.checked?.());
const [split, rest] = splitProps(props, ['class', 'checked', 'onChecked', 'text']);
// eslint-disable-next-line solid/reactivity -- i hate mylife
if (props.checked)
createEffect(() => {
setChecked(props.checked!());
});
createEffect(() => {
if (props.checked)
setChecked(props.checked());
});
function toggle() {
const newValue = !checked();

View file

@ -1,4 +1,4 @@
import { Index, Show, createMemo, createSignal, untrack } from 'solid-js';
import { Index, Show, createEffect, createMemo, createSignal, untrack } from 'solid-js';
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from 'overlayscrollbars-solid';
import type { Ref } from '@solid-primitives/refs';
import { AlignBottom01Icon } from '@untitled-theme/icons-solid';
@ -15,8 +15,11 @@ interface FormattedLogProps {
function FormattedLog(props: FormattedLogProps) {
// TODO(perf): Do a "infinite scroll" method of rendering the log. Adding 15918590815 dom elements at once lags the render thread for a bit
const lines = createMemo(() => (props.log?.trim().split('\n') || []));
// eslint-disable-next-line solid/reactivity -- Shouldn't really be tracked
const [shouldScroll, setShouldScroll] = createSignal(props.enableAutoScroll === true);
const [shouldScroll, setShouldScroll] = createSignal(false);
createEffect(() => {
setShouldScroll(props.enableAutoScroll === true);
});
let overlayScrollbars!: OverlayScrollbarsComponentRef;
@ -38,7 +41,7 @@ function FormattedLog(props: FormattedLogProps) {
}
return (
<div class="relative h-full flex flex-1 flex-col rounded-md bg-component-bg p-2">
<div class="relative h-full flex flex-1 flex-col rounded-md bg-component-bg">
<Show when={props.enableAutoScroll === true}>
<div class="absolute right-0 top-0 z-1 h-6 w-full flex flex-row justify-end border border-gray-05 rounded-t-md bg-page p-px">
<Button.Toggle

View file

@ -0,0 +1,21 @@
.markdown {
@apply line-height-relaxed flex flex-col items-start gap-y-2;
}
.markdown {
table {
width: auto;
}
hr,
h1,
h2 {
max-width: max(60rem, 90%);
}
ul,
ol {
margin-left: 2rem;
list-style: initial;
}
}

View file

@ -0,0 +1,37 @@
import type { ParentProps } from 'solid-js';
import { renderHighlightedString } from '@onelauncher/client';
import styles from './Markdown.module.scss';
import usePromptOpener from '~ui/hooks/usePromptOpener';
export type MarkdownProps = ParentProps & {
body: string;
};
function Markdown(props: MarkdownProps) {
const promptOpen = usePromptOpener();
return (
<div
class={styles.markdown}
// eslint-disable-next-line solid/no-innerhtml -- Should be sanitised properly
innerHTML={renderHighlightedString(props.body)}
onContextMenu={(e) => {
e.stopImmediatePropagation();
e.preventDefault();
}}
onClick={(e) => {
e.stopImmediatePropagation();
e.preventDefault();
// TODO: Create a custom renderer or just use something else instead of markdown-it :D
if (e.target.nodeName === 'A') {
const target = e.target as HTMLAnchorElement;
if (target.href.startsWith('http'))
promptOpen(target.href);
}
}}
/>
);
}
export default Markdown;

View file

@ -1,63 +0,0 @@
import { Download01Icon, HeartIcon } from '@untitled-theme/icons-solid';
import { useNavigate } from '@solidjs/router';
import { abbreviateNumber } from '~utils';
import BrowserMod from '~ui/pages/browser/BrowserMod';
import type { ManagedPackage } from '~bindings';
// TODO: Remove in place for specta generated types
export enum Provider {
Curseforge,
Modrinth,
Polyfrost,
Skyclient,
}
function ModCard(props: ManagedPackage) {
const navigate = useNavigate();
function redirect() {
navigate(BrowserMod.getUrl({
id: props.id,
provider: 'modrinth',
}));
}
return (
<div onClick={redirect} class="h-full max-h-68 max-w-53 min-h-68 min-w-53 w-full flex flex-col overflow-hidden rounded-lg bg-component-bg">
<div class="relative h-28 flex items-center justify-center overflow-hidden">
<img class="absolute z-0 max-w-none w-7/6 filter-blur-xl" src={props.icon_url || ''} alt={`Icon for ${props.title}`} />
<img class="relative z-1 aspect-ratio-square w-2/5 rounded-md image-render-auto" src={props.icon_url || ''} alt={`Icon for ${props.title}`} />
</div>
<div class="flex flex-1 flex-col gap-4 p-3">
<div class="flex flex-col gap-2">
<h5 class="text-fg-primary font-medium">{props.title}</h5>
<p class="text-xs text-fg-secondary">
By
{' '}
<span class="text-fg-primary">Author TODO</span>
{' '}
on
{' '}
Modrinth
</p>
</div>
<p class="max-h-13 flex-1 overflow-hidden text-sm text-fg-secondary line-height-snug">{props.description}</p>
<div class="flex flex-row gap-4 text-xs">
<div class="flex flex-row items-center gap-2">
<Download01Icon class="h-4 w-4" />
{abbreviateNumber(props.downloads)}
</div>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.followers)}
</div>
</div>
</div>
</div>
);
}
export default ModCard;

View file

@ -0,0 +1,149 @@
import type { ProviderSearchResults, Providers, SearchResult } from '@onelauncher/client/bindings';
import { ChevronDownIcon, Download01Icon, HeartIcon } from '@untitled-theme/icons-solid';
import { For, Match, Show, Switch, createSignal } from 'solid-js';
import Button from '../base/Button';
import useBrowser from '~ui/hooks/useBrowser';
import useSettings from '~ui/hooks/useSettings';
import { abbreviateNumber } from '~utils';
type SearchResultsContainerProps = ProviderSearchResults & {
header: string;
category: string;
collapsable?: boolean;
};
function SearchResultsContainer(props: SearchResultsContainerProps) {
const { settings } = useSettings();
// eslint-disable-next-line solid/reactivity -- -
const [collapsed, setCollapsed] = createSignal(props.collapsable || false);
return (
<div class="relative flex flex-1 flex-col gap-3">
<div class="flex flex-1 flex-row justify-between">
<h5 class="ml-2">{props.header}</h5>
</div>
<div
class="w-full flex-1"
classList={{
'max-h-96 overflow-y-hidden relative after:(pointer-events-none content-[""] absolute z-1 top-0 left-0 w-full h-full bg-[linear-gradient(0deg,_theme(colors.page),transparent_25%)])': collapsed(),
}}
>
<Switch>
<Match when={settings().browser_list_view === 'list'}>
<PackageList {...props} />
</Match>
<Match when>
<PackageGrid {...props} />
</Match>
</Switch>
</div>
<Show when={collapsed() === true}>
<div class="absolute bottom-4 left-1/2 z-2 -translate-x-1/2">
<Button
buttonStyle="secondary"
children="Show more"
iconLeft={<ChevronDownIcon />}
onClick={() => setCollapsed(false)}
/>
</div>
</Show>
</div>
);
}
export default SearchResultsContainer;
function PackageGrid(props: SearchResultsContainerProps) {
return (
<div class="grid grid-cols-2 grid-rows-1 gap-2 xl:grid-cols-3">
<For each={props.results}>
{mod => (
<PackageItem {...mod} provider={props.provider} />
)}
</For>
</div>
);
}
function PackageList(props: SearchResultsContainerProps) {
return (
<div class="flex flex-col gap-2">
<For each={props.results}>
{mod => (
<PackageItem {...mod} row provider={props.provider} />
)}
</For>
</div>
);
}
function PackageItem(props: SearchResult & { provider: Providers; row?: boolean }) {
const controller = useBrowser();
function redirect() {
controller.displayPackage(props.project_id, props.provider);
}
return (
<div
tabIndex={0}
onClick={redirect}
class="h-full min-w-50 flex overflow-hidden rounded-lg bg-component-bg hover:bg-component-bg-hover"
classList={{
'flex-row max-h-32 min-h-32': props.row,
'flex-col max-h-74 min-h-74': !props.row,
}}
>
<div
class="relative flex items-center justify-center overflow-hidden"
classList={{
'h-32 aspect-ratio-square': props.row,
'w-full h-28': !props.row,
}}
>
<img class="absolute z-0 max-w-none w-7/6 opacity-50 filter-blur-xl" src={props.icon_url || ''} alt={`Icon for ${props.title}`} />
<img
class="relative z-1 aspect-ratio-square rounded-md image-render-auto"
classList={{
'w-2/5': !props.row,
'w-3/4': props.row,
}}
src={props.icon_url || ''}
alt={`Icon for ${props.title}`}
/>
</div>
<div class="flex flex-1 flex-col gap-2 p-3">
<div class="flex flex-col gap-2">
<h4 class="text-fg-primary font-medium">{props.title}</h4>
<p class="text-xs text-fg-secondary">
By
{' '}
<span class="text-fg-primary">{props.author}</span>
{' '}
on
{' '}
<span class="text-fg-primary">{props.provider}</span>
</p>
</div>
<p class="max-h-22 flex-1 overflow-hidden text-sm text-fg-secondary line-height-snug">{props.description}</p>
<div class="flex flex-row gap-4 text-xs">
<div class="flex flex-row items-center gap-2">
<Download01Icon class="h-4 w-4" />
{abbreviateNumber(props.downloads)}
</div>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.follows)}
</div>
</div>
</div>
</div>
);
}

View file

@ -1,8 +1,8 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import { type JSX, type ParentProps, Show, splitProps } from 'solid-js';
import type { Cluster } from '@onelauncher/client/bindings';
import styles from './ClusterCover.module.scss';
import defaultCover from '~assets/images/default_instance_cover.jpg';
import type { Cluster } from '~bindings';
type ClusterCoverProps = JSX.HTMLAttributes<HTMLImageElement> & {
cluster: Cluster | undefined;
@ -47,12 +47,12 @@ function ClusterCover(props: ClusterCoverProps) {
<Wrapper>
<img
{...rest}
class={`${styles.cover} ${split.class || ''}`}
class={`${split.class || ''}`}
src={image()}
onError={(e) => {
e.currentTarget.src = split.override ? convertFileSrc(split.override) : defaultCover;
// @ts-expect-error -- JSX doesn't seem to use the same type
split.onError?.(e);
if (typeof split.onError === 'function')
split.onError(e);
}}
/>
</Wrapper>

View file

@ -1,9 +1,9 @@
import { type JSX, splitProps } from 'solid-js';
import type { Cluster, Loader } from '@onelauncher/client/bindings';
import FabricImage from '~assets/logos/fabric.png';
import ForgeImage from '~assets/logos/forge.png';
import QuiltImage from '~assets/logos/quilt.png';
import VanillaImage from '~assets/logos/vanilla.png';
import type { Cluster, Loader } from '~bindings';
export function getLoaderLogoSrc(loader: Cluster | Loader): string {
const loaderName = (typeof loader === 'string' ? loader : loader.meta.loader)?.toLowerCase() as Loader;

View file

@ -1,7 +1,7 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import { join } from 'pathe';
import { type JSX, splitProps } from 'solid-js';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
import DefaultWorld from '~assets/images/default_world.png?url';
export type WorldIconProps = JSX.HTMLAttributes<HTMLImageElement> & {
@ -11,7 +11,7 @@ export type WorldIconProps = JSX.HTMLAttributes<HTMLImageElement> & {
function WorldIcon(props: WorldIconProps) {
const [split, rest] = splitProps(props, ['cluster_name', 'world_name', 'class']);
const { settings } = useSettingsContext();
const { settings } = useSettings();
const path = () => convertFileSrc(join(settings().config_dir || '', 'clusters', props.cluster_name, 'saves', props.world_name, 'icon.png'));
return (

View file

@ -8,9 +8,9 @@ import { For, Show, createContext, createSignal, onCleanup, onMount, splitProps,
import { mergeRefs } from '@solid-primitives/refs';
import { createStore } from 'solid-js/store';
import { Transition } from 'solid-transition-group';
import type { MakeOptional } from '@onelauncher/client';
import Button from '../base/Button';
import FullscreenOverlay from './FullscreenOverlay';
import type { MakeOptional } from '~types';
type ModalsList = (() => JSX.Element)[];
@ -183,12 +183,17 @@ Modal.Error = function (props: ModalErrorProps) {
/>,
]}
>
<div class="max-w-84 flex flex-col items-center gap-y-2">
<div class="flex flex-col items-center gap-y-2">
{split.children || (
<span>Something went wrong.</span>
)}
{split.message && (
<code class="rounded-md bg-component-bg p-2 text-left">
// <OverlayScrollbarsComponent class="max-w-84 rounded-md bg-component-bg p-2">
// <code class="bg-transparent text-left!">
// {split.message}
// </code>
// </OverlayScrollbarsComponent>
<code class="max-w-120 break-all">
{split.message}
</code>
)}

View file

@ -1,11 +1,11 @@
import { For, Show } from 'solid-js';
import { PlusIcon, Settings01Icon } from '@untitled-theme/icons-solid';
import { useNavigate } from '@solidjs/router';
import type { MinecraftCredentials } from '@onelauncher/client/bindings';
import Button from '../../base/Button';
import PlayerHead from '../../game/PlayerHead';
import Popup, { type PopupProps } from '../Popup';
import useAccountController from './AddAccountModal';
import type { MinecraftCredentials } from '~bindings';
interface AccountComponentProps {
account: MinecraftCredentials | null | undefined;

View file

@ -1,7 +1,7 @@
import { type Context, Match, type ParentProps, type Resource, Switch, createContext, createSignal, useContext } from 'solid-js';
import { LinkExternal01Icon } from '@untitled-theme/icons-solid';
import type { MinecraftCredentials } from '@onelauncher/client/bindings';
import Modal, { type ModalProps, createModal } from '../Modal';
import type { MinecraftCredentials } from '~bindings';
import { bridge } from '~imports';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
import Button from '~ui/components/base/Button';
@ -18,8 +18,8 @@ interface AccountControllerContextFunc {
const AccountControllerContext = createContext<AccountControllerContextFunc>() as Context<AccountControllerContextFunc>;
export function AccountControllerProvider(props: ParentProps) {
const [accounts, { refetch: refetchAccounts }] = useCommand(bridge.commands.getUsers);
const [defaultAccount, { refetch: refetchDefaultAccount }] = useCommand(bridge.commands.getDefaultUser, true);
const [accounts, { refetch: refetchAccounts }] = useCommand(() => bridge.commands.getUsers());
const [defaultAccount, { refetch: refetchDefaultAccount }] = useCommand(() => bridge.commands.getDefaultUser(true));
const [deleteModalUuid, setDeleteModalUuid] = createSignal<string>();
@ -37,7 +37,7 @@ export function AccountControllerProvider(props: ParentProps) {
}
async function setDefaultAccount(uuid: string | null) {
await tryResult(bridge.commands.setDefaultUser, uuid).then(refetch);
await tryResult(() => bridge.commands.setDefaultUser(uuid)).then(refetch);
}
async function removeAccount(uuid: string, force?: boolean) {
@ -52,7 +52,7 @@ export function AccountControllerProvider(props: ParentProps) {
if (uuid === undefined)
return;
await tryResult(bridge.commands.removeUser, uuid);
await tryResult(() => bridge.commands.removeUser(uuid));
refetch();
}

View file

@ -1,12 +1,13 @@
import { type Accessor, type Context, type JSX, type ParentProps, type Setter, Show, createContext, createSignal, untrack, useContext } from 'solid-js';
import { ArrowRightIcon, Server01Icon } from '@untitled-theme/icons-solid';
import { createStore } from 'solid-js/store';
import type { CreateCluster } from '@onelauncher/client/bindings';
import HeaderImage from '../../../../assets/images/header.png';
import { createModal } from '../Modal';
import ClusterGameSetup from './ClusterGameSetup';
import ClusterProviderSelection from './ClusterProviderSelection';
import Button from '~ui/components/base/Button';
import type { CreateCluster } from '~bindings';
import { bridge } from '~imports';
export enum CreationStage {
PROVIDER_SELECTION = 0,
@ -70,7 +71,6 @@ export function ClusterModalControllerProvider(props: ParentProps) {
async start() {
setPartialCluster({});
controller.setStep(CreationStage.PROVIDER_SELECTION);
// eslint-disable-next-line ts/no-use-before-define -- It should still work
modal.show();
},
@ -80,7 +80,6 @@ export function ClusterModalControllerProvider(props: ParentProps) {
cancel() {
controller.setStep(undefined);
// eslint-disable-next-line ts/no-use-before-define -- It should still work
modal.hide();
},
@ -91,7 +90,8 @@ export function ClusterModalControllerProvider(props: ParentProps) {
if (!untracked[prop])
throw new Error(`Missing required property ${prop}`);
// @ts-expect-error -- TODO: Do this properly
modal.hide();
await bridge.commands.createCluster({
icon: null,
icon_url: null,
@ -100,7 +100,7 @@ export function ClusterModalControllerProvider(props: ParentProps) {
skip: null,
skip_watch: null,
...untracked,
});
} as CreateCluster);
},
};

View file

@ -1,5 +1,6 @@
import { TextInputIcon } from '@untitled-theme/icons-solid';
import { For, Index, type JSX, Show, createEffect, createSignal, onMount, splitProps, untrack } from 'solid-js';
import type { Loader, VersionType } from '@onelauncher/client/bindings';
import { type ClusterStepProps, createClusterStep } from './ClusterCreationModal';
import Dropdown from '~ui/components/base/Dropdown';
import TextField from '~ui/components/base/TextField';
@ -7,7 +8,6 @@ import useCommand from '~ui/hooks/useCommand';
import { bridge } from '~imports';
import SelectList from '~ui/components/base/SelectList';
import Checkbox from '~ui/components/base/Checkbox';
import type { Loader, VersionType } from '~bindings';
import { LOADERS, formatVersionRelease } from '~utils';
import LoaderIcon from '~ui/components/game/LoaderIcon';
@ -75,7 +75,7 @@ type VersionReleaseFilters = {
};
function VersionSelector(props: { setVersion: (version: string) => any }) {
const [versions] = useCommand(bridge.commands.getMinecraftVersions);
const [versions] = useCommand(() => bridge.commands.getMinecraftVersions());
const [filteredVersions, setFilteredVersions] = createSignal<bridge.Version[]>([]);
const [filters, setFilters] = createSignal<VersionReleaseFilters>({
old_alpha: false,

View file

@ -32,7 +32,7 @@ export default createClusterStep({
function ClusterProviderSelection(props: ClusterStepProps) {
const [selected, setSelected] = createSignal<number>();
const check = () => {
createEffect(() => {
// TODO: Add more stages
// eslint-disable-next-line solid/reactivity -- It's fine
props.setCanGoForward(() => {
@ -43,9 +43,7 @@ function ClusterProviderSelection(props: ClusterStepProps) {
return isTrue;
});
};
createEffect(check);
});
return (
<div class="grid grid-cols-3 gap-2">

View file

@ -1,49 +1,39 @@
import { type JSX, Match, Show, Switch, createEffect, createSignal } from 'solid-js';
import { type ComponentProps, type JSX, Match, Show, Switch, createEffect, createSignal } from 'solid-js';
import { InfoCircleIcon } from '@untitled-theme/icons-solid';
import { TimeAgo } from '../../DynamicTime';
import { PausableTimer } from '~utils';
import { PausableTimer } from '@onelauncher/client';
import type { IngressPayload, IngressType } from '@onelauncher/client/bindings';
type NotificationComponentProps = Core.Notification & {
type NotificationComponentProps = IngressPayload & {
overlay: boolean;
};
type NotificationType = '';
function IconFromNotificationType(type: NotificationType): (props: JSX.HTMLAttributes<HTMLDivElement>) => JSX.Element {
function IconFromNotificationType(type: IngressType['type']): (props: ComponentProps<'svg'>) => JSX.Element {
switch (type) {
// case NotificationType.Alert:
// return AlertTriangleIcon as any;
// case NotificationType.Download:
// return FolderDownloadIcon as any;
// case NotificationType.DownloadSuccess:
// return FolderCheckIcon as any;
// case NotificationType.DownloadError:
// return FolderXIcon as any;
// case NotificationType.Refresh:
// return RefreshCcw02Icon as any;
// case NotificationType.Success:
// return CheckCircleIcon as any;
// case NotificationType.Info:
case 'initialize':
case 'download_java':
case 'download_loader':
case 'sync_cluster':
case 'copy_cluster':
case 'sync_config':
case 'archival':
case 'download_package':
case 'download_pack':
default:
return InfoCircleIcon as any;
return InfoCircleIcon;
}
}
function ColorFromNotificationType(type: NotificationType): string {
function ColorFromNotificationType(type: IngressType['type']): string {
switch (type) {
// case NotificationType.Info:
// case NotificationType.Refresh:
// return 'text-brand';
// case NotificationType.Alert:
// case NotificationType.DownloadError:
// return 'text-danger';
// case NotificationType.DownloadSuccess:
// case NotificationType.Success:
// return 'text-success';
// case NotificationType.Download:
case 'initialize':
case 'download_java':
case 'download_loader':
case 'sync_cluster':
case 'copy_cluster':
case 'sync_config':
case 'archival':
case 'download_package':
case 'download_pack':
default:
return 'text-fg-primary';
}
@ -57,9 +47,9 @@ function NotificationOverlayComponent(props: NotificationComponentProps) {
const [secondsLeft, setSecondsLeft] = createSignal<number>(TOTAL_SECONDS);
createEffect(() => {
setDisappearing(props.progress === undefined);
setDisappearing(props.fraction === undefined);
if (props.progress !== undefined && props.progress >= 1) {
if (props.fraction !== null && props.fraction >= 1) {
setVisible(false);
return;
}
@ -121,7 +111,7 @@ function NotificationOverlayComponent(props: NotificationComponentProps) {
<NotificationPopupComponent {...props} />
</div>
<Show when={disappearing() === true && props.progress === undefined}>
<Show when={disappearing() === true && props.fraction === undefined}>
<div class="h-1.5 w-full bg-brand-disabled">
<div
style={{
@ -141,31 +131,28 @@ function NotificationPopupComponent(props: NotificationComponentProps) {
return (
<div class="flex flex-col gap-y-1 p-2">
<div class="grid grid-cols-[24px_1fr_auto] min-h-10 place-items-center gap-3">
{IconFromNotificationType(props.notification_type)({
class: `w-6 h-6 ${ColorFromNotificationType(props.notification_type)}`,
{IconFromNotificationType(props.event.type)({
class: `w-6 h-6 ${ColorFromNotificationType(props.event.type)}`,
})}
<div class="w-full flex flex-col">
<span class={`font-medium ${ColorFromNotificationType(props.notification_type)}`}>{props.title}</span>
<span class={`font-medium ${ColorFromNotificationType(props.event.type)}`}>{props.message}</span>
<span class="text-sm text-white/60">{props.message}</span>
</div>
<Show when={props.overlay !== true}>
<div class="flex flex-row items-center justify-end gap-1">
<span class="text-sm text-white/40">
<TimeAgo timestamp={props.created_at * 1000} />
</span>
<span class="h-1.5 w-1.5 rounded-full bg-brand" />
</div>
</Show>
</div>
<Show when={props.progress !== undefined}>
<Show when={props.fraction !== null}>
<div class="h-1.5 w-full overflow-hidden rounded-full bg-brand-disabled">
<div
class="h-full max-w-full min-w-0 rounded-full bg-brand transition-width"
style={{
width: `${Math.floor(props.progress! * 100)}%`,
width: `${Math.floor(props.fraction!)}%`,
}}
/>
</div>

View file

@ -1,10 +1,10 @@
import { For } from 'solid-js';
import Popup from '../Popup';
import NotificationComponent from './NotificationComponent';
import useNotifications from '~ui/hooks/useNotifications';
import useIngress from '~ui/hooks/useIngress';
export default function NotificationOverlay() {
const [notifications] = useNotifications();
const [ingress] = useIngress();
return (
<Popup
@ -14,7 +14,7 @@ export default function NotificationOverlay() {
ref={ref => ref.classList.add('fixed', 'bottom-8', 'right-8')}
>
<div class="flex flex-col-reverse gap-2">
<For each={notifications()}>
<For each={ingress()}>
{notification => (
<NotificationComponent overlay {...notification} />
)}

View file

@ -4,10 +4,10 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import Popup, { type PopupProps } from '../Popup';
import Button from '../../base/Button';
import NotificationComponent from './NotificationComponent';
import useNotifications from '~ui/hooks/useNotifications';
import useIngress from '~ui/hooks/useIngress';
function NotificationPopup(props: PopupProps) {
const [notifications] = useNotifications(updateSize);
const [ingress] = useIngress(updateSize);
let inner!: HTMLDivElement;
let parent!: HTMLDivElement;
@ -40,10 +40,10 @@ function NotificationPopup(props: PopupProps) {
<div class="flex flex-col items-stretch justify-start gap-2 text-start" ref={inner}>
<p class="px-2 pt-1 text-2lg">Notifications</p>
<Switch>
<Match when={notifications().length > 0}>
<Match when={ingress().length > 0}>
<OverlayScrollbarsComponent class="max-h-[min(500px,60vh)] overflow-auto">
<div class="flex flex-col-reverse items-stretch justify-center">
<For each={notifications()}>
<For each={ingress()}>
{noti => (
<div class="w-full flex flex-col">
<NotificationComponent {...noti} overlay={false} />
@ -54,7 +54,7 @@ function NotificationPopup(props: PopupProps) {
</div>
</OverlayScrollbarsComponent>
</Match>
<Match when={notifications().length === 0}>
<Match when={ingress().length === 0}>
<span class="px-2">You have no notifications</span>
<span class="h-px bg-gray-05" />
</Match>

View file

@ -0,0 +1,182 @@
import { useNavigate } from '@solidjs/router';
import { type Accessor, type Context, For, type ParentProps, type Setter, Show, createContext, createEffect, createSignal, onMount, untrack, useContext } from 'solid-js';
import type { Cluster, ManagedPackage, PackageType, ProviderSearchResults, Providers } from '@onelauncher/client/bindings';
import useCommand, { tryResult } from './useCommand';
import { useRecentCluster } from './useCluster';
import { bridge } from '~imports';
import BrowserPackage from '~ui/pages/browser/BrowserPackage';
import type { ModalProps } from '~ui/components/overlay/Modal';
import Modal, { createModal } from '~ui/components/overlay/Modal';
import Dropdown from '~ui/components/base/Dropdown';
import Button from '~ui/components/base/Button';
interface BrowserControllerType {
cluster: Accessor<Cluster | undefined>;
setCluster: Setter<Cluster | undefined>;
displayBrowser: (cluster?: Cluster | undefined) => void;
displayPackage: (id: string, provider: Providers) => void;
displayCategory: (category: string) => void;
displayClusterSelector: () => void;
packageType: Accessor<PackageType>;
setPackageType: Setter<PackageType>;
refreshCache: () => void;
cache: Accessor<ProviderSearchResults | undefined>;
featured: Accessor<ManagedPackage | undefined>;
};
const BrowserContext = createContext() as Context<BrowserControllerType>;
export function BrowserProvider(props: ParentProps) {
const [mainPageCache, setMainPageCache] = createSignal<ProviderSearchResults>();
const [featured, setFeatured] = createSignal<ManagedPackage | undefined>();
const [packageType, setPackageType] = createSignal<PackageType>('mod');
const [cluster, setCluster] = createSignal<Cluster>();
const recentCluster = useRecentCluster();
const navigate = useNavigate();
const modal = createModal(props => (
<ChooseClusterModal
{...props}
/>
));
const controller: BrowserControllerType = {
cluster,
setCluster,
displayBrowser(cluster?: Cluster) {
setCluster(cluster);
},
displayPackage(id: string, provider: Providers) {
navigate(BrowserPackage.buildUrl({ id, provider }));
},
displayCategory(_category: string) {
},
displayClusterSelector() {
modal.show();
},
packageType,
setPackageType,
async refreshCache() {
const res = await tryResult(() => bridge.commands.searchProviderPackages('Modrinth', null, 10, null, null, null, null));
setMainPageCache(res);
// TODO: Better algorithm for selecting a featured package
const firstPackage = res.results[0];
if (firstPackage !== undefined) {
const featuredPackage = await tryResult(() => bridge.commands.getProviderPackage('Modrinth', firstPackage.project_id));
setFeatured(featuredPackage);
}
},
cache: mainPageCache,
featured,
};
onMount(() => {
if (mainPageCache() === undefined)
controller.refreshCache();
});
createEffect(() => {
setCluster(recentCluster());
});
return (
<BrowserContext.Provider value={controller}>
{props.children}
</BrowserContext.Provider>
);
}
export default function useBrowser() {
const controller = useContext(BrowserContext);
if (!controller)
throw new Error('useBrowserController should be called inside its BrowserProvider');
return controller;
}
function ChooseClusterModal(props: ModalProps) {
const [selected, setSelected] = createSignal<number>(0);
const [clusters] = useCommand(() => bridge.commands.getClusters());
const controller = useBrowser();
const currentlySelectedCluster = (list: Cluster[]) => {
const cluster = controller.cluster();
if (cluster === undefined)
return 0;
if (list === undefined)
return 0;
const index = list.findIndex(c => c.uuid === cluster.uuid) || 0;
if (index === -1)
return 0;
return index;
};
createEffect(() => {
setSelected(currentlySelectedCluster(clusters() || []));
});
function chooseCluster() {
const index = untrack(selected);
const clusterz = untrack(clusters);
if (clusterz !== undefined && index !== undefined)
controller.setCluster(clusterz[index]);
props.hide();
}
return (
<Modal.Simple
{...props}
title="Choose a cluster"
children={(
<Show
when={clusters !== undefined}
fallback={<div>Loading...</div>}
>
<Dropdown
onChange={setSelected}
selected={selected}
>
<For each={clusters()!}>
{cluster => (
<Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
)}
</For>
</Dropdown>
</Show>
)}
buttons={[
<Button
children="Close"
onClick={props.hide}
buttonStyle="secondary"
/>,
<Button
children="Save"
onClick={chooseCluster}
/>,
]}
/>
);
}

View file

@ -1,20 +0,0 @@
// import { type Context, type ParentProps, createContext, useContext } from 'solid-js';
// interface BrowserContextType {
// getMod: (id: string) => void;
// getMods: (category: string) => void;
// };
// const BrowserContext = createContext() as Context<BrowserContextType>;
// export function BrowserContextProvider(props: ParentProps) {
// return (
// <BrowserContext.Provider value={}>
// {props.children}
// </BrowserContext.Provider>
// );
// }
// export default function useBrowserController() {
// return useContext(BrowserContext);
// }

View file

@ -1,7 +1,7 @@
import { type Accessor, type Context, type ParentProps, type ResourceReturn, Show, createContext, createSignal, useContext } from 'solid-js';
import { type Accessor, type Context, type ParentProps, type ResourceReturn, Show, createContext, createEffect, createSignal, useContext } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import type { Cluster } from '@onelauncher/client/bindings';
import useCommand, { tryResult } from './useCommand';
import type { Cluster } from '~bindings';
import { bridge } from '~imports';
import ClusterGame from '~ui/pages/cluster/ClusterGame';
import Modal, { createModal } from '~ui/components/overlay/Modal';
@ -12,7 +12,7 @@ export function getCluster(uuid: string | undefined | null): ResourceReturn<Clus
if (typeof uuid !== 'string' || uuid.length === 0)
return undefined;
const resource = useCommand(bridge.commands.getCluster, uuid);
const resource = useCommand(() => bridge.commands.getCluster(uuid));
return resource;
}
@ -57,7 +57,7 @@ export function useLaunchCluster(uuid: string | Accessor<string | undefined> | (
if (uuid === undefined)
return;
tryResult(bridge.commands.runCluster, uuid).then((details) => {
tryResult(() => bridge.commands.runCluster(uuid)).then((details) => {
navigate(`/clusters/game?${ClusterGame.buildUrl(uuid, details).toString()}`);
}).catch((err) => {
setError(err);
@ -66,4 +66,41 @@ export function useLaunchCluster(uuid: string | Accessor<string | undefined> | (
};
}
export function useRecentCluster() {
const [clusters] = useCommand(() => bridge.commands.getClusters());
const [cluster, setCluster] = createSignal<Cluster>();
createEffect(() => {
let mostRecentCluster: Cluster | undefined;
const list = clusters();
if (list === undefined)
return;
for (const cluster of list) {
if (mostRecentCluster === undefined) {
mostRecentCluster = cluster;
continue;
}
if (typeof mostRecentCluster.meta.played_at !== 'string' && typeof cluster.meta.played_at === 'string') {
mostRecentCluster = cluster;
continue;
}
if (typeof mostRecentCluster.meta.played_at === 'string' && typeof cluster.meta.played_at === 'string') {
const playedAt = new Date(mostRecentCluster.meta.played_at);
const clusterPlayedAt = new Date(cluster.meta.played_at);
if (clusterPlayedAt > playedAt)
mostRecentCluster = cluster;
}
}
setCluster(mostRecentCluster);
});
return cluster;
}
export default useClusterContext;

View file

@ -1,23 +1,63 @@
import { type ResourceReturn, createResource } from 'solid-js';
import type { Result } from '~bindings';
import { type ResourceOptions, type ResourceReturn, type ResourceSource, createResource } from 'solid-js';
import type { Result } from '@onelauncher/client/bindings';
/**
* A helper function which handles a Rust Result properly
* @param cmd The command to call
* @throws
*/
export default function useCommand<R, E = string, Args extends unknown[] = []>(
cmd: (...args: Args) => Promise<Result<R, E>>,
...args: Args
type Command<R, E> = () => Promise<Result<R, E>>;
export function useCommand<R, E = string>(
cmd: Command<R, E>,
options?: ResourceOptions<R>,
): ResourceReturn<R>;
export function useCommand<R, S, E = string>(
source: ResourceSource<S>,
cmd: Command<R, E>,
options?: ResourceOptions<R>,
): ResourceReturn<R>;
export default function useCommand<R, S, E = string>(
pSource: ResourceSource<S> | Command<R, E>,
pCmd?: Command<R, E> | ResourceOptions<R>,
options?: ResourceOptions<R>,
): ResourceReturn<R> {
return createResource(async () => await tryResult(cmd, ...args));
let source: ResourceSource<S>;
let command: Command<R, E>;
let opts: ResourceOptions<R> | undefined;
switch (arguments.length) {
case 1: {
source = true as ResourceSource<S>;
command = pSource as Command<R, E>;
opts = {} as ResourceOptions<R>;
break;
}
case 2: {
if (pCmd instanceof Function) {
source = pSource as ResourceSource<S>;
command = pCmd;
opts = {} as ResourceOptions<R>;
}
else {
source = true as ResourceSource<S>;
command = pSource as Command<R, E>;
opts = pCmd as ResourceOptions<R>;
}
break;
}
case 3: {
source = pSource as ResourceSource<S>;
command = pCmd! as Command<R, E>;
opts = options as ResourceOptions<R>;
break;
}
}
return createResource(source, async () => await tryResult(command), opts);
}
export async function tryResult<R, E, Args extends unknown[]>(
cmd: (...args: Args) => Promise<Result<R, E>>,
...args: Args
export async function tryResult<R, E>(
cmd: () => Promise<Result<R, E>>,
): Promise<R> {
const value = await cmd(...args);
const value = await cmd();
if (value.status === 'ok')
return value.data;

View file

@ -0,0 +1,26 @@
import { type IngressPayload, type IngressType, events } from '@onelauncher/client/bindings';
import { type Signal, createSignal, onMount } from 'solid-js';
type Ingresses = IngressPayload[];
type OnUpdateFn = (event: IngressType['type']) => unknown;
function useIngress(onUpdate?: OnUpdateFn): Signal<Ingresses> {
const [ingresses, setIngresses] = createSignal<Ingresses>([]);
async function update(event: IngressType['type']) {
const ingress: Ingresses = []; // await manager.getNotifications();
setIngresses(ingress);
if (onUpdate)
onUpdate(event);
}
onMount(() => {
update('initialize');
events.ingressPayload.listen(e => update(e.payload.event.type));
});
return [ingresses, setIngresses];
}
export default useIngress;

View file

@ -1,29 +0,0 @@
import { type Signal, createSignal, onMount } from 'solid-js';
type Notifications = Core.Notification[];
type OnUpdateFn = (event: Core.NotificationEvents) => unknown;
function useNotifications(onUpdate?: OnUpdateFn): Signal<Notifications> {
const [notifications, setNotifications] = createSignal<Notifications>([]);
async function update(event: Core.NotificationEvents) {
const notis: Notifications = []; // await manager.getNotifications();
setNotifications(notis);
if (onUpdate)
onUpdate(event);
}
onMount(() => {
update('init');
// manager.on('added', () => update('added'));
// manager.on('removed', () => update('removed'));
// manager.on('cleared', () => update('cleared'));
// manager.on('modified', () => update('modified'));
});
return [notifications, setNotifications];
}
export default useNotifications;

View file

@ -0,0 +1,43 @@
import type { BeforeLeaveEventArgs } from '@solidjs/router';
import { useBeforeLeave } from '@solidjs/router';
import { createSignal } from 'solid-js';
interface PreventLeaveContext {
continue: () => void;
cancel: () => void;
preventNavigation: () => void;
triedNavigating: () => boolean;
};
function usePreventLeave(listener: (ctx: PreventLeaveContext) => void): PreventLeaveContext {
const [event, setEvent] = createSignal<BeforeLeaveEventArgs | null>(null);
const ctx: PreventLeaveContext = {
continue: () => {
if (ctx.triedNavigating() !== true) {
console.warn('PreventLeaveContext.continue() called without an event');
return;
}
event()?.retry(true);
setEvent(null);
},
cancel: () => {
setEvent(null);
},
preventNavigation: () => event()?.preventDefault(),
triedNavigating: () => event() !== null,
};
useBeforeLeave((e) => {
setEvent(e);
listener(ctx);
});
return ctx;
}
export default usePreventLeave;

View file

@ -0,0 +1,65 @@
import { open } from '@tauri-apps/plugin-shell';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import { createSignal } from 'solid-js';
import Button from '~ui/components/base/Button';
import Modal, { createModal } from '~ui/components/overlay/Modal';
function usePromptOpener() {
const [url, setUrl] = createSignal<string | null>(null);
const modal = createModal(props => (
<Modal.Simple
{...props}
title="Third Party Link"
children={(
<div class="flex flex-col items-center gap-y-4">
<span class="max-w-58">You are about to visit an external website that may malicious.</span>
<span>Do you wish to proceed?</span>
<OverlayScrollbarsComponent class="max-w-100">
<div class="h-8 flex flex-row">
<code>{url()}</code>
</div>
</OverlayScrollbarsComponent>
</div>
)}
buttons={[
<Button
buttonStyle="secondary"
children="Cancel"
onClick={cancel}
/>,
<Button
buttonStyle="danger"
children="Proceed"
onClick={proceed}
/>,
]}
/>
));
function cancel() {
modal.hide();
}
function proceed() {
modal.hide();
if (url() !== null)
open(url()!);
}
return (url: string | undefined | null, force: boolean = false) => {
if (url === undefined || url === null)
return;
if (force) {
open(url);
return;
}
setUrl(url);
modal.show();
};
}
export default usePromptOpener;

View file

@ -1,7 +1,7 @@
import { type Context, type ParentProps, Show, createContext, createEffect, useContext } from 'solid-js';
import { useBeforeLeave } from '@solidjs/router';
import type { Settings } from '@onelauncher/client/bindings';
import useCommand from './useCommand';
import type { Settings } from '~bindings';
import { bridge } from '~imports';
/**
@ -20,7 +20,7 @@ interface SettingsControllerType {
const SettingsContext = createContext() as Context<SettingsControllerType>;
export function SettingsProvider(props: ParentProps) {
const [settings, { refetch }] = useCommand(bridge.commands.getSettings);
const [settings, { refetch }] = useCommand(() => bridge.commands.getSettings());
createEffect(() => {
if (settings !== undefined && settings() !== undefined)
@ -54,7 +54,7 @@ export function SettingsProvider(props: ParentProps) {
);
}
export function useSettingsContext() {
export function useSettings() {
const context = useContext(SettingsContext);
if (!context)
@ -63,4 +63,4 @@ export function useSettingsContext() {
return context;
}
export default useSettingsContext;
export default useSettings;

View file

@ -2,32 +2,39 @@ import {
DotsVerticalIcon,
PlayIcon,
PlusIcon,
RefreshCw01Icon,
SearchMdIcon,
} from '@untitled-theme/icons-solid';
import { For, Show, createEffect, createSignal } from 'solid-js';
import { For, Show, onMount } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { mergeRefs } from '@solid-primitives/refs';
import type { Cluster } from '@onelauncher/client/bindings';
import BannerBackground from '../../assets/images/header.png';
import Button from '../components/base/Button';
import TextField from '../components/base/TextField';
import ClusterRoot from './cluster/ClusterRoot';
import ClusterCover from '~ui/components/game/ClusterCover';
import type { Cluster } from '~bindings';
import useCommand from '~ui/hooks/useCommand';
import { bridge } from '~imports';
import { secondsToWords, upperFirst } from '~utils';
import { formatAsDuration, upperFirst } from '~utils';
import { useClusterCreator } from '~ui/components/overlay/cluster/ClusterCreationModal';
import { useLaunchCluster } from '~ui/hooks/useCluster';
import { useLaunchCluster, useRecentCluster } from '~ui/hooks/useCluster';
type GroupedClusters = Record<string, Cluster[]>;
function HomePage() {
const [clusters, { refetch }] = useCommand<GroupedClusters>(bridge.commands.getClustersGrouped);
const [clusters, { refetch }] = useCommand(bridge.commands.getClustersGrouped);
const controller = useClusterCreator();
const containerIds = (list: GroupedClusters | undefined) => Object.keys(list || []);
onMount(() => {
bridge.events.clusterPayload.listen(({ payload }) => {
if (payload.event === 'created' || payload.event === 'deleted')
refetch();
});
});
async function newCluster() {
try {
await controller.start();
@ -39,77 +46,21 @@ function HomePage() {
refetch();
}
// function getContainerByCluster(id: string) {
// const list = clusters();
// if (list === undefined)
// return undefined;
// if (list[id] !== undefined)
// return id;
// for (const [key, value] of Object.entries(list))
// if (value.find(cluster => cluster.uuid === id))
// return key;
// return undefined;
// }
// function move(fromId: string, toId: string) {
// const from = getContainerByCluster(fromId);
// const to = getContainerByCluster(toId);
// console.log(from, to);
// if (from === undefined || to === undefined)
// return;
// if (from === to) {
// const list = clusters();
// const fromList = list[from];
// const fromIndex = fromList.findIndex(cluster => cluster.uuid === fromId);
// const toIndex = fromList.findIndex(cluster => cluster.uuid === toId);
// if (fromIndex === toIndex)
// return;
// const newList = [...fromList];
// const [removed] = newList.splice(fromIndex, 1);
// newList.splice(toIndex, 0, removed);
// mutate({ ...list, [from]: newList });
// }
// else {
// const list = clusters();
// const fromList = list[from];
// const toList = list[to];
// const fromIndex = fromList.findIndex(cluster => cluster.uuid === fromId);
// const toIndex = toList.findIndex(cluster => cluster.uuid === toId);
// const newListFrom = [...fromList];
// const newListTo = [...toList];
// const [removed] = newListFrom.splice(fromIndex, 1);
// newListTo.splice(toIndex, 0, removed);
// mutate({ ...list, [from]: newListFrom, [to]: newListTo });
// }
// }
// const onDragEnd: DragEventHandler = (event) => {
// if (event.draggable && event.droppable)
// move(event.draggable.id as string, event.droppable.id as string);
// };
return (
<div class="h-full flex flex-col gap-y-4 text-fg-primary">
<Banner clusters={clusters()} />
<Banner />
<div class="flex flex-row items-center justify-between">
<div>
<TextField iconLeft={<SearchMdIcon />} placeholder="Search for clusters..." />
</div>
<div class="flex flex-row gap-x-4">
<div class="flex flex-row items-center gap-x-2">
<Button
buttonStyle="icon"
children={<RefreshCw01Icon />}
onClick={refetch}
/>
<Button
buttonStyle="primary"
iconLeft={<PlusIcon class="stroke-[2.2] !w-5" />}
@ -122,21 +73,11 @@ function HomePage() {
<Show
when={containerIds(clusters()).length > 0}
children={(
// <DragDropProvider onDragEnd={onDragEnd}>
// <DragDropSensors />
<For each={Object.entries(clusters() ?? {})}>
{([group, clusters]) => (
<ClusterGroup title={group} clusters={clusters} />
)}
</For>
// <DragOverlay>
// {draggable => (
// draggable?.node.cloneNode(true)
// )}
// </DragOverlay>
// </DragDropProvider>
)}
fallback={(
<div class="max-h-64 flex flex-1 flex-col items-center justify-center gap-y-4">
@ -151,30 +92,7 @@ function HomePage() {
export default HomePage;
// TODO: Replace this into it's own component
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
// function _OneConfigLogo() {
// return (
// <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
// <g id="Group 3544">
// <g id="Group 3545">
// <path id="Secondary" d="M15.635 6.4514C15.6593 6.49541 15.6848 6.54148 15.7046 6.58943C16.1185 7.45825 16.1017 8.7122 15.6275 9.56181L13.9018 12.5487C13.3915 11.7689 12.6768 11.0998 11.8176 10.5943C11.8392 10.5607 11.8585 10.5269 11.8753 10.4933C13.3988 7.41013 11.8176 3.33781 8.0003 3.39798C4.11578 3.333 2.58014 7.47994 4.17113 10.604C3.31664 11.1094 2.6067 11.7761 2.09878 12.5535C2.08691 12.5283 1.91791 12.2378 1.68616 11.8394C1.12277 10.8708 0.18813 9.26419 0.235961 9.27782C-0.0841643 8.50283 -0.0793459 7.4438 0.255235 6.67845C0.27533 6.62445 0.300245 6.57238 0.326452 6.52232C0.341024 6.49435 0.356067 6.46697 0.370757 6.44017L2.83046 2.1753C2.8341 2.17043 2.83716 2.1649 2.84021 2.15944C2.84327 2.15409 2.84621 2.1488 2.84973 2.14404C3.35754 1.29925 4.5586 0.613281 5.53813 0.613281H10.4576C11.5744 0.613281 12.607 1.21017 13.163 2.1753C13.1686 2.18241 14.024 3.66317 14.7254 4.87721C15.2115 5.7186 15.6235 6.43183 15.6275 6.43777L15.635 6.4514Z" fill="#DFEAFF" fill-opacity="0.5" />
// <g id="Primary">
// <path d="M5.73802 12.1854C7.03039 12.9171 8.98957 12.9147 10.2532 12.1806C11.3314 12.7149 12.1161 13.6247 12.3591 14.6933C11.8272 15.0856 11.1725 15.3527 10.5804 15.3816C10.542 15.3865 10.4987 15.3865 10.4601 15.3865C10.4265 15.3856 9.7571 15.386 8.9193 15.3865H8.87546C7.43983 15.3874 5.53976 15.3886 5.47807 15.3841C4.87154 15.3696 4.1904 15.1 3.64404 14.6933C3.88237 13.6271 4.66459 12.7197 5.73802 12.1854Z" fill="#DFEAFF" fill-opacity="0.5" />
// <path d="M10.4314 8.0023C10.5156 11.3598 5.48535 11.3574 5.56961 8.0023C5.5239 4.62553 10.4747 4.62553 10.4314 8.0023Z" fill="#DFEAFF" fill-opacity="0.5" />
// </g>
// </g>
// </g>
// </svg>
// );
// }
interface BannerProps {
clusters: GroupedClusters | undefined;
};
function Banner(props: BannerProps) {
// TODO: Banner information
function Banner() {
/**
* If there are any clusters, display the most recent cluster name + some statistics as the "description".
* The background would prioritise
@ -183,41 +101,10 @@ function Banner(props: BannerProps) {
*
* If there are no clusters, display a generic background with the button action creating a new cluster.
*/
const [cluster, setCluster] = createSignal<Cluster>();
const cluster = useRecentCluster();
const launch = useLaunchCluster(() => cluster()?.uuid);
const navigate = useNavigate();
createEffect(() => {
// eslint-disable-next-line no-undef-init -- This is fine. I love eslint rule clash though
let mostRecentCluster: Cluster | undefined = undefined;
if (props.clusters === undefined)
return;
for (const [_, clusters] of Object.entries(props.clusters))
for (const cluster of clusters) {
if (mostRecentCluster === undefined) {
mostRecentCluster = cluster;
continue;
}
if (typeof mostRecentCluster.meta.played_at !== 'string' && typeof cluster.meta.played_at === 'string') {
mostRecentCluster = cluster;
continue;
}
if (typeof mostRecentCluster.meta.played_at === 'string' && typeof cluster.meta.played_at === 'string') {
const playedAt = new Date(mostRecentCluster.meta.played_at);
const clusterPlayedAt = new Date(cluster.meta.played_at);
if (clusterPlayedAt > playedAt)
mostRecentCluster = cluster;
}
}
setCluster(mostRecentCluster);
});
return (
<div class="relative h-52 min-h-52 w-full overflow-hidden rounded-xl">
<ClusterCover
@ -241,12 +128,12 @@ function Banner(props: BannerProps) {
<strong>
{cluster()!.meta.mc_version}
{' '}
{upperFirst(cluster()!.meta.loader) || 'Unknown'}
{upperFirst(cluster()!.meta.loader || 'Unknown')}
</strong>
{' '}
for
{' '}
<strong>{secondsToWords((cluster()!.meta.overall_played || 0n))}</strong>
<strong>{formatAsDuration((cluster()!.meta.overall_played || 0))}</strong>
.
</p>
</Show>
@ -286,8 +173,6 @@ function Banner(props: BannerProps) {
function ClusterCard(props: Cluster) {
const navigate = useNavigate();
// const sortable = createSortable(props.uuid, props);
let ref!: HTMLDivElement;
function openClusterPage(_e: MouseEvent) {
navigate(`/clusters/?id=${props.uuid}`);
@ -299,10 +184,6 @@ function ClusterCard(props: Cluster) {
<>
<div
onClick={e => openClusterPage(e)}
ref={mergeRefs(
ref,
// sortable
)}
class="group relative h-[152px] flex flex-col border border-gray-05 rounded-xl bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
>
<div class="relative flex-1 overflow-hidden rounded-t-xl">
@ -320,11 +201,11 @@ function ClusterCard(props: Cluster) {
<div class="h-8 flex flex-col gap-1.5 overflow-hidden">
<p class="h-4 text-ellipsis whitespace-nowrap font-medium">{props.meta.name}</p>
<p class="h-4 text-xs">
{upperFirst(props.meta.loader)}
{upperFirst(props.meta.loader || 'Unknown')}
{' '}
{props.meta.mc_version}
{/* {' '}
{props.instance.mods && `• ${props.mods} mods`} */}
{props.packages.mods && `• ${props.mods} mods`} */}
</p>
</div>
<Button
@ -349,19 +230,13 @@ interface ClusterGroupProps {
}
function ClusterGroup(props: ClusterGroupProps) {
// const droppable = createDroppable(props.title);
// const ids = createMemo(() => props.clusters.map(cluster => cluster.uuid));
return (
<div
// ref={droppable}
class="flex flex-col gap-y-4"
>
<div class="flex flex-col gap-y-4">
<h4>{props.title}</h4>
<div class="grid grid-cols-4 min-h-38 gap-4 2xl:grid-cols-6">
{/* <SortableProvider ids={ids()}> */}
<For each={props.clusters}>{item => <ClusterCard {...item} />}</For>
{/* </SortableProvider> */}
<For each={props.clusters}>
{item => <ClusterCard {...item} />}
</For>
</div>
</div>
);

View file

@ -1,136 +1,65 @@
import { A } from '@solidjs/router';
import { Download01Icon, FileCode01Icon, FilterLinesIcon, SearchMdIcon } from '@untitled-theme/icons-solid';
import { For, createSignal, onMount } from 'solid-js';
import type { ManagedPackage } from '~bindings';
import { bridge } from '~imports';
import { ChevronRightIcon } from '@untitled-theme/icons-solid';
import { Show } from 'solid-js';
import type { ManagedPackage } from '@onelauncher/client/bindings';
import { BrowserContent } from './BrowserRoot';
import useBrowser from '~ui/hooks/useBrowser';
import OneConfigLogo from '~assets/logos/oneconfig.svg?component-solid';
import Button from '~ui/components/base/Button';
import Dropdown from '~ui/components/base/Dropdown';
import TextField from '~ui/components/base/TextField';
import ModCard, { Provider } from '~ui/components/content/ModCard';
import { tryResult } from '~ui/hooks/useCommand';
import { createSortable } from '~utils';
interface CardProps {
id: string;
name: string;
date_added: number;
date_updated: number;
provider: Provider;
};
interface BrowserFilters {
provider: Provider[]; // TODO: Change with Provider types from Rust
}
import SearchResultsContainer from '~ui/components/content/SearchResults';
function BrowserMain() {
const [_filters, _setFilters] = createSignal<BrowserFilters>({
provider: [Provider.Curseforge],
});
const [modRowData, setModRowData] = createSignal<ModsRowProps[]>([]);
onMount(() => {
Promise.all([
tryResult(bridge.commands.randomMods),
tryResult(bridge.commands.getMod, 'oneconfig'),
tryResult(bridge.commands.getMod, 'chatting'),
tryResult(bridge.commands.getMod, 'patcher'),
]).then((pkgs) => {
const list: ModsRowProps[] = [
{
header: 'Polyfrost',
category: 'polyfrost',
packages: [
pkgs[1],
pkgs[2],
pkgs[3],
],
},
{
header: 'Random Mods',
category: 'random',
packages: pkgs[0],
},
];
setModRowData(list);
});
});
const sortable = createSortable<CardProps>([], {
'A-Z': (a, b) => a.name.localeCompare(b.name),
'Z-A': (a, b) => b.name.localeCompare(a.name),
'Last Updated': (a, b) => a.date_updated - b.date_updated,
'New': (a, b) => b.date_added - a.date_added,
});
const browser = useBrowser();
return (
<div class="relative h-full flex flex-1 flex-col gap-2">
<div class="sticky top-0 z-10 flex flex-row justify-between bg-page">
<div class="h-8 flex flex-row gap-2">
<BrowserContent>
<div class="flex flex-col gap-8">
<Show when={browser.cache() !== undefined && browser.featured() !== undefined}>
<Featured package={browser.featured()!} />
<TextField
iconLeft={<SearchMdIcon />}
placeholder="Search for content"
<SearchResultsContainer
header="Modrinth"
category="modrinth"
collapsable
{...browser.cache()!}
/>
<Dropdown.Minimal
onChange={sortable.setKey}
icon={<FilterLinesIcon />}
>
<For each={Object.keys(sortable.sortables)}>
{sortable => (
<Dropdown.Row>{sortable}</Dropdown.Row>
)}
</For>
</Dropdown.Minimal>
</div>
<div class="flex flex-row justify-end gap-2">
<Button
iconLeft={<Download01Icon />}
children="From URL"
/>
<Button
iconLeft={<FileCode01Icon />}
children="From File"
buttonStyle="secondary"
/>
</div>
</Show>
</div>
<div class="flex flex-col gap-4 py-2">
<For each={modRowData()}>
{row => (
<ModsRow {...row} />
)}
</For>
</div>
</div>
</BrowserContent>
);
}
interface ModsRowProps {
header: string;
category: string;
packages: ManagedPackage[];
};
interface FeaturedProps {
package: ManagedPackage;
}
function ModsRow(props: ModsRowProps) {
function Featured(props: FeaturedProps) {
return (
<div class="flex flex-1 flex-col gap-3">
<div class="flex flex-1 flex-row justify-between">
<h4>{props.header}</h4>
<A class="text-fg-secondary active:text-fg-secondary-pressed hover:text-fg-secondary-hover" href={`category?category=${props.category}`}>See all</A>
</div>
<div class="flex flex-col gap-y-1">
<h5 class="ml-2">Featured</h5>
<div class="w-full flex flex-row overflow-hidden rounded-lg bg-page-elevated">
<div class="w-full p-1">
<img class="aspect-ratio-video w-full rounded-md object-cover object-center" src="" alt="" />
</div>
<div class="max-w-84 min-w-52 flex flex-col gap-y-1 p-4">
<h2>{props.package.title}</h2>
<div class="max-w-full flex flex-row flex-wrap gap-2">
<For each={props.packages}>
{mod => <ModCard {...mod} />}
</For>
</div>
<div class="w-fit flex flex-row items-center gap-x-1 rounded-lg bg-gray-10 px-1.5 py-1 text-fg-primary transition hover:opacity-80">
<OneConfigLogo class="h-3.5 w-3.5" />
<span class="text-sm font-medium">OneConfig Integrated</span>
</div>
<p class="mt-1 flex-1">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Aliquid, veniam odio. Animi quia corporis id fugiat, libero commodi. Repudiandae repellat placeat sed tempora molestias id consequuntur, corrupti ullam mollitia amet?</p>
<div class="flex flex-row justify-end">
<Button
buttonStyle="ghost"
iconRight={<ChevronRightIcon />}
children="View Package"
onClick={() => { }}
/>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,97 +0,0 @@
import { type Params, useSearchParams } from '@solidjs/router';
import { Download01Icon } from '@untitled-theme/icons-solid';
import { onMount } from 'solid-js';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import ModCard from '~ui/components/content/ModCard';
import useCommand from '~ui/hooks/useCommand';
interface BrowserModParams extends Params {
id: string;
provider: string;
}
// TODO: Refactor the entire browser page and subpages
function BrowserMod() {
const [params] = useSearchParams<BrowserModParams>();
// const _isInvalid = !params.id || !params.provider;
// const [cluster, setCluster] = createSignal<string>();
// const [clusters, setClusters] = createSignal<[string, string][]>();
const [pkg] = useCommand(bridge.commands.getMod, params.id!);
function installTo() {
// setVisible(true);
}
// function download() {
// setVisible(false);
// tryResult(bridge.commands.downloadMod, cluster()!, params.id!).then().catch(err => console.error(err));
// }
// function onChange(selected: number) {
// setCluster(clusters()![selected]![1]);
// }
onMount(() => {
// tryResult(bridge.commands.getClusters).then((res) => {
// const list: [string, string][] = res.map(cluster => [cluster.meta.name, cluster.uuid]);
// setClusters(list);
// setCluster(clusters()![0]![1]);
// });
});
return (
<>
<div class="flex flex-row gap-x-12">
<ModCard {...pkg()!} />
<div class="flex flex-1 flex-col items-start justify-between rounded-lg bg-component-bg p-4 px-6">
<div class="w-full flex flex-row items-center justify-between">
<h1>{pkg()?.title}</h1>
<Button
buttonStyle="primary"
iconLeft={<Download01Icon />}
children="Install to..."
onClick={installTo}
/>
</div>
<p>TO DO</p>
</div>
</div>
</>
);
}
// function InstallToCluster(props: CreateModal) {
// return (
// <Modal.Simple
// {...props}
// title="Install To..."
// buttons={[
// <Button
// buttonStyle="secondary"
// children="Cancel"
// onClick={() => props.hide()}
// />,
// <Button
// buttonStyle="primary"
// children="Download"
// onClick={download}
// />,
// ]}
// >
// <Dropdown onChange={onChange}>
// <For each={clusters()}>
// {cluster => (
// <Dropdown.Row>{cluster[0]}</Dropdown.Row>
// )}
// </For>
// </Dropdown>
// </Modal.Simple>
// );
// }
BrowserMod.getUrl = function (params: BrowserModParams): string {
return `/browser/mod?id=${params.id}&provider=${params.provider}`;
};
export default BrowserMod;

View file

@ -0,0 +1,361 @@
import { For, type ParentProps, Show, createContext, createEffect, createSignal, useContext } from 'solid-js';
import { A, type Params, Route, useSearchParams } from '@solidjs/router';
import { CalendarIcon, ChevronDownIcon, ClockRewindIcon, Download01Icon, File02Icon, HeartIcon, LinkExternal01Icon } from '@untitled-theme/icons-solid';
import type { Cluster, ManagedPackage, ManagedUser, Providers } from '@onelauncher/client/bindings';
import { getLicenseUrl, getPackageUrl } from '../../../utils';
import { bridge } from '~imports';
import useCommand from '~ui/hooks/useCommand';
import { abbreviateNumber, formatAsRelative } from '~utils';
import Tooltip from '~ui/components/base/Tooltip';
import Markdown from '~ui/components/content/Markdown';
import Button from '~ui/components/base/Button';
import Dropdown from '~ui/components/base/Dropdown';
import useBrowser from '~ui/hooks/useBrowser';
import SteveHead from '~assets/images/steve.png';
import usePromptOpener from '~ui/hooks/usePromptOpener';
import Link from '~ui/components/base/Link';
interface BrowserModParams extends Params {
id: string;
provider: Providers;
}
const BrowserPackageContext = createContext<ManagedPackage | null>(null);
const basePath = '/browser/package';
function BrowserPackageProvider(props: ParentProps & { pkg: ManagedPackage }) {
return (
// eslint-disable-next-line solid/reactivity -- Should be fine
<BrowserPackageContext.Provider value={props.pkg}>
{props.children}
</BrowserPackageContext.Provider>
);
}
function BrowserPackageRoutes() {
return (
<>
<Route path="/" component={BrowserPackageBody} />
<Route path="/gallery" component={BrowserPackageGallery} />
<Route path="/versions" component={BrowserPackageVersions} />
</>
);
}
function NavLink(props: ParentProps & { href: string }) {
const [params] = useSearchParams<BrowserModParams>();
const url = () => {
const searchParams = new URLSearchParams(params as Record<string, string>);
return `${basePath}${props.href}?${searchParams.toString()}`;
};
return (
<A
class="rounded-md px-4 py-2 text-sm text-fg-primary font-semibold uppercase active:bg-component-bg-pressed hover:bg-component-bg-hover"
activeClass="bg-component-bg-pressed hover:bg-component-bg-pressed"
href={url()}
children={props.children}
/>
);
}
function BrowserPackage(props: ParentProps) {
const [params] = useSearchParams<BrowserModParams>();
const [pkg] = useCommand(() => bridge.commands.getProviderPackage(params.provider!, params.id!));
const [authors] = useCommand(pkg, () => bridge.commands.getProviderAuthors(pkg()!.provider, pkg()!.author));
const links = [
['About', '/'],
['Gallery', '/gallery'],
['Versions', '/versions'],
];
return (
<>
<div class="flex flex-row items-start gap-x-4 pb-8">
<Show
when={pkg() !== undefined && authors() !== undefined}
fallback={<div>Loading...</div>}
children={(
<>
<BrowserSidebar package={pkg()!} authors={authors()!} />
<div class="flex flex-1 flex-col items-start justify-between gap-y-4">
<div class="flex flex-row gap-x-1 rounded-lg bg-component-bg p-1">
<For each={links}>
{link => (
<NavLink href={link[1] || ''}>
{link[0]}
</NavLink>
)}
</For>
</div>
<BrowserPackageProvider pkg={pkg()!}>
{/* <AnimatedRoutes> */}
{props.children}
{/* </AnimatedRoutes> */}
</BrowserPackageProvider>
</div>
</>
)}
/>
</div>
</>
);
}
BrowserPackage.buildUrl = function (params: BrowserModParams): string {
return `${basePath}/?id=${params.id}&provider=${params.provider}`;
};
BrowserPackage.Routes = BrowserPackageRoutes;
export default BrowserPackage;
function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[] }) {
const createdAt = () => new Date(props.package.created);
const updatedAt = () => new Date(props.package.updated);
const promptOpen = usePromptOpener();
return (
<div class="sticky top-0 max-w-60 min-w-54 flex flex-col gap-y-4">
<div class="min-h-72 flex flex-col overflow-hidden rounded-lg bg-component-bg">
<div class="relative h-28 flex items-center justify-center overflow-hidden">
<img class="absolute z-0 max-w-none w-7/6 filter-blur-xl" src={props.package.icon_url || ''} alt={`Icon for ${props.package.title}`} />
<img class="relative z-1 aspect-ratio-square w-2/5 rounded-md image-render-auto" src={props.package.icon_url || ''} alt={`Icon for ${props.package.title}`} />
</div>
<div class="flex flex-1 flex-col gap-2 p-3">
<div class="flex flex-col gap-2">
<h4 class="text-fg-primary font-medium">{props.package.title}</h4>
<p class="text-xs text-fg-secondary">
<span class="text-fg-primary capitalize">{props.package.package_type}</span>
{' '}
on
{' '}
<span class="text-fg-primary">{props.package.provider}</span>
</p>
</div>
<p class="max-h-22 flex-1 overflow-hidden text-sm text-fg-secondary line-height-snug">{props.package.description}</p>
<div class="flex flex-row gap-4 text-xs">
<div class="flex flex-row items-center gap-2">
<Download01Icon class="h-4 w-4" />
{abbreviateNumber(props.package.downloads)}
</div>
<div class="flex flex-row items-center gap-2">
<HeartIcon class="h-4 w-4" />
{abbreviateNumber(props.package.followers)}
</div>
</div>
</div>
</div>
<InstallButton {...props.package} />
<div class="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
<h4 class="text-fg-primary font-bold">Links</h4>
<Link includeIcon href={getPackageUrl(props.package.provider, props.package.id, props.package.package_type)}>
{props.package.provider}
{' '}
Page
</Link>
</div>
<div class="flex flex-col gap-2 rounded-lg bg-component-bg p-3">
<h4 class="text-fg-primary font-bold">Authors</h4>
<For each={props.authors}>
{author => (
<div
class="flex flex-row items-center gap-x-1 rounded-md p-1 active:bg-component-bg-pressed hover:bg-component-bg-hover"
onClick={() => promptOpen(author.url)}
>
<img class="h-8 min-h-8 min-w-8 w-8 rounded-md" src={author.avatar_url || SteveHead} alt={`${author.username}'s avatar`} />
<div class="flex flex-1 flex-col justify-center gap-y-1">
<span>{author.username}</span>
<Show when={author.is_organization_user}>
<span class="text-xs text-fg-secondary">Organization</span>
</Show>
<Show when={author.role !== null}>
<span class="text-xs text-fg-secondary">{author.role}</span>
</Show>
</div>
<LinkExternal01Icon class="h-4 w-4" />
</div>
)}
</For>
</div>
<div class="flex flex-col gap-2 rounded-lg bg-component-bg p-3 text-xs!">
<h4 class="text-fg-primary font-bold">Details</h4>
<Show when={props.package.license !== null}>
<div class="flex flex-row items-start gap-x-1">
<File02Icon class="h-3 min-w-3 w-3" />
License
<Link includeIcon href={getLicenseUrl(props.package.license)}>
{props.package.license?.name || props.package.license?.id || 'Unknown'}
</Link>
</div>
</Show>
<Tooltip text={createdAt().toLocaleString()}>
<div class="flex flex-row items-center gap-x-1">
<CalendarIcon class="h-3 min-w-3 w-3" />
Created
<span class="text-fg-primary font-medium">
{formatAsRelative(createdAt().getTime(), 'en', 'long')}
</span>
</div>
</Tooltip>
<Tooltip text={updatedAt().toLocaleString()}>
<div class="flex flex-row items-center gap-x-1">
<ClockRewindIcon class="h-3 min-w-3 w-3" />
Last Updated
<span class="text-fg-primary font-medium">
{formatAsRelative(updatedAt().getTime(), 'en', 'long')}
</span>
</div>
</Tooltip>
</div>
</div>
);
}
function InstallButton(props: ManagedPackage) {
const [selected, setSelected] = createSignal(0);
const [clusters] = useCommand(() => bridge.commands.getClusters());
const controller = useBrowser();
const meetsRequirements = (cluster: Cluster) => {
const game_version = props.game_versions.includes(cluster.meta.mc_version);
const loader = props.loaders.includes(cluster.meta.loader || 'vanilla');
return game_version && loader;
};
const filtered = () => clusters()?.filter(meetsRequirements);
const getSelectedCluster = () => filtered()?.[selected()];
async function download() {
const cluster = getSelectedCluster();
if (cluster === undefined)
return;
// TODO: Add a progress bar
try {
await bridge.commands.downloadProviderPackage(
props.provider,
props.id,
cluster.uuid,
cluster.meta.mc_version,
cluster.meta.loader || null,
null, // TODO: Specific package version
);
await bridge.commands.syncClusterPackages(cluster.path || '');
}
catch (err) {
console.error(err);
}
}
createEffect(() => {
if (filtered() === undefined)
return;
const cluster = controller.cluster();
if (cluster !== undefined && meetsRequirements(cluster)) {
const index = filtered()!.findIndex(c => c.uuid === cluster.uuid);
if (index !== -1)
setSelected(index);
}
});
return (
<div class="h-12 flex flex-row">
<Button
buttonStyle="primary"
iconLeft={<Download01Icon />}
class="max-w-full flex-1 rounded-r-none!"
disabled={filtered()?.length === 0}
onClick={download}
children={(
<div class="flex flex-1 flex-col items-center justify-center">
<p class="text-xs">Download latest to</p>
<span class="mt-0.5 h-3.5 max-w-38 overflow-x-hidden text-sm font-bold">{filtered?.()?.[selected?.()]?.meta.name || 'Unknown'}</span>
</div>
)}
/>
<Dropdown
class="w-8"
dropdownClass="w-58! right-0"
disabled={filtered()?.length === 0}
selected={selected}
onChange={setSelected}
component={props => (
<Button
buttonStyle="primary"
iconLeft={<ChevronDownIcon />}
class="h-full w-full border-l border-white/5 rounded-l-none! px-0!"
onClick={() => props.setVisible(true)}
/>
)}
>
<For each={filtered()}>
{cluster => (
<Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
)}
</For>
</Dropdown>
</div>
);
}
// Sub pages
function BrowserPackageBody() {
const context = useContext(BrowserPackageContext);
return (
<div class="w-full flex-1 rounded-lg bg-component-bg p-4 px-6">
<Markdown body={context?.body || ''} />
</div>
);
}
function BrowserPackageGallery() {
// const context = useContext(BrowserPackageContext);
return (
<div class="w-full flex-1 rounded-lg bg-component-bg p-4 px-6">
<div>Gallery</div>
{/* <For each={[]}>
{image => (
<img src={image.url} alt={image.alt} />
)}
</For> */}
</div>
);
}
function BrowserPackageVersions() {
// const context = useContext(BrowserPackageContext);
return (
<div class="w-full flex-1 rounded-lg bg-component-bg p-4 px-6">
<div>versions</div>
</div>
);
}

View file

@ -1,21 +1,140 @@
import { Route } from '@solidjs/router';
import type { ParentProps } from 'solid-js';
import { For, type ParentProps } from 'solid-js';
import { SearchMdIcon, Settings01Icon } from '@untitled-theme/icons-solid';
import type { PackageType } from '@onelauncher/client/bindings';
import BrowserMain from './BrowserMain';
import BrowserCategory from './BrowserCategory';
import BrowserMod from './BrowserMod';
import BrowserPackage from './BrowserPackage';
import Button from '~ui/components/base/Button';
import TextField from '~ui/components/base/TextField';
import useBrowser from '~ui/hooks/useBrowser';
import { browserCategories } from '~utils/browser';
function BrowserRoutes() {
return (
<>
<Route path="/" component={BrowserMain} />
<Route path="/category" component={BrowserCategory} />
<Route path="/mod" component={BrowserMod} />
<Route path="/package" component={BrowserPackage} children={<BrowserPackage.Routes />} />
</>
);
}
function BrowserRoot(props: ParentProps) {
return <>{props.children}</>;
return (
<>{props.children}</>
);
}
function BrowserToolbar() {
// const controller = useBrowser();
return (
<div class="w-full flex flex-row justify-between bg-page">
<div class="flex flex-row gap-2">
{/* <Button
buttonStyle="secondary"
children={controller.cluster()?.meta.name || 'None'}
iconLeft={<Settings01Icon />}
onClick={controller.displayClusterSelector}
/> */}
</div>
<div class="flex flex-row gap-2">
<TextField
iconLeft={<SearchMdIcon />}
placeholder="Search for content"
/>
</div>
</div>
);
}
export interface BrowserSidebarCategory {
name: string;
sub: [string, string][];
};
function BrowserCategories(props: { categories: BrowserSidebarCategory[] }) {
return (
<div class="top-0 grid grid-cols-[1fr_auto] h-fit min-w-50 gap-y-6">
<div />
<div class="flex flex-col gap-y-6">
<For each={props.categories.concat(browserCategories.provider())}>
{category => (
<div class="flex flex-col gap-y-2">
<h6 class="my-1">{category.name}</h6>
<For each={category.sub}>
{sub => (
<a
href={sub[1]}
class="text-md text-fg-primary capitalize hover:text-fg-secondary"
children={sub[0]}
/>
)}
</For>
</div>
)}
</For>
</div>
</div>
);
}
function BrowserSidebar() {
const controller = useBrowser();
return (
<div class="flex flex-col gap-y-4">
<div class="flex flex-col gap-y-1">
<h6 class="my-1">Active Cluster</h6>
<Button
buttonStyle="secondary"
children={controller.cluster()?.meta.name || 'None'}
iconLeft={<Settings01Icon />}
onClick={controller.displayClusterSelector}
/>
</div>
<div class="flex flex-col gap-y-1">
<h6 class="my-1">Search Filters</h6>
</div>
</div>
);
}
export type BrowserContentProps = ParentProps & {
categories?: BrowserSidebarCategory[];
};
const categories = (packageType: PackageType) => browserCategories.byPackageType(packageType);
export function BrowserContent(props: BrowserContentProps) {
const browser = useBrowser();
return (
<div class="relative h-full flex flex-1 flex-col items-center gap-2">
<div class="h-full w-full max-w-screen-xl flex flex-1 flex-col items-center gap-y-2">
<div class="grid grid-cols-[220px_auto_220px] w-full gap-x-6">
<div />
<BrowserToolbar />
<div />
</div>
<div class="grid grid-cols-[220px_auto_220px] w-full max-w-screen-xl gap-x-6 pb-8">
<BrowserCategories categories={categories(browser.packageType()).concat(props.categories || [])} />
<div class="h-full flex flex-col gap-y-4">
<div class="h-full flex-1">
{props.children}
</div>
</div>
<BrowserSidebar />
</div>
</div>
</div>
);
}
BrowserRoot.Routes = BrowserRoutes;

View file

@ -3,6 +3,7 @@ import { createSignal, onCleanup, onMount } from 'solid-js';
import type { UnlistenFn } from '@tauri-apps/api/event';
import { SlashOctagonIcon } from '@untitled-theme/icons-solid';
import { render } from 'solid-js/web';
import type { DetailedProcess } from '@onelauncher/client/bindings';
import { bridge } from '~imports';
import Sidebar from '~ui/components/Sidebar';
import useClusterContext from '~ui/hooks/useCluster';
@ -11,7 +12,6 @@ import Tooltip from '~ui/components/base/Tooltip';
import FormattedLog, { Line } from '~ui/components/content/FormattedLog';
import Button from '~ui/components/base/Button';
import Modal, { createModal } from '~ui/components/overlay/Modal';
import type { DetailedProcess } from '~bindings';
import useCommand from '~ui/hooks/useCommand';
interface ClusterGameParams extends Params {
@ -24,7 +24,7 @@ interface ClusterGameParams extends Params {
function ClusterGame() {
const [cluster] = useClusterContext();
const [params] = useSearchParams<ClusterGameParams>();
const [log] = useCommand(bridge.commands.getClusterLog, cluster()!.uuid, 'latest.log');
const [log] = useCommand(() => bridge.commands.getClusterLog(cluster()!.uuid, 'latest.log'));
const [isRunning, setIsRunning] = createSignal(true);
const [unlisten, setUnlisten] = createSignal<UnlistenFn>();

View file

@ -7,19 +7,19 @@ import { bridge } from '~imports';
import useClusterContext from '~ui/hooks/useCluster';
import Dropdown from '~ui/components/base/Dropdown';
import Button from '~ui/components/base/Button';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
import FormattedLog from '~ui/components/content/FormattedLog';
function ClusterLogs() {
const [cluster] = useClusterContext();
const [logs] = useCommand(bridge.commands.getClusterLogs, cluster()!.uuid);
const { settings } = useSettingsContext();
const [logs] = useCommand(() => bridge.commands.getClusterLogs(cluster()!.uuid));
const { settings } = useSettings();
const [activeLogFile, setActiveLogFile] = createSignal<string | null>(null);
const [logContent, setLogContent] = createSignal<string | null>(null);
function getAndSetLog(log_name: string) {
tryResult(bridge.commands.getClusterLog, cluster()!.uuid, log_name).then(setLogContent);
tryResult(() => bridge.commands.getClusterLog(cluster()!.uuid, log_name)).then(setLogContent);
}
function changeLog(index: number) {
@ -45,7 +45,7 @@ function ClusterLogs() {
if (log === null)
return;
const id = await tryResult(bridge.commands.uploadLog, cluster()!.uuid, log);
const id = await tryResult(() => bridge.commands.uploadLog(cluster()!.uuid, log));
open(`https://mclo.gs/${id}`);
}

View file

@ -1,47 +1,35 @@
import { ArrowRightIcon, ChevronDownIcon, ChevronUpIcon, Edit02Icon, SearchMdIcon, Trash03Icon } from '@untitled-theme/icons-solid';
import { For, Match, Switch, createSignal, onMount } from 'solid-js';
import * as uuid from 'uuid';
import { Edit02Icon, FilterFunnel01Icon, SearchMdIcon, Trash03Icon } from '@untitled-theme/icons-solid';
import { For, Match, Switch, createEffect, createSignal } from 'solid-js';
import UFuzzy from '@leeoniya/ufuzzy';
import type { Package } from '@onelauncher/client/bindings';
import Button from '~ui/components/base/Button';
import TextField from '~ui/components/base/TextField';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
const randomModNames = [
'Sodium',
'Nvidium',
'Iris Shaders',
'Create',
'Fabric API',
'Create: Estrogen',
'OneConfig',
'PolySprint',
'PolyEffects',
];
const _mods: readonly ModEntryProps[] = Array(randomModNames.length).fill(undefined).map((_, index) => ({
id: uuid.v4(),
name: randomModNames[index] || 'Unknown',
author: 'Author Name',
version: '1.0.0',
description: 'This is a mod description',
provider: 'curseforge',
thumbnail: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
}));
import useClusterContext from '~ui/hooks/useCluster';
import useCommand from '~ui/hooks/useCommand';
import { bridge } from '~imports';
// TODO: Possibly optimise this as it has 2 cloned lists, and another containing only the names
function ClusterMods() {
// Initial mods for this cluster
const [mods] = createSignal<ModEntryProps[]>([..._mods]); // TODO: Replace with hook
const [cluster] = useClusterContext();
const [mods] = useCommand(cluster, () => bridge.commands.getClusterPackages(cluster()?.path || '', 'mod'));
// Mods to display, should be a clone of the mods array but sorted
const [displayedMods, setDisplayedMods] = createSignal<ModEntryProps[]>([]);
const [displayedMods, setDisplayedMods] = createSignal<Package[]>([]);
// true - `A to Z` & false - `Z to A`
const [sortingAtoZ, setSortingAtoZ] = createSignal<boolean>(true);
function getModName(mod: Package) {
if (mod.meta.type === 'unknown')
return mod.file_name;
return mod.meta.title || mod.file_name;
}
const uf = new UFuzzy();
const [modsSearchable] = createSignal(() => mods().map(mod => mod.name));
const [modsSearchable] = createSignal(() => mods()?.map(getModName) || []);
function search(value: string) {
if (value === '' || value === undefined) {
@ -51,9 +39,9 @@ function ClusterMods() {
const result = uf.search(modsSearchable()(), value);
const filtered: ModEntryProps[] = [];
const filtered: Package[] = [];
result[0]?.forEach((index) => {
const mod = mods()[index];
const mod = mods()?.[index];
if (mod)
filtered.push(mod);
});
@ -61,11 +49,11 @@ function ClusterMods() {
setDisplayedMods(sortListByName(filtered));
}
function sortListByName(list: ModEntryProps[]): ModEntryProps[] {
function sortListByName(list: Package[]): Package[] {
if (sortingAtoZ())
return list.sort((a, b) => a.name.localeCompare(b.name));
return list.sort((a, b) => getModName(a).localeCompare(getModName(b)));
else
return list.sort((a, b) => b.name.localeCompare(a.name));
return list.sort((a, b) => getModName(b).localeCompare(getModName(a)));
}
function toggleNameSort() {
@ -74,10 +62,10 @@ function ClusterMods() {
}
function resetMods() {
setDisplayedMods(sortListByName([...mods()]));
setDisplayedMods(sortListByName([...(mods() || [])]));
}
onMount(() => {
createEffect(() => {
resetMods();
});
@ -88,9 +76,9 @@ function ClusterMods() {
<div class="flex flex-row items-center justify-end gap-x-2">
<Button
buttonStyle="secondary"
iconLeft={sortingAtoZ() ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => toggleNameSort()}
children="Name"
children={sortingAtoZ() ? 'A-Z' : 'Z-A'}
iconLeft={<FilterFunnel01Icon />}
/>
<TextField iconLeft={<SearchMdIcon />} placeholder="Search..." onInput={e => search(e.target.value)} />
@ -102,7 +90,7 @@ function ClusterMods() {
<Match when={displayedMods().length > 0}>
<For each={displayedMods()}>
{mod => (
<ModEntry {...mod} />
<ModEntry pkg={mod} />
)}
</For>
</Match>
@ -118,34 +106,63 @@ function ClusterMods() {
export default ClusterMods;
// TODO: Mod
interface ModEntryProps {
id: string;
thumbnail: string;
name: string;
author: string;
version: string;
description: string;
provider: 'curseforge' | 'modrinth' | 'polyfrost';
pkg: Package;
};
function icon() {
return '';
}
function ModEntry(props: ModEntryProps) {
const name = () => {
if (props.pkg.meta.type === 'unknown')
return props.pkg.file_name;
return props.pkg.meta.title || props.pkg.file_name;
};
const version = () => {
let formatted = 'Unknown';
if (props.pkg.meta.type === 'managed')
formatted = props.pkg.meta.version_formatted;
else if (props.pkg.meta.type === 'mapped')
formatted = props.pkg.meta.version || formatted;
return formatted;
};
const tag = () => {
let tag = 'Unknown';
if (props.pkg.meta.type === 'managed')
tag = props.pkg.meta.provider;
else if (props.pkg.meta.type === 'mapped')
tag = 'Mapped';
return tag;
};
return (
<div class="flex flex-row items-center gap-3 rounded-xl bg-component-bg p-3 active:bg-component-bg-pressed hover:bg-component-bg-hover">
<div>
<img src={props.thumbnail} alt={props.name} class="aspect-ratio-square h-10 rounded-lg" />
<img src={icon()} alt={name()} class="aspect-ratio-square h-10 rounded-lg" />
</div>
<div class="flex flex-1 flex-col">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col items-start justify-center">
<div class="flex flex-col items-start justify-center gap-y-2">
<h4>{props.name}</h4>
<span class="flex flex-row items-center justify-start gap-x-1">
<h4>{name()}</h4>
<span class="rounded-xl bg-gray-05 px-1.5 py-0.5 text-xs text-white/60">{tag()}</span>
</span>
<span class="h-2 flex flex-row items-center justify-start text-xs text-fg-secondary/50 font-600">
{props.version}
{version()}
{/* TODO: Add version checker */}
{/* <Show when={props.version.includes()}> */}
<ArrowRightIcon class="w-4 stroke-success" />
<span class="text-success">1.0.1</span>
{/* <ArrowRightIcon class="w-4 stroke-success" />
<span class="text-success">1.0.1</span> */}
{/* </Show> */}
</span>
</div>

View file

@ -1,26 +1,26 @@
import { Edit02Icon, FolderIcon, ImagePlusIcon, LinkExternal01Icon, PlayIcon, Save01Icon, Share07Icon, Trash01Icon } from '@untitled-theme/icons-solid';
import { type Accessor, type Setter, Show, createSignal, untrack } from 'solid-js';
import { useBeforeLeave, useNavigate } from '@solidjs/router';
import { useNavigate } from '@solidjs/router';
import * as dialog from '@tauri-apps/plugin-dialog';
import { open } from '@tauri-apps/plugin-shell';
import { join } from 'pathe';
import type { Cluster } from '@onelauncher/client/bindings';
import ClusterCover from '../../components/game/ClusterCover';
import LoaderIcon from '../../components/game/LoaderIcon';
import Button from '../../components/base/Button';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import type { Cluster } from '~bindings';
import { secondsToWords, upperFirst } from '~utils';
import { formatAsDuration, upperFirst } from '~utils';
import useClusterContext, { useLaunchCluster } from '~ui/hooks/useCluster';
import { bridge } from '~imports';
import TextField from '~ui/components/base/TextField';
import SettingsRow from '~ui/components/SettingsRow';
import useSettingsContext from '~ui/hooks/useSettings';
import type { ModalProps } from '~ui/components/overlay/Modal';
import Modal, { createModal } from '~ui/components/overlay/Modal';
import useSettings from '~ui/hooks/useSettings';
import Modal, { type ModalProps, createModal } from '~ui/components/overlay/Modal';
import usePreventLeave from '~ui/hooks/usePreventLeave';
function ClusterOverview() {
const { settings } = useSettingsContext();
const { settings } = useSettings();
const [cluster, { refetch }] = useClusterContext();
const [editMode, setEditMode] = createSignal(false);
@ -30,20 +30,20 @@ function ClusterOverview() {
const navigate = useNavigate();
const saveModal = createModal(props => (
<SaveModal {...props} save={save} dontSave={dontSave} />
<SaveModal
{...props}
save={save}
dontSave={dontSave}
/>
));
const deleteModal = createModal(props => (
<Modal.Delete {...props} onDelete={deleteCluster} />
<Modal.Delete
{...props}
onDelete={deleteCluster}
/>
));
useBeforeLeave((e) => {
if (editMode() && madeChanges()) {
e.preventDefault();
saveModal.show();
}
});
function getPath() {
const clusterPath = cluster()?.path;
const configDir = settings().config_dir;
@ -103,15 +103,28 @@ function ClusterOverview() {
refetch();
}
function dontSave() {
const preventLeave = usePreventLeave((ctx) => {
if (editMode() && madeChanges()) {
ctx.preventNavigation();
saveModal.show();
}
});
async function dontSave() {
saveModal.hide();
setEditMode(false);
if (preventLeave.triedNavigating())
preventLeave.continue();
}
function save() {
pushEdits();
async function save() {
await pushEdits();
saveModal.hide();
setEditMode(false);
if (preventLeave.triedNavigating())
preventLeave.continue();
}
return (
@ -263,7 +276,7 @@ function Banner(props: BannerProps) {
<Show when={props.editMode()}>
<div
onClick={launchFilePicker}
class="absolute h-full w-full flex items-center justify-center bg-black/50 opacity-50 hover:opacity-100"
class="absolute z-1 h-full w-full flex items-center justify-center bg-black/50 opacity-50 hover:opacity-100"
>
<ImagePlusIcon class="h-12 w-12" />
</div>
@ -304,9 +317,9 @@ function Banner(props: BannerProps) {
'opacity-50': props.editMode(),
}}
/>
<span>{props.cluster.meta.mc_version}</span>
<span>{upperFirst(props.cluster.meta.loader || 'unknown')}</span>
{props.cluster.meta.loader_version && <span>{props.cluster.meta.loader_version.id}</span>}
<span>{props.cluster.meta.mc_version}</span>
</span>
<span
class="text-xs text-fg-secondary"
@ -316,13 +329,12 @@ function Banner(props: BannerProps) {
>
Played for
{' '}
<b>{secondsToWords((props.cluster.meta.overall_played || 0n))}</b>
<b>{formatAsDuration((props.cluster.meta.overall_played || 0))}</b>
.
</span>
</div>
<div class="flex flex-row items-end gap-x-2.5 *:h-8">
<Button
buttonStyle="iconSecondary"
children={<Share07Icon />}

View file

@ -77,7 +77,7 @@ function ClusterSidebar() {
if (!c || typeof c.path !== 'string')
return [];
return tryResult(bridge.commands.getProcessesDetailedByPath, c.path);
return tryResult(() => bridge.commands.getProcessesDetailedByPath(c.path!));
});
const isRouting = useIsRouting();

View file

@ -9,12 +9,12 @@ import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import useClusterContext from '~ui/hooks/useCluster';
import useCommand from '~ui/hooks/useCommand';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
function ClusterScreenshots() {
const { settings } = useSettingsContext();
const { settings } = useSettings();
const [cluster] = useClusterContext();
const [list] = useCommand(bridge.commands.getScreenshots, cluster()!.uuid!);
const [list] = useCommand(() => bridge.commands.getScreenshots(cluster()!.uuid!));
function openFolder() {
open(join(settings().config_dir || '', 'clusters', cluster()?.path || '', 'screenshots'));
@ -53,7 +53,7 @@ function ClusterScreenshots() {
export default ClusterScreenshots;
function ScreenshotEntry(props: { path: string; cluster_path: string }) {
const { settings } = useSettingsContext();
const { settings } = useSettings();
const dir = () => join(settings().config_dir || '', 'clusters', props.cluster_path, 'screenshots');
const pathSrc = () => convertFileSrc(join(dir(), props.path));

View file

@ -1,11 +1,11 @@
import { useBeforeLeave } from '@solidjs/router';
import { type Accessor, Show } from 'solid-js';
import type { Cluster } from '@onelauncher/client/bindings';
import { GameSettings, JvmSettings, LauncherSettings, ProcessSettings, createSetting } from '../settings/game/SettingsMinecraft';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import useClusterContext from '~ui/hooks/useCluster';
import type { Cluster } from '~bindings';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
import { tryResult } from '~ui/hooks/useCommand';
import { bridge } from '~imports';
@ -25,7 +25,7 @@ function ClusterSettings() {
}
function PageSettings(cluster: Accessor<Cluster>) {
const { settings } = useSettingsContext();
const { settings } = useSettings();
// Game
const fullscreen = createSetting(cluster().force_fullscreen, settings().force_fullscreen ?? false);
@ -44,7 +44,7 @@ function PageSettings(cluster: Accessor<Cluster>) {
const envVars = createSetting(cluster().java?.custom_env_arguments, settings().custom_env_args);
useBeforeLeave(() => {
tryResult(bridge.commands.editGameSettings, cluster().uuid, {
tryResult(() => bridge.commands.editGameSettings(cluster().uuid, {
...cluster(),
// Game
@ -64,7 +64,7 @@ function PageSettings(cluster: Accessor<Cluster>) {
custom_arguments: javaArgs.getRaw(),
custom_env_arguments: envVars.getRaw(),
},
});
}));
});
return (

View file

@ -10,12 +10,12 @@ import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
import useClusterContext from '~ui/hooks/useCluster';
import useCommand from '~ui/hooks/useCommand';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
function ClusterWorlds() {
const { settings } = useSettingsContext();
const { settings } = useSettings();
const [cluster] = useClusterContext();
const [list] = useCommand(bridge.commands.getWorlds, cluster()!.uuid!);
const [list] = useCommand(() => bridge.commands.getWorlds(cluster()!.uuid));
function openFolder() {
open(join(settings().config_dir || '', 'clusters', cluster()?.path || '', 'saves'));
@ -54,7 +54,7 @@ function ClusterWorlds() {
export default ClusterWorlds;
function WorldEntry(props: { name: string; cluster_path: string }) {
const { settings } = useSettingsContext();
const { settings } = useSettings();
const dir = () => join(settings().config_dir || '', 'clusters', props.cluster_path, 'saves', props.name);

View file

@ -2,6 +2,7 @@ import { type ParentProps, createSignal } from 'solid-js';
import { Brush01Icon, CodeSnippet02Icon, MessageTextSquare01Icon, RefreshCcw02Icon, Rocket02Icon, Sliders04Icon, Users01Icon } from '@untitled-theme/icons-solid';
import { Route } from '@solidjs/router';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { PROGRAM_INFO } from '@onelauncher/client/bindings';
import Sidebar from '../../components/Sidebar';
import SettingsGeneral from './launcher/SettingsGeneral';
import SettingsAppearance from './launcher/SettingsAppearance';
@ -13,7 +14,6 @@ import SettingsChangelog from './about/SettingsChangelog';
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
import ErrorBoundary from '~ui/components/ErrorBoundary';
import Tooltip from '~ui/components/base/Tooltip';
import { PROGRAM_INFO } from '~bindings';
function SettingsRoutes() {
return (

View file

@ -4,10 +4,10 @@ import Toggle from '~ui/components/base/Toggle';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import SettingsRow from '~ui/components/SettingsRow';
import Sidebar from '~ui/components/Sidebar';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
function SettingsDeveloper() {
const { settings } = useSettingsContext();
const { settings } = useSettings();
return (
<Sidebar.Page>

View file

@ -1,12 +1,12 @@
import { ActivityIcon, CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid';
import { type Accessor, type Setter, Show, createSignal, onMount, splitProps, untrack } from 'solid-js';
import type { Memory, Resolution } from '~bindings';
import type { Memory, Resolution } from '@onelauncher/client/bindings';
import TextField from '~ui/components/base/TextField';
import Toggle from '~ui/components/base/Toggle';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import BaseSettingsRow, { type SettingsRowProps } from '~ui/components/SettingsRow';
import Sidebar from '~ui/components/Sidebar';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
import { asEnvVariables } from '~utils';
function SettingsMinecraft() {
@ -48,12 +48,12 @@ export function createSetting<T>(initial: T | undefined | null, fallback?: T): C
checkGlobal();
});
// @ts-expect-error -- Can't be bothered to fix this type issue
const set: Setter<T> = (value) => {
setRaw(value!);
setValue(value!);
const set: Setter<T> = (newValue?: any) => {
setRaw(newValue!);
setValue(newValue!);
checkGlobal();
return value as any;
};
const resetToFallback = (raw?: T) => {
@ -146,6 +146,7 @@ export function GameSettings(props: {
</div>
</SettingsRow>
{/* TODO: make this a memory slider */}
<SettingsRow
title="Memory"
description="The amount of memory in megabytes allocated for the game."
@ -343,7 +344,7 @@ export function JvmSettings(props: {
}
function PageSettings() {
const { settings, saveOnLeave } = useSettingsContext();
const { settings, saveOnLeave } = useSettings();
// Game
const fullscreen = createSetting(settings().force_fullscreen ?? false);

View file

@ -1,15 +1,17 @@
import { ColorsIcon, PaintPourIcon, Speedometer04Icon } from '@untitled-theme/icons-solid';
import { ColorsIcon, PackageIcon, PaintPourIcon, Speedometer04Icon } from '@untitled-theme/icons-solid';
import { useBeforeLeave } from '@solidjs/router';
import { createSignal } from 'solid-js';
import { For, createSignal } from 'solid-js';
import SettingsRow from '../../../components/SettingsRow';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Button from '~ui/components/base/Button';
import Toggle from '~ui/components/base/Toggle';
import Sidebar from '~ui/components/Sidebar';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
import Dropdown from '~ui/components/base/Dropdown';
import { BROWSER_VIEWS } from '~utils/browser';
function SettingsAppearance() {
const { settings, saveOnLeave } = useSettingsContext();
const { settings, saveOnLeave } = useSettings();
const [shouldReload, setShouldReload] = createSignal(false);
useBeforeLeave((e) => {
@ -40,6 +42,23 @@ function SettingsAppearance() {
<Button iconLeft={<ColorsIcon />}>#ff0000</Button>
</SettingsRow>
<SettingsRow
title="Package List Style"
description="Change the look of the package list."
icon={<PackageIcon />}
>
<Dropdown
selected={() => BROWSER_VIEWS.indexOf(settings().browser_list_view ?? 'grid')}
onChange={value => settings().browser_list_view = BROWSER_VIEWS[value] ?? 'grid'}
>
<For each={BROWSER_VIEWS}>
{view => (
<Dropdown.Row>{view}</Dropdown.Row>
)}
</For>
</Dropdown>
</SettingsRow>
<SettingsRow
title="Disable Animations"
description="Disables all animations in the launcher."

View file

@ -4,12 +4,12 @@ import SettingsRow from '../../../components/SettingsRow';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Toggle from '~ui/components/base/Toggle';
import Sidebar from '~ui/components/Sidebar';
import useSettingsContext from '~ui/hooks/useSettings';
import useSettings from '~ui/hooks/useSettings';
import Button from '~ui/components/base/Button';
import DiscordIcon from '~assets/logos/discord.svg?component-solid';
function SettingsGeneral() {
const { settings, saveOnLeave } = useSettingsContext();
const { settings, saveOnLeave } = useSettings();
saveOnLeave(() => ({
disable_discord: settings().disable_discord!,
@ -56,7 +56,6 @@ function SettingsGeneral() {
</SettingsRow>
<SettingsRow.Header>Folders and Files</SettingsRow.Header>
<SettingsRow
title="Launcher Folder"
description={settings().config_dir || 'Unknown'}

View file

@ -0,0 +1,76 @@
import type { BrowserListView, PackageType } from '@onelauncher/client/bindings';
import type { BrowserSidebarCategory } from '~ui/pages/browser/BrowserRoot';
import { LOADERS, PROVIDERS } from '~utils';
export const BROWSER_VIEWS: BrowserListView[] = ['grid', 'list', 'preview'] as const;
export const browserCategories = {
byPackageType(packageType: PackageType): BrowserSidebarCategory[] {
switch (packageType) {
case 'mod':
return this.mod();
case 'datapack':
return this.datapack();
case 'resourcepack':
return this.resourcepack();
case 'shaderpack':
return this.shaderpack();
}
},
mod(): BrowserSidebarCategory[] {
return [
{
name: 'Categories',
sub: [
['Adventure', '/adventure'],
['Magic', '/magic'],
['Technology', '/technology'],
['Utility', '/utility'],
['Worldgen', '/worldgen'],
['Miscellaneous', '/miscellaneous'],
],
},
...this.loader(false),
];
},
datapack(): BrowserSidebarCategory[] {
return [];
},
resourcepack(): BrowserSidebarCategory[] {
return [];
},
shaderpack(): BrowserSidebarCategory[] {
return [];
},
provider(): BrowserSidebarCategory[] {
return [
{
name: 'Providers',
sub: PROVIDERS.map(provider => [provider, `/${provider.toLowerCase()}`]),
},
];
},
loader(vanilla: boolean = false): BrowserSidebarCategory[] {
const loaders: [string, string][] = [];
for (const loader of LOADERS) {
if (vanilla === false && loader === 'vanilla')
continue;
loaders.push([loader, `/${loader.toLowerCase()}`]);
}
return [
{
name: 'Loaders',
sub: loaders,
},
];
},
};

View file

@ -1,8 +1,6 @@
import { open } from '@tauri-apps/plugin-shell';
import type { Cluster, License, Loader, PackageType, Providers, VersionType } from '@onelauncher/client/bindings';
import { DurationFormat } from '@formatjs/intl-durationformat';
import type { Cluster, Loader, VersionType } from '~bindings';
export * from './timer';
export * from './sorting';
export function supportsMods(loader?: Cluster | Loader): boolean {
if (loader === undefined)
@ -25,12 +23,20 @@ export function formatVersionRelease(release: VersionType): string {
return mapping[release];
}
export function upperFirst(object: any): string {
const str = object.toString();
return str.charAt(0).toUpperCase() + str.slice(1);
export function upperFirst<T extends string>(object: T): Capitalize<T> {
const string = object.toString();
return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize<T>;
}
export function abbreviateNumber(n: number, locale: string = 'en-US'): string {
export function uint32ToBigInt([high, low]: [number, number]): bigint {
return (BigInt(high >>> 0) << 32n) | BigInt(low >>> 0);
}
export function int32ToBigInt([high, low]: [number, number]): bigint {
return (BigInt(high | 0) << 32n) | BigInt(low >>> 0);
}
export function abbreviateNumber(n: number | bigint, locale: string = 'en-US'): string {
return new Intl.NumberFormat(locale, {
notation: 'compact',
compactDisplay: 'short',
@ -44,15 +50,21 @@ export function pluralize(n: number, word: string, locale: string = 'en'): strin
return pluralForm === 'one' ? word : `${word}s`;
}
export function secondsToWords(
seconds: number | bigint,
export function formatAsDuration(
seconds: number | bigint | Date,
locale: string = 'en',
style: 'long' | 'short' | 'narrow' | 'digital' = 'long',
): string {
const n = Number(seconds);
let n: number | undefined;
if (seconds instanceof Date)
n = seconds.getTime();
else
n = Number(seconds);
const formatter = new DurationFormat(locale, { style });
return formatter.format({
seconds: n % 60,
const duration = formatter.format({
seconds: Math.floor(n % 60),
minutes: Math.floor(n / 60) % 60,
hours: Math.floor(n / (60 * 60)) % 24,
days: Math.floor(n / (60 * 60 * 24)) % 7,
@ -60,6 +72,43 @@ export function secondsToWords(
months: Math.floor(n / (60 * 60 * 24 * 30)) % 12,
years: Math.floor(n / (60 * 60 * 24 * 365)),
});
if (duration.length === 0)
return 'never';
return duration;
}
export function formatAsRelative(
seconds: number | bigint | Date,
locale: string = 'en',
style: 'long' | 'short' | 'narrow' | 'digital' = 'long',
): string {
let elapsed: number | undefined;
if (seconds instanceof Date)
elapsed = seconds.getTime();
else
elapsed = Number(seconds);
elapsed -= Date.now();
const units = {
year: 31536000000,
month: 2592000000,
week: 604800000,
day: 86400000,
hour: 3600000,
minute: 60000,
second: 1000,
};
const formatter = new Intl.RelativeTimeFormat(locale, { style: style as Intl.RelativeTimeFormatStyle });
for (const [unit, ms] of Object.entries(units))
if (Math.abs(elapsed) > ms || unit === 'second')
return formatter.format(Math.round(elapsed / ms), unit as Intl.RelativeTimeFormatUnit);
return 'now';
}
export function asEnvVariables(str: string): [string, string][] {
@ -72,4 +121,34 @@ export function asEnvVariables(str: string): [string, string][] {
.filter(pair => pair !== null);
}
export function getLicenseUrl(licenseId: string | License | null | undefined): string | undefined {
if (licenseId === null || licenseId === undefined)
return;
let id: string | undefined;
if (typeof licenseId !== 'string') {
if (licenseId.url !== null && licenseId.url !== undefined) {
open(licenseId.url);
return;
}
id = licenseId.id || licenseId.name;
}
else {
id = licenseId;
}
return `https://spdx.org/licenses/${id}.html`;
}
export function getPackageUrl(provider: Providers, id: string, package_type: PackageType = 'mod'): string {
const mapping: Record<Providers, string> = {
Modrinth: `https://modrinth.com/${package_type}/${id}`,
};
return mapping[provider];
}
export const LOADERS: Loader[] = ['vanilla', 'fabric', 'forge', 'neoforge', 'quilt'] as const;
export const PROVIDERS: Providers[] = ['Modrinth'] as const;

View file

@ -1,5 +1,5 @@
import { listen } from '@tauri-apps/api/event';
import { commands } from '~bindings';
import { commands } from '@onelauncher/client/bindings';
declare global {
interface Window {
@ -17,6 +17,22 @@ export type UpdateStore =
| { status: 'noUpdateAvailable' }
| { status: 'installing' };
async function checkForUpdate() {
const result = await commands.checkForUpdate();
if (result.status === 'error') {
console.error('[updater]: ', result.error);
return null;
}
if (!result.data)
return null;
// TODO: ui/cb
return result.data;
}
export function createUpdater() {
if (!window.__LAUNCHER_UPDATER__)
return;
@ -25,23 +41,6 @@ export function createUpdater() {
listen<UpdateStore>('updater', e => Object.assign(updateStore, e.payload));
const onInstallCallbacks = new Set<() => void>();
async function checkForUpdate() {
const result = await commands.checkForUpdate();
if (result.status === 'error') {
console.error('[updater]: ', result.error);
return null;
}
if (!result.data)
return null;
// TODO: ui
// TODO: cb
return result.data;
}
function installUpdate() {
for (const cb of onInstallCallbacks) cb();
// TODO: ui/toast

View file

@ -17,8 +17,7 @@
"types": [
"vitest/globals",
"@testing-library/jest-dom",
"vite-plugin-solid-svg/types",
"@formatjs/intl-durationformat"
"vite-plugin-solid-svg/types"
],
"allowImportingTsExtensions": true,
"strict": true,

View file

@ -111,6 +111,13 @@ export default defineConfig({
trace: '#FDFDFD',
},
link: {
DEFAULT: '#61AFEF',
hover: '#5F87FF',
pressed: '#72AFFF',
disabled: '#61AFEF80',
},
page: {
DEFAULT: '#11171c',
elevated: '#151c22',

View file

@ -6,7 +6,7 @@ import solid from 'vite-plugin-solid';
import solidSvg from 'vite-plugin-solid-svg';
import paths from 'vite-tsconfig-paths';
import unocss from 'unocss/vite';
import * as sentry from '@sentry/vite-plugin';
import { sentryVitePlugin as sentry } from '@sentry/vite-plugin';
export default defineConfig(async ({ mode }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd(), '') };
@ -30,6 +30,7 @@ export default defineConfig(async ({ mode }) => {
rollupOptions: {
treeshake: 'recommended',
},
emptyOutDir: false,
},
clearScreen: false,
@ -40,7 +41,7 @@ export default defineConfig(async ({ mode }) => {
};
if (process.env.SENTRY_AUTH_TOKEN)
config.plugins.push(sentry.sentryVitePlugin({
config.plugins.push(sentry({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'polyfrost',
project: 'onelauncher_frontend',

View file

@ -0,0 +1,112 @@
use onelauncher::data::{Loader, PackageType};
use onelauncher::package::content::Providers;
use onelauncher::store::Cluster;
use onelauncher::{cluster, Result};
const CLUSTER_NAME: &str = "Test Packages";
const MODRINTH_PACKAGE_ONE: &str = "behindyou";
const MODRINTH_PACKAGE_ONE_VERSION: &str = "v3.2.2";
const MODRINTH_PACKAGE_TWO: &str = "keystrokes";
const MODRINTH_PACKAGE_TWO_VERSION: &str = "v1.0.0";
#[tokio::main]
async fn main() -> Result<()> {
let mut cluster = create_cluster().await?;
// Print Packages first
print_packages(&cluster).await?;
// Download the first mod, add it to the PackageManager and sync packages
download_mod(
&mut cluster,
MODRINTH_PACKAGE_ONE,
MODRINTH_PACKAGE_ONE_VERSION,
true,
)
.await?;
cluster::content::package::sync_packages_by_type(&cluster.cluster_path(), PackageType::Mod)
.await?;
// Print Packages again
print_packages(&cluster).await?;
// Download the second mod, DON'T add it to the PackageManager and sync packages
download_mod(
&mut cluster,
MODRINTH_PACKAGE_TWO,
MODRINTH_PACKAGE_TWO_VERSION,
false,
)
.await?;
cluster::content::package::sync_packages_by_type(&cluster.cluster_path(), PackageType::Mod)
.await?;
// Print Packages again
print_packages(&cluster).await?;
Ok(())
}
async fn create_cluster() -> Result<Cluster> {
if let Some(cluster) = cluster::get_by_name(CLUSTER_NAME).await? {
return Ok(cluster);
}
let cluster_path = cluster::create::create_cluster(
CLUSTER_NAME.to_owned(),
"1.8.9".to_owned(),
Loader::Forge,
None,
None,
None,
None,
Some(true),
Some(false),
)
.await?;
let cluster = cluster::get(&cluster_path)
.await?
.expect("Cluster not found");
Ok(cluster)
}
async fn download_mod(cluster: &mut Cluster, pkg: &str, ver: &str, add: bool) -> Result<()> {
let package = Providers::Modrinth.get(pkg).await?;
let (package_path, package) = cluster::content::package::download_package(
&package,
cluster,
None,
None,
Some(ver.to_owned()),
)
.await?;
if add {
cluster::content::package::add_package(
&cluster.cluster_path(),
package_path.clone(),
package,
Some(PackageType::Mod),
)
.await?;
}
Ok(())
}
async fn print_packages(cluster: &Cluster) -> Result<()> {
let packages =
cluster::content::package::get_packages(&cluster.cluster_path(), PackageType::Mod).await?;
println!("Packages for cluster: {:?}", cluster.cluster_path());
for package in packages {
println!("{:?}: {:?}", package.file_name, package.meta);
}
Ok(())
}

View file

@ -6,9 +6,9 @@
<title>OneLauncher Testing</title>
</head>
<body>
<!-- <a href='onelauncher://test/command'>this will test a onelauncher command from an untrusted site</a>
<a href='onelauncher://test/command'>this will test a onelauncher command from an untrusted site</a>
<a href='onelauncher://test/launch'>this command will launch onelauncher</a>
<a href="onelauncher://test/'">this command will test onelauncher url sanitization</a>
<a href='onelauncher://test/laudnfsngfkjbdkfsghdjg'>unknown command</a> -->
<a href='onelauncher://test/laudnfsngfkjbdkfsghdjg'>unknown command</a>
</body>
</html>

View file

@ -11,5 +11,7 @@
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {}
"scripts": {
"example": "cargo run --example "
}
}

View file

@ -63,37 +63,37 @@ pub async fn modrinth() -> onelauncher::Result<()> {
let result = provider.get("oneconfig").await?;
println!("{:#?}", result);
let managed_version = provider
.get_version_for_game_version(&result.id, "1.8.9")
.await?;
println!("{:#?}", result);
// let managed_version = provider
// .get_version_for_game_version(&result.id, "1.8.9")
// .await?;
// println!("{:#?}", result);
let name = "Example".to_string();
let game = "1.21".to_string();
let loader = Loader::Fabric;
// let name = "Example".to_string();
// let game = "1.21".to_string();
// let loader = Loader::Fabric;
let cluster_path = create_cluster(
name.clone(),
game,
loader,
None, // latest
None,
None,
None,
None,
None,
)
.await?;
// let cluster_path = create_cluster(
// name.clone(),
// game,
// loader,
// None, // latest
// None,
// None,
// None,
// None,
// None,
// )
// .await?;
let cluster = cluster::get(&cluster_path, None)
.await?
.expect("couldnt get cluster");
managed_version
.files
.first()
.expect("no files found")
.download_to_cluster(&cluster)
.await?;
// let cluster = cluster::get(&cluster_path, None)
// .await?
// .expect("couldnt get cluster");
// managed_version
// .files
// .first()
// .expect("no files found")
// .download_to_cluster(&cluster)
// .await?;
Ok(())
}
@ -113,7 +113,7 @@ pub async fn launch_and_authenticate() -> onelauncher::Result<()> {
println!("clearing clusters");
{
let c = cluster::list(None).await?;
let c = cluster::list().await?;
for cluster in c.into_iter() {
cluster::remove(&cluster.cluster_path()).await?;
}

View file

@ -4,13 +4,8 @@ export default defineConfig({
typescript: true,
solid: true,
unocss: true,
toml: true,
gitignore: true,
ignores: [
'**/types',
'**/cache',
'**/*.svg',
'**/gen',
'*.rs',
],
ignores: ['*.rs', '**/migrations/**'],
rules: {
'ts/no-use-before-define': 'off',
},
});

13
flake.nix Normal file
View file

@ -0,0 +1,13 @@
{
description = "Next-generation open source Minecraft launcher";
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
inputs.flake-parts.url = "github:hercules-ci/flake-parts";
inputs.crane.url = "github:ipetkov/crane";
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"];
perSystem = { config, pkgs, ... }: {};
};
}

View file

@ -4,7 +4,7 @@
"version": "1.0.0-alpha.1",
"private": true,
"engineStrict": true,
"packageManager": "pnpm@9.7.1",
"packageManager": "pnpm@9.9.0",
"description": "Next-generation open source Minecraft launcher and tools for interacting with Polyfrost",
"license": "GPL-3.0-only",
"homepage": "https://polyfrost.org",
@ -16,18 +16,16 @@
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"engines": {
"pnpm": "9.7.1",
"pnpm": ">=9.0.0",
"npm": "pnpm",
"yarn": "pnpm",
"node": ">=18"
"node": ">=18.18 <19 || >=20"
},
"scripts": {
"prep": "pnpm --filter=@onelauncher/scripts -- int:prepare",
"build": "pnpm --filter=./* run build",
"dev": "pnpm --filter=./* run dev",
"start": "pnpm --filter=./* run start",
"prep": "pnpm scripts prep",
"typecheck": "pnpm -r typecheck",
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep",
"codegen": "pnpm prisma:gen",
"bootstrap": "cargo clean && ./packages/scripts/setup.sh && pnpm install && pnpm prep && pnpm codegen",
"autoformat": "./packages/scripts/format.sh",
"test": "pnpm test:vi && pnpm --filter=./* run test",
"format": "pnpm format:es && pnpm format:rs",
@ -35,9 +33,12 @@
"frontend": "pnpm --filter @onelauncher/frontend -- ",
"client": "pnpm --filter @onelauncher/client -- ",
"desktop": "pnpm --filter @onelauncher/desktop -- ",
"scripts": "pnpm --filter @onelauncher/scripts -- ",
"actions": "pnpm --filter @onelauncher/action* --",
"testing": "pnpm --filter @onelauncher/testing --",
"tauri": "pnpm desktop tauri-ext",
"postupdate": "cargo update",
"----------------------": "----------------------",
"test:vi": "pnpm vitest run",
"test:update": "pnpm vitest -u",
"test:ui": "pnpm vitest --ui",
@ -45,7 +46,9 @@
"format:es": "pnpm lint:es --fix",
"lint:rs": "cargo +nightly fmt --check && cargo clippy -- -D warnings",
"format:rs": "cargo +nightly fmt && cargo clippy --fix",
"release:node": "bumpp -r"
"release:node": "bumpp -r",
"prisma:gen": "pnpm prisma generate --schema packages/core/prisma/schema.prisma",
"prisma:migrate": "pnpm prisma migrate dev --schema packages/core/prisma/schema.prisma"
},
"workspaces": {
"packages": [
@ -54,18 +57,20 @@
]
},
"devDependencies": {
"@flowr/eslint-config": "^3.12.1",
"@types/node": "^22.4.1",
"@unocss/eslint-plugin": "^0.62.2",
"@flowr/eslint-config": "^4.0.0",
"@types/node": "^22.5.1",
"@unocss/eslint-plugin": "^0.62.3",
"@vitest/ui": "^2.0.5",
"bumpp": "^9.5.1",
"eslint": "^9.9.0",
"bumpp": "^9.5.2",
"eslint": "^9.9.1",
"eslint-plugin-solid": "^0.14.2",
"jiti": "^1.21.6",
"prisma": "^5.19.0",
"rimraf": "^6.0.1",
"ts-node": "^10.9.2",
"tsx": "^4.17.0",
"tsx": "^4.19.0",
"typescript": "^5.5.4",
"vite": "^5.4.1",
"vite": "^5.4.2",
"vitest": "^2.0.5"
}
}

Some files were not shown because too many files have changed in this diff Show more