feat(channel): add logo support with position and opacity settings

This commit is contained in:
2026-03-14 02:27:16 +01:00
parent e610c23fea
commit da714840ee
11 changed files with 204 additions and 6 deletions

View File

@@ -76,6 +76,10 @@ pub struct UpdateChannelRequest {
pub access_mode: Option<domain::AccessMode>,
/// Empty string clears the password; non-empty re-hashes.
pub access_password: Option<String>,
/// `Some(None)` = clear logo, `Some(Some(url))` = set logo, `None` = unchanged.
pub logo: Option<Option<String>>,
pub logo_position: Option<domain::LogoPosition>,
pub logo_opacity: Option<f32>,
}
#[derive(Debug, Serialize)]
@@ -89,6 +93,9 @@ pub struct ChannelResponse {
pub recycle_policy: domain::RecyclePolicy,
pub auto_schedule: bool,
pub access_mode: domain::AccessMode,
pub logo: Option<String>,
pub logo_position: domain::LogoPosition,
pub logo_opacity: f32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -105,6 +112,9 @@ impl From<domain::Channel> for ChannelResponse {
recycle_policy: c.recycle_policy,
auto_schedule: c.auto_schedule,
access_mode: c.access_mode,
logo: c.logo,
logo_position: c.logo_position,
logo_opacity: c.logo_opacity,
created_at: c.created_at,
updated_at: c.updated_at,
}

View File

@@ -101,6 +101,15 @@ pub(super) async fn update_channel(
channel.access_password_hash = Some(infra::auth::hash_password(&pw));
}
}
if let Some(logo) = payload.logo {
channel.logo = logo;
}
if let Some(pos) = payload.logo_position {
channel.logo_position = pos;
}
if let Some(opacity) = payload.logo_opacity {
channel.logo_opacity = opacity.clamp(0.0, 1.0);
}
channel.updated_at = Utc::now();
let channel = state.channel_service.update(channel).await?;

View File

@@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::value_objects::{
AccessMode, BlockId, ChannelId, ContentType, FillStrategy, MediaFilter, MediaItemId,
RecyclePolicy, SlotId,
AccessMode, BlockId, ChannelId, ContentType, FillStrategy, LogoPosition, MediaFilter,
MediaItemId, RecyclePolicy, SlotId,
};
/// A user in the system.
@@ -85,6 +85,9 @@ pub struct Channel {
pub auto_schedule: bool,
pub access_mode: AccessMode,
pub access_password_hash: Option<String>,
pub logo: Option<String>,
pub logo_position: LogoPosition,
pub logo_opacity: f32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -107,6 +110,9 @@ impl Channel {
auto_schedule: false,
access_mode: AccessMode::default(),
access_password_hash: None,
logo: None,
logo_position: LogoPosition::default(),
logo_opacity: 1.0,
created_at: now,
updated_at: now,
}

View File

@@ -1,6 +1,17 @@
use serde::{Deserialize, Serialize};
use std::fmt;
/// Position of the channel logo watermark overlay.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum LogoPosition {
TopLeft,
#[default]
TopRight,
BottomLeft,
BottomRight,
}
/// Controls who can view a channel's broadcast and stream.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]

View File

@@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
use sqlx::FromRow;
use uuid::Uuid;
use domain::{AccessMode, Channel, ChannelId, DomainError, RecyclePolicy, ScheduleConfig, UserId};
use domain::{AccessMode, Channel, ChannelId, DomainError, LogoPosition, RecyclePolicy, ScheduleConfig, UserId};
#[derive(Debug, FromRow)]
pub(super) struct ChannelRow {
@@ -16,6 +16,9 @@ pub(super) struct ChannelRow {
pub auto_schedule: i64,
pub access_mode: String,
pub access_password_hash: Option<String>,
pub logo: Option<String>,
pub logo_position: String,
pub logo_opacity: f32,
pub created_at: String,
pub updated_at: String,
}
@@ -51,6 +54,11 @@ impl TryFrom<ChannelRow> for Channel {
)
.unwrap_or_default();
let logo_position: LogoPosition = serde_json::from_value(
serde_json::Value::String(row.logo_position),
)
.unwrap_or_default();
Ok(Channel {
id,
owner_id,
@@ -62,6 +70,9 @@ impl TryFrom<ChannelRow> for Channel {
auto_schedule: row.auto_schedule != 0,
access_mode,
access_password_hash: row.access_password_hash,
logo: row.logo,
logo_position,
logo_opacity: row.logo_opacity,
created_at: parse_dt(&row.created_at)?,
updated_at: parse_dt(&row.updated_at)?,
})
@@ -69,4 +80,4 @@ impl TryFrom<ChannelRow> for Channel {
}
pub(super) const SELECT_COLS: &str =
"id, owner_id, name, description, timezone, schedule_config, recycle_policy, auto_schedule, access_mode, access_password_hash, created_at, updated_at";
"id, owner_id, name, description, timezone, schedule_config, recycle_policy, auto_schedule, access_mode, access_password_hash, logo, logo_position, logo_opacity, created_at, updated_at";

View File

@@ -63,11 +63,16 @@ impl ChannelRepository for SqliteChannelRepository {
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_else(|| "public".to_owned());
let logo_position = serde_json::to_value(&channel.logo_position)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_else(|| "top_right".to_owned());
sqlx::query(
r#"
INSERT INTO channels
(id, owner_id, name, description, timezone, schedule_config, recycle_policy, auto_schedule, access_mode, access_password_hash, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(id, owner_id, name, description, timezone, schedule_config, recycle_policy, auto_schedule, access_mode, access_password_hash, logo, logo_position, logo_opacity, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
description = excluded.description,
@@ -77,6 +82,9 @@ impl ChannelRepository for SqliteChannelRepository {
auto_schedule = excluded.auto_schedule,
access_mode = excluded.access_mode,
access_password_hash = excluded.access_password_hash,
logo = excluded.logo,
logo_position = excluded.logo_position,
logo_opacity = excluded.logo_opacity,
updated_at = excluded.updated_at
"#,
)
@@ -90,6 +98,9 @@ impl ChannelRepository for SqliteChannelRepository {
.bind(channel.auto_schedule as i64)
.bind(&access_mode)
.bind(&channel.access_password_hash)
.bind(&channel.logo)
.bind(&logo_position)
.bind(channel.logo_opacity)
.bind(channel.created_at.to_rfc3339())
.bind(channel.updated_at.to_rfc3339())
.execute(&self.pool)

View File

@@ -0,0 +1,3 @@
ALTER TABLE channels ADD COLUMN logo TEXT;
ALTER TABLE channels ADD COLUMN logo_position TEXT NOT NULL DEFAULT 'top_right';
ALTER TABLE channels ADD COLUMN logo_opacity REAL NOT NULL DEFAULT 1.0;