feat: 移植截图中心后端(索引/分组/编辑保存)
Sync source to CNB / Sync Git ref (push) Has been cancelled

This commit is contained in:
AxTps committed 2026-10-01 21:08:57 +08:00
1 parent e646598078
commit ead5d96b78
13 files changed
+1829 -1

No files matched your search

@@ -0,0 +1,348 @@
use crate::State;
use crate::event::InstancePayloadType;
use crate::event::emit::emit_instance;
use crate::state::instances::adapters::sqlite::screenshot_rows;
use serde::{Deserialize, Serialize};
use sqlx::{Sqlite, Transaction};
use std::collections::HashSet;
use uuid::Uuid;
const MAX_GROUP_NAME_LENGTH: usize = 256;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ScreenshotGroup {
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ScreenshotGroupMembershipUpdate {
pub screenshot_id: String,
pub group_id: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ScreenshotGroupImport {
pub id: String,
pub name: String,
pub screenshot_ids: Vec<String>,
}
fn validate_group_name(name: &str) -> crate::Result<&str> {
let name = name.trim();
if name.is_empty() {
return Err(crate::ErrorKind::InputError(
"Group name cannot be empty".to_string(),
)
.into());
}
if name.chars().count() > MAX_GROUP_NAME_LENGTH {
return Err(crate::ErrorKind::InputError(format!(
"Group name cannot exceed {MAX_GROUP_NAME_LENGTH} characters"
))
.into());
}
Ok(name)
}
pub async fn list_screenshot_groups() -> crate::Result<Vec<ScreenshotGroup>> {
let state = State::get().await?;
Ok(screenshot_rows::list_groups(&state.pool)
.await?
.into_iter()
.map(|group| ScreenshotGroup {
id: group.id,
name: group.name,
})
.collect())
}
pub async fn create_screenshot_group(
name: String,
screenshot_ids: Vec<String>,
) -> crate::Result<ScreenshotGroup> {
let name = validate_group_name(&name)?;
let state = State::get().await?;
let id = Uuid::new_v4().to_string();
let mut tx = state.pool.begin().await?;
sqlx::query!(
"UPDATE screenshot_groups SET display_order = display_order + 1",
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO screenshot_groups (id, name, display_order) VALUES (?, ?, 0)",
id,
name,
)
.execute(&mut *tx)
.await?;
let instance_ids = set_group_members(
&id,
normalize_screenshot_ids(screenshot_ids)?,
&mut tx,
)
.await?;
tx.commit().await?;
emit_screenshot_updates(instance_ids).await;
Ok(ScreenshotGroup {
id,
name: name.to_string(),
})
}
pub async fn rename_screenshot_group(
id: String,
new_name: String,
) -> crate::Result<ScreenshotGroup> {
let new_name = validate_group_name(&new_name)?;
let state = State::get().await?;
let result = sqlx::query!(
"UPDATE screenshot_groups SET name = ? WHERE id = ?",
new_name,
id,
)
.execute(&state.pool)
.await?;
if result.rows_affected() == 0 {
return Err(crate::ErrorKind::InputError(format!(
"Unknown screenshot group {id}"
))
.into());
}
Ok(ScreenshotGroup {
id,
name: new_name.to_string(),
})
}
pub async fn delete_screenshot_group(id: String) -> crate::Result<()> {
let state = State::get().await?;
let instance_ids = sqlx::query_scalar!(
"
SELECT DISTINCT screenshots.instance_id
FROM screenshot_group_memberships memberships
INNER JOIN screenshots ON screenshots.id = memberships.screenshot_id
WHERE memberships.group_id = ?
",
id,
)
.fetch_all(&state.pool)
.await?;
let result =
sqlx::query!("DELETE FROM screenshot_groups WHERE id = ?", id,)
.execute(&state.pool)
.await?;
if result.rows_affected() == 0 {
return Err(crate::ErrorKind::InputError(format!(
"Unknown screenshot group {id}"
))
.into());
}
emit_screenshot_updates(instance_ids).await;
Ok(())
}
pub async fn set_screenshot_group_memberships(
updates: Vec<ScreenshotGroupMembershipUpdate>,
) -> crate::Result<()> {
let updates = normalize_membership_updates(updates)?;
if updates.is_empty() {
return Ok(());
}
let state = State::get().await?;
let mut tx = state.pool.begin().await?;
let group_ids = updates
.iter()
.filter_map(|update| update.group_id.as_deref())
.collect::<HashSet<_>>();
for group_id in group_ids {
ensure_group_exists(group_id, &mut tx).await?;
}
let mut instance_ids = HashSet::new();
for update in updates {
let instance_id =
screenshot_instance_id(&update.screenshot_id, &mut tx).await?;
instance_ids.insert(instance_id);
sqlx::query!(
"DELETE FROM screenshot_group_memberships WHERE screenshot_id = ?",
update.screenshot_id,
)
.execute(&mut *tx)
.await?;
if let Some(group_id) = update.group_id {
sqlx::query!(
"INSERT INTO screenshot_group_memberships (screenshot_id, group_id) VALUES (?, ?)",
update.screenshot_id,
group_id,
)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
emit_screenshot_updates(instance_ids).await;
Ok(())
}
pub async fn import_screenshot_groups(
groups: Vec<ScreenshotGroupImport>,
) -> crate::Result<()> {
if groups.is_empty() {
return Ok(());
}
let state = State::get().await?;
let mut tx = state.pool.begin().await?;
let base_order = sqlx::query_scalar!(
"SELECT COALESCE(MAX(display_order) + 1, 0) AS 'display_order!: i64' FROM screenshot_groups",
)
.fetch_one(&mut *tx)
.await?;
let mut group_ids = HashSet::new();
let mut affected_instance_ids = HashSet::new();
for (index, group) in groups.into_iter().enumerate() {
if group.id.trim().is_empty() || !group_ids.insert(group.id.clone()) {
return Err(crate::ErrorKind::InputError(
"Screenshot group import contains an invalid group ID"
.to_string(),
)
.into());
}
let name = validate_group_name(&group.name)?;
let display_order = base_order + index as i64;
sqlx::query!(
"
INSERT INTO screenshot_groups (id, name, display_order)
VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET name = excluded.name
",
group.id,
name,
display_order,
)
.execute(&mut *tx)
.await?;
affected_instance_ids.extend(
set_group_members(
&group.id,
normalize_screenshot_ids(group.screenshot_ids)?,
&mut tx,
)
.await?,
);
}
tx.commit().await?;
emit_screenshot_updates(affected_instance_ids).await;
Ok(())
}
fn normalize_screenshot_ids(
screenshot_ids: Vec<String>,
) -> crate::Result<Vec<String>> {
let mut unique = HashSet::new();
if screenshot_ids
.iter()
.all(|screenshot_id| unique.insert(screenshot_id.clone()))
{
return Ok(screenshot_ids);
}
Err(crate::ErrorKind::InputError(
"Screenshot group contains duplicate screenshots".to_string(),
)
.into())
}
fn normalize_membership_updates(
updates: Vec<ScreenshotGroupMembershipUpdate>,
) -> crate::Result<Vec<ScreenshotGroupMembershipUpdate>> {
let mut screenshot_ids = HashSet::new();
if updates
.iter()
.all(|update| screenshot_ids.insert(update.screenshot_id.clone()))
{
return Ok(updates);
}
Err(crate::ErrorKind::InputError(
"Screenshot membership update contains duplicate screenshots".to_string(),
)
.into())
}
async fn set_group_members(
group_id: &str,
screenshot_ids: Vec<String>,
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<HashSet<String>> {
let mut instance_ids = HashSet::new();
for screenshot_id in screenshot_ids {
instance_ids.insert(screenshot_instance_id(&screenshot_id, tx).await?);
sqlx::query!(
"
INSERT INTO screenshot_group_memberships (screenshot_id, group_id)
VALUES (?, ?)
ON CONFLICT(screenshot_id) DO UPDATE SET group_id = excluded.group_id
",
screenshot_id,
group_id,
)
.execute(&mut **tx)
.await?;
}
Ok(instance_ids)
}
async fn ensure_group_exists(
group_id: &str,
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<()> {
let exists = sqlx::query_scalar!(
r#"SELECT EXISTS(SELECT 1 FROM screenshot_groups WHERE id = ?) AS "exists!: bool""#,
group_id,
)
.fetch_one(&mut **tx)
.await?;
if !exists {
return Err(crate::ErrorKind::InputError(format!(
"Unknown screenshot group {group_id}"
))
.into());
}
Ok(())
}
async fn screenshot_instance_id(
screenshot_id: &str,
tx: &mut Transaction<'_, Sqlite>,
) -> crate::Result<String> {
sqlx::query_scalar!(
"SELECT instance_id FROM screenshots WHERE id = ?",
screenshot_id,
)
.fetch_optional(&mut **tx)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unknown screenshot {screenshot_id}"
))
})
.map_err(Into::into)
}
async fn emit_screenshot_updates(
instance_ids: impl IntoIterator<Item = String>,
) {
for instance_id in instance_ids {
let _ = emit_instance(
&instance_id,
InstancePayloadType::ScreenshotsUpdated,
)
.await;
}
}