mirror of
https://github.com/modrinth/code.git
synced 2026-08-02 14:15:53 +00:00
Compare commits
136
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2d5b3692a | ||
|
|
8cdd555cc5 | ||
|
|
8ec9975307 | ||
|
|
89602c195d | ||
|
|
45ed092d49 | ||
|
|
254c69cf4e | ||
|
|
0f5fafa146 | ||
|
|
21f256fef8 | ||
|
|
32007aa559 | ||
|
|
cebfa04395 | ||
|
|
75f40cdda8 | ||
|
|
24285baebb | ||
|
|
e2515369a3 | ||
|
|
e74ec144c7 | ||
|
|
4fea22819d | ||
|
|
b925f2dbec | ||
|
|
46b17b3175 | ||
|
|
db8c999268 | ||
|
|
258957db3c | ||
|
|
ab53a49bbc | ||
|
|
0caa447228 | ||
|
|
f27847d34a | ||
|
|
b49132a9a6 | ||
|
|
0afbcf785c | ||
|
|
a430614c9f | ||
|
|
f3104c7d1f | ||
|
|
91ae871bc7 | ||
|
|
7e8bdcaf4a | ||
|
|
67d45f5039 | ||
|
|
3e7e4ece1b | ||
|
|
2e6d5d743c | ||
|
|
35b7f05167 | ||
|
|
a89634687f | ||
|
|
0144943ef4 | ||
|
|
c96c97e9d9 | ||
|
|
b816b30a0b | ||
|
|
c0efdd8143 | ||
|
|
dc7c1cbe5d | ||
|
|
fa57e0d148 | ||
|
|
ed29013a56 | ||
|
|
32d3cdc5b3 | ||
|
|
e4ce0bda8c | ||
|
|
d6fb569875 | ||
|
|
4250fa6fb1 | ||
|
|
6db02be4ff | ||
|
|
4e84215bd3 | ||
|
|
3236acfee3 | ||
|
|
a5e73aa4fc | ||
|
|
ceee3a064c | ||
|
|
81cb36548d | ||
|
|
ae939cd4ca | ||
|
|
8faa36b014 | ||
|
|
a45a320512 | ||
|
|
98c70633ee | ||
|
|
d858901568 | ||
|
|
b9b03796e3 | ||
|
|
e8475086ec | ||
|
|
655f899785 | ||
|
|
9bcee5ab52 | ||
|
|
9f050f21e6 | ||
|
|
782c82d3e8 | ||
|
|
da828f4db8 | ||
|
|
f6f2cb8d8b | ||
|
|
1a0633b6db | ||
|
|
3ae50a2abb | ||
|
|
408927a23e | ||
|
|
3c29a62036 | ||
|
|
407aa4b4cf | ||
|
|
d709bbeb54 | ||
|
|
b5662371d8 | ||
|
|
db9689b335 | ||
|
|
0f1483d9df | ||
|
|
d48ad873fb | ||
|
|
ff7c0cb5b8 | ||
|
|
faed8c3f6d | ||
|
|
63d00cb660 | ||
|
|
27d54a98af | ||
|
|
05a1a10788 | ||
|
|
addb6c6045 | ||
|
|
e08da23fb5 | ||
|
|
ecd69f88b2 | ||
|
|
d834ce3645 | ||
|
|
cf58559381 | ||
|
|
01a7ee8d1e | ||
|
|
5480826a53 | ||
|
|
9184de92df | ||
|
|
d753b8735c | ||
|
|
c7a5336a51 | ||
|
|
292b8f4cd2 | ||
|
|
c391fb12b9 | ||
|
|
62f20b9aee | ||
|
|
2aca44bbcc | ||
|
|
b9be6abf0b | ||
|
|
a5be0114c5 | ||
|
|
f32f732c31 | ||
|
|
e974672e71 | ||
|
|
d6356ea8e5 | ||
|
|
872dcbcf78 | ||
|
|
44da0ed5ff | ||
|
|
ec7c893681 | ||
|
|
3ce8f85463 | ||
|
|
4ce82b01e6 | ||
|
|
f4d65437df | ||
|
|
9646083025 | ||
|
|
652a9b0b5c | ||
|
|
918c4a9c83 | ||
|
|
0a0b19233d | ||
|
|
6f2e04c3de | ||
|
|
ece35f9158 | ||
|
|
3ccb8132d8 | ||
|
|
cad6bd4f92 | ||
|
|
65bd8937a6 | ||
|
|
abcac5f40a | ||
|
|
43893158ac | ||
|
|
fcab720943 | ||
|
|
4183702a72 | ||
|
|
8946e4c394 | ||
|
|
f1ce31001a | ||
|
|
c99564f8ea | ||
|
|
71c1deb5cd | ||
|
|
042dde075b | ||
|
|
3bfe670d47 | ||
|
|
ed53fe5330 | ||
|
|
547921badc | ||
|
|
aac61dcfaa | ||
|
|
52a972d949 | ||
|
|
bdc2b4abf8 | ||
|
|
c19ce11ea4 | ||
|
|
c54906e6de | ||
|
|
57b4f21080 | ||
|
|
66f3c39c13 | ||
|
|
7484afa18e | ||
|
|
7ea0635d86 | ||
|
|
fe3aba52ab | ||
|
|
64f87551ff | ||
|
|
c8a586c6f1 |
@@ -73,6 +73,17 @@ jobs:
|
||||
|
||||
docker:
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
env:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
steps:
|
||||
@@ -94,6 +105,17 @@ jobs:
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-daedalus-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Build daedalus_client
|
||||
run: cargo build --release --package daedalus_client
|
||||
|
||||
|
||||
@@ -76,6 +76,16 @@ jobs:
|
||||
env:
|
||||
SQLX_OFFLINE: 'true'
|
||||
GIT_HASH: ${{ github.sha }}
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -86,6 +96,9 @@ jobs:
|
||||
rustflags: ''
|
||||
cache: false
|
||||
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Install build dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
with:
|
||||
@@ -101,6 +114,17 @@ jobs:
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-labrinth-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Build labrinth
|
||||
run: cargo build --profile release-labrinth --package labrinth
|
||||
|
||||
|
||||
@@ -44,6 +44,16 @@ jobs:
|
||||
name: Build
|
||||
env:
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5
|
||||
# SCCACHE_DIR: '/mnt/sccache'
|
||||
# SCCACHE_CACHE_SIZE: '10G'
|
||||
# SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
|
||||
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: 'sccache'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -74,6 +84,9 @@ jobs:
|
||||
rustflags: ''
|
||||
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
|
||||
@@ -18,11 +18,25 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
internal: ${{ steps.check-internal.outputs.internal }}
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then
|
||||
echo "internal=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "internal=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Merge Queue CI Check Skipper
|
||||
id: merge-queue-ci-skipper
|
||||
uses: ./.github/merge-queue-ci-skipper
|
||||
@@ -48,7 +62,7 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
|
||||
@@ -62,6 +76,19 @@ jobs:
|
||||
# since we don't want warnings to become errors
|
||||
# while developing)
|
||||
RUSTFLAGS: -Dwarnings
|
||||
# sccache config (only populated for internal branches; secrets aren't
|
||||
# available to forked PRs, and blacksmith stickydisk requires a
|
||||
# blacksmith runner)
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
@@ -111,6 +138,17 @@ jobs:
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-turbo-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Setup binstall
|
||||
uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
|
||||
|
||||
|
||||
Generated
-1
@@ -10807,7 +10807,6 @@ dependencies = [
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid 1.23.3",
|
||||
"whoami",
|
||||
"windows",
|
||||
|
||||
@@ -95,7 +95,7 @@ import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user, get_version } from '@/helpers/cache.js'
|
||||
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
|
||||
import { list, run } from '@/helpers/instance'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
|
||||
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
@@ -110,7 +110,6 @@ import {
|
||||
isNetworkMetered,
|
||||
setRestartAfterPendingUpdate,
|
||||
} from '@/helpers/utils.js'
|
||||
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
|
||||
import i18n from '@/i18n.config'
|
||||
import {
|
||||
appUpdateState,
|
||||
@@ -869,14 +868,6 @@ async function handleCommand(e) {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
} else if (e.event === 'LaunchInstance') {
|
||||
if (e.server) {
|
||||
await start_join_server(e.id, e.server).catch(handleError)
|
||||
} else if (e.singleplayer_world) {
|
||||
await start_join_singleplayer_world(e.id, e.singleplayer_world).catch(handleError)
|
||||
} else {
|
||||
await run(e.id).catch(handleError)
|
||||
}
|
||||
} else if (e.event === 'InstallServer') {
|
||||
await router.push(`/project/${e.id}`)
|
||||
await playServerProject(e.id).catch(handleError)
|
||||
@@ -1341,11 +1332,12 @@ async function processPendingSurveys() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
const userId = creds?.user_id
|
||||
|
||||
const instances = (await list().catch(handleError)) ?? []
|
||||
const isActivePlayer = instances.some(
|
||||
(instance) =>
|
||||
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||
)
|
||||
const instances = await list().catch(handleError)
|
||||
const isActivePlayer =
|
||||
instances.findIndex(
|
||||
(instance) =>
|
||||
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||
) >= 0
|
||||
|
||||
let surveys = []
|
||||
try {
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import type { Manifest } from '../../../helpers/types'
|
||||
|
||||
@@ -44,12 +43,8 @@ const filePicker = injectFilePicker()
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('AppInstallationSettings')
|
||||
const themeStore = useTheming()
|
||||
|
||||
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
|
||||
const skipNonEssentialWarnings = computed(() =>
|
||||
themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
)
|
||||
|
||||
debug('metadata load: start', {
|
||||
instanceId: instance.value.id,
|
||||
@@ -216,7 +211,6 @@ provideInstallationSettings({
|
||||
reinstalling.value ||
|
||||
!!offline,
|
||||
),
|
||||
skipNonEssentialWarnings,
|
||||
modpack: computed(() => {
|
||||
if (isImportedModpack.value && instance.value.link?.type === 'imported_modpack') {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,6 @@ const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||
|
||||
@@ -102,15 +101,6 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
|
||||
},
|
||||
skipNonEssentialWarningsTitle: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.title',
|
||||
defaultMessage: 'Skip non-essential warnings',
|
||||
},
|
||||
skipNonEssentialWarningsDescription: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.description',
|
||||
defaultMessage:
|
||||
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
|
||||
},
|
||||
showPlayTimeTitle: {
|
||||
id: 'app.appearance-settings.show-play-time.title',
|
||||
defaultMessage: 'Show play time',
|
||||
@@ -295,25 +285,6 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
|
||||
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
|
||||
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
|
||||
@@ -8,12 +8,9 @@ import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const themeStore = useTheming()
|
||||
const settings = ref(await get())
|
||||
const purgeCacheConfirmModal = ref(null)
|
||||
|
||||
watch(
|
||||
settings,
|
||||
@@ -52,15 +49,6 @@ async function purgeCache() {
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
function handlePurgeCacheClick() {
|
||||
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
void purgeCache()
|
||||
return
|
||||
}
|
||||
|
||||
purgeCacheConfirmModal.value?.show()
|
||||
}
|
||||
|
||||
async function openDbBackupsFolder() {
|
||||
await showAppDbBackupsFolder().catch(handleError)
|
||||
}
|
||||
@@ -114,7 +102,7 @@ async function findLauncherDir() {
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
|
||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
ClipboardCopyIcon,
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
FolderOpenIcon,
|
||||
IssuesIcon,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TagItem,
|
||||
@@ -41,7 +39,7 @@ import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { copyToClipboard, createInstanceShortcut } from '@/helpers/utils'
|
||||
import { copyToClipboard } from '@/helpers/utils'
|
||||
import type {
|
||||
ProtocolVersion,
|
||||
ServerStatus,
|
||||
@@ -62,7 +60,6 @@ const formatDateTime = useFormatDateTime({
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
@@ -97,7 +94,6 @@ const props = withDefaults(
|
||||
instanceId?: string
|
||||
instanceName?: string
|
||||
instanceIcon?: string
|
||||
shortcutInstanceId?: string
|
||||
}>(),
|
||||
{
|
||||
playingInstance: false,
|
||||
@@ -117,7 +113,6 @@ const props = withDefaults(
|
||||
instanceId: undefined,
|
||||
instanceName: undefined,
|
||||
instanceIcon: undefined,
|
||||
shortcutInstanceId: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -136,33 +131,6 @@ const serverIncompatible = computed(
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
const managed = computed(() => props.managed)
|
||||
const shortcutInstanceId = computed(() => props.shortcutInstanceId ?? props.instanceId)
|
||||
|
||||
async function createShortcut() {
|
||||
if (!shortcutInstanceId.value) return
|
||||
|
||||
try {
|
||||
const shortcutPath = await createInstanceShortcut(
|
||||
props.world.name,
|
||||
shortcutInstanceId.value,
|
||||
props.world.type === 'server'
|
||||
? { server: (props.world as ServerWorld).address }
|
||||
: { singleplayerWorld: (props.world as SingleplayerWorld).path },
|
||||
)
|
||||
if (!shortcutPath) return
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Shortcut created',
|
||||
})
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to create shortcut',
|
||||
text: error instanceof Error ? error.message : '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
hardcore: {
|
||||
@@ -217,10 +185,6 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.dont_show_on_home',
|
||||
defaultMessage: `Don't show on Home`,
|
||||
},
|
||||
createShortcut: {
|
||||
id: 'instance.worlds.create_shortcut',
|
||||
defaultMessage: 'Create shortcut',
|
||||
},
|
||||
linkedServer: {
|
||||
id: 'instance.worlds.linked_server',
|
||||
defaultMessage: 'Managed by server project',
|
||||
@@ -509,11 +473,6 @@ const messages = defineMessages({
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
shown: !!shortcutInstanceId,
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !instanceId,
|
||||
@@ -558,10 +517,6 @@ const messages = defineMessages({
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #create-shortcut>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createShortcut) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
|
||||
-1
@@ -97,7 +97,6 @@ type ReleaseChannel = 'release' | 'beta' | 'alpha'
|
||||
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
type ContentFile = {
|
||||
enabled: boolean
|
||||
metadata?: {
|
||||
project_id: string
|
||||
version_id: string
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
|
||||
import { get_full_path, get_mod_full_path } from '@/helpers/instance'
|
||||
|
||||
@@ -48,22 +47,6 @@ export async function showLauncherLogsFolder() {
|
||||
return await invoke('plugin:utils|show_launcher_logs_folder', {})
|
||||
}
|
||||
|
||||
export async function createInstanceShortcut(instanceName, instanceId, options = {}) {
|
||||
const outputPath = await save({
|
||||
defaultPath: `Modrinth - ${instanceName}`,
|
||||
})
|
||||
|
||||
if (!outputPath) return null
|
||||
|
||||
return await invoke('plugin:shortcuts|create_instance_shortcut', {
|
||||
instanceName,
|
||||
instanceId,
|
||||
outputPath,
|
||||
server: options.server,
|
||||
singleplayerWorld: options.singleplayerWorld,
|
||||
})
|
||||
}
|
||||
|
||||
export async function showAppDbBackupsFolder() {
|
||||
return await invoke('plugin:utils|show_app_db_backups_folder', {})
|
||||
}
|
||||
|
||||
@@ -128,12 +128,6 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Show play time"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Skip non-essential warnings"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Enables the ability to toggle the sidebar."
|
||||
},
|
||||
@@ -959,9 +953,6 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Copy address"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Create shortcut"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Don't show on Home"
|
||||
},
|
||||
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
provideServerInstallContent,
|
||||
} from '@/providers/setup/server-install-content'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -68,7 +67,6 @@ const debugLog = useDebugLogger('Browse')
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const themeStore = useTheming()
|
||||
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
||||
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
|
||||
provideServerInstallContent(serverInstallContent)
|
||||
@@ -579,7 +577,6 @@ const installContext = computed(() => {
|
||||
queuedCount: queuedServerInstallCount.value,
|
||||
selectedProjects: selectedServerInstallProjects.value,
|
||||
isInstallingSelected: isInstallingQueuedServerInstalls.value,
|
||||
skipNonEssentialWarnings: themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
installProgress: queuedInstallProgress.value,
|
||||
clearQueued: clearQueuedServerInstalls,
|
||||
clearSelected: clearQueuedServerInstalls,
|
||||
|
||||
@@ -197,10 +197,6 @@
|
||||
id: 'export-mrpack',
|
||||
action: () => exportModal?.show(),
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
@@ -208,7 +204,6 @@
|
||||
<template #host-a-server> <ServerIcon /> Create a server </template>
|
||||
<template #open-folder> <FolderOpenIcon /> Open folder </template>
|
||||
<template #export-mrpack> <PackageIcon /> Export modpack </template>
|
||||
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -326,7 +321,7 @@ import { get, get_full_path, kill, run } from '@/helpers/instance'
|
||||
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
@@ -335,7 +330,7 @@ import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { addNotification, handleError } = injectNotificationManager()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { playServerProject } = injectServerInstall()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
@@ -603,25 +598,6 @@ const repairInstance = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const createShortcut = async () => {
|
||||
if (!instance.value) return
|
||||
try {
|
||||
const shortcutPath = await createInstanceShortcut(instance.value.name, instance.value.id)
|
||||
if (!shortcutPath) return
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Shortcut created',
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: `Error creating shortcut`,
|
||||
text: `${error}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRightClick = (event: MouseEvent) => {
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
|
||||
@@ -124,7 +124,6 @@ import { type InstanceContentData, loadInstanceContentData } from '@/helpers/ins
|
||||
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
|
||||
import { highlightModInInstance } from '@/helpers/utils.js'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const messages = defineMessages({
|
||||
shareTitle: {
|
||||
@@ -174,10 +173,6 @@ const { installingItems, installRevisionByInstance, installFailureRevisionByInst
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('Mods:ContentUpdate')
|
||||
const themeStore = useTheming()
|
||||
const skipNonEssentialWarnings = computed(() =>
|
||||
themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
@@ -1064,7 +1059,7 @@ async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.
|
||||
isModpackUpdateDowngrade.value ||
|
||||
versionChangesGameVersion(selectedVersion, props.instance.game_version)
|
||||
|
||||
if (skipNonEssentialWarnings.value || !shouldShowWarning) {
|
||||
if (!shouldShowWarning) {
|
||||
await handleModpackUpdateConfirm()
|
||||
return
|
||||
}
|
||||
@@ -1311,7 +1306,6 @@ provideContentManager({
|
||||
isPackLocked,
|
||||
isBusy: isInstanceBusy,
|
||||
isBulkOperating,
|
||||
skipNonEssentialWarnings,
|
||||
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
|
||||
toggleEnabled: toggleDisableDebounced,
|
||||
bulkEnableItems: (items: ContentItem[]) =>
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
||||
"
|
||||
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
||||
:shortcut-instance-id="instance.id"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
} from '@/helpers/instance'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import type { GameInstance, InstanceLoader } from '@/helpers/types'
|
||||
import { useTheming } from '@/store/state'
|
||||
interface ModalRef {
|
||||
show: (initialVersionId?: string) => void
|
||||
hide: () => void
|
||||
@@ -188,7 +187,6 @@ export function createContentInstall(opts: {
|
||||
handleError: (err: unknown) => void
|
||||
}): ContentInstallContext {
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const instances = ref<ContentInstallInstance[]>([])
|
||||
const compatibleLoaders = ref<string[]>([])
|
||||
const gameVersions = ref<string[]>([])
|
||||
@@ -812,7 +810,7 @@ export function createContentInstall(opts: {
|
||||
const packs = await list()
|
||||
const existingPack = packs.find((pack) => pack.link?.project_id === project.id)
|
||||
|
||||
if (existingPack && !themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
if (existingPack) {
|
||||
pendingModpackInstall = { project, version, source, callback, createInstanceCallback }
|
||||
modpackAlreadyInstalledModalRef?.show(existingPack.name, existingPack.id)
|
||||
return
|
||||
|
||||
@@ -21,12 +21,10 @@ import {
|
||||
import { list } from '@/helpers/instance'
|
||||
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
|
||||
import type { InstanceLoader } from '@/helpers/types'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
export function setupCreationModal(notificationManager: AbstractWebNotificationManager) {
|
||||
const { handleError } = notificationManager
|
||||
const router = useRouter()
|
||||
const themeStore = useTheming()
|
||||
|
||||
const installationModal =
|
||||
useTemplateRef<ComponentExposed<typeof CreationFlowModal>>('installationModal')
|
||||
@@ -73,7 +71,7 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
const instances = await list().catch(handleError)
|
||||
const existingInstance = instances?.find((i) => i.link?.project_id === projectId)
|
||||
|
||||
if (existingInstance && !themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
if (existingInstance) {
|
||||
pendingModpackCreation.value = { projectId, versionId, name, iconUrl }
|
||||
installationModal.value?.hide()
|
||||
modpackAlreadyInstalledModal.value?.show(existingInstance.name, existingInstance.id)
|
||||
|
||||
@@ -10,7 +10,6 @@ export const DEFAULT_FEATURE_FLAGS = {
|
||||
server_project_qa: false,
|
||||
server_ram_as_bytes_always_on: false,
|
||||
always_show_app_controls: false,
|
||||
skip_non_essential_warnings: false,
|
||||
skip_unknown_pack_warning: false,
|
||||
pride_fundraiser: true,
|
||||
i18n_debug: false,
|
||||
|
||||
@@ -47,7 +47,7 @@ async fn main() -> theseus::Result<()> {
|
||||
for world in worlds {
|
||||
println!(
|
||||
"World: {:?}/{:?} played at {:?}: {:#?}",
|
||||
world.instance_id,
|
||||
world.profile,
|
||||
world.world.name,
|
||||
world.world.last_played,
|
||||
world.world.details
|
||||
|
||||
+11
-13
@@ -22,6 +22,11 @@ path-util = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
tauri = { workspace = true, features = [
|
||||
"devtools",
|
||||
"macos-private-api",
|
||||
"protocol-asset",
|
||||
] }
|
||||
tauri-plugin-deep-link = { workspace = true }
|
||||
tauri-plugin-dialog = { workspace = true }
|
||||
tauri-plugin-fs = { workspace = true }
|
||||
@@ -40,10 +45,6 @@ url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
|
||||
[dependencies.tauri]
|
||||
workspace = true
|
||||
features = ["devtools", "macos-private-api", "protocol-asset"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { workspace = true, features = ["codegen"] }
|
||||
|
||||
@@ -53,22 +54,19 @@ tauri-plugin-updater = { workspace = true, optional = true }
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation.workspace = true
|
||||
core-graphics.workspace = true
|
||||
objc2-app-kit = { workspace = true, features = ["NSWindow"] }
|
||||
objc2-app-kit = { workspace = true, features = [
|
||||
"NSWindow",
|
||||
] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
webview2-com.workspace = true
|
||||
windows-core.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
workspace = true
|
||||
features = [
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]
|
||||
] }
|
||||
windows-core.workspace = true
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
@@ -242,14 +242,6 @@ fn main() {
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"shortcuts",
|
||||
InlinedPlugin::new()
|
||||
.commands(&["create_instance_shortcut"])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"utils",
|
||||
InlinedPlugin::new()
|
||||
|
||||
@@ -105,7 +105,6 @@
|
||||
"cache:default",
|
||||
"files:default",
|
||||
"settings:default",
|
||||
"shortcuts:default",
|
||||
"tags:default",
|
||||
"utils:default",
|
||||
"ads:default",
|
||||
|
||||
@@ -13,7 +13,6 @@ pub mod minecraft_skins;
|
||||
pub mod mr_auth;
|
||||
pub mod process;
|
||||
pub mod settings;
|
||||
pub mod shortcuts;
|
||||
pub mod tags;
|
||||
pub mod utils;
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::api::Result;
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
pub(super) const SHORTCUT_EXTENSION: &str = "desktop";
|
||||
|
||||
pub(super) async fn create_shortcut(
|
||||
profile_name: &str,
|
||||
launch_url: &Url,
|
||||
output_path: &Path,
|
||||
) -> Result<()> {
|
||||
let target_path = std::env::current_exe()?;
|
||||
tokio::fs::write(
|
||||
output_path,
|
||||
format!(
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name={}\n\
|
||||
Exec={} {}\n\
|
||||
Icon=ModrinthApp\n\
|
||||
Terminal=false\n\
|
||||
Categories=Game;\n",
|
||||
escape_desktop_entry_value(&format!("Launch {profile_name}")),
|
||||
quote_desktop_exec_arg(&target_path.to_string_lossy()),
|
||||
quote_desktop_exec_arg(launch_url.as_str()),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut permissions = tokio::fs::metadata(output_path).await?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
tokio::fs::set_permissions(output_path, permissions).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn escape_desktop_entry_value(input: &str) -> String {
|
||||
input
|
||||
.replace('\\', "\\\\")
|
||||
.replace('\n', "\\n")
|
||||
.replace('\r', "")
|
||||
}
|
||||
|
||||
fn quote_desktop_exec_arg(input: &str) -> String {
|
||||
format!(
|
||||
"\"{}\"",
|
||||
input
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('$', "\\$")
|
||||
.replace('`', "\\`")
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use crate::api::Result;
|
||||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
path::Path,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
pub(super) const SHORTCUT_EXTENSION: &str = "app";
|
||||
|
||||
pub(super) async fn create_shortcut(
|
||||
profile_name: &str,
|
||||
launch_url: &Url,
|
||||
output_path: &Path,
|
||||
) -> Result<()> {
|
||||
let contents_dir = output_path.join("Contents");
|
||||
let macos_dir = contents_dir.join("MacOS");
|
||||
let resources_dir = contents_dir.join("Resources");
|
||||
tokio::fs::create_dir_all(&macos_dir).await?;
|
||||
tokio::fs::create_dir_all(&resources_dir).await?;
|
||||
|
||||
let executable_path = macos_dir.join("launch");
|
||||
tokio::fs::write(
|
||||
&executable_path,
|
||||
format!(
|
||||
"#!/bin/sh\nexec /usr/bin/open {}\n",
|
||||
shell_quote(launch_url.as_str()),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tokio::fs::write(
|
||||
resources_dir.join("icon.icns"),
|
||||
include_bytes!("../../../icons/icon.icns"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tokio::fs::write(
|
||||
contents_dir.join("Info.plist"),
|
||||
format!(r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>launch</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>{}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon.icns</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
</dict>
|
||||
</plist>
|
||||
"#,
|
||||
macos_shortcut_identifier(launch_url.as_str()),
|
||||
escape_xml(&format!("Launch {profile_name}")),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let mut permissions =
|
||||
tokio::fs::metadata(&executable_path).await?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
tokio::fs::set_permissions(&executable_path, permissions).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn macos_shortcut_identifier(launch_url: &str) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
launch_url.hash(&mut hasher);
|
||||
|
||||
format!("com.modrinth.instance-shortcut.{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
fn shell_quote(input: &str) -> String {
|
||||
format!("'{}'", input.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
fn escape_xml(input: &str) -> String {
|
||||
input
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
use crate::api::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::Runtime;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use linux::{SHORTCUT_EXTENSION, create_shortcut};
|
||||
#[cfg(target_os = "macos")]
|
||||
use macos::{SHORTCUT_EXTENSION, create_shortcut};
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows::{SHORTCUT_EXTENSION, create_shortcut};
|
||||
|
||||
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("shortcuts")
|
||||
.invoke_handler(tauri::generate_handler![create_instance_shortcut])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_instance_shortcut(
|
||||
instance_name: String,
|
||||
instance_id: String,
|
||||
output_path: PathBuf,
|
||||
server: Option<String>,
|
||||
singleplayer_world: Option<String>,
|
||||
) -> Result<PathBuf> {
|
||||
if server.is_some() && singleplayer_world.is_some() {
|
||||
return Err(std::io::Error::other(
|
||||
"shortcut cannot launch both a server and a singleplayer world",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let launch_url =
|
||||
instance_launch_url(instance_id, server, singleplayer_world);
|
||||
|
||||
let output_path = shortcut_path_with_extension(output_path);
|
||||
let output_path_existed =
|
||||
tokio::fs::try_exists(&output_path).await.unwrap_or(false);
|
||||
|
||||
if let Err(error) =
|
||||
create_shortcut(&instance_name, &launch_url, &output_path).await
|
||||
{
|
||||
cleanup_shortcut_artifact(&output_path, output_path_existed).await;
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
fn instance_launch_url(
|
||||
instance_id: String,
|
||||
server: Option<String>,
|
||||
singleplayer_world: Option<String>,
|
||||
) -> Url {
|
||||
let mut launch_url = Url::parse("modrinth://launch/instance")
|
||||
.expect("static launch URL should parse");
|
||||
|
||||
launch_url
|
||||
.path_segments_mut()
|
||||
.expect("launch URL should support path segments")
|
||||
.push(&instance_id);
|
||||
|
||||
if let Some(server) = server {
|
||||
launch_url.query_pairs_mut().append_pair("server", &server);
|
||||
} else if let Some(singleplayer_world) = singleplayer_world {
|
||||
launch_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("singleplayer_world", &singleplayer_world);
|
||||
}
|
||||
|
||||
launch_url
|
||||
}
|
||||
|
||||
fn shortcut_path_with_extension(mut path: PathBuf) -> PathBuf {
|
||||
if path
|
||||
.extension()
|
||||
.is_none_or(|current_extension| current_extension != SHORTCUT_EXTENSION)
|
||||
{
|
||||
path.set_extension(SHORTCUT_EXTENSION);
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
async fn cleanup_shortcut_artifact(path: &Path, existed: bool) {
|
||||
if existed {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = match tokio::fs::metadata(path).await {
|
||||
Ok(metadata) if metadata.is_dir() => {
|
||||
tokio::fs::remove_dir_all(path).await
|
||||
}
|
||||
_ => tokio::fs::remove_file(path).await,
|
||||
};
|
||||
|
||||
if let Err(error) = result
|
||||
&& error.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
tracing::warn!(
|
||||
"failed to clean up shortcut artifact {}: {}",
|
||||
path.display(),
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
use crate::api::Result;
|
||||
use std::{
|
||||
os::windows::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use url::Url;
|
||||
use windows::{
|
||||
Win32::{
|
||||
System::Com::{
|
||||
CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
|
||||
COINIT_DISABLE_OLE1DDE, CoCreateInstance, CoInitializeEx,
|
||||
CoUninitialize, IPersistFile,
|
||||
},
|
||||
UI::Shell::{IShellLinkW, ShellLink},
|
||||
},
|
||||
core::{Interface, PCWSTR},
|
||||
};
|
||||
|
||||
pub(super) const SHORTCUT_EXTENSION: &str = "lnk";
|
||||
|
||||
pub(super) async fn create_shortcut(
|
||||
_profile_name: &str,
|
||||
launch_url: &Url,
|
||||
output_path: &Path,
|
||||
) -> Result<()> {
|
||||
let target_path = std::env::current_exe()?;
|
||||
let working_dir = target_path
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_default();
|
||||
let output_path = output_path.to_path_buf();
|
||||
let launch_url = launch_url.to_string();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
create_windows_shortcut(
|
||||
output_path,
|
||||
target_path,
|
||||
working_dir,
|
||||
launch_url,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
std::io::Error::other(format!(
|
||||
"failed to join shortcut creation task: {error}"
|
||||
))
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_windows_shortcut(
|
||||
output_path: PathBuf,
|
||||
target_path: PathBuf,
|
||||
working_dir: PathBuf,
|
||||
launch_url: String,
|
||||
) -> std::io::Result<()> {
|
||||
let output_path = windows_wide_path(&output_path);
|
||||
let target_path = windows_wide_path(&target_path);
|
||||
let working_dir = windows_wide_path(&working_dir);
|
||||
let launch_url = windows_wide_string(&launch_url);
|
||||
|
||||
// SAFETY:
|
||||
// - COM is initialized for this blocking thread before any COM object is created.
|
||||
// - `_com` is declared before the COM interface values, so it is dropped
|
||||
// after them and calls `CoUninitialize` only once they are released.
|
||||
// - Every PCWSTR points to a NUL-terminated UTF-16 buffer that lives until
|
||||
// each call using it has returned.
|
||||
unsafe {
|
||||
let init_result = CoInitializeEx(
|
||||
None,
|
||||
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
|
||||
);
|
||||
windows_result(init_result.ok())?;
|
||||
let _com = WindowsComGuard;
|
||||
|
||||
let shortcut: IShellLinkW = windows_result(CoCreateInstance(
|
||||
&ShellLink,
|
||||
None,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
))?;
|
||||
windows_result(shortcut.SetPath(windows_pcwstr(&target_path)))?;
|
||||
windows_result(shortcut.SetArguments(windows_pcwstr(&launch_url)))?;
|
||||
windows_result(
|
||||
shortcut.SetWorkingDirectory(windows_pcwstr(&working_dir)),
|
||||
)?;
|
||||
windows_result(
|
||||
shortcut.SetIconLocation(windows_pcwstr(&target_path), 0),
|
||||
)?;
|
||||
|
||||
let persist_file: IPersistFile = windows_result(shortcut.cast())?;
|
||||
windows_result(persist_file.Save(windows_pcwstr(&output_path), true))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn windows_result<T>(result: windows::core::Result<T>) -> std::io::Result<T> {
|
||||
result.map_err(std::io::Error::other)
|
||||
}
|
||||
|
||||
struct WindowsComGuard;
|
||||
|
||||
impl Drop for WindowsComGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
CoUninitialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_wide_path(path: &Path) -> Vec<u16> {
|
||||
path.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn windows_wide_string(value: &str) -> Vec<u16> {
|
||||
value.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
fn windows_pcwstr(value: &[u16]) -> PCWSTR {
|
||||
PCWSTR::from_raw(value.as_ptr())
|
||||
}
|
||||
@@ -140,18 +140,11 @@ pub async fn get_opening_command(
|
||||
state: tauri::State<'_, crate::macos::deep_link::InitialPayload>,
|
||||
) -> Result<Option<CommandPayload>> {
|
||||
let payload = state.payload.lock().await;
|
||||
let cmd_arg = std::env::args_os()
|
||||
.nth(1)
|
||||
.map(|path| path.to_string_lossy().to_string());
|
||||
|
||||
return if let Some(payload) = payload.as_ref() {
|
||||
tracing::info!("opening command {payload}");
|
||||
|
||||
Ok(Some(handler::parse_command(payload).await?))
|
||||
} else if let Some(cmd_arg) = cmd_arg {
|
||||
tracing::info!("opening command {cmd_arg:?}");
|
||||
|
||||
Ok(Some(handler::parse_command(&cmd_arg).await?))
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
|
||||
@@ -241,7 +241,6 @@ fn main() {
|
||||
.plugin(api::minecraft_skins::init())
|
||||
.plugin(api::process::init())
|
||||
.plugin(api::settings::init())
|
||||
.plugin(api::shortcuts::init())
|
||||
.plugin(api::tags::init())
|
||||
.plugin(api::utils::init())
|
||||
.plugin(api::cache::init())
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div
|
||||
v-for="passkey in passkeys"
|
||||
:key="passkey.id"
|
||||
class="universal-card recessed flex items-center justify-between"
|
||||
class="universal-card recessed passkey"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
@@ -54,45 +54,43 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled circular>
|
||||
<div class="passkey-actions flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.renameButton)"
|
||||
@click="
|
||||
() => {
|
||||
passkeyToRename = { ...passkey }
|
||||
renamePasskeyModal?.show()
|
||||
$refs.renamePasskeyModal.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<EditIcon />
|
||||
<EditIcon /> {{ formatMessage(commonMessages.renameButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.removeButton)"
|
||||
@click="
|
||||
() => {
|
||||
passkeyToRemove = passkey
|
||||
removePasskeyModal?.show()
|
||||
$refs.removePasskeyModal.show()
|
||||
}
|
||||
"
|
||||
>
|
||||
<TrashIcon />
|
||||
<TrashIcon /> {{ formatMessage(commonMessages.removeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="input-group self-end">
|
||||
<ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="registerPasskey()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.managePasskeyAddPasskey) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="managePasskeyModal?.hide()">
|
||||
<button @click="$refs.managePasskeyModal.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.closeButton) }}
|
||||
</button>
|
||||
@@ -108,30 +106,32 @@
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="text-md font-semibold text-contrast">
|
||||
{{ formatMessage(messages.passkeyNameLabel) }}
|
||||
</div>
|
||||
<label for="passkey-name">
|
||||
<div class="text-md mb-2 font-semibold text-contrast">
|
||||
{{ formatMessage(messages.passkeyNameLabel) }}
|
||||
</div>
|
||||
<div class="label__description mb-2">
|
||||
{{ formatMessage(messages.passkeyNameDescription) }}
|
||||
</div>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="passkey-name"
|
||||
v-model="pendingPasskeyName"
|
||||
v-model="pendingPasskey.name"
|
||||
:maxlength="255"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
|
||||
@keyup.enter="pendingPasskeyName && finishRegisterPasskey()"
|
||||
@keyup.enter="pendingPasskey.name && finishRegisterPasskey()"
|
||||
/>
|
||||
<div class="label__description">
|
||||
{{ formatMessage(messages.passkeyNameDescription) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="addPasskeyModal?.hide()">
|
||||
<button @click="$refs.addPasskeyModal.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!pendingPasskeyName" @click="finishRegisterPasskey()">
|
||||
<button :disabled="!pendingPasskey.name" @click="finishRegisterPasskey()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.managePasskeyAddPasskey) }}
|
||||
</button>
|
||||
@@ -145,32 +145,34 @@
|
||||
width="500px"
|
||||
:header="formatMessage(messages.renamePasskeyModalHeader)"
|
||||
>
|
||||
<div v-if="passkeyToRename" class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="text-md font-semibold text-contrast">
|
||||
{{ formatMessage(messages.passkeyNameLabel) }}
|
||||
</div>
|
||||
<label for="passkey-rename">
|
||||
<div class="text-md mb-2 font-semibold text-contrast">
|
||||
{{ formatMessage(messages.passkeyNameLabel) }}
|
||||
</div>
|
||||
<div class="label__description mb-2">
|
||||
{{ formatMessage(messages.passkeyNameDescription) }}
|
||||
</div>
|
||||
</label>
|
||||
<StyledInput
|
||||
id="passkey-rename"
|
||||
v-model="passkeyToRenameName"
|
||||
v-model="passkeyToRename.name"
|
||||
:maxlength="255"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.passkeyNamePlaceholder)"
|
||||
@keyup.enter="passkeyToRenameName && renamePasskey()"
|
||||
@keyup.enter="passkeyToRename.name && renamePasskey()"
|
||||
/>
|
||||
<div class="label__description">
|
||||
{{ formatMessage(messages.passkeyNameDescription) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="renamePasskeyModal?.hide()">
|
||||
<button @click="$refs.renamePasskeyModal.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!passkeyToRenameName" @click="renamePasskey()">
|
||||
<button :disabled="!passkeyToRename.name" @click="renamePasskey()">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveButton) }}
|
||||
</button>
|
||||
@@ -221,7 +223,6 @@ import {
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { createPasskeyCredential } from '~/helpers/passkey.ts'
|
||||
|
||||
@@ -261,7 +262,7 @@ const messages = defineMessages({
|
||||
},
|
||||
managePasskeyModalNoPasskeys: {
|
||||
id: 'settings.account.security.passkey.modal.no-passkeys',
|
||||
defaultMessage: 'You do not have any passkeys registered.',
|
||||
defaultMessage: 'You have no passkeys registered yet.',
|
||||
},
|
||||
managePasskeyModalAdded: {
|
||||
id: 'settings.account.security.passkey.modal.added',
|
||||
@@ -303,30 +304,13 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const removePasskeyModal = useTemplateRef<InstanceType<typeof ConfirmModal>>('removePasskeyModal')
|
||||
const managePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('managePasskeyModal')
|
||||
const addPasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('addPasskeyModal')
|
||||
const renamePasskeyModal = useTemplateRef<InstanceType<typeof NewModal>>('renamePasskeyModal')
|
||||
const managePasskeyModal = ref()
|
||||
const renamePasskeyModal = ref()
|
||||
const addPasskeyModal = ref()
|
||||
|
||||
const pendingPasskey = ref<Labrinth.Auth.v2.PasskeyRegisterFinishRequest | null>(null)
|
||||
const passkeyToRemove = ref<Labrinth.Auth.v2.Passkey | null>(null)
|
||||
const passkeyToRename = ref<Labrinth.Auth.v2.Passkey | null>(null)
|
||||
const pendingPasskeyName = computed({
|
||||
get: () => pendingPasskey.value?.name ?? '',
|
||||
set: (name) => {
|
||||
if (pendingPasskey.value) {
|
||||
pendingPasskey.value.name = name
|
||||
}
|
||||
},
|
||||
})
|
||||
const passkeyToRenameName = computed({
|
||||
get: () => passkeyToRename.value?.name ?? '',
|
||||
set: (name) => {
|
||||
if (passkeyToRename.value) {
|
||||
passkeyToRename.value.name = name
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const passkeys = ref<Labrinth.Auth.v2.Passkey[]>([])
|
||||
const passkeysLoading = ref(false)
|
||||
@@ -342,7 +326,7 @@ async function fetchPasskeys() {
|
||||
}
|
||||
|
||||
async function showPasskeyModal() {
|
||||
managePasskeyModal.value?.show()
|
||||
managePasskeyModal.value.show()
|
||||
await fetchPasskeys()
|
||||
}
|
||||
|
||||
@@ -358,7 +342,7 @@ async function registerPasskey() {
|
||||
name: '',
|
||||
}
|
||||
|
||||
addPasskeyModal.value?.show()
|
||||
addPasskeyModal.value.show()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
@@ -373,7 +357,7 @@ async function finishRegisterPasskey() {
|
||||
const passkey = await labrinth.auth_v2.registerPasskeyFinish(pendingPasskey.value)
|
||||
passkeys.value.unshift(passkey)
|
||||
pendingPasskey.value = null
|
||||
addPasskeyModal.value?.hide()
|
||||
addPasskeyModal.value.hide()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
@@ -392,7 +376,7 @@ async function renamePasskey() {
|
||||
existing.name = name
|
||||
}
|
||||
passkeyToRename.value = null
|
||||
renamePasskeyModal.value?.hide()
|
||||
renamePasskeyModal.value.hide()
|
||||
} catch (err) {
|
||||
notifyError(err)
|
||||
}
|
||||
@@ -414,3 +398,25 @@ async function removePasskey() {
|
||||
stopLoading()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.passkey {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
@media screen and (min-width: 650px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.passkey-actions {
|
||||
flex-direction: row;
|
||||
|
||||
@media screen and (min-width: 650px) {
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4128,7 +4128,7 @@
|
||||
"message": "Never used"
|
||||
},
|
||||
"settings.account.security.passkey.modal.no-passkeys": {
|
||||
"message": "You do not have any passkeys registered."
|
||||
"message": "You have no passkeys registered yet."
|
||||
},
|
||||
"settings.account.security.passkey.remove.description": {
|
||||
"message": "This will permanently remove the passkey \"{name}\". You will no longer be able to sign in with it."
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
@keyup.enter="saveEmail()"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.changeEmailModal.hide()">
|
||||
<XIcon />
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.managePasswordModal.hide()">
|
||||
<XIcon />
|
||||
@@ -201,7 +201,7 @@
|
||||
<p v-if="twoFactorIncorrect" class="known-errors m-0">
|
||||
{{ formatMessage(messages.twoFactorIncorrectError) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.manageTwoFactorModal.hide()">
|
||||
<XIcon />
|
||||
@@ -279,7 +279,7 @@
|
||||
<li v-for="code in backupCodes" :key="code">{{ code }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled v-if="twoFactorStep === 1">
|
||||
<button @click="twoFactorStep = 0">
|
||||
<LeftArrowIcon />
|
||||
|
||||
@@ -526,11 +526,4 @@ yahoo.com, 100033, DIRECT, e1a5b5b6e3255540
|
||||
|
||||
#IndexExchange
|
||||
indexexchange.com, 184169, DIRECT, 50b1c356f2c5c8fc
|
||||
indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc
|
||||
|
||||
#AppNexus
|
||||
Xandr.com, 2398, DIRECT
|
||||
|
||||
#Rubicon
|
||||
rubiconproject.com, 24584, DIRECT, 0bfd66d529a55807
|
||||
rubiconproject.com, 24586, DIRECT, 0bfd66d529a55807
|
||||
indexexchange.com, 198988, DIRECT, 50b1c356f2c5c8fc
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = CASE\n\t\t\t\tWHEN submitted_time_played < 0 THEN ?\n\t\t\t\tWHEN submitted_time_played > ? THEN ?\n\t\t\t\tELSE submitted_time_played + ?\n\t\t\tEND,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3af5ce6fdc84ce12da06b92950d0249bdc4023c0b3d0c683318079ce93c9dd56"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\tsubmitted_time_played = submitted_time_played + ?,\n\t\t\trecent_time_played = 0,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "44f82d6bbf66f685e20bee5c9c58587c035eb94098831c690739393870d4e814"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tUPDATE instances\n\t\tSET recent_time_played = recent_time_played + ?, modified = ?\n\t\tWHERE id = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8c0ef949dc78aa5d58b8fcd3e04c9c425cfac0520785d4e25d687c2d9ea82088"
|
||||
}
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmissing = 0,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c5f78eaae528b6f930bcc9ade15384564c86cd98a1e407515ea718127a3b1777"
|
||||
}
|
||||
Generated
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tUPDATE instances\n\t\tSET\n\t\t\trecent_time_played = CASE\n\t\t\t\tWHEN recent_time_played < 0 THEN ?\n\t\t\t\tWHEN recent_time_played > ? THEN ?\n\t\t\t\tELSE recent_time_played + ?\n\t\t\tEND,\n\t\t\tmodified = ?\n\t\tWHERE id = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d0bdfcba97a212ad80f56c1884ec2f984ce07a6d9498f19c24c6e2c031122fc5"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n\t\tUPDATE instance_files\n\t\tSET\n\t\t\trelative_path = ?,\n\t\t\tfile_name = ?,\n\t\t\tenabled = ?,\n\t\t\tmodified_at = ?\n\t\tWHERE instance_id = ? AND relative_path = ?\n\t\t",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f93f5fca4f004dc70cab05f879993eb92fa6199c7356bbc2c5ca36468f916380"
|
||||
}
|
||||
@@ -105,7 +105,6 @@ tracing = { workspace = true }
|
||||
tracing-error = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["chrono", "env-filter"] }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
whoami = { workspace = true }
|
||||
zbus = { workspace = true }
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
UPDATE instances
|
||||
SET
|
||||
submitted_time_played = CASE
|
||||
WHEN submitted_time_played < 0 THEN 0
|
||||
ELSE submitted_time_played
|
||||
END,
|
||||
recent_time_played = CASE
|
||||
WHEN recent_time_played < 0 THEN 0
|
||||
ELSE recent_time_played
|
||||
END
|
||||
WHERE submitted_time_played < 0
|
||||
OR recent_time_played < 0;
|
||||
@@ -7,8 +7,6 @@ use crate::{
|
||||
},
|
||||
util::io,
|
||||
};
|
||||
use url::form_urlencoded;
|
||||
use urlencoding::decode;
|
||||
|
||||
/// Handles external functions (such as through URL deep linkage)
|
||||
/// Link is extracted value (link) in somewhat URL format, such as
|
||||
@@ -30,53 +28,6 @@ pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
|
||||
Some(("server", id)) => {
|
||||
CommandPayload::InstallServer { id: id.to_string() }
|
||||
}
|
||||
// /launch/instance/{id} - Launches an instance
|
||||
Some(("launch", rest)) if rest.starts_with("instance/") => {
|
||||
let raw = rest.trim_start_matches("instance/");
|
||||
let (raw, query) = raw.split_once('?').unwrap_or((raw, ""));
|
||||
let mut server = None;
|
||||
let mut singleplayer_world = None;
|
||||
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match &*key {
|
||||
"server" => server = Some(value.into_owned()),
|
||||
"singleplayer_world" => {
|
||||
singleplayer_world = Some(value.into_owned());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if server.is_some() && singleplayer_world.is_some() {
|
||||
emit_warning(
|
||||
"Invalid command, cannot launch both a server and a singleplayer world",
|
||||
)
|
||||
.await?;
|
||||
return Err(crate::ErrorKind::InputError(
|
||||
"Cannot launch both a server and a singleplayer world"
|
||||
.to_string(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
match decode(raw) {
|
||||
Ok(decoded) => CommandPayload::LaunchInstance {
|
||||
id: decoded.to_string(),
|
||||
server,
|
||||
singleplayer_world,
|
||||
},
|
||||
Err(e) => {
|
||||
emit_warning(&format!(
|
||||
"Invalid UTF-8 in instance path: {e}"
|
||||
))
|
||||
.await?;
|
||||
return Err(crate::ErrorKind::InputError(format!(
|
||||
"Invalid UTF-8 in instance path: {e}"
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
emit_warning(&format!(
|
||||
"Invalid command, unrecognized path: {sublink}"
|
||||
|
||||
@@ -250,15 +250,9 @@ impl Logs {
|
||||
output: if clear_contents.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
let state = State::get().await?;
|
||||
Some(
|
||||
get_output_by_filename_from_path(
|
||||
&state,
|
||||
instance_path,
|
||||
log_type,
|
||||
&filename,
|
||||
)
|
||||
.await?,
|
||||
get_output_by_filename(instance_path, log_type, &filename)
|
||||
.await?,
|
||||
)
|
||||
},
|
||||
filename,
|
||||
@@ -364,12 +358,14 @@ pub async fn get_logs_by_filename(
|
||||
Logs::build(log_type, age, &instance_path, filename, Some(true)).await
|
||||
}
|
||||
|
||||
async fn get_output_by_filename_from_path(
|
||||
state: &State,
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_filename(
|
||||
instance_path: &str,
|
||||
log_type: LogType,
|
||||
file_name: &str,
|
||||
) -> crate::Result<CensoredString> {
|
||||
let state = State::get().await?;
|
||||
|
||||
let logs_folder = match log_type {
|
||||
LogType::InfoLog => state.directories.instance_logs_dir(instance_path),
|
||||
LogType::CrashReport => {
|
||||
@@ -413,23 +409,6 @@ async fn get_output_by_filename_from_path(
|
||||
.into())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_output_by_filename(
|
||||
instance_id: &str,
|
||||
log_type: LogType,
|
||||
file_name: &str,
|
||||
) -> crate::Result<CensoredString> {
|
||||
let state = State::get().await?;
|
||||
let instance_path = resolve_instance_path(instance_id, &state).await?;
|
||||
get_output_by_filename_from_path(
|
||||
&state,
|
||||
&instance_path,
|
||||
log_type,
|
||||
file_name,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(instance_id: &str) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
|
||||
@@ -229,11 +229,6 @@ pub enum CommandPayload {
|
||||
InstallServer {
|
||||
id: String,
|
||||
},
|
||||
LaunchInstance {
|
||||
id: String,
|
||||
server: Option<String>,
|
||||
singleplayer_world: Option<String>,
|
||||
},
|
||||
RunMRPack {
|
||||
// run or install .mrpack
|
||||
path: PathBuf,
|
||||
|
||||
@@ -117,7 +117,6 @@ impl ModLoader {
|
||||
pub struct ContentFile {
|
||||
pub hash: String,
|
||||
pub file_name: String,
|
||||
pub enabled: bool,
|
||||
pub size: u64,
|
||||
pub metadata: Option<FileMetadata>,
|
||||
pub update_version_id: Option<String>,
|
||||
|
||||
@@ -627,7 +627,6 @@ pub(crate) async fn rename_instance_file(
|
||||
relative_path = ?,
|
||||
file_name = ?,
|
||||
enabled = ?,
|
||||
missing = 0,
|
||||
modified_at = ?
|
||||
WHERE instance_id = ? AND relative_path = ?
|
||||
",
|
||||
@@ -857,11 +856,11 @@ pub(crate) async fn set_content_entry_enabled_for_file(
|
||||
file_id: &str,
|
||||
enabled: bool,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<bool> {
|
||||
) -> crate::Result<()> {
|
||||
let enabled = i64::from(enabled);
|
||||
let modified_at = Utc::now().timestamp();
|
||||
|
||||
let result = sqlx::query!(
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE instance_content_entries
|
||||
SET enabled = ?, modified_at = ?
|
||||
@@ -875,7 +874,7 @@ pub(crate) async fn set_content_entry_enabled_for_file(
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_content_entries_for_file(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::state::instances::{
|
||||
ContentSet, ContentSetStatus, ContentSourceKind, Instance,
|
||||
InstanceLaunchContext, InstanceLaunchOverrides,
|
||||
InstanceLaunchOverridesData, InstanceLink, playtime_to_storage,
|
||||
InstanceLaunchOverridesData, InstanceLink,
|
||||
};
|
||||
use crate::state::{
|
||||
InstanceInstallStage, LauncherFeatureVersion, ModLoader, ReleaseChannel,
|
||||
@@ -831,12 +831,8 @@ pub(crate) async fn insert_instance(
|
||||
let created = instance.created.timestamp();
|
||||
let modified = instance.modified.timestamp();
|
||||
let last_played = instance.last_played.map(|value| value.timestamp());
|
||||
let submitted_time_played = playtime_to_storage(
|
||||
instance.submitted_time_played,
|
||||
"submitted_time_played",
|
||||
)?;
|
||||
let recent_time_played =
|
||||
playtime_to_storage(instance.recent_time_played, "recent_time_played")?;
|
||||
let submitted_time_played = instance.submitted_time_played as i64;
|
||||
let recent_time_played = instance.recent_time_played as i64;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -891,12 +887,8 @@ pub(crate) async fn update_instance(
|
||||
let icon_path = instance.icon_path.as_deref();
|
||||
let modified = instance.modified.timestamp();
|
||||
let last_played = instance.last_played.map(|value| value.timestamp());
|
||||
let submitted_time_played = playtime_to_storage(
|
||||
instance.submitted_time_played,
|
||||
"submitted_time_played",
|
||||
)?;
|
||||
let recent_time_played =
|
||||
playtime_to_storage(instance.recent_time_played, "recent_time_played")?;
|
||||
let submitted_time_played = instance.submitted_time_played as i64;
|
||||
let recent_time_played = instance.recent_time_played as i64;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
|
||||
@@ -661,31 +661,13 @@ pub(crate) async fn toggle_disable_project(
|
||||
}
|
||||
None => index_existing_file(&scope, &new_path, state).await?,
|
||||
};
|
||||
let updated_entry = content_rows::set_content_entry_enabled_for_file(
|
||||
content_rows::set_content_entry_enabled_for_file(
|
||||
&scope.content_set_id,
|
||||
&file.id,
|
||||
enabled,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
if !updated_entry {
|
||||
let project_type = ProjectType::get_from_parent_folder(&new_path)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Unable to infer project type from {new_path}"
|
||||
))
|
||||
})?;
|
||||
upsert_entry_for_file(
|
||||
&scope,
|
||||
&file,
|
||||
project_type,
|
||||
None,
|
||||
None,
|
||||
ContentSourceKind::Local,
|
||||
&state.pool,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(new_path)
|
||||
}
|
||||
|
||||
@@ -281,31 +281,12 @@ async fn plan_bulk_update(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<BulkUpdatePlan> {
|
||||
let updateable_paths =
|
||||
bulk_updateable_project_paths(instance_id, state).await?;
|
||||
if updateable_paths.is_empty() {
|
||||
return Ok(BulkUpdatePlan {
|
||||
project_updates: Vec::new(),
|
||||
dependency_additions: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let updates = check_content_updates(
|
||||
instance_id,
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
state,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|update| updateable_paths.contains(&update.relative_path))
|
||||
.collect::<Vec<_>>();
|
||||
if updates.is_empty() {
|
||||
return Ok(BulkUpdatePlan {
|
||||
project_updates: Vec::new(),
|
||||
dependency_additions: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
.await?;
|
||||
let content_set =
|
||||
content_rows::get_applied_content_set(instance_id, &state.pool)
|
||||
.await?
|
||||
@@ -339,7 +320,6 @@ async fn plan_bulk_update(
|
||||
.collect::<HashMap<_, _>>();
|
||||
let version_ids = installed
|
||||
.iter()
|
||||
.filter(|project| updateable_paths.contains(&project.relative_path))
|
||||
.filter_map(|project| project.version_id.clone())
|
||||
.chain(
|
||||
updates
|
||||
@@ -363,7 +343,6 @@ async fn plan_bulk_update(
|
||||
let planned_versions = installed
|
||||
.iter()
|
||||
.filter(|project| project.enabled)
|
||||
.filter(|project| updateable_paths.contains(&project.relative_path))
|
||||
.filter_map(|project| {
|
||||
let target_version_id = updates_by_path
|
||||
.get(&project.relative_path)
|
||||
@@ -400,21 +379,6 @@ async fn plan_bulk_update(
|
||||
})
|
||||
}
|
||||
|
||||
async fn bulk_updateable_project_paths(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<HashSet<String>> {
|
||||
let items = super::list_content::list_content(
|
||||
instance_id,
|
||||
None,
|
||||
Some(CacheBehaviour::MustRevalidate),
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(items.into_iter().map(|item| item.file_path).collect())
|
||||
}
|
||||
|
||||
async fn installed_projects(
|
||||
instance_id: &str,
|
||||
content_set: &ContentSet,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::state::InstanceInstallStage;
|
||||
use crate::state::instances::{
|
||||
InstanceLaunchContext, adapters::sqlite::instance_rows, playtime_to_storage,
|
||||
InstanceLaunchContext, adapters::sqlite::instance_rows,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::SqlitePool;
|
||||
@@ -120,31 +120,16 @@ pub(crate) async fn add_instance_recent_playtime(
|
||||
seconds: u64,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
if seconds == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let seconds = playtime_to_storage(seconds, "recent_time_played")?;
|
||||
let max_playtime = i64::MAX;
|
||||
let max_playtime_before_increment = max_playtime - seconds;
|
||||
let seconds = seconds as i64;
|
||||
let modified = Utc::now().timestamp();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE instances
|
||||
SET
|
||||
recent_time_played = CASE
|
||||
WHEN recent_time_played < 0 THEN ?
|
||||
WHEN recent_time_played > ? THEN ?
|
||||
ELSE recent_time_played + ?
|
||||
END,
|
||||
modified = ?
|
||||
SET recent_time_played = recent_time_played + ?, modified = ?
|
||||
WHERE id = ?
|
||||
",
|
||||
seconds,
|
||||
max_playtime_before_increment,
|
||||
max_playtime,
|
||||
seconds,
|
||||
modified,
|
||||
instance_id,
|
||||
)
|
||||
@@ -159,33 +144,19 @@ pub(crate) async fn mark_instance_playtime_submitted(
|
||||
recent_time_played: u64,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
if recent_time_played == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let recent_time_played =
|
||||
playtime_to_storage(recent_time_played, "recent_time_played")?;
|
||||
let max_playtime = i64::MAX;
|
||||
let max_playtime_before_increment = max_playtime - recent_time_played;
|
||||
let recent_time_played = recent_time_played as i64;
|
||||
let modified = Utc::now().timestamp();
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE instances
|
||||
SET
|
||||
submitted_time_played = CASE
|
||||
WHEN submitted_time_played < 0 THEN ?
|
||||
WHEN submitted_time_played > ? THEN ?
|
||||
ELSE submitted_time_played + ?
|
||||
END,
|
||||
submitted_time_played = submitted_time_played + ?,
|
||||
recent_time_played = 0,
|
||||
modified = ?
|
||||
WHERE id = ?
|
||||
",
|
||||
recent_time_played,
|
||||
max_playtime_before_increment,
|
||||
max_playtime,
|
||||
recent_time_played,
|
||||
modified,
|
||||
instance_id,
|
||||
)
|
||||
|
||||
@@ -208,8 +208,6 @@ pub(crate) async fn list_content(
|
||||
)
|
||||
.await?;
|
||||
let imported_modpack_scope = is_imported_modpack_scope(&resolved, &link);
|
||||
let linked_modpack_source_kind = linked_modpack_source_kind(&link);
|
||||
let mut failed_modpack_identifier_lookup = false;
|
||||
let modpack_ids = if imported_modpack_scope {
|
||||
None
|
||||
} else {
|
||||
@@ -228,7 +226,6 @@ pub(crate) async fn list_content(
|
||||
"Failed to fetch modpack identifiers: {}",
|
||||
err
|
||||
);
|
||||
failed_modpack_identifier_lookup = true;
|
||||
None
|
||||
}
|
||||
},
|
||||
@@ -243,12 +240,6 @@ pub(crate) async fn list_content(
|
||||
}
|
||||
} else if let Some(ids) = modpack_ids.as_ref() {
|
||||
ContentFilter::ExcludeModpack(ids)
|
||||
} else if failed_modpack_identifier_lookup {
|
||||
ContentFilter::ExcludeSourceKind {
|
||||
source_kind: linked_modpack_source_kind
|
||||
.unwrap_or(ContentSourceKind::ModrinthModpack),
|
||||
exclude_untracked: true,
|
||||
}
|
||||
} else {
|
||||
ContentFilter::All
|
||||
};
|
||||
@@ -744,9 +735,6 @@ async fn content_projects_for_scope(
|
||||
update_version_id,
|
||||
hash: file.sha1,
|
||||
file_name: file.file_name,
|
||||
enabled: entry.map_or(file.enabled, |entry| {
|
||||
entry.enabled && file.enabled
|
||||
}),
|
||||
size: file.size,
|
||||
metadata: file_metadata_from_entry_or_cache(entry, metadata),
|
||||
project_type,
|
||||
@@ -894,7 +882,7 @@ async fn content_files_to_content_items(
|
||||
file_path: path.clone(),
|
||||
id: file.hash.clone(),
|
||||
size: file.size,
|
||||
enabled: file.enabled,
|
||||
enabled: !file.file_name.ends_with(".disabled"),
|
||||
project_type: file.project_type,
|
||||
project: project.map(|project| ContentItemProject {
|
||||
id: project.id.clone(),
|
||||
@@ -1109,20 +1097,6 @@ fn linked_modpack_ids(link: &InstanceLink) -> Option<(String, String)> {
|
||||
}
|
||||
}
|
||||
|
||||
fn linked_modpack_source_kind(
|
||||
link: &InstanceLink,
|
||||
) -> Option<ContentSourceKind> {
|
||||
match link {
|
||||
InstanceLink::ModrinthModpack { .. } => {
|
||||
Some(ContentSourceKind::ModrinthModpack)
|
||||
}
|
||||
InstanceLink::ServerProjectModpack { .. } => {
|
||||
Some(ContentSourceKind::ServerProject)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_modpack_update(
|
||||
installed_version_id: &str,
|
||||
installed_version: &Version,
|
||||
|
||||
@@ -39,18 +39,9 @@ pub(crate) async fn sync_instance_content_files(
|
||||
&state.api_semaphore,
|
||||
)
|
||||
.await?;
|
||||
let hashes_by_key = hashes
|
||||
let hashes_by_path = hashes
|
||||
.into_iter()
|
||||
.map(|hash| {
|
||||
(
|
||||
format!(
|
||||
"{}-{}",
|
||||
hash.size,
|
||||
hash.path.trim_end_matches(".disabled")
|
||||
),
|
||||
hash,
|
||||
)
|
||||
})
|
||||
.map(|hash| (hash.path.clone(), hash))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let existing_files =
|
||||
sqlite::content_rows::get_instance_files(&instance.id, &state.pool)
|
||||
@@ -64,8 +55,8 @@ pub(crate) async fn sync_instance_content_files(
|
||||
let mut files = Vec::new();
|
||||
|
||||
for file in scanned {
|
||||
let hash_key = file.hash_cache_key.trim_end_matches(".disabled");
|
||||
let Some(hash) = hashes_by_key.get(hash_key) else {
|
||||
let cache_path = format!("{}/{}", instance.path, file.relative_path);
|
||||
let Some(hash) = hashes_by_path.get(&cache_path) else {
|
||||
continue;
|
||||
};
|
||||
let existing_file = existing_files_by_path.get(&file.relative_path);
|
||||
|
||||
@@ -20,15 +20,3 @@ pub struct Instance {
|
||||
pub submitted_time_played: u64,
|
||||
pub recent_time_played: u64,
|
||||
}
|
||||
|
||||
pub(crate) fn playtime_to_storage(
|
||||
value: u64,
|
||||
column: &str,
|
||||
) -> crate::Result<i64> {
|
||||
i64::try_from(value).map_err(|_| {
|
||||
crate::ErrorKind::InputError(format!(
|
||||
"Expected {column} to fit in SQLite INTEGER"
|
||||
))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::jre::check_jre;
|
||||
use crate::prelude::ModLoader;
|
||||
use crate::state;
|
||||
use crate::state::instances::{
|
||||
InstanceLaunchOverrides, InstanceLaunchOverridesData, playtime_to_storage,
|
||||
InstanceLaunchOverrides, InstanceLaunchOverridesData,
|
||||
};
|
||||
use crate::state::{
|
||||
CacheValue, CachedEntry, CachedFile, CachedFileHash, CachedFileUpdate,
|
||||
@@ -443,12 +443,8 @@ where
|
||||
let created = input.created.timestamp();
|
||||
let modified = input.modified.timestamp();
|
||||
let last_played = input.last_played.map(|value| value.timestamp());
|
||||
let submitted_time_played = playtime_to_storage(
|
||||
input.submitted_time_played,
|
||||
"submitted_time_played",
|
||||
)?;
|
||||
let recent_time_played =
|
||||
playtime_to_storage(input.recent_time_played, "recent_time_played")?;
|
||||
let submitted_time_played = input.submitted_time_played as i64;
|
||||
let recent_time_played = input.recent_time_played as i64;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
|
||||
@@ -17,7 +17,6 @@ use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Instant;
|
||||
#[cfg(feature = "tauri")]
|
||||
use tauri::Emitter;
|
||||
use tempfile::TempDir;
|
||||
@@ -753,46 +752,46 @@ impl Process {
|
||||
uuid: Uuid,
|
||||
) -> crate::Result<()> {
|
||||
async fn update_playtime(
|
||||
last_updated_playtime: &mut Instant,
|
||||
last_updated_playtime: &mut DateTime<Utc>,
|
||||
instance_id: &str,
|
||||
force_update: bool,
|
||||
) {
|
||||
let elapsed = last_updated_playtime.elapsed().as_secs();
|
||||
if elapsed == 0 || (!force_update && elapsed < 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = match crate::State::get().await {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
let diff = Utc::now()
|
||||
.signed_duration_since(*last_updated_playtime)
|
||||
.num_seconds();
|
||||
if diff >= 60 || force_update {
|
||||
let state = match crate::State::get().await {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Failed to get state for playtime update on instance {}: {}",
|
||||
instance_id,
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) =
|
||||
crate::state::instances::commands::add_instance_recent_playtime(
|
||||
instance_id,
|
||||
diff as u64,
|
||||
&state.pool,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to get state for playtime update on instance {}: {}",
|
||||
"Failed to update playtime for instance {}: {}",
|
||||
instance_id,
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) =
|
||||
crate::state::instances::commands::add_instance_recent_playtime(
|
||||
instance_id,
|
||||
elapsed,
|
||||
&state.pool,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"Failed to update playtime for instance {}: {}",
|
||||
instance_id,
|
||||
e
|
||||
);
|
||||
*last_updated_playtime = Utc::now();
|
||||
}
|
||||
*last_updated_playtime = Instant::now();
|
||||
}
|
||||
|
||||
// Wait on current Minecraft Child
|
||||
let mc_exit_status;
|
||||
let mut last_updated_playtime = Instant::now();
|
||||
let mut last_updated_playtime = Utc::now();
|
||||
|
||||
let state = crate::State::get().await?;
|
||||
loop {
|
||||
|
||||
@@ -63,7 +63,6 @@ pub enum FeatureFlag {
|
||||
ServerProjectQa,
|
||||
I18nDebug,
|
||||
ShowInstancePlayTime,
|
||||
SkipNonEssentialWarnings,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
||||
@@ -10,45 +10,6 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2026-06-25T21:22:14+00:00`,
|
||||
product: 'app',
|
||||
version: '0.15.0',
|
||||
body: `## Added
|
||||
- Existing app data is now backed up after the app updates.
|
||||
- Added an option to open the folder which contains the app database backups in the Resource management tab in the app settings.
|
||||
- Added an option to skip non-essential warnings across the app.
|
||||
- Added the ability to create desktop shortcuts for your instances.
|
||||
|
||||
## Changed
|
||||
- Instance installs in the Modrinth App now show detailed progress for preparing Java, downloading modpack files, downloading content, extracting overrides, and downloading Minecraft.
|
||||
- Java, modpack, content, overrides and Minecraft downloads/extracting now show downloaded/extracted and total size.
|
||||
- The Content tab’s "Update all" action now shows live progress while resolving versions, downloading projects, and finishing updates.
|
||||
- Changed the visual style of the Delete server/Delete world modal on the Worlds tab of instances to match the Content tab's modals.
|
||||
|
||||
## Fixed
|
||||
- Fixed broken Delete server modal on the Worlds tab of instances.
|
||||
- Fixed the navigation bars across the app not animating when clicking between pages.
|
||||
- Fixed fade in animation incorrectly playing on the Mods page even when the instance content is already loaded into the UI.
|
||||
- Failed or interrupted instance installs now roll back partial changes instead of leaving incomplete new instances or modified/broken existing instances behind.
|
||||
- Failed or interrupted instance installs now explain why the failure or interruption occurred, and provide a retry action.
|
||||
- The Home page, Library page, and the quick instance switcher in the left sidebar now correctly refresh when an instance begins or finishes installing.
|
||||
- Fixed issue where going from an instance page to the browse page via the left sidebar would cause the app to completely freeze up.
|
||||
- Fixed issue where using the "Add to instance" button on search results in the browse page would take a long time to process if you have a lot of instances.
|
||||
- Fixed many performance issues with loading/editing of instances.`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-25T21:22:14+00:00`,
|
||||
product: 'web',
|
||||
body: `## Added
|
||||
- Create account page where user can input username for new account.
|
||||
- Log in with passkeys support, thanks [@DeDiamondPro](https://github.com/DeDiamondPro)!
|
||||
|
||||
## Changed
|
||||
- Refreshed sign in and sign up page styles.
|
||||
- Refreshed forget password and verify email pages.
|
||||
- Updated account settings modals to match website styles for consistency.`,
|
||||
},
|
||||
{
|
||||
date: `2026-06-24T00:30:00-07:00`,
|
||||
product: 'web',
|
||||
|
||||
@@ -148,135 +148,122 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<NewModal ref="shareModal" :header="header" :noblur="noblur" :on-hide="onHide">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div
|
||||
:class="['flex items-center justify-center', link ? 'flex-wrap gap-4' : 'flex-col gap-2']"
|
||||
<div class="flex flex-col flex-wrap items-center gap-2">
|
||||
<div v-if="link" class="group relative mx-auto">
|
||||
<div ref="qrCode">
|
||||
<QrcodeVue :value="url" class="!bg-white rounded-[var(--radius-md)]" margin="3" />
|
||||
</div>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy QR code'"
|
||||
class="absolute top-0 right-0 m-2"
|
||||
aria-label="Copy QR code"
|
||||
@click="copyImage"
|
||||
>
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-else
|
||||
v-model="content"
|
||||
multiline
|
||||
resize="vertical"
|
||||
wrapper-class="h-full w-[30rem]"
|
||||
>
|
||||
<div v-if="link" class="group relative shrink-0">
|
||||
<div ref="qrCode">
|
||||
<QrcodeVue :value="url" class="!bg-white rounded-[var(--radius-md)]" margin="3" />
|
||||
</div>
|
||||
<template #right>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy QR code'"
|
||||
v-tooltip="'Copy Text'"
|
||||
type="button"
|
||||
aria-label="Copy Text"
|
||||
class="absolute top-0 right-0 m-2"
|
||||
aria-label="Copy QR code"
|
||||
@click="copyImage"
|
||||
@click="copyText"
|
||||
>
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-else
|
||||
v-model="content"
|
||||
multiline
|
||||
resize="vertical"
|
||||
wrapper-class="h-full w-[30rem]"
|
||||
</template>
|
||||
</StyledInput>
|
||||
<div class="flex flex-grow flex-col justify-center gap-2">
|
||||
<button
|
||||
v-if="link"
|
||||
v-tooltip="'Copy Link'"
|
||||
type="button"
|
||||
aria-label="Copy Link"
|
||||
class="flex h-10 w-full cursor-pointer items-center justify-between gap-2 rounded-xl border-none bg-button-bg px-3 pr-1.5 text-primary transition-all hover:bg-button-bg-hover hover:brightness-125 active:scale-95"
|
||||
@click="copyText"
|
||||
>
|
||||
<template #right>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="'Copy Text'"
|
||||
type="button"
|
||||
aria-label="Copy Text"
|
||||
class="absolute top-0 right-0 m-2"
|
||||
@click="copyText"
|
||||
>
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<div
|
||||
v-if="link || socialButtons"
|
||||
:class="['flex flex-col justify-center gap-2', link ? 'w-64 max-w-full' : 'flex-grow']"
|
||||
>
|
||||
<button
|
||||
v-if="link"
|
||||
v-tooltip="'Copy Link'"
|
||||
type="button"
|
||||
aria-label="Copy Link"
|
||||
class="flex h-10 w-full cursor-pointer items-center justify-between gap-2 rounded-xl border-none bg-button-bg px-3 pr-1.5 text-primary transition-all hover:bg-button-bg-hover hover:brightness-125 active:scale-95"
|
||||
@click="copyText"
|
||||
>
|
||||
<span class="min-w-0 cursor-pointer truncate text-left font-semibold text-primary">
|
||||
{{ url }}
|
||||
</span>
|
||||
<div class="grid h-10 w-10 place-content-center">
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</button>
|
||||
<ButtonStyled v-if="link">
|
||||
<span class="cursor-pointer truncate text-left font-semibold text-primary">
|
||||
{{ url }}
|
||||
</span>
|
||||
<div class="grid h-10 w-10 place-content-center">
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</button>
|
||||
<ButtonStyled v-if="link">
|
||||
<a :href="url" target="_blank" rel="noopener noreferrer" aria-label="Open in new tab">
|
||||
Open in new tab
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<div v-if="socialButtons" class="flex flex-row gap-1">
|
||||
<ButtonStyled v-if="canShare" circular>
|
||||
<button v-tooltip="'Share'" aria-label="Share" @click="share">
|
||||
<ShareIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Open in new tab"
|
||||
class="w-full"
|
||||
v-tooltip="'Send as an email'"
|
||||
:href="sendEmail"
|
||||
:target="targetParameter"
|
||||
aria-label="Send as an email"
|
||||
>
|
||||
Open in new tab
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
<MailIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-if="link"
|
||||
v-tooltip="'Open link in browser'"
|
||||
:target="targetParameter"
|
||||
:href="url"
|
||||
aria-label="Open link in browser"
|
||||
>
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Toot about it'"
|
||||
:target="targetParameter"
|
||||
:href="sendToot"
|
||||
aria-label="Toot about it"
|
||||
>
|
||||
<MastodonIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Tweet about it'"
|
||||
:target="targetParameter"
|
||||
:href="sendTweet"
|
||||
aria-label="Tweet about it"
|
||||
>
|
||||
<TwitterIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Share on Reddit'"
|
||||
:target="targetParameter"
|
||||
:href="postOnReddit"
|
||||
aria-label="Share on Reddit"
|
||||
>
|
||||
<RedditIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<div v-if="socialButtons" class="flex flex-row gap-1">
|
||||
<ButtonStyled v-if="canShare" circular>
|
||||
<button v-tooltip="'Share'" aria-label="Share" @click="share">
|
||||
<ShareIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Send as an email'"
|
||||
:href="sendEmail"
|
||||
:target="targetParameter"
|
||||
aria-label="Send as an email"
|
||||
>
|
||||
<MailIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-if="link"
|
||||
v-tooltip="'Open link in browser'"
|
||||
:target="targetParameter"
|
||||
:href="url"
|
||||
aria-label="Open link in browser"
|
||||
>
|
||||
<GlobeIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Toot about it'"
|
||||
:target="targetParameter"
|
||||
:href="sendToot"
|
||||
aria-label="Toot about it"
|
||||
>
|
||||
<MastodonIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Tweet about it'"
|
||||
:target="targetParameter"
|
||||
:href="sendTweet"
|
||||
aria-label="Tweet about it"
|
||||
>
|
||||
<TwitterIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
v-tooltip="'Share on Reddit'"
|
||||
:target="targetParameter"
|
||||
:href="postOnReddit"
|
||||
aria-label="Share on Reddit"
|
||||
>
|
||||
<RedditIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,11 +55,6 @@ async function handleBack() {
|
||||
if (!context) return
|
||||
|
||||
if (selectedCount.value > 0 && !isInstallingSelected.value) {
|
||||
if (context.skipNonEssentialWarnings) {
|
||||
await handleSelectedProjectsLeaveResult('discard', context)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await selectedProjectsLeaveModal.value?.prompt()
|
||||
await handleSelectedProjectsLeaveResult(result ?? 'cancel', context)
|
||||
return
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface BrowseInstallContext {
|
||||
onBack?: () => boolean | void | Promise<boolean | void>
|
||||
selectedProjects?: BrowseSelectedProject[]
|
||||
isInstallingSelected?: boolean
|
||||
skipNonEssentialWarnings?: boolean
|
||||
installProgress?: {
|
||||
completed: number
|
||||
total: number
|
||||
|
||||
@@ -151,7 +151,6 @@ const messages = defineMessages({
|
||||
})
|
||||
|
||||
const ctx = injectContentManager()
|
||||
const skipNonEssentialWarnings = computed(() => ctx.skipNonEssentialWarnings?.value ?? false)
|
||||
|
||||
function getItemId(item: ContentItem) {
|
||||
return ctx.getItemId?.(item) ?? item.file_path ?? item.file_name ?? item.id
|
||||
@@ -390,7 +389,7 @@ async function promptDeleteItems(items: ContentItem[], event?: MouseEvent) {
|
||||
}
|
||||
|
||||
function showDeletionConfirmation(event?: MouseEvent) {
|
||||
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
|
||||
if (event?.shiftKey && !ctx.isBusy.value) {
|
||||
confirmDelete()
|
||||
} else {
|
||||
confirmDeletionModal.value?.show()
|
||||
@@ -582,7 +581,7 @@ function promptUpdateAll(event?: MouseEvent) {
|
||||
if (items.length === 0) return
|
||||
pendingBulkUpdateItems.value = items
|
||||
pendingBulkUpdateAll.value = true
|
||||
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
|
||||
if (event?.shiftKey && !ctx.isBusy.value) {
|
||||
confirmBulkUpdate()
|
||||
} else {
|
||||
confirmBulkUpdateModal.value?.show()
|
||||
@@ -595,7 +594,7 @@ function promptUpdateSelected(event?: MouseEvent) {
|
||||
if (items.length === 0) return
|
||||
pendingBulkUpdateItems.value = items
|
||||
pendingBulkUpdateAll.value = false
|
||||
if ((event?.shiftKey || skipNonEssentialWarnings.value) && !ctx.isBusy.value) {
|
||||
if (event?.shiftKey && !ctx.isBusy.value) {
|
||||
confirmBulkUpdate()
|
||||
} else {
|
||||
confirmBulkUpdateModal.value?.show()
|
||||
|
||||
@@ -47,7 +47,6 @@ export interface ContentManagerContext {
|
||||
// Guards
|
||||
isBusy: Ref<boolean> | ComputedRef<boolean>
|
||||
busyMessage?: Ref<string | null> | ComputedRef<string | null>
|
||||
skipNonEssentialWarnings?: Ref<boolean> | ComputedRef<boolean>
|
||||
disableAddContent?: Ref<boolean> | ComputedRef<boolean>
|
||||
disableAddContentTooltip?: string
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import type { LoaderVersionEntry } from './types'
|
||||
const { formatMessage } = useVIntl()
|
||||
const ctx = injectInstallationSettings()
|
||||
const debug = useDebugLogger('InstallationSettingsLayout')
|
||||
const skipNonEssentialWarnings = computed(() => ctx.skipNonEssentialWarnings?.value ?? false)
|
||||
const availablePlatforms = computed(() =>
|
||||
Array.isArray(ctx.availablePlatforms) ? ctx.availablePlatforms : ctx.availablePlatforms.value,
|
||||
)
|
||||
@@ -229,7 +228,7 @@ function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event
|
||||
isUpdateDowngrade.value ||
|
||||
versionChangesGameVersion(version, ctx.updaterModalProps.value.currentGameVersion)
|
||||
|
||||
if (event?.shiftKey || skipNonEssentialWarnings.value || !shouldShowWarning) {
|
||||
if (event?.shiftKey || !shouldShowWarning) {
|
||||
debug('handleModpackUpdateRequest: confirming without warning', {
|
||||
isUpdateDowngrade: isUpdateDowngrade.value,
|
||||
shouldShowWarning,
|
||||
@@ -372,10 +371,6 @@ function handleShowRepairModal() {
|
||||
snapshot: stateSnapshot(),
|
||||
refs: modalRefsSnapshot(),
|
||||
})
|
||||
if (skipNonEssentialWarnings.value) {
|
||||
handleRepair()
|
||||
return
|
||||
}
|
||||
repairModal.value?.show()
|
||||
nextTick(() => {
|
||||
debug('handleShowRepairModal: after nextTick', {
|
||||
@@ -391,7 +386,7 @@ function handleShowUnlinkModal(event: MouseEvent) {
|
||||
snapshot: stateSnapshot(),
|
||||
refs: modalRefsSnapshot(),
|
||||
})
|
||||
if (event.shiftKey || skipNonEssentialWarnings.value) {
|
||||
if (event.shiftKey) {
|
||||
handleUnlink()
|
||||
return
|
||||
}
|
||||
|
||||
-1
@@ -17,7 +17,6 @@ export interface InstallationSettingsContext {
|
||||
isLinked: ComputedRef<boolean>
|
||||
isBusy: Ref<boolean> | ComputedRef<boolean>
|
||||
busyMessage?: Ref<string | null> | ComputedRef<string | null>
|
||||
skipNonEssentialWarnings?: Ref<boolean> | ComputedRef<boolean>
|
||||
|
||||
modpack: Ref<InstallationModpackData | null> | ComputedRef<InstallationModpackData | null>
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
"defaultMessage": "Re-install modpack"
|
||||
},
|
||||
"button.remove": {
|
||||
"defaultMessage": "Delete passkey"
|
||||
"defaultMessage": "Remove"
|
||||
},
|
||||
"button.remove-image": {
|
||||
"defaultMessage": "Remove image"
|
||||
|
||||
@@ -245,7 +245,7 @@ export const commonMessages = defineMessages({
|
||||
},
|
||||
removeButton: {
|
||||
id: 'button.remove',
|
||||
defaultMessage: 'Delete passkey',
|
||||
defaultMessage: 'Remove',
|
||||
},
|
||||
removeImageButton: {
|
||||
id: 'button.remove-image',
|
||||
|
||||
Reference in New Issue
Block a user