mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
Merge branch 'main' into truman/dependents-search-in-discovery
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -481,7 +481,7 @@ impl PayoutsQueue {
|
||||
Ok(options.options)
|
||||
}
|
||||
|
||||
pub async fn get_brex_balance() -> eyre::Result<Option<AccountBalance>> {
|
||||
pub async fn get_brex_balance() -> eyre::Result<AccountBalance> {
|
||||
#[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::<i64>(),
|
||||
) / Decimal::from(100),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_paypal_balance() -> eyre::Result<Option<AccountBalance>> {
|
||||
pub async fn get_paypal_balance() -> eyre::Result<AccountBalance> {
|
||||
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<Option<AccountBalance>> {
|
||||
pub async fn get_tremendous_balance(&self) -> eyre::Result<AccountBalance> {
|
||||
#[derive(Deserialize)]
|
||||
struct FundingSourceMeta {
|
||||
available_cents: Option<u64>,
|
||||
@@ -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<Option<AccountBalance>>,
|
||||
) -> eyre::Result<Option<AccountBalance>> {
|
||||
result: eyre::Result<AccountBalance>,
|
||||
) -> 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)]
|
||||
|
||||
@@ -104,9 +104,7 @@ impl PayoutsQueue {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_mural_balance(
|
||||
&self,
|
||||
) -> eyre::Result<Option<AccountBalance>> {
|
||||
pub async fn get_mural_balance(&self) -> eyre::Result<AccountBalance> {
|
||||
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::<Decimal>();
|
||||
Ok(Some(AccountBalance {
|
||||
Ok(AccountBalance {
|
||||
available,
|
||||
pending: Decimal::ZERO,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
@@ -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.</br>
|
||||
|
||||
- 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.</br>
|
||||
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.
|
||||
+9
-2
@@ -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.
|
||||
|
||||
+31
-3
@@ -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.</br>
|
||||
|
||||
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.</br>
|
||||
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.</br>
|
||||
|
||||
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.
|
||||
|
||||
@@ -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 () =>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<TokenAmount>,
|
||||
pub balances_v2: Vec<Balance>,
|
||||
pub payin_methods: Vec<PayinMethod>,
|
||||
}
|
||||
|
||||
#[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")]
|
||||
|
||||
@@ -65,6 +65,7 @@ const assignableEntries = ref<AssignableFileEntry[]>([])
|
||||
const searchQuery = ref('')
|
||||
const searchInputRef = ref<{ focus: () => void } | null>(null)
|
||||
const selectedSha1s = ref<Set<string>>(new Set())
|
||||
const selectionAnchorSha1 = ref<string | null>(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)"
|
||||
>
|
||||
<span class="flex gap-2 font-medium">
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user