From 31ef22767eeaf37ca016eb594f0dcc2a952eda6b Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 13 Jul 2026 15:24:00 -0700 Subject: [PATCH 1/7] feat: add norisk client source support (#6707) Signed-off-by: Robin --- .../src/routes/v3/analytics_get/metrics/project_downloads.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs index 73bf19845f..36e8aabe16 100644 --- a/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs +++ b/apps/labrinth/src/routes/v3/analytics_get/metrics/project_downloads.rs @@ -657,6 +657,7 @@ static DOWNLOAD_SOURCE_PATTERNS: LazyLock> = (r"^mrpack4server", P::Named("mrpack4server")), (r"^DawnLauncher/", P::Named("Dawn")), (r"^Complementary-Installer", P::Named("Complementary Installer")), + (r"^noriskclient-launcher-v3/", P::Named("NoRisk Client")), ( r"^(Mozilla/|Chrome/|Chromium/|Firefox/|Safari/|AppleWebKit/|Edg/|OPR/)", P::Website, From 4a09ee7ac9fc9f0f69015836a3a622a104c33115 Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:00:48 -0700 Subject: [PATCH 2/7] changelog --- packages/blog/changelog.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/blog/changelog.ts b/packages/blog/changelog.ts index 45544abbf2..e999c4bbe8 100644 --- a/packages/blog/changelog.ts +++ b/packages/blog/changelog.ts @@ -10,6 +10,12 @@ export type VersionEntry = { } const VERSIONS: VersionEntry[] = [ + { + date: `2026-07-13T19:00:42+00:00`, + product: 'web', + body: `## Changed +- Updated translations`, + }, { date: `2026-07-10T17:38:17+00:00`, product: 'app', From c431d1fc89632ae9d3872cca46fbdd2d1e1004e8 Mon Sep 17 00:00:00 2001 From: Truman Gao <106889354+tdgao@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:43:37 -0600 Subject: [PATCH 3/7] feat: add shift click to toggle file selection (#6714) --- .../AddFilesToAttributionGroupModal.vue | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue b/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue index 68c0940d39..fc94cf5af1 100644 --- a/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue +++ b/packages/ui/src/components/external_files/AddFilesToAttributionGroupModal.vue @@ -65,6 +65,7 @@ const assignableEntries = ref([]) const searchQuery = ref('') const searchInputRef = ref<{ focus: () => void } | null>(null) const selectedSha1s = ref>(new Set()) +const selectionAnchorSha1 = ref(null) const filteredEntries = computed(() => { const q = searchQuery.value.trim().toLowerCase() @@ -85,18 +86,35 @@ function isFileSelected(sha1: string) { return selectedSha1s.value.has(sha1) } -function toggleFileSelection(sha1: string) { +function toggleFileSelection(sha1: string, event: MouseEvent) { const next = new Set(selectedSha1s.value) - if (next.has(sha1)) { - next.delete(sha1) - } else { - next.add(sha1) + const shouldSelect = !next.has(sha1) + const entryIndex = filteredEntries.value.findIndex((entry) => entry.sha1 === sha1) + const anchorIndex = filteredEntries.value.findIndex( + (entry) => entry.sha1 === selectionAnchorSha1.value, + ) + + const sha1sToToggle = + event.shiftKey && entryIndex !== -1 && anchorIndex !== -1 + ? filteredEntries.value + .slice(Math.min(entryIndex, anchorIndex), Math.max(entryIndex, anchorIndex) + 1) + .map((entry) => entry.sha1) + : [sha1] + + for (const sha1ToToggle of sha1sToToggle) { + if (shouldSelect) { + next.add(sha1ToToggle) + } else { + next.delete(sha1ToToggle) + } } selectedSha1s.value = next + selectionAnchorSha1.value = sha1 } function clearSelectedFiles() { selectedSha1s.value = new Set() + selectionAnchorSha1.value = null } function focusSearchOnOpen() { @@ -208,7 +226,7 @@ defineExpose({ show, hide }) class="w-full text-left p-3 flex flex-col gap-1 appearance-none bg-transparent 3 transition-colors disabled:opacity-50" :disabled="pending" :aria-pressed="isFileSelected(entry.sha1)" - @click="toggleFileSelection(entry.sha1)" + @click="toggleFileSelection(entry.sha1, $event)" > Date: Tue, 14 Jul 2026 08:52:50 +0100 Subject: [PATCH 4/7] fix: add more logging to index-billing task (#6708) * more logging to index billing * schema update --- apps/labrinth/src/queue/payouts/mod.rs | 110 +++++++++++------------ apps/labrinth/src/queue/payouts/mural.rs | 29 +++--- packages/muralpay/src/account.rs | 25 +++++- 3 files changed, 94 insertions(+), 70 deletions(-) diff --git a/apps/labrinth/src/queue/payouts/mod.rs b/apps/labrinth/src/queue/payouts/mod.rs index d26bef34a3..e2a5102560 100644 --- a/apps/labrinth/src/queue/payouts/mod.rs +++ b/apps/labrinth/src/queue/payouts/mod.rs @@ -481,7 +481,7 @@ impl PayoutsQueue { Ok(options.options) } - pub async fn get_brex_balance() -> eyre::Result> { + pub async fn get_brex_balance() -> eyre::Result { #[derive(Deserialize)] struct BrexBalance { pub amount: i64, @@ -510,7 +510,7 @@ impl PayoutsQueue { .await .wrap_request_err("reading `accounts/cash` request")?; - Ok(Some(AccountBalance { + Ok(AccountBalance { available: Decimal::from( res.items .iter() @@ -525,10 +525,10 @@ impl PayoutsQueue { }) .sum::(), ) / Decimal::from(100), - })) + }) } - pub async fn get_paypal_balance() -> eyre::Result> { + pub async fn get_paypal_balance() -> eyre::Result { let api_username = &ENV.PAYPAL_NVP_USERNAME; let api_password = &ENV.PAYPAL_NVP_PASSWORD; let api_signature = &ENV.PAYPAL_NVP_SIGNATURE; @@ -560,28 +560,23 @@ impl PayoutsQueue { let mut key_value_map = HashMap::new(); for pair in body.split('&') { - let mut iter = pair.splitn(2, '='); - if let (Some(key), Some(value)) = (iter.next(), iter.next()) { + if let Some((key, value)) = pair.split_once('=') { key_value_map.insert(key.to_string(), value.to_string()); } } - if let Some(amount) = key_value_map - .get("L_AMT0") - .and_then(|x| Decimal::from_str_exact(x).ok()) - { - Ok(Some(AccountBalance { - available: amount, - pending: Decimal::ZERO, - })) - } else { - Ok(None) - } + let amount = + key_value_map.get("L_AMT0").wrap_err("missing `L_AMT0`")?; + let amount = Decimal::from_str_exact(amount) + .wrap_err("cannot parse `L_AMT0` as decimal")?; + + Ok(AccountBalance { + available: amount, + pending: Decimal::ZERO, + }) } - pub async fn get_tremendous_balance( - &self, - ) -> eyre::Result> { + pub async fn get_tremendous_balance(&self) -> eyre::Result { #[derive(Deserialize)] struct FundingSourceMeta { available_cents: Option, @@ -606,18 +601,22 @@ impl PayoutsQueue { None, ) .await - .wrap_request_err("fetching funding sources")?; + .wrap_err("fetching funding sources")?; - Ok(val + let funding_source = val .funding_sources .into_iter() .find(|x| x.method == "balance") - .map(|x| AccountBalance { - available: Decimal::from(x.meta.available_cents.unwrap_or(0)) - / Decimal::from(100), - pending: Decimal::from(x.meta.pending_cents.unwrap_or(0)) - / Decimal::from(100), - })) + .wrap_err("no balance funding source")?; + + Ok(AccountBalance { + available: Decimal::from( + funding_source.meta.available_cents.unwrap_or(0), + ) / Decimal::from(100), + pending: Decimal::from( + funding_source.meta.pending_cents.unwrap_or(0), + ) / Decimal::from(100), + }) } } @@ -1282,30 +1281,30 @@ pub async fn insert_bank_balances_and_webhook( let now = Utc::now(); let today = now.date_naive().and_time(NaiveTime::MIN).and_utc(); - let mut add_balance = |account_type: &str, balance: &AccountBalance| { - insert_account_types.push(account_type.to_string()); - insert_amounts.push(balance.available); - insert_pending.push(false); - insert_recorded.push(today); + let mut add_balance = + |account_type: &str, + balance: Result<&AccountBalance, &eyre::Report>| match balance + { + Ok(balance) => { + insert_account_types.push(account_type.to_string()); + insert_amounts.push(balance.available); + insert_pending.push(false); + insert_recorded.push(today); - insert_account_types.push(account_type.to_string()); - insert_amounts.push(balance.pending); - insert_pending.push(true); - insert_recorded.push(today); - }; + insert_account_types.push(account_type.to_string()); + insert_amounts.push(balance.pending); + insert_pending.push(true); + insert_recorded.push(today); + } + Err(err) => { + warn!("Failed to check balance for '{account_type}': {err:?}"); + } + }; - if let Ok(Some(ref paypal)) = paypal_result { - add_balance("paypal", paypal); - } - if let Ok(Some(ref brex)) = brex_result { - add_balance("brex", brex); - } - if let Ok(Some(ref tremendous)) = tremendous_result { - add_balance("tremendous", tremendous); - } - if let Ok(Some(ref mural)) = mural_result { - add_balance("mural", mural); - } + add_balance("paypal", paypal_result.as_ref()); + add_balance("brex", brex_result.as_ref()); + add_balance("tremendous", tremendous_result.as_ref()); + add_balance("mural", mural_result.as_ref()); let inserted = sqlx::query_scalar!( r#" @@ -1362,13 +1361,13 @@ pub async fn insert_bank_balances_and_webhook( async fn check_balance_with_webhook( source: &str, threshold: u64, - result: eyre::Result>, -) -> eyre::Result> { + result: eyre::Result, +) -> eyre::Result<()> { let maybe_threshold = if threshold > 0 { Some(threshold) } else { None }; let payout_alert_webhook = &ENV.PAYOUT_ALERT_SLACK_WEBHOOK; match &result { - Ok(Some(account_balance)) => { + Ok(account_balance) => { if let Some(threshold) = maybe_threshold && let Some(available) = account_balance.available.trunc().to_u64() @@ -1385,7 +1384,6 @@ async fn check_balance_with_webhook( .await?; } } - Err(error) => { // use compact single-line error repr here error!( @@ -1405,11 +1403,9 @@ async fn check_balance_with_webhook( .await?; } } - - _ => {} } - Ok(result.ok().flatten()) + Ok(()) } #[cfg(test)] diff --git a/apps/labrinth/src/queue/payouts/mural.rs b/apps/labrinth/src/queue/payouts/mural.rs index 93345f94c7..3a1b8d1438 100644 --- a/apps/labrinth/src/queue/payouts/mural.rs +++ b/apps/labrinth/src/queue/payouts/mural.rs @@ -104,9 +104,7 @@ impl PayoutsQueue { Ok(()) } - pub async fn get_mural_balance( - &self, - ) -> eyre::Result> { + pub async fn get_mural_balance(&self) -> eyre::Result { let muralpay = self.muralpay.load(); let muralpay = muralpay .as_ref() @@ -121,20 +119,29 @@ impl PayoutsQueue { .account_details .wrap_err("source account does not have details")?; let available = details - .balances + .balances_v2 .iter() - .map(|balance| { - if balance.token_symbol == muralpay::USDC { - balance.token_amount - } else { - Decimal::ZERO + .map(|balance| match balance { + muralpay::Balance::Blockchain { + token_symbol, + exponent, + value, + .. + } if token_symbol == muralpay::USDC => { + *value * Decimal::new(1, *exponent) } + muralpay::Balance::Fiat { + currency_symbol: muralpay::UsdSymbol::Usd, + exponent, + value, + } => *value * Decimal::new(1, *exponent), + _ => Decimal::ZERO, }) .sum::(); - Ok(Some(AccountBalance { + Ok(AccountBalance { available, pending: Decimal::ZERO, - })) + }) } } diff --git a/packages/muralpay/src/account.rs b/packages/muralpay/src/account.rs index 959c1c054a..d8b369d03e 100644 --- a/packages/muralpay/src/account.rs +++ b/packages/muralpay/src/account.rs @@ -1,5 +1,5 @@ use { - crate::{Blockchain, FiatAmount, TokenAmount, WalletDetails}, + crate::{Blockchain, FiatAmount, UsdSymbol, WalletDetails}, chrono::{DateTime, Utc}, derive_more::{Deref, Display}, rust_decimal::Decimal, @@ -124,10 +124,31 @@ pub enum AccountStatus { #[serde(rename_all = "camelCase")] pub struct AccountDetails { pub wallet_details: WalletDetails, - pub balances: Vec, + pub balances_v2: Vec, pub payin_methods: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Balance { + #[serde(rename_all = "camelCase")] + Blockchain { + token_symbol: String, + exponent: u32, + #[serde(with = "rust_decimal::serde::str")] + value: Decimal, + blockchain: Blockchain, + }, + #[serde(rename_all = "camelCase")] + Fiat { + currency_symbol: UsdSymbol, + exponent: u32, + #[serde(with = "rust_decimal::serde::str")] + value: Decimal, + }, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] From 1884912126b0030bf2789dcf25a5c3ffce227227 Mon Sep 17 00:00:00 2001 From: Artyom Ezri <61311568+Artezon@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:58:07 +0300 Subject: [PATCH 5/7] feat: rename shader config file on version change (#5636) * feat: rename shader settings sidecar file on version switch * fix: cleanup --------- Co-authored-by: Calum H. Co-authored-by: Calum H. (IMB11) --- .../commands/apply_content_install.rs | 43 +++++++++++++++++++ .../commands/apply_content_update.rs | 16 ++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/app-lib/src/state/instances/commands/apply_content_install.rs b/packages/app-lib/src/state/instances/commands/apply_content_install.rs index 0c780c7e76..433dd03977 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_install.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_install.rs @@ -285,6 +285,13 @@ pub(crate) async fn switch_project_version_with_dependencies( } if new_path != project_path { + rename_project_companion_file( + instance_id, + project_path, + &new_path, + state, + ) + .await?; remove_project(instance_id, project_path, state).await?; } @@ -724,6 +731,42 @@ pub(crate) async fn remove_project( Ok(()) } +pub(crate) async fn rename_project_companion_file( + instance_id: &str, + old_project_path: &str, + new_project_path: &str, + state: &State, +) -> crate::Result<()> { + let project_type = ProjectType::get_from_parent_folder(new_project_path); + if project_type == Some(ProjectType::ShaderPack) { + let scope = resolve_content_scope(instance_id, None, state).await?; + let base = instance_full_path(state, &scope.instance); + + let old_txt_path = base.join(format!( + "{}.txt", + old_project_path.trim_end_matches(".disabled") + )); + let new_txt_path = base.join(format!( + "{}.txt", + new_project_path.trim_end_matches(".disabled") + )); + + if old_txt_path.exists() { + if new_txt_path.exists() + && io::canonicalize(&old_txt_path)? + == io::canonicalize(&new_txt_path)? + { + return Ok(()); + } + + io::copy(&old_txt_path, &new_txt_path).await?; + io::remove_file(&old_txt_path).await?; + } + } + + Ok(()) +} + pub(crate) async fn list_project_files( instance_id: &str, state: &State, diff --git a/packages/app-lib/src/state/instances/commands/apply_content_update.rs b/packages/app-lib/src/state/instances/commands/apply_content_update.rs index e89d5b5378..09aa358921 100644 --- a/packages/app-lib/src/state/instances/commands/apply_content_update.rs +++ b/packages/app-lib/src/state/instances/commands/apply_content_update.rs @@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet}; use super::apply_content_install::{ DownloadedProjectVersion, add_downloaded_project_version, add_project_from_version, download_project_version, remove_project, - toggle_disable_project, + rename_project_companion_file, toggle_disable_project, }; use super::check_content_updates::{ContentUpdate, check_content_updates}; @@ -108,6 +108,13 @@ async fn apply_content_update( } if new_path != project_path { + rename_project_companion_file( + instance_id, + project_path, + &new_path, + state, + ) + .await?; remove_project(instance_id, project_path, state).await?; } @@ -162,6 +169,13 @@ pub(crate) async fn update_all_projects( } if new_path != update.relative_path { + rename_project_companion_file( + instance_id, + &update.relative_path, + &new_path, + state, + ) + .await?; remove_project(instance_id, &update.relative_path, state) .await?; } From 13e6ebf4e01e488f45ce9d76dbe59031f4342506 Mon Sep 17 00:00:00 2001 From: coolbot <76798835+coolbot100s@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:21:23 -0700 Subject: [PATCH 6/7] Moderation: Update tec rev quick replies. (#6719) update tec rev quick replies. --- .../tech-review/misused-obfuscation.md | 13 +++++++ .../tech-review/request-source-bin.md | 11 ++++-- .../tech-review/request-source-obf.md | 34 +++++++++++++++++-- .../tech-review-quick-replies.ts | 6 ++++ 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 packages/moderation/src/data/messages/quick-replies/tech-review/misused-obfuscation.md diff --git a/packages/moderation/src/data/messages/quick-replies/tech-review/misused-obfuscation.md b/packages/moderation/src/data/messages/quick-replies/tech-review/misused-obfuscation.md new file mode 100644 index 0000000000..09b62c3bc5 --- /dev/null +++ b/packages/moderation/src/data/messages/quick-replies/tech-review/misused-obfuscation.md @@ -0,0 +1,13 @@ +## Obfuscation on Modrinth + +To ensure the safety of all Modrinth users, projects may only be uploaded with obfuscated code under specific circumstances.
+ +- Projects that use third-party code or assets required by law or licensing restrictions to remain obfuscated. +- Projects where the obfuscation demonstrably benefits end users in a way critical to its functionality or safety. +- Projects where obfuscation is required to prevent the bypass of critical authorization checks. + +Upon review, our moderation team has determined that this project does not qualify under one of the above exemptions.
+With this in mind, we ask that you: + +- Remove the use of obfuscation from your project. +- Remove all versions containing obfuscated code from your project before resubmission. diff --git a/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-bin.md b/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-bin.md index 14a855978b..0abf4e85c1 100644 --- a/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-bin.md +++ b/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-bin.md @@ -1,7 +1,14 @@ ## Source Code Requested -To ensure the safety of all Modrinth users, we ask that you provide the source code for this project before resubmission so that it can be reviewed by our Moderation Team. +To ensure the safety of all Modrinth users, we ask that you provide the source code or equivalent origin for any native code or binary files in use by this project. -We also ask that you provide the source for any included binary files, as well as detailed build instructions allowing us to verify that the compiled code you are distributing matches the provided source. +If these files are your own work: + +- Ensure that our moderation team is able to verify the safety of your source code and that compiled outputs match provided sources. +- Ensure that binary files are built through transparent automation so we can verify from the provided source code is always identical to the files uploaded to Modrinth. We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub. + +If these files are third-party work: + +- Please provide a publicly available link to the origin of the files or source-code from a known safe source. diff --git a/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-obf.md b/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-obf.md index b87a43fa79..d568728f2d 100644 --- a/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-obf.md +++ b/packages/moderation/src/data/messages/quick-replies/tech-review/request-source-obf.md @@ -1,5 +1,33 @@ -## Source Code Requested +## Obfuscation on Modrinth -To ensure the safety of all Modrinth users, we ask that you provide the source code for this project, steps on how to build it, and the process you used to obfuscate it before resubmission so that it can be reviewed by our Moderation Team. +To ensure the safety of all Modrinth users, projects may only be uploaded with obfuscated code under specific circumstances.
-We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub. +- Projects that use third-party code or assets required by law or licensing restrictions to remain obfuscated. +- Projects where the obfuscation demonstrably benefits end users in a way critical to its functionality or safety. +- Projects where obfuscation is required to prevent the bypass of critical authorization checks. + +### Uploading your project to Modrinth without obfuscation + +If your project does NOT qualify for one of the above exemptions, we ask that you: + +- Remove the use of obfuscation from your project. +- Remove all versions containing obfuscated code from your project before resubmission. + +### Uploading your qualifying project with obfuscation + +If you believe your project should be permitted to use obfuscation, you must follow all steps when resubmitting your project: + +- Provide sufficient evidence that your project falls into one of the allowed exemptions. +- Ensure that our moderation team is able to verify the safety of your source code and that compiled outputs match provided sources. + +We understand that you may not want to publish the source code for this project, so you are welcome to share it privately to the [Modrinth Content Moderation Team](https://github.com/ModrinthModeration) on GitHub.
+Please be aware that you will be required to maintain up-to-date sources indefinitely, your project may be rejected without warning if our moderation team is unable to confirm that any version of your project originates from verifiably safe sources. + +We strongly recommend that you use an automated build system to ensure that your project's outputs verifiably originate from the provided source code and are always identical to the files uploaded to Modrinth.
+ +Alternatively, please ensure your provided sources contain: + +- Instructions to reliably produce both non-obfuscated and obfuscated builds within a fresh environment. +- No non-deterministic obfuscation methods. + +Finally, please note that we broadly discourage the use of obfuscation and advise against it unless absolutely required, and that the review of your project will require significantly more time when obfuscation is used. diff --git a/packages/moderation/src/data/quick-replies/tech-review-quick-replies.ts b/packages/moderation/src/data/quick-replies/tech-review-quick-replies.ts index d77a1c349c..7faf7a8bcf 100644 --- a/packages/moderation/src/data/quick-replies/tech-review-quick-replies.ts +++ b/packages/moderation/src/data/quick-replies/tech-review-quick-replies.ts @@ -33,6 +33,12 @@ export default [ (await import('../messages/quick-replies/tech-review/request-source-bin.md?raw')).default, private: false, }, + { + label: '🚫 Misused Obfuscation', + message: async () => + (await import('../messages/quick-replies/tech-review/misused-obfuscation.md?raw')).default, + private: false, + }, { label: '🚫 Malware', message: async () => From cafe58dc3cb3d6f2f36fdd733f0f3c022adc4afd Mon Sep 17 00:00:00 2001 From: coolbot <76798835+coolbot100s@users.noreply.github.com> Date: Tue, 14 Jul 2026 03:21:40 -0700 Subject: [PATCH 7/7] Moderation: Add keybinds for link copying and setting project status (#6716) Add keybinds for link copying and setting project status --- .../checklist/ModerationChecklist.vue | 23 +++++++++++ packages/moderation/src/data/keybinds.ts | 40 +++++++++++++++++++ packages/moderation/src/types/keybinds.ts | 3 ++ 3 files changed, 66 insertions(+) diff --git a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue index da774cd743..015ac19c75 100644 --- a/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue +++ b/apps/frontend/src/components/ui/moderation/checklist/ModerationChecklist.vue @@ -1207,6 +1207,29 @@ function handleKeybinds(event: KeyboardEvent) { tryWithhold: () => sendMessage('withheld'), tryEditMessage: goBackToStages, + tryCopyLink: async (permalink: boolean, relative: boolean, page: boolean) => { + let url = `` + if (relative) { + url += `${globalThis.location.origin}` + } else { + url += `https://modrinth.com` + } + + if (permalink) { + url += `/project/${projectV2.value.id}` + } else { + url += `/${projectV2.value.project_type}/${projectV2.value.slug}` + } + + if (page) { + url += `/${globalThis.location.pathname.split('/').slice(3).join('/')}` + } + + await navigator.clipboard.writeText(url) + }, + + tryCopyId: async () => await navigator.clipboard.writeText(projectV2.value.id), + tryToggleAction: (actionIndex: number) => { const action = visibleActions.value[actionIndex] if (action) { diff --git a/packages/moderation/src/data/keybinds.ts b/packages/moderation/src/data/keybinds.ts index 7a15670dba..45be4b1a14 100644 --- a/packages/moderation/src/data/keybinds.ts +++ b/packages/moderation/src/data/keybinds.ts @@ -34,6 +34,46 @@ const keybinds: { [id: string]: KeybindListener } = { enabled: (ctx) => ctx.state.futureProjectCount > 0 && !ctx.state.isDone, action: (ctx) => ctx.actions.trySkipProject(), }, + 'copy-permalink': { + keybind: 'Ctrl+Alt+C', + description: 'Copy permalink', + action: (ctx) => ctx.actions.tryCopyLink(true, false, false), + }, + 'copy-relative-permalink': { + keybind: 'Ctrl+Alt+R', + description: 'Copy relative permalink', + action: (ctx) => ctx.actions.tryCopyLink(true, true, false), + }, + 'copy-page-permalink': { + keybind: 'Shift+Ctrl+Alt+C', + description: 'Copy permalink with page', + action: (ctx) => ctx.actions.tryCopyLink(true, false, true), + }, + 'copy-page-relative-permalink': { + keybind: 'Shift+Ctrl+Alt+R', + description: 'Copy relative permalink with page', + action: (ctx) => ctx.actions.tryCopyLink(true, true, true), + }, + 'copy-id': { + keybind: 'Ctrl+Alt+D', + description: 'Copy Project ID', + action: (ctx) => ctx.actions.tryCopyId(), + }, + 'approve-project': { + keybind: 'Shift+Alt+A', + description: 'Approve project', + action: (ctx) => ctx.actions.tryApprove(), + }, + 'withhold-project': { + keybind: 'Shift+Alt+W', + description: 'Withhold project', + action: (ctx) => ctx.actions.tryWithhold(), + }, + 'reject-project': { + keybind: 'Shift+Alt+R', + description: 'Reject project', + action: (ctx) => ctx.actions.tryReject(), + }, } export default keybinds diff --git a/packages/moderation/src/types/keybinds.ts b/packages/moderation/src/types/keybinds.ts index 86f1e75291..5b31222049 100644 --- a/packages/moderation/src/types/keybinds.ts +++ b/packages/moderation/src/types/keybinds.ts @@ -22,6 +22,9 @@ export interface ModerationActions { tryFocusNextAction: () => void tryFocusPreviousAction: () => void tryActivateFocusedAction: () => void + + tryCopyLink: (permalink: boolean, relative: boolean, page: boolean) => void + tryCopyId: () => void } export interface ModerationState {