use crate::LxAppError;
use crate::lxapp::LxApp;
use crate::lxapp::navbar::{NavigationBarConfig, NavigationBarState};
use crate::warn;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PageOrientation {
Portrait,
Landscape,
Auto,
}
impl Default for PageOrientation {
fn default() -> Self {
Self::Portrait
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct OrientationConfig {
#[serde(default)]
pub mode: PageOrientation,
#[serde(default)]
pub rotation: u16,
}
impl Default for OrientationConfig {
fn default() -> Self {
Self {
mode: PageOrientation::Portrait,
rotation: 0,
}
}
}
impl OrientationConfig {
pub fn normalize(mode: PageOrientation, rotation: u16) -> Self {
let rotation = match rotation {
0 | 180 => rotation,
_ => 0,
};
let rotation = if matches!(mode, PageOrientation::Auto) {
0
} else {
rotation
};
Self { mode, rotation }
}
pub fn from_label(label: &str) -> Option<Self> {
match label.trim().to_lowercase().as_str() {
"auto" => Some(Self::normalize(PageOrientation::Auto, 0)),
"portrait" => Some(Self::normalize(PageOrientation::Portrait, 0)),
"landscape" => Some(Self::normalize(PageOrientation::Landscape, 0)),
"reverse-portrait" => Some(Self::normalize(PageOrientation::Portrait, 180)),
"reverse-landscape" => Some(Self::normalize(PageOrientation::Landscape, 180)),
_ => None,
}
}
pub fn to_label(self) -> &'static str {
match (self.mode, self.rotation) {
(PageOrientation::Auto, _) => "auto",
(PageOrientation::Portrait, 180) => "reverse-portrait",
(PageOrientation::Portrait, _) => "portrait",
(PageOrientation::Landscape, 180) => "reverse-landscape",
(PageOrientation::Landscape, _) => "landscape",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct OrientationOverride {
#[serde(default)]
pub mode: Option<PageOrientation>,
#[serde(default)]
pub rotation: Option<u16>,
}
impl OrientationOverride {
pub fn apply(self, base: OrientationConfig) -> OrientationConfig {
let mode = self.mode.unwrap_or(base.mode);
let rotation = self.rotation.unwrap_or(base.rotation);
OrientationConfig::normalize(mode, rotation)
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrientationObject {
#[serde(default)]
mode: Option<PageOrientation>,
#[serde(default)]
rotation: Option<u16>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum OrientationInput {
Label(String),
Object(OrientationObject),
}
fn deserialize_orientation<'de, D>(
deserializer: D,
) -> Result<(Option<PageOrientation>, Option<u16>), D::Error>
where
D: Deserializer<'de>,
{
let input = OrientationInput::deserialize(deserializer)?;
match input {
OrientationInput::Label(label) => {
let config = OrientationConfig::from_label(&label).ok_or_else(|| {
serde::de::Error::custom(format!("invalid orientation: {}", label))
})?;
Ok((Some(config.mode), Some(config.rotation)))
}
OrientationInput::Object(obj) => Ok((obj.mode, obj.rotation)),
}
}
impl<'de> Deserialize<'de> for OrientationConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let (mode, rotation) = deserialize_orientation(deserializer)?;
Ok(Self::normalize(
mode.unwrap_or_default(),
rotation.unwrap_or_default(),
))
}
}
impl<'de> Deserialize<'de> for OrientationOverride {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let (mode, rotation) = deserialize_orientation(deserializer)?;
Ok(Self { mode, rotation })
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct PageConfig {
#[serde(flatten)]
pub navigation_bar: NavigationBarConfig,
#[serde(default)]
pub enable_pull_down_refresh: bool,
#[serde(default)]
pub page_orientation: Option<PageOrientation>,
}
impl PageConfig {
fn parse_page_orientation_value(value: &Value) -> Option<PageOrientation> {
let raw = value.as_str()?;
match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Some(PageOrientation::Auto),
"portrait" => Some(PageOrientation::Portrait),
"landscape" => Some(PageOrientation::Landscape),
_ => None,
}
}
fn sanitize_page_orientation(path: &str, json_value: &mut Value) {
let Some(obj) = json_value.as_object_mut() else {
return;
};
let Some(raw_orientation) = obj.get("pageOrientation").cloned() else {
return;
};
if Self::parse_page_orientation_value(&raw_orientation).is_none() {
warn!(
"Ignoring invalid pageOrientation for {}: {:?}",
path, raw_orientation
);
obj.remove("pageOrientation");
}
}
pub fn from_json(lxapp: &LxApp, path: &str) -> Self {
if path.trim().is_empty() {
return Self::default();
}
let json_path = path_to_json_path(path);
match lxapp.read_json(&json_path) {
Ok(mut json_value) => {
Self::sanitize_page_orientation(path, &mut json_value);
match serde_json::from_value::<PageConfig>(json_value) {
Ok(config) => config,
Err(e) => {
warn!("Failed to parse page config for {}: {}", path, e);
Self::default()
}
}
}
Err(LxAppError::ResourceNotFound(_)) => Self::default(),
Err(e) => {
warn!(
"PageInstance config read failed for {} ({}); falling back to default",
path, e
);
Self::default()
}
}
}
pub fn create_navbar_state(&self) -> NavigationBarState {
NavigationBarState::from_config(&self.navigation_bar)
}
pub fn is_pull_down_refresh_enabled(&self) -> bool {
self.enable_pull_down_refresh
}
pub fn get_orientation_override(&self) -> OrientationOverride {
match self.page_orientation {
Some(mode) => OrientationOverride {
mode: Some(mode),
rotation: Some(0),
},
None => OrientationOverride::default(),
}
}
}
fn path_to_json_path(path: &str) -> String {
if path.is_empty() || path == "/" {
return "pages/index/index.json".to_string();
}
let mut trimmed = path.trim_start_matches('/').to_string();
if trimmed.is_empty() {
return "pages/index/index.json".to_string();
}
if let Some(dot_pos) = trimmed.rfind('.') {
let last_slash = trimmed.rfind('/');
if last_slash.map_or(true, |slash| dot_pos > slash) {
trimmed.truncate(dot_pos);
}
}
format!("{}.json", trimmed)
}