use crate::i18n::{Locale, MessageKey, files_indexed, source_summary, tr};
use crate::state::{AppState, Message};
use iced::widget::{button, column, container, row, text, text_input};
use iced::{Element, Length, Padding};
use orbok_models::SearchCapability;
fn page<'a>(content: iced::widget::Column<'a, Message>) -> Element<'a, Message> {
container(content.spacing(10))
.padding(Padding::from([24.0, 32.0]))
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn heading(label: &str) -> iced::widget::Text<'_> {
text(label.to_string()).size(26)
}
pub fn search_view(state: &AppState) -> Element<'_, Message> {
let locale = state.locale;
let input = text_input(tr(locale, MessageKey::SearchPlaceholder), &state.query)
.on_input(Message::QueryChanged)
.on_submit(Message::SubmitSearch)
.padding(8);
let submit = button(text(tr(locale, MessageKey::SearchButton)).size(13))
.on_press(Message::SubmitSearch);
let mut content = column![
heading(tr(locale, MessageKey::NavSearch)),
row![container(input).width(Length::Fill), submit].spacing(8),
];
if state.sources.is_empty() {
content = content.push(
column![
text(tr(locale, MessageKey::SearchNoSourcesTitle)).size(18),
text(tr(locale, MessageKey::SearchNoSourcesBody)).size(13),
button(text(tr(locale, MessageKey::SearchAddSource)).size(13))
.on_press(Message::Switch(crate::state::ViewId::Sources)),
]
.spacing(6),
);
} else {
if state.capability == SearchCapability::KeywordOnly {
content = content
.push(text(tr(locale, MessageKey::SearchKeywordOnlyNotice)).size(12));
}
if let Some(last) = &state.last_query {
content = content.push(text(format!("» {last}")).size(13));
content = content.push(text(tr(locale, MessageKey::SearchNoResults)).size(13));
}
}
page(content)
}
pub fn sources_view(state: &AppState) -> Element<'_, Message> {
let locale = state.locale;
let mut content = column![heading(tr(locale, MessageKey::SourcesTitle))];
if state.sources.is_empty() {
content = content.push(
column![
text(tr(locale, MessageKey::SourcesEmptyTitle)).size(18),
text(tr(locale, MessageKey::SourcesEmptyBody)).size(13),
button(text(tr(locale, MessageKey::SourcesAddFolder)).size(13)),
]
.spacing(6),
);
} else {
for card in &state.sources {
let status = if card.active {
tr(locale, MessageKey::SourcesStatusActive)
} else {
tr(locale, MessageKey::SourcesStatusPaused)
};
content = content.push(
container(
column![
text(card.display_name.clone()).size(15),
text(card.display_path.clone()).size(11),
text(source_summary(locale, card.indexed, card.stale, card.failed))
.size(12),
text(status).size(11),
]
.spacing(2),
)
.padding(10),
);
}
}
page(content)
}
pub fn indexing_view(state: &AppState) -> Element<'_, Message> {
let locale = state.locale;
let h = state.health;
let cell = |label: &'static str, value: u64| {
container(column![text(label).size(12), text(value.to_string()).size(20)].spacing(2))
.padding(10)
};
let content = column![
heading(tr(locale, MessageKey::IndexingTitle)),
row![
cell(tr(locale, MessageKey::IndexingHealthIndexed), h.indexed),
cell(tr(locale, MessageKey::IndexingHealthQueued), h.queued),
cell(tr(locale, MessageKey::IndexingHealthStale), h.stale),
cell(tr(locale, MessageKey::IndexingHealthFailed), h.failed),
]
.spacing(10),
text(if h.queued == 0 {
tr(locale, MessageKey::IndexingIdle).to_string()
} else {
files_indexed(locale, h.indexed)
})
.size(13),
];
page(content)
}
pub fn storage_view(state: &AppState) -> Element<'_, Message> {
let locale = state.locale;
let gib = state.storage_total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let content = column![
heading(tr(locale, MessageKey::StorageTitle)),
text(tr(locale, MessageKey::StorageIntro)).size(13),
text(format!("{gib:.2} GiB")).size(20),
text(tr(locale, MessageKey::StorageSafeCleanupHeading)).size(15),
row![
button(text(tr(locale, MessageKey::StorageClearSnippets)).size(13)),
button(text(tr(locale, MessageKey::StorageClearSearchCache)).size(13)),
]
.spacing(8),
text(tr(locale, MessageKey::StorageDangerHeading)).size(15),
button(text(tr(locale, MessageKey::StorageResetCatalog)).size(13)),
text(tr(locale, MessageKey::StorageResetWarning)).size(11),
];
page(content)
}
pub fn models_view(state: &AppState) -> Element<'_, Message> {
let locale = state.locale;
let available = tr(locale, MessageKey::ModelsStatusAvailable);
let missing = tr(locale, MessageKey::ModelsStatusMissing);
let (embedding, reranker) = match state.capability {
SearchCapability::KeywordOnly => (missing, missing),
SearchCapability::Hybrid => (available, missing),
SearchCapability::HybridWithRerank => (available, available),
};
let mut content = column![
heading(tr(locale, MessageKey::ModelsTitle)),
text(format!("{}: {embedding}", tr(locale, MessageKey::ModelsEmbeddingRole))).size(14),
text(format!("{}: {reranker}", tr(locale, MessageKey::ModelsRerankerRole))).size(14),
];
if state.capability == SearchCapability::KeywordOnly {
content = content.push(text(tr(locale, MessageKey::ModelsKeywordOnlyHint)).size(12));
}
page(content)
}
pub fn settings_view(state: &AppState) -> Element<'_, Message> {
let locale = state.locale;
let mut language_row = row![].spacing(8);
for candidate in Locale::ALL {
let label = text(candidate.display_name()).size(13);
let mut b = button(label);
if *candidate != locale {
b = b.on_press(Message::SetLocale(*candidate));
}
language_row = language_row.push(b);
}
let content = column![
heading(tr(locale, MessageKey::SettingsTitle)),
text(tr(locale, MessageKey::SettingsLanguageHeading)).size(15),
language_row,
text(tr(locale, MessageKey::SettingsPrivacyHeading)).size(15),
text(tr(locale, MessageKey::SettingsPrivacyLocalOnly)).size(13),
];
page(content)
}