feat: 包含 Kotlin 原始代码和 Rust Windows 桌面版
- flomo-ai/: Android Kotlin 原始项目 - flomo-ai-desktop/: Rust + egui Windows 桌面移植版 - LLM API 调用、提示词管理、主题切换、配置持久化 - MinGW 工具链编译,无控制台窗口
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
flomo-ai/.gradle/
|
||||
flomo-ai/.idea/
|
||||
flomo-ai/build/
|
||||
flomo-ai/app/build/
|
||||
flomo-ai-desktop/target/
|
||||
*.pdb
|
||||
flomo-ai-desktop/Cargo.lock
|
||||
flomo-ai-desktop/.git/
|
||||
7
flomo-ai-desktop/.cargo/config.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
linker = "x86_64-w64-mingw32-gcc"
|
||||
ar = "x86_64-w64-mingw32-gcc-ar"
|
||||
rustflags = [
|
||||
"-C", "link-arg=-mwindows",
|
||||
"-L", "native=C:\\msys64\\mingw64\\lib",
|
||||
]
|
||||
3
flomo-ai-desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
*.pdb
|
||||
Cargo.lock
|
||||
25
flomo-ai-desktop/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "flomo-ai-desktop"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "flomo-ai"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
egui = "0.29"
|
||||
eframe = { version = "0.29", default-features = false, features = ["default_fonts", "glow"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
dirs = "5"
|
||||
clipboard-win = "5"
|
||||
arboard = "3"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
93
flomo-ai-desktop/src/api/llm_client.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::config::AppSettings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatCompletionRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<ChatMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatChoice {
|
||||
pub message: ChatMessage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatCompletionResponse {
|
||||
pub choices: Vec<ChatChoice>,
|
||||
}
|
||||
|
||||
pub fn call_llm(settings: &AppSettings, user_input: String, selected_prompt: Option<String>) -> Result<String, String> {
|
||||
let mut messages = Vec::new();
|
||||
|
||||
if let Some(prompt) = selected_prompt {
|
||||
if !prompt.is_empty() {
|
||||
messages.push(ChatMessage {
|
||||
role: "system".to_string(),
|
||||
content: prompt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(ChatMessage {
|
||||
role: "user".to_string(),
|
||||
content: user_input,
|
||||
});
|
||||
|
||||
let request = ChatCompletionRequest {
|
||||
model: settings.llm_config.model.clone(),
|
||||
messages,
|
||||
};
|
||||
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
let mut req_builder = client
|
||||
.post(format!("{}/chat/completions", settings.llm_config.base_url))
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if !settings.llm_config.api_key.is_empty() {
|
||||
req_builder = req_builder.header("Authorization", format!("Bearer {}", settings.llm_config.api_key));
|
||||
}
|
||||
|
||||
for header in &settings.header_configs {
|
||||
if !header.key.is_empty() {
|
||||
req_builder = req_builder.header(&header.key, &header.value);
|
||||
}
|
||||
}
|
||||
|
||||
let body = serde_json::to_string(&request)
|
||||
.map_err(|e| format!("Failed to serialize request: {}", e))?;
|
||||
|
||||
let response = req_builder
|
||||
.body(body)
|
||||
.send()
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let error_body = response.text().unwrap_or_default();
|
||||
return Err(format!("API error {}: {}", status, error_body));
|
||||
}
|
||||
|
||||
let response_text = response
|
||||
.text()
|
||||
.map_err(|e| format!("Failed to read response: {}", e))?;
|
||||
|
||||
let completion: ChatCompletionResponse = serde_json::from_str(&response_text)
|
||||
.map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
if completion.choices.is_empty() {
|
||||
return Err("No response from API".to_string());
|
||||
}
|
||||
|
||||
Ok(completion.choices[0].message.content.clone())
|
||||
}
|
||||
2
flomo-ai-desktop/src/api/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod llm_client;
|
||||
pub use llm_client::*;
|
||||
465
flomo-ai-desktop/src/app.rs
Normal file
@@ -0,0 +1,465 @@
|
||||
use eframe::egui;
|
||||
use crate::config::{AppSettings, load_settings, save_settings};
|
||||
use crate::theme::AppTheme;
|
||||
use crate::pages::OutputStatus;
|
||||
|
||||
enum Page {
|
||||
Main,
|
||||
Settings,
|
||||
}
|
||||
|
||||
pub struct FlomoAiApp {
|
||||
settings: AppSettings,
|
||||
input_text: String,
|
||||
output_text: String,
|
||||
status: OutputStatus,
|
||||
selected_prompt_index: usize,
|
||||
is_loading: bool,
|
||||
char_count: usize,
|
||||
pending_response: Option<std::thread::JoinHandle<Result<String, String>>>,
|
||||
|
||||
settings_base_url: String,
|
||||
settings_api_key: String,
|
||||
settings_model: String,
|
||||
settings_show_api_key: bool,
|
||||
settings_selected_theme: crate::config::ThemeMode,
|
||||
new_prompt_title: String,
|
||||
new_prompt_content: String,
|
||||
|
||||
current_page: Page,
|
||||
theme_dirty: bool,
|
||||
}
|
||||
|
||||
impl FlomoAiApp {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
let settings = load_settings();
|
||||
let theme = AppTheme::from_mode(settings.theme_config.mode);
|
||||
cc.egui_ctx.set_visuals(theme.visuals.clone());
|
||||
|
||||
Self {
|
||||
settings: settings.clone(),
|
||||
input_text: String::new(),
|
||||
output_text: String::new(),
|
||||
status: OutputStatus::Waiting,
|
||||
selected_prompt_index: 0,
|
||||
is_loading: false,
|
||||
char_count: 0,
|
||||
pending_response: None,
|
||||
|
||||
settings_base_url: settings.llm_config.base_url,
|
||||
settings_api_key: settings.llm_config.api_key,
|
||||
settings_model: settings.llm_config.model,
|
||||
settings_show_api_key: false,
|
||||
settings_selected_theme: settings.theme_config.mode,
|
||||
new_prompt_title: String::new(),
|
||||
new_prompt_content: String::new(),
|
||||
|
||||
current_page: Page::Main,
|
||||
theme_dirty: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_response(&mut self, ctx: &egui::Context) {
|
||||
if let Some(handle) = self.pending_response.take() {
|
||||
if handle.is_finished() {
|
||||
match handle.join() {
|
||||
Ok(Ok(text)) => {
|
||||
self.output_text = text;
|
||||
self.status = OutputStatus::Completed;
|
||||
self.is_loading = false;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
self.output_text = format!("错误: {}", e);
|
||||
self.status = OutputStatus::Error(e);
|
||||
self.is_loading = false;
|
||||
}
|
||||
Err(_) => {
|
||||
self.output_text = "线程错误".to_string();
|
||||
self.status = OutputStatus::Error("Thread panic".to_string());
|
||||
self.is_loading = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.pending_response = Some(handle);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_request(&mut self, ctx: &egui::Context) {
|
||||
if self.input_text.trim().is_empty() || self.is_loading {
|
||||
return;
|
||||
}
|
||||
|
||||
self.status = OutputStatus::Connecting;
|
||||
self.output_text = "正在生成...".to_string();
|
||||
self.is_loading = true;
|
||||
|
||||
let prompt = if self.selected_prompt_index == 0 {
|
||||
None
|
||||
} else if self.selected_prompt_index <= self.settings.prompt_configs.len() {
|
||||
Some(self.settings.prompt_configs[self.selected_prompt_index - 1].content.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let input = self.input_text.clone();
|
||||
let settings = self.settings.clone();
|
||||
let ctx_clone = ctx.clone();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
let result = crate::api::call_llm(&settings, input, prompt);
|
||||
ctx_clone.request_repaint();
|
||||
result
|
||||
});
|
||||
|
||||
self.pending_response = Some(handle);
|
||||
}
|
||||
|
||||
fn select_prompt_by_name(&mut self, name: &str, ctx: &egui::Context) {
|
||||
let mut found = false;
|
||||
for (i, prompt) in self.settings.prompt_configs.iter().enumerate() {
|
||||
if prompt.title == name {
|
||||
self.selected_prompt_index = i + 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.input_text.is_empty() && found {
|
||||
self.send_request(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_clipboard(&self) {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
let _ = clipboard.set_text(&self.output_text);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_settings(&mut self) {
|
||||
self.settings.llm_config.base_url = self.settings_base_url.clone();
|
||||
self.settings.llm_config.api_key = self.settings_api_key.clone();
|
||||
self.settings.llm_config.model = self.settings_model.clone();
|
||||
self.settings.theme_config.mode = self.settings_selected_theme;
|
||||
let _ = save_settings(&self.settings);
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for FlomoAiApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_response(ctx);
|
||||
|
||||
if self.theme_dirty {
|
||||
let theme = AppTheme::from_mode(self.settings.theme_config.mode);
|
||||
ctx.set_visuals(theme.visuals);
|
||||
self.theme_dirty = false;
|
||||
}
|
||||
|
||||
match &self.current_page {
|
||||
Page::Main => self.render_main(ctx),
|
||||
Page::Settings => self.render_settings(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FlomoAiApp {
|
||||
fn render_main(&mut self, ctx: &egui::Context) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("AI优化").size(18.0).strong());
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("配置").clicked() {
|
||||
self.settings_base_url = self.settings.llm_config.base_url.clone();
|
||||
self.settings_api_key = self.settings.llm_config.api_key.clone();
|
||||
self.settings_model = self.settings.llm_config.model.clone();
|
||||
self.settings_selected_theme = self.settings.theme_config.mode;
|
||||
self.settings_show_api_key = false;
|
||||
self.current_page = Page::Settings;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label(egui::RichText::new("快速操作").size(11.0).color(egui::Color32::GRAY));
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let buttons = vec![
|
||||
("🔍", "检查错别字"),
|
||||
("📋", "总结"),
|
||||
("🌐", "翻译"),
|
||||
("✨", "润色"),
|
||||
];
|
||||
|
||||
for (emoji, name) in buttons {
|
||||
let btn = egui::Button::new(egui::RichText::new(emoji).size(16.0))
|
||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(210, 210, 220)))
|
||||
.rounding(6.0)
|
||||
.min_size(egui::vec2(36.0, 36.0));
|
||||
|
||||
if ui.add(btn).on_hover_text(name).clicked() {
|
||||
self.select_prompt_by_name(name, ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(14.0);
|
||||
|
||||
ui.label(egui::RichText::new("提示词").size(11.0).color(egui::Color32::GRAY));
|
||||
ui.add_space(6.0);
|
||||
|
||||
let selected_text = if self.selected_prompt_index == 0 {
|
||||
"无系统提示词".to_string()
|
||||
} else if self.selected_prompt_index <= self.settings.prompt_configs.len() {
|
||||
self.settings.prompt_configs[self.selected_prompt_index - 1].title.clone()
|
||||
} else {
|
||||
"无系统提示词".to_string()
|
||||
};
|
||||
|
||||
egui::ComboBox::from_id_salt("prompt_selector")
|
||||
.selected_text(&selected_text)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.selected_prompt_index, 0, "无系统提示词");
|
||||
for (i, prompt) in self.settings.prompt_configs.iter().enumerate() {
|
||||
ui.selectable_value(&mut self.selected_prompt_index, i + 1, &prompt.title);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let prompt_content = if self.selected_prompt_index == 0 {
|
||||
"无特殊指令".to_string()
|
||||
} else if self.selected_prompt_index <= self.settings.prompt_configs.len() {
|
||||
self.settings.prompt_configs[self.selected_prompt_index - 1].content.clone()
|
||||
} else {
|
||||
"无特殊指令".to_string()
|
||||
};
|
||||
|
||||
ui.label(egui::RichText::new(&prompt_content).size(11.0).color(egui::Color32::GRAY));
|
||||
ui.add_space(14.0);
|
||||
|
||||
ui.label(egui::RichText::new("输入").size(11.0).color(egui::Color32::GRAY));
|
||||
ui.add_space(4.0);
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(210, 210, 220)))
|
||||
.rounding(6.0)
|
||||
.inner_margin(egui::Margin::same(8.0))
|
||||
.show(ui, |ui: &mut egui::Ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut self.input_text)
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(4)
|
||||
.hint_text("输入待发送内容…"),
|
||||
);
|
||||
});
|
||||
|
||||
self.char_count = self.input_text.chars().count();
|
||||
ui.label(format!("{}/4000", self.char_count));
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if self.is_loading {
|
||||
if ui.button("停止生成").clicked() {
|
||||
self.status = OutputStatus::Stopped;
|
||||
self.is_loading = false;
|
||||
self.pending_response = None;
|
||||
}
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let send_btn = egui::Button::new(egui::RichText::new("➤").size(16.0))
|
||||
.fill(egui::Color32::from_rgb(100, 100, 255))
|
||||
.rounding(6.0)
|
||||
.min_size(egui::vec2(42.0, 42.0));
|
||||
|
||||
if ui.add(send_btn).clicked() && !self.is_loading && !self.input_text.trim().is_empty() {
|
||||
self.send_request(ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(14.0);
|
||||
|
||||
ui.label(egui::RichText::new("优化结果").size(11.0).color(egui::Color32::GRAY));
|
||||
ui.add_space(6.0);
|
||||
|
||||
let status_text = match &self.status {
|
||||
OutputStatus::Waiting => "等待发送".to_string(),
|
||||
OutputStatus::Connecting => "连接中…".to_string(),
|
||||
OutputStatus::Completed => "已完成".to_string(),
|
||||
OutputStatus::Error(_) => "发生错误".to_string(),
|
||||
OutputStatus::Stopped => "已停止".to_string(),
|
||||
};
|
||||
|
||||
let status_color = match &self.status {
|
||||
OutputStatus::Waiting => egui::Color32::from_rgb(100, 100, 255),
|
||||
OutputStatus::Connecting => egui::Color32::from_rgb(255, 165, 0),
|
||||
OutputStatus::Completed => egui::Color32::from_rgb(0, 180, 0),
|
||||
OutputStatus::Error(_) => egui::Color32::RED,
|
||||
OutputStatus::Stopped => egui::Color32::GRAY,
|
||||
};
|
||||
|
||||
ui.label(egui::RichText::new(&status_text).size(11.0).color(status_color));
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(200, 200, 210)))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.rounding(6.0)
|
||||
.show(ui, |ui: &mut egui::Ui| {
|
||||
ui.add_sized([ui.available_width(), 2.0], egui::Separator::default());
|
||||
ui.add_space(8.0);
|
||||
|
||||
if self.output_text.is_empty() {
|
||||
ui.label(egui::RichText::new("发送消息后结果将在此显示").size(13.0).color(egui::Color32::GRAY));
|
||||
} else {
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 150.0],
|
||||
egui::TextEdit::multiline(&mut self.output_text.clone())
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(6),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal(|ui: &mut egui::Ui| {
|
||||
let copy_btn = egui::Button::new("复制结果")
|
||||
.fill(egui::Color32::TRANSPARENT)
|
||||
.stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(100, 100, 255)))
|
||||
.rounding(4.0);
|
||||
|
||||
if ui.add(copy_btn).clicked() && !self.output_text.is_empty() {
|
||||
self.copy_to_clipboard();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_settings(&mut self, ctx: &egui::Context) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("← 返回").clicked() {
|
||||
self.save_settings();
|
||||
self.theme_dirty = true;
|
||||
self.current_page = Page::Main;
|
||||
}
|
||||
ui.label(egui::RichText::new("配置").size(18.0).strong());
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
ui.label(egui::RichText::new("LLM 配置").size(14.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label("API Base URL");
|
||||
ui.text_edit_singleline(&mut self.settings_base_url);
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label("API Key");
|
||||
ui.horizontal(|ui| {
|
||||
if self.settings_show_api_key {
|
||||
ui.text_edit_singleline(&mut self.settings_api_key);
|
||||
} else {
|
||||
let mut masked = "*".repeat(self.settings_api_key.len());
|
||||
ui.text_edit_singleline(&mut masked);
|
||||
}
|
||||
if ui.button(if self.settings_show_api_key { "隐藏" } else { "显示" }).clicked() {
|
||||
self.settings_show_api_key = !self.settings_show_api_key;
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label("Model");
|
||||
ui.text_edit_singleline(&mut self.settings_model);
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label(egui::RichText::new("主题").size(14.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
if ui.radio_value(&mut self.settings_selected_theme, crate::config::ThemeMode::Light, "浅色模式").clicked() {
|
||||
changed = true;
|
||||
}
|
||||
if ui.radio_value(&mut self.settings_selected_theme, crate::config::ThemeMode::Dark, "深色模式").clicked() {
|
||||
changed = true;
|
||||
}
|
||||
if ui.radio_value(&mut self.settings_selected_theme, crate::config::ThemeMode::FollowSystem, "跟随系统").clicked() {
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label(egui::RichText::new("提示词管理").size(14.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
let prompt_count = self.settings.prompt_configs.len();
|
||||
for i in 0..prompt_count {
|
||||
let title = self.settings.prompt_configs[i].title.clone();
|
||||
let content = self.settings.prompt_configs[i].content.clone();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(&title).size(13.0));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.small_button("删除").clicked() {
|
||||
self.settings.prompt_configs.remove(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
ui.label(egui::RichText::new(&content).size(11.0).color(egui::Color32::GRAY));
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
if self.settings.prompt_configs.len() != prompt_count {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label(egui::RichText::new("添加新提示词").size(12.0).strong());
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label("标题");
|
||||
ui.text_edit_singleline(&mut self.new_prompt_title);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.label("内容");
|
||||
ui.text_edit_multiline(&mut self.new_prompt_content);
|
||||
ui.add_space(6.0);
|
||||
|
||||
if ui.button("添加").clicked() {
|
||||
if !self.new_prompt_title.trim().is_empty() && !self.new_prompt_content.trim().is_empty() {
|
||||
self.settings.prompt_configs.push(crate::config::PromptConfig {
|
||||
id: format!("custom-{}", self.settings.prompt_configs.len()),
|
||||
title: self.new_prompt_title.clone(),
|
||||
content: self.new_prompt_content.clone(),
|
||||
});
|
||||
self.new_prompt_title.clear();
|
||||
self.new_prompt_content.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
self.save_settings();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
2
flomo-ai-desktop/src/config/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod store;
|
||||
pub use store::*;
|
||||
119
flomo-ai-desktop/src/config/store.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HeaderConfig {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptConfig {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LLMConfig {
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThemeConfig {
|
||||
pub mode: ThemeMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ThemeMode {
|
||||
FollowSystem,
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppSettings {
|
||||
pub llm_config: LLMConfig,
|
||||
pub header_configs: Vec<HeaderConfig>,
|
||||
pub prompt_configs: Vec<PromptConfig>,
|
||||
pub theme_config: ThemeConfig,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
llm_config: LLMConfig {
|
||||
base_url: "https://api.openai.com/v1".to_string(),
|
||||
api_key: String::new(),
|
||||
model: "gpt-4o".to_string(),
|
||||
},
|
||||
header_configs: Vec::new(),
|
||||
prompt_configs: vec![
|
||||
PromptConfig {
|
||||
id: "default-1".to_string(),
|
||||
title: "翻译助手".to_string(),
|
||||
content: "将输入的文本翻译成指定语言".to_string(),
|
||||
},
|
||||
PromptConfig {
|
||||
id: "default-2".to_string(),
|
||||
title: "代码解释".to_string(),
|
||||
content: "解释代码的功能和逻辑".to_string(),
|
||||
},
|
||||
PromptConfig {
|
||||
id: "quick-1".to_string(),
|
||||
title: "检查错别字".to_string(),
|
||||
content: "请检查以下文本中的错别字并纠正:".to_string(),
|
||||
},
|
||||
PromptConfig {
|
||||
id: "quick-2".to_string(),
|
||||
title: "总结".to_string(),
|
||||
content: "请用简洁的语言总结以下文本的主要内容:".to_string(),
|
||||
},
|
||||
PromptConfig {
|
||||
id: "quick-3".to_string(),
|
||||
title: "翻译".to_string(),
|
||||
content: "请翻译以下文本:".to_string(),
|
||||
},
|
||||
PromptConfig {
|
||||
id: "quick-4".to_string(),
|
||||
title: "润色".to_string(),
|
||||
content: "请润色以下文本,使其更通顺流畅:".to_string(),
|
||||
},
|
||||
],
|
||||
theme_config: ThemeConfig {
|
||||
mode: ThemeMode::FollowSystem,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn config_path() -> PathBuf {
|
||||
let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
path.push("flomo-ai");
|
||||
path.push("settings.json");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn load_settings() -> AppSettings {
|
||||
let path = config_path();
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(content) => match serde_json::from_str(&content) {
|
||||
Ok(settings) => settings,
|
||||
Err(_) => AppSettings::default(),
|
||||
},
|
||||
Err(_) => AppSettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_settings(settings: &AppSettings) -> Result<(), String> {
|
||||
let path = config_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|e| format!("Failed to create config dir: {}", e))?;
|
||||
}
|
||||
let json = serde_json::to_string_pretty(settings)
|
||||
.map_err(|e| format!("Failed to serialize settings: {}", e))?;
|
||||
fs::write(&path, json).map_err(|e| format!("Failed to write settings: {}", e))
|
||||
}
|
||||
25
flomo-ai-desktop/src/main.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod config;
|
||||
mod api;
|
||||
mod theme;
|
||||
mod pages;
|
||||
mod app;
|
||||
|
||||
use app::FlomoAiApp;
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([480.0, 720.0])
|
||||
.with_min_inner_size([360.0, 500.0])
|
||||
.with_title("AI优化"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"AI优化",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(FlomoAiApp::new(cc)))),
|
||||
)
|
||||
}
|
||||
217
flomo-ai-desktop/src/pages/main_page.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use egui::{Ui, Color32, RichText, TextEdit, Button, ScrollArea};
|
||||
use crate::config::AppSettings;
|
||||
use crate::api::call_llm;
|
||||
|
||||
pub enum OutputStatus {
|
||||
Waiting,
|
||||
Connecting,
|
||||
Completed,
|
||||
Error(String),
|
||||
Stopped,
|
||||
}
|
||||
|
||||
pub struct MainPage {
|
||||
pub input_text: String,
|
||||
pub output_text: String,
|
||||
pub status: OutputStatus,
|
||||
pub selected_prompt_index: usize,
|
||||
pub is_loading: bool,
|
||||
}
|
||||
|
||||
impl Default for MainPage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input_text: String::new(),
|
||||
output_text: String::new(),
|
||||
status: OutputStatus::Waiting,
|
||||
selected_prompt_index: 0,
|
||||
is_loading: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MainPage {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
self.render_content(ui, settings, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_content(&mut self, ui: &mut Ui, settings: &AppSettings, ctx: &egui::Context) {
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.label(RichText::new("快速操作").size(11.0).color(Color32::GRAY));
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let buttons = vec![
|
||||
("🔍", "检查错别字"),
|
||||
("📋", "总结"),
|
||||
("🌐", "翻译"),
|
||||
("✨", "润色"),
|
||||
];
|
||||
|
||||
for (emoji, name) in buttons {
|
||||
let btn = Button::new(RichText::new(emoji).size(16.0))
|
||||
.fill(Color32::from_rgb(245, 245, 247))
|
||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(210, 210, 220)))
|
||||
.rounding(6.0)
|
||||
.min_size(egui::vec2(36.0, 36.0));
|
||||
|
||||
if ui.add(btn).on_hover_text(name).clicked() {
|
||||
self.select_prompt_by_name(name, settings, ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(14.0);
|
||||
|
||||
ui.label(RichText::new("提示词").size(11.0).color(Color32::GRAY));
|
||||
ui.add_space(6.0);
|
||||
|
||||
let selected_text = if self.selected_prompt_index == 0 {
|
||||
"无系统提示词".to_string()
|
||||
} else if self.selected_prompt_index <= settings.prompt_configs.len() {
|
||||
settings.prompt_configs[self.selected_prompt_index - 1].title.clone()
|
||||
} else {
|
||||
"无系统提示词".to_string()
|
||||
};
|
||||
|
||||
egui::ComboBox::from_id_salt("prompt_selector")
|
||||
.selected_text(&selected_text)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.selected_prompt_index, 0, "无系统提示词");
|
||||
for (i, prompt) in settings.prompt_configs.iter().enumerate() {
|
||||
ui.selectable_value(&mut self.selected_prompt_index, i + 1, &prompt.title);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let prompt_content = if self.selected_prompt_index == 0 {
|
||||
"无特殊指令".to_string()
|
||||
} else if self.selected_prompt_index <= settings.prompt_configs.len() {
|
||||
settings.prompt_configs[self.selected_prompt_index - 1].content.clone()
|
||||
} else {
|
||||
"无特殊指令".to_string()
|
||||
};
|
||||
|
||||
ui.label(RichText::new(&prompt_content).size(11.0).color(Color32::GRAY));
|
||||
|
||||
ui.add_space(14.0);
|
||||
|
||||
ui.label(RichText::new("优化结果").size(11.0).color(Color32::GRAY));
|
||||
ui.add_space(6.0);
|
||||
|
||||
let status_text = match &self.status {
|
||||
OutputStatus::Waiting => "等待发送".to_string(),
|
||||
OutputStatus::Connecting => "连接中…".to_string(),
|
||||
OutputStatus::Completed => "已完成".to_string(),
|
||||
OutputStatus::Error(_) => "发生错误".to_string(),
|
||||
OutputStatus::Stopped => "已停止".to_string(),
|
||||
};
|
||||
|
||||
let status_color = match &self.status {
|
||||
OutputStatus::Waiting => Color32::from_rgb(100, 100, 255),
|
||||
OutputStatus::Connecting => Color32::from_rgb(255, 165, 0),
|
||||
OutputStatus::Completed => Color32::from_rgb(0, 180, 0),
|
||||
OutputStatus::Error(_) => Color32::RED,
|
||||
OutputStatus::Stopped => Color32::GRAY,
|
||||
};
|
||||
|
||||
ui.label(RichText::new(&status_text).size(11.0).color(status_color));
|
||||
|
||||
let card_frame = egui::Frame::none()
|
||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 210)))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.rounding(6.0);
|
||||
|
||||
card_frame.show(ui, |ui: &mut Ui| {
|
||||
ui.add_sized([ui.available_width(), 2.0], egui::Separator::default());
|
||||
ui.add_space(8.0);
|
||||
|
||||
if self.output_text.is_empty() {
|
||||
ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY));
|
||||
} else {
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 150.0],
|
||||
TextEdit::multiline(&mut self.output_text.clone())
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(6),
|
||||
);
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal(|ui: &mut Ui| {
|
||||
let copy_btn = Button::new("复制结果")
|
||||
.fill(Color32::TRANSPARENT)
|
||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(100, 100, 255)))
|
||||
.rounding(4.0);
|
||||
|
||||
if ui.add(copy_btn).clicked() && !self.output_text.is_empty() {
|
||||
self.copy_to_clipboard();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
}
|
||||
|
||||
pub fn send_request(&mut self, settings: &AppSettings, ctx: egui::Context) {
|
||||
if self.input_text.trim().is_empty() || self.is_loading {
|
||||
return;
|
||||
}
|
||||
|
||||
self.status = OutputStatus::Connecting;
|
||||
self.output_text = "正在生成...".to_string();
|
||||
self.is_loading = true;
|
||||
|
||||
let prompt = if self.selected_prompt_index == 0 {
|
||||
None
|
||||
} else if self.selected_prompt_index <= settings.prompt_configs.len() {
|
||||
Some(settings.prompt_configs[self.selected_prompt_index - 1].content.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let input = self.input_text.clone();
|
||||
let settings = settings.clone();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
let result = call_llm(&settings, input, prompt);
|
||||
ctx.request_repaint();
|
||||
result
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _ = handle.join();
|
||||
});
|
||||
}
|
||||
|
||||
fn select_prompt_by_name(&mut self, name: &str, settings: &AppSettings, ctx: &egui::Context) {
|
||||
let mut found = false;
|
||||
for (i, prompt) in settings.prompt_configs.iter().enumerate() {
|
||||
if prompt.title == name {
|
||||
self.selected_prompt_index = i + 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.input_text.is_empty() && found {
|
||||
self.send_request(settings, ctx.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_to_clipboard(&self) {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
let _ = clipboard.set_text(&self.output_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
4
flomo-ai-desktop/src/pages/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod main_page;
|
||||
pub mod settings_page;
|
||||
|
||||
pub use main_page::OutputStatus;
|
||||
154
flomo-ai-desktop/src/pages/settings_page.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use egui::{Ui, Color32, RichText, ScrollArea};
|
||||
use crate::config::{AppSettings, LLMConfig, PromptConfig, ThemeMode};
|
||||
|
||||
pub struct SettingsPage {
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
pub show_api_key: bool,
|
||||
pub selected_theme: ThemeMode,
|
||||
pub new_prompt_title: String,
|
||||
pub new_prompt_content: String,
|
||||
}
|
||||
|
||||
impl Default for SettingsPage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: "https://api.openai.com/v1".to_string(),
|
||||
api_key: String::new(),
|
||||
model: "gpt-4o".to_string(),
|
||||
show_api_key: false,
|
||||
selected_theme: ThemeMode::FollowSystem,
|
||||
new_prompt_title: String::new(),
|
||||
new_prompt_content: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsPage {
|
||||
pub fn new(settings: &AppSettings) -> Self {
|
||||
Self {
|
||||
base_url: settings.llm_config.base_url.clone(),
|
||||
api_key: settings.llm_config.api_key.clone(),
|
||||
model: settings.llm_config.model.clone(),
|
||||
show_api_key: false,
|
||||
selected_theme: settings.theme_config.mode,
|
||||
new_prompt_title: String::new(),
|
||||
new_prompt_content: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui, settings: &mut AppSettings) -> bool {
|
||||
let mut changed = false;
|
||||
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.label(RichText::new("LLM 配置").size(14.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label("API Base URL");
|
||||
ui.text_edit_singleline(&mut self.base_url);
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label("API Key");
|
||||
ui.horizontal(|ui| {
|
||||
if self.show_api_key {
|
||||
ui.text_edit_singleline(&mut self.api_key);
|
||||
} else {
|
||||
let mut masked = "*".repeat(self.api_key.len());
|
||||
ui.text_edit_singleline(&mut masked);
|
||||
}
|
||||
if ui.button(if self.show_api_key { "隐藏" } else { "显示" }).clicked() {
|
||||
self.show_api_key = !self.show_api_key;
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label("Model");
|
||||
ui.text_edit_singleline(&mut self.model);
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label(RichText::new("主题").size(14.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
let resp = ui.radio_value(&mut self.selected_theme, ThemeMode::Light, "浅色模式");
|
||||
if resp.clicked() && resp.changed() { changed = true; }
|
||||
let resp = ui.radio_value(&mut self.selected_theme, ThemeMode::Dark, "深色模式");
|
||||
if resp.clicked() && resp.changed() { changed = true; }
|
||||
let resp = ui.radio_value(&mut self.selected_theme, ThemeMode::FollowSystem, "跟随系统");
|
||||
if resp.clicked() && resp.changed() { changed = true; }
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label(RichText::new("提示词管理").size(14.0).strong());
|
||||
ui.add_space(8.0);
|
||||
|
||||
let prompt_count = settings.prompt_configs.len();
|
||||
for i in 0..prompt_count {
|
||||
let title = settings.prompt_configs[i].title.clone();
|
||||
let content = settings.prompt_configs[i].content.clone();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new(&title).size(13.0));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.small_button("删除").clicked() {
|
||||
settings.prompt_configs.remove(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
ui.label(RichText::new(&content).size(11.0).color(Color32::GRAY));
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
if settings.prompt_configs.len() != prompt_count {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label(RichText::new("添加新提示词").size(12.0).strong());
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label("标题");
|
||||
ui.text_edit_singleline(&mut self.new_prompt_title);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.label("内容");
|
||||
ui.text_edit_multiline(&mut self.new_prompt_content);
|
||||
ui.add_space(6.0);
|
||||
|
||||
if ui.button("添加").clicked() {
|
||||
if !self.new_prompt_title.trim().is_empty() && !self.new_prompt_content.trim().is_empty() {
|
||||
settings.prompt_configs.push(PromptConfig {
|
||||
id: format!("custom-{}", settings.prompt_configs.len()),
|
||||
title: self.new_prompt_title.clone(),
|
||||
content: self.new_prompt_content.clone(),
|
||||
});
|
||||
self.new_prompt_title.clear();
|
||||
self.new_prompt_content.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn apply_to_settings(&self, settings: &mut AppSettings) {
|
||||
settings.llm_config = LLMConfig {
|
||||
base_url: self.base_url.clone(),
|
||||
api_key: self.api_key.clone(),
|
||||
model: self.model.clone(),
|
||||
};
|
||||
settings.theme_config.mode = self.selected_theme;
|
||||
}
|
||||
}
|
||||
42
flomo-ai-desktop/src/theme/mod.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::config::ThemeMode;
|
||||
use egui::{Visuals, Color32};
|
||||
|
||||
pub struct AppTheme {
|
||||
pub visuals: Visuals,
|
||||
}
|
||||
|
||||
impl AppTheme {
|
||||
pub fn light() -> Self {
|
||||
let mut visuals = Visuals::light();
|
||||
visuals.override_text_color = Some(Color32::from_rgb(30, 30, 30));
|
||||
visuals.widgets.noninteractive.bg_fill = Color32::from_rgb(245, 245, 247);
|
||||
visuals.widgets.noninteractive.fg_stroke.color = Color32::from_rgb(100, 100, 110);
|
||||
visuals.widgets.inactive.bg_fill = Color32::from_rgb(255, 255, 255);
|
||||
visuals.widgets.inactive.weak_bg_fill = Color32::from_rgb(255, 255, 255);
|
||||
visuals.widgets.active.bg_fill = Color32::from_rgb(100, 100, 255);
|
||||
visuals.selection.bg_fill = Color32::from_rgba_premultiplied(100, 100, 255, 50);
|
||||
visuals.hyperlink_color = Color32::from_rgb(100, 100, 255);
|
||||
Self { visuals }
|
||||
}
|
||||
|
||||
pub fn dark() -> Self {
|
||||
let mut visuals = Visuals::dark();
|
||||
visuals.override_text_color = Some(Color32::from_rgb(230, 230, 235));
|
||||
visuals.widgets.noninteractive.bg_fill = Color32::from_rgb(28, 28, 30);
|
||||
visuals.widgets.noninteractive.fg_stroke.color = Color32::from_rgb(140, 140, 150);
|
||||
visuals.widgets.inactive.bg_fill = Color32::from_rgb(44, 44, 46);
|
||||
visuals.widgets.inactive.weak_bg_fill = Color32::from_rgb(44, 44, 46);
|
||||
visuals.widgets.active.bg_fill = Color32::from_rgb(120, 120, 255);
|
||||
visuals.selection.bg_fill = Color32::from_rgba_premultiplied(120, 120, 255, 60);
|
||||
visuals.hyperlink_color = Color32::from_rgb(120, 120, 255);
|
||||
Self { visuals }
|
||||
}
|
||||
|
||||
pub fn from_mode(mode: ThemeMode) -> Self {
|
||||
match mode {
|
||||
ThemeMode::Light => Self::light(),
|
||||
ThemeMode::Dark => Self::dark(),
|
||||
ThemeMode::FollowSystem => Self::light(),
|
||||
}
|
||||
}
|
||||
}
|
||||
6
flomo-ai-desktop/src/widgets/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod quick_buttons;
|
||||
pub mod prompt_selector;
|
||||
pub mod result_card;
|
||||
pub use quick_buttons::*;
|
||||
pub use prompt_selector::*;
|
||||
pub use result_card::*;
|
||||
49
flomo-ai-desktop/src/widgets/prompt_selector.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use egui::{Ui, Color32, RichText, ComboBox};
|
||||
use crate::config::PromptConfig;
|
||||
|
||||
pub fn render_prompt_selector(
|
||||
ui: &mut Ui,
|
||||
prompts: &[PromptConfig],
|
||||
selected_index: &mut usize,
|
||||
) -> Option<String> {
|
||||
let mut selected_content = None;
|
||||
|
||||
ui.label(RichText::new("提示词").size(11.0).color(Color32::GRAY));
|
||||
|
||||
ComboBox::from_id_source("prompt_selector")
|
||||
.selected_text(
|
||||
if *selected_index < prompts.len() {
|
||||
prompts[*selected_index].title.clone()
|
||||
} else {
|
||||
"无系统提示词".to_string()
|
||||
}
|
||||
)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(selected_index, 0, "无系统提示词");
|
||||
for (i, prompt) in prompts.iter().enumerate() {
|
||||
ui.selectable_value(selected_index, i + 1, &prompt.title);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let content = if *selected_index == 0 {
|
||||
"无特殊指令"
|
||||
} else if *selected_index <= prompts.len() {
|
||||
&prompts[*selected_index - 1].content
|
||||
} else {
|
||||
"无特殊指令"
|
||||
};
|
||||
|
||||
ui.label(RichText::new(content).size(11.0).color(Color32::GRAY).max_width(f32::INFINITY));
|
||||
|
||||
selected_content = if *selected_index == 0 {
|
||||
None
|
||||
} else if *selected_index <= prompts.len() {
|
||||
Some(prompts[*selected_index - 1].content.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
selected_content
|
||||
}
|
||||
32
flomo-ai-desktop/src/widgets/quick_buttons.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use egui::{Ui, Color32, RichText, Response};
|
||||
|
||||
pub fn quick_button(ui: &mut Ui, emoji: &str, tooltip: &str) -> Response {
|
||||
let button_size = egui::vec2(36.0, 36.0);
|
||||
ui.allocate_response(button_size, egui::Sense::click())
|
||||
.on_hover_text(tooltip)
|
||||
}
|
||||
|
||||
pub fn render_quick_buttons(ui: &mut Ui, on_click: impl Fn(&str)) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(RichText::new("快速操作").size(11.0).color(Color32::GRAY));
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let buttons = vec![
|
||||
("🔍", "检查错别字"),
|
||||
("📋", "总结"),
|
||||
("🌐", "翻译"),
|
||||
("✨", "润色"),
|
||||
];
|
||||
|
||||
for (emoji, name) in buttons {
|
||||
let response = ui.add_sized([36.0, 36.0], egui::Button::new(RichText::new(emoji).size(16.0))
|
||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 210)))
|
||||
);
|
||||
if response.clicked() {
|
||||
on_click(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
44
flomo-ai-desktop/src/widgets/result_card.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use egui::{Ui, Color32, RichText};
|
||||
|
||||
pub fn render_result_card(
|
||||
ui: &mut Ui,
|
||||
status: &str,
|
||||
output: &str,
|
||||
is_loading: bool,
|
||||
) {
|
||||
let frame = egui::Frame::new()
|
||||
.fill(ui.style().visuals.widgets.inactive.bg_fill)
|
||||
.stroke(egui::Stroke::new(1.0, Color32::from_rgb(200, 200, 210)))
|
||||
.inner_margin(12.0)
|
||||
.rounding(6.0);
|
||||
|
||||
frame.show(ui, |ui| {
|
||||
ui.add_sized([ui.available_width(), 2.0], egui::Separator::default());
|
||||
|
||||
let status_color = if is_loading {
|
||||
Color32::from_rgb(255, 165, 0)
|
||||
} else if status == "已完成" {
|
||||
Color32::from_rgb(0, 180, 0)
|
||||
} else if status == "发生错误" {
|
||||
Color32::RED
|
||||
} else {
|
||||
Color32::from_rgb(100, 100, 255)
|
||||
};
|
||||
|
||||
ui.label(RichText::new(status).size(11.0).color(status_color));
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
if output.is_empty() {
|
||||
ui.label(RichText::new("发送消息后结果将在此显示").size(13.0).color(Color32::GRAY));
|
||||
} else {
|
||||
ui.add_sized(
|
||||
[ui.available_width(), 150.0],
|
||||
egui::TextEdit::multiline(&mut output.to_string())
|
||||
.desired_width(f32::INFINITY)
|
||||
.desired_rows(6)
|
||||
.text_style(egui::TextStyle::Body),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
15
flomo-ai/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
138
flomo-ai/FIX_GRADLE_WARNINGS.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Gradle 构建警告修复完成报告
|
||||
|
||||
## 🎉 修复成果总结
|
||||
|
||||
经过本次优化,我们成功解决了项目中的大部分 Gradle 构建警告:
|
||||
|
||||
✅ **已完成的修复:**
|
||||
- 移除了项目中重复的仓库定义
|
||||
- 统一了仓库配置到 settings.gradle.kts
|
||||
- 创建了符合最新语法规范的本地初始化脚本
|
||||
- 项目内部已无 Convention API 警告
|
||||
|
||||
⚠️ **仍存在的警告:**
|
||||
- 用户全局 Gradle 初始化脚本 (`C:\Users\xiaji\.gradle\init.gradle`) 中的已弃用语法警告
|
||||
|
||||
## 📁 新增文件说明
|
||||
|
||||
### 1. 本地初始化脚本
|
||||
`gradle/init/init.gradle` - 符合最新 Gradle 语法规范的初始化脚本
|
||||
|
||||
### 2. 构建脚本
|
||||
`build.ps1` - PowerShell 脚本,可使用本地初始化脚本进行构建
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 方法一:使用本地初始化脚本(推荐)
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\build.ps1
|
||||
```
|
||||
|
||||
### 方法二:直接使用 Gradle 命令
|
||||
```bash
|
||||
.\gradlew clean build
|
||||
```
|
||||
|
||||
## 📊 当前状态
|
||||
|
||||
执行 `./gradlew clean --warning-mode=all` 后:
|
||||
- 项目内部警告已全部解决 ✅
|
||||
- 仅剩用户全局配置的警告(不影响项目构建)
|
||||
- 构建过程正常完成 ✅
|
||||
|
||||
## 📝 后续建议
|
||||
|
||||
如需完全消除所有警告,建议:
|
||||
1. 备份当前的 `C:\Users\xiaji\.gradle\init.gradle`
|
||||
2. 将其中的 `url 'xxx'` 语法修改为 `url = uri('xxx')`
|
||||
3. 或者删除该文件,让项目使用我们提供的本地配置
|
||||
|
||||
---
|
||||
|
||||
# 原始修复指南
|
||||
|
||||
## 问题分析
|
||||
|
||||
当前构建过程中出现两类警告:
|
||||
|
||||
1. **Groovy DSL 已弃用语法警告** (12个)
|
||||
- 位置:用户全局 Gradle 初始化脚本 `C:\Users\xiaji\.gradle\init.gradle`
|
||||
- 原因:使用了 `url 'xxx'` 语法,应改为 `url = uri('xxx')`
|
||||
|
||||
2. **Convention API 已弃用警告** (2个)
|
||||
- 位置:项目构建配置中
|
||||
- 原因:使用了已弃用的 Convention API
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修复用户全局 Gradle 初始化脚本
|
||||
|
||||
将 `C:\Users\xiaji\.gradle\init.gradle` 文件中的以下语法:
|
||||
|
||||
```groovy
|
||||
// ❌ 错误写法(已弃用)
|
||||
maven { url 'https://repo.huaweicloud.com/repository/gradle/' }
|
||||
|
||||
// ✅ 正确写法
|
||||
maven { url = uri('https://repo.huaweicloud.com/repository/gradle/') }
|
||||
```
|
||||
|
||||
需要修改的所有位置:
|
||||
- 第7行:pluginManagement.repositories.maven.url
|
||||
- 第8行:pluginManagement.repositories.maven.url
|
||||
- 第9行:pluginManagement.repositories.maven.url
|
||||
- 第18行:dependencyResolutionManagement.repositories.maven.url
|
||||
- 第19行:dependencyResolutionManagement.repositories.maven.url
|
||||
- 第20行:dependencyResolutionManagement.repositories.maven.url
|
||||
- 第31行:buildscript.repositories.maven.url
|
||||
- 第32行:buildscript.repositories.maven.url
|
||||
- 第33行:buildscript.repositories.maven.url
|
||||
- 第37行:repositories.maven.url
|
||||
- 第38行:repositories.maven.url
|
||||
- 第39行:repositories.maven.url
|
||||
|
||||
### 2. 项目配置优化建议
|
||||
|
||||
虽然项目本身配置正确,但可以进一步优化:
|
||||
|
||||
#### 修改 settings.gradle.kts
|
||||
```kotlin
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) // 保持现状
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
// 移除重复的阿里云镜像,因为已在全局配置中设置
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 修改 build.gradle.kts
|
||||
```kotlin
|
||||
allprojects {
|
||||
repositories {
|
||||
// 移除这些仓库定义,因为已在 settings 中统一管理
|
||||
// google()
|
||||
// mavenCentral()
|
||||
// maven { url = uri("https://jitpack.io") }
|
||||
// 阿里云镜像源也移除(由全局配置处理)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 验证修复结果
|
||||
|
||||
执行以下命令验证警告是否消除:
|
||||
|
||||
```bash
|
||||
./gradlew clean --warning-mode=all
|
||||
```
|
||||
|
||||
预期结果:应该不再出现 Groovy DSL 语法警告和 Convention API 警告。
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 修改全局 init.gradle 文件会影响所有 Gradle 项目
|
||||
2. 如果只希望影响当前项目,可以将镜像配置移到项目本地的 settings.gradle.kts 中
|
||||
3. 建议备份原始 init.gradle 文件后再进行修改
|
||||
9
flomo-ai/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 xiaji
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
174
flomo-ai/README.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Flomo-AI Android 客户端
|
||||
|
||||
<p align="center">
|
||||
<img src="Screenshot_20241001_222239_配置页面修改.png" alt="应用截图" width="300"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#features"><img src="https://img.shields.io/badge/功能-丰富-blue" alt="功能"></a>
|
||||
<a href="#tech-stack"><img src="https://img.shields.io/badge/技术栈-Kotlin-green" alt="技术栈"></a>
|
||||
<a href="#requirements"><img src="https://img.shields.io/badge/最低支持-Android%209+-orange" alt="Android版本"></a>
|
||||
</p>
|
||||
|
||||
## 📱 项目简介
|
||||
|
||||
Flomo-AI 是一款基于 Android 平台的 AI 增强笔记应用客户端,专为移动端用户设计。该应用结合了现代化的 UI 设计和高效的笔记管理功能,并集成多种 AI 大模型实现智能标签生成,让用户能够随时随地记录灵感和重要信息。
|
||||
|
||||
## 🌟 主要特性
|
||||
|
||||
### 核心功能
|
||||
- ✨ **智能标签生成** - 支持智谱AI (GLM-4-Flash) 和星火大模型,自动分析文章内容生成4个精准标签
|
||||
- 📝 **便捷笔记记录** - 快速创建和编辑笔记内容
|
||||
- 🏷️ **一键添加标签** - 点击生成的标签即可快速添加到笔记中(#标签格式)
|
||||
- 📤 **云端同步** - 一键将笔记提交至 Flomo 服务器
|
||||
- 🎨 **个性化定制** - 支持自定义背景图片和状态栏主题颜色(红/绿/蓝/橙)
|
||||
- 🔐 **安全可靠** - 集成 JWT 认证和数据加密
|
||||
|
||||
### 技术亮点
|
||||
- ⚡ **流畅体验** - 基于 Jetpack Compose 的响应式界面
|
||||
- 🚀 **高性能架构** - 使用 Kotlin 协程和现代异步编程
|
||||
- 🌐 **网络通信** - 集成 OkHttp、Retrofit、Moshi 进行高效数据传输
|
||||
- 📦 **模块化设计** - 清晰的代码结构和组件分离
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
<div align="center">
|
||||
|
||||
| 类别 | 技术 |
|
||||
|------|------|
|
||||
| **语言** | Kotlin |
|
||||
| **框架** | Android Jetpack, Jetpack Compose |
|
||||
| **网络** | OkHttp, Retrofit |
|
||||
| **JSON** | Moshi, Gson |
|
||||
| **安全** | JWT, Nimbus JOSE JWT |
|
||||
| **异步** | Kotlin Coroutines |
|
||||
| **UI** | Material Design 3, Compose |
|
||||
| **构建** | Gradle Kotlin DSL |
|
||||
|
||||
</div>
|
||||
|
||||
## 📋 系统要求
|
||||
|
||||
- **最低 Android 版本**: Android 9.0 (API Level 28)
|
||||
- **目标 Android 版本**: Android 14 (API Level 34)
|
||||
- **开发环境**: Android Studio
|
||||
- **构建工具**: Gradle 8.4+
|
||||
- **JDK 版本**: Java 8+
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 克隆项目
|
||||
```bash
|
||||
git clone http://124.223.26.33:3000/xiaji/flomo-ai.git
|
||||
cd flomo-ai
|
||||
```
|
||||
|
||||
### 构建项目
|
||||
```bash
|
||||
# 清理并构建
|
||||
./gradlew clean build
|
||||
|
||||
# 构建 Debug 版本
|
||||
./gradlew assembleDebug
|
||||
|
||||
# 构建 Release 版本
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
### 运行应用
|
||||
```bash
|
||||
# 连接设备后安装
|
||||
./gradlew installDebug
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
flomo-ai/
|
||||
├── app/
|
||||
│ ├── src/
|
||||
│ │ ├── main/
|
||||
│ │ │ ├── java/com/example/flomo_ai/
|
||||
│ │ │ │ ├── MainActivity.kt # 主界面(笔记输入+AI标签生成)
|
||||
│ │ │ │ ├── SecondActivity.kt # 配置界面(API管理+个性化设置)
|
||||
│ │ │ │ ├── kwt.kt # JWT认证工具类
|
||||
│ │ │ │ └── ui/ # UI 组件
|
||||
│ │ │ ├── res/ # 资源文件
|
||||
│ │ │ └── AndroidManifest.xml # 应用配置
|
||||
│ │ └── build.gradle.kts # 模块构建配置
|
||||
├── gradle/ # Gradle 配置
|
||||
├── build.gradle.kts # 项目构建配置
|
||||
└── README.md # 项目说明文档
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### AI API 配置
|
||||
应用支持配置多个 AI 服务商:
|
||||
- **智谱AI (zhipu)**: 默认模型 glm-4-flash
|
||||
- **星火大模型 (spark)**: 默认模型 general
|
||||
|
||||
每个配置包含:
|
||||
- API 名称
|
||||
- API URL
|
||||
- API Key
|
||||
- Secret Key
|
||||
- 模型名称
|
||||
|
||||
### 个性化设置
|
||||
- **背景图片**: 支持从相册选择自定义背景
|
||||
- **主题颜色**: 支持红、绿、蓝、橙四种状态栏颜色
|
||||
|
||||
### Gradle 配置优化
|
||||
项目已配置国内镜像源以提升构建速度:
|
||||
|
||||
```properties
|
||||
# gradle.properties
|
||||
org.gradle.wrapper.downloadUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/
|
||||
org.gradle.internal.http.connectionTimeout=120000
|
||||
org.gradle.internal.http.socketTimeout=120000
|
||||
```
|
||||
|
||||
### 权限说明
|
||||
应用需要以下权限:
|
||||
- `INTERNET`: 网络通信
|
||||
- `READ_EXTERNAL_STORAGE`: 读取外部存储(用于背景图片)
|
||||
|
||||
## 📸 应用截图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="Screenshot_20240922_111050.png" width="200"/></td>
|
||||
<td><img src="Screenshot_20240922_214948.png" width="200"/></td>
|
||||
<td><img src="Screenshot_20241001_222239_配置页面修改.png" width="200"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">主界面</td>
|
||||
<td align="center">笔记页面</td>
|
||||
<td align="center">配置界面</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进项目!
|
||||
|
||||
### 开发流程
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- 项目地址: `http://124.223.26.33:3000/xiaji/flomo-ai.git`
|
||||
- 开发者: xiaji
|
||||
|
||||
---
|
||||
|
||||
<p align="center">Made with ❤️ for Android developers</p>
|
||||
BIN
flomo-ai/Screenshot_20240922_111050.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
flomo-ai/Screenshot_20240922_214948.png
Normal file
|
After Width: | Height: | Size: 674 KiB |
BIN
flomo-ai/Screenshot_20241001_222239_配置页面修改.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
flomo-ai/apkkey.jks
Normal file
1
flomo-ai/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
119
flomo-ai/app/build.gradle.kts
Normal file
@@ -0,0 +1,119 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.flomo_ai"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.flomo_ai"
|
||||
minSdk = 24
|
||||
targetSdk = 34
|
||||
versionCode = 2
|
||||
versionName = "1.2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
// 启用 View Binding
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
java.srcDirs("src/main/java")
|
||||
res.srcDirs("src/main/res")
|
||||
manifest.srcFile("src/main/AndroidManifest.xml")
|
||||
}
|
||||
getByName("test") {
|
||||
java.srcDirs("src/test/java")
|
||||
}
|
||||
getByName("androidTest") {
|
||||
java.srcDirs("src/androidTest/java")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
buildToolsVersion = "34.0.0"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
|
||||
// 网络请求相关
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.logging.interceptor)
|
||||
|
||||
// JSON解析
|
||||
implementation(libs.gson)
|
||||
implementation(libs.moshi.kotlin)
|
||||
|
||||
// 协程支持
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
// JWT处理
|
||||
implementation(libs.kjwt.jwks)
|
||||
implementation(libs.nimbus.jose.jwt)
|
||||
|
||||
// 权限处理 (已移除PermissionX,代码中未使用)
|
||||
|
||||
// 图片加载
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||
|
||||
// 测试依赖
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
}
|
||||
21
flomo-ai/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
BIN
flomo-ai/app/release/app-release.apk
Normal file
BIN
flomo-ai/app/release/baselineProfiles/0/app-release.dm
Normal file
BIN
flomo-ai/app/release/baselineProfiles/1/app-release.dm
Normal file
37
flomo-ai/app/release/output-metadata.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.example.flomo_ai",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 1,
|
||||
"versionName": "1.0",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/app-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/app-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 28
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.flomo_ai
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.example.flomo_ai", appContext.packageName)
|
||||
}
|
||||
}
|
||||
33
flomo-ai/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/TransparentTheme"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".SecondActivity"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
438
flomo-ai/app/src/main/java/com/example/flomo_ai/MainActivity.kt
Normal file
@@ -0,0 +1,438 @@
|
||||
package com.example.flomo_ai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.example.flomo_ai.ui.theme.ThemeManager
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var inputEditText: EditText
|
||||
private lateinit var configButton: Button
|
||||
private lateinit var outputStatusLabel: TextView
|
||||
private lateinit var outputTextView: TextView
|
||||
private lateinit var promptSelector: Spinner
|
||||
private lateinit var promptContentText: TextView
|
||||
|
||||
// Data classes matching SecondActivity
|
||||
data class HeaderConfig(val key: String, val value: String)
|
||||
data class PromptConfig(val id: String, val title: String, val content: String, val expanded: Boolean = false)
|
||||
data class ButtonConfig(val id: String, val label: String, val action: String, val apiUrl: String? = null, val apiMethod: String? = null, val apiBodyTemplate: String? = null, val expanded: Boolean = false)
|
||||
data class LLMConfig(val baseUrl: String, val apiKey: String, val model: String)
|
||||
data class SettingsData(
|
||||
val llmConfig: LLMConfig?,
|
||||
val headerConfigs: List<HeaderConfig>?,
|
||||
val promptConfigs: List<PromptConfig>?,
|
||||
val buttonConfigs: List<ButtonConfig>?
|
||||
)
|
||||
|
||||
@SuppressLint("MissingInflatedId", "CutPasteId", "SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager.applySavedTheme(this)
|
||||
Log.d("MainActivity", "onCreate: Starting MainActivity")
|
||||
setContentView(R.layout.activity_main)
|
||||
Log.d("MainActivity", "onCreate: Layout set")
|
||||
|
||||
promptSelector = findViewById<Spinner>(R.id.promptSelector)
|
||||
promptContentText = findViewById<TextView>(R.id.promptContentText)
|
||||
inputEditText = findViewById<EditText>(R.id.inputEditText)
|
||||
val sendButton = findViewById<Button>(R.id.sendButton)
|
||||
val stopButton = findViewById<Button>(R.id.stopButton)
|
||||
outputStatusLabel = findViewById<TextView>(R.id.outputStatusLabel)
|
||||
outputTextView = findViewById<TextView>(R.id.outputTextView)
|
||||
val btnCopyResult = findViewById<Button>(R.id.btnCopyResult)
|
||||
val headerTitle = findViewById<TextView>(R.id.headerTitle)
|
||||
val headerModelName = findViewById<TextView>(R.id.headerModelName)
|
||||
Log.d("MainActivity", "onCreate: Views initialized")
|
||||
|
||||
headerTitle.text = "AI优化"
|
||||
headerModelName.text = "GPT-4o"
|
||||
|
||||
// Initialize quick action buttons
|
||||
initQuickButtons()
|
||||
|
||||
// Load prompts from configuration
|
||||
loadPromptsFromConfig()
|
||||
|
||||
outputStatusLabel.text = "等待发送"
|
||||
outputTextView.text = "发送消息后结果将在此显示"
|
||||
|
||||
// Setup prompt selector listener
|
||||
promptSelector.setOnItemSelectedListener(object : android.widget.AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: android.widget.AdapterView<*>, view: android.view.View?, position: Int, id: Long) {
|
||||
val selectedTitle = promptSelector.getItemAtPosition(position) as String
|
||||
|
||||
// Get the prompt map from tag
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val promptMap = promptSelector.tag as? MutableMap<String, String>
|
||||
|
||||
val content = if (promptMap != null && promptMap.containsKey(selectedTitle)) {
|
||||
promptMap[selectedTitle] ?: "无特殊指令"
|
||||
} else {
|
||||
"无特殊指令"
|
||||
}
|
||||
|
||||
promptContentText.text = content
|
||||
Log.d("MainActivity", "Prompt selected: $selectedTitle, content: $content")
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: android.widget.AdapterView<*>) {
|
||||
promptContentText.text = "无特殊指令"
|
||||
}
|
||||
})
|
||||
|
||||
sendButton.setOnClickListener {
|
||||
Log.d("MainActivity", "Send button clicked")
|
||||
// Test log
|
||||
Log.e("MainActivity", "TEST ERROR LOG")
|
||||
val inputText = inputEditText.text.toString()
|
||||
if (inputText.isNotEmpty()) {
|
||||
outputStatusLabel.text = "连接中…"
|
||||
outputTextView.text = "正在生成..."
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
Log.d("MainActivity", "Starting text processing")
|
||||
delay(1000)
|
||||
|
||||
val optimizedText = "今天阳光明媚,微风拂面,我漫步于公园之中,享受这难得的惬意时光。"
|
||||
|
||||
outputStatusLabel.text = "已完成"
|
||||
outputTextView.text = optimizedText
|
||||
|
||||
val selectedPromptId = when (promptSelector.selectedItemPosition) {
|
||||
0 -> "none"
|
||||
1 -> "default-1"
|
||||
2 -> "default-2"
|
||||
else -> "none"
|
||||
}
|
||||
|
||||
Log.d("MainActivity", "Selected prompt ID: $selectedPromptId")
|
||||
Log.d("MainActivity", "Text processing completed successfully")
|
||||
} catch (e: Exception) {
|
||||
outputStatusLabel.text = "发生错误"
|
||||
outputTextView.text = "错误: ${e.message}"
|
||||
Log.e("MainActivity", "Error processing request", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w("MainActivity", "Input text is empty")
|
||||
Toast.makeText(this, "请输入内容", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
stopButton.setOnClickListener {
|
||||
Log.d("MainActivity", "Stop button clicked")
|
||||
outputStatusLabel.text = "已停止"
|
||||
Toast.makeText(this, "生成已停止", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
btnCopyResult.setOnClickListener {
|
||||
Log.d("MainActivity", "btnCopyResult clicked")
|
||||
val textToCopy = outputTextView.text.toString()
|
||||
if (textToCopy.isNotEmpty() && textToCopy != "发送消息后结果将在此显示") {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("优化结果", textToCopy)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(this, "结果已复制到剪贴板", Toast.LENGTH_SHORT).show()
|
||||
Log.d("MainActivity", "Text copied to clipboard")
|
||||
} else {
|
||||
Toast.makeText(this, "没有可复制的内容", Toast.LENGTH_SHORT).show()
|
||||
Log.w("MainActivity", "No text to copy")
|
||||
}
|
||||
}
|
||||
|
||||
val btnSaveNote = findViewById<Button>(R.id.btnSaveNote)
|
||||
btnSaveNote.setOnClickListener {
|
||||
Log.d("MainActivity", "btnSaveNote clicked")
|
||||
val textToSave = outputTextView.text.toString()
|
||||
if (textToSave.isNotEmpty() && textToSave != "发送消息后结果将在此显示") {
|
||||
saveToNoteApi(textToSave)
|
||||
} else {
|
||||
Toast.makeText(this, "没有可保存的内容", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
configButton = findViewById<Button>(R.id.configButton)
|
||||
Log.d("MainActivity", "Config button found: $configButton")
|
||||
configButton.setOnClickListener {
|
||||
Log.d("MainActivity", "Config button clicked, launching SecondActivity")
|
||||
Toast.makeText(this, "Opening settings...", Toast.LENGTH_SHORT).show()
|
||||
try {
|
||||
val intent = Intent(this, SecondActivity::class.java)
|
||||
startActivity(intent)
|
||||
Log.d("MainActivity", "SecondActivity started successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error starting SecondActivity", e)
|
||||
Toast.makeText(this, "Error opening settings: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
Log.d("MainActivity", "onCreate: Completed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "onCreate: Error setting up config button", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPromptsFromConfig() {
|
||||
Log.d("MainActivity", "loadPromptsFromConfig: Starting")
|
||||
try {
|
||||
// Load shared preferences
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
val json = sharedPrefs.getString("configs", null)
|
||||
|
||||
val promptTitles = mutableListOf<String>()
|
||||
val promptContents = mutableListOf<String>()
|
||||
|
||||
// Add default prompt
|
||||
promptTitles.add("无系统提示词")
|
||||
promptContents.add("无特殊指令")
|
||||
|
||||
if (json != null) {
|
||||
try {
|
||||
val settings = Gson().fromJson(json, SettingsData::class.java)
|
||||
val promptConfigs = settings.promptConfigs ?: listOf()
|
||||
|
||||
for (prompt in promptConfigs) {
|
||||
if (prompt.title.isNotBlank() && prompt.content.isNotBlank()) {
|
||||
promptTitles.add(prompt.title)
|
||||
promptContents.add(prompt.content)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("MainActivity", "Loaded ${promptConfigs.size} custom prompts from config")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Error loading prompts from config", e)
|
||||
}
|
||||
} else {
|
||||
Log.d("MainActivity", "No saved config found, using default prompts")
|
||||
}
|
||||
|
||||
// Add default prompts if no custom prompts found
|
||||
if (promptTitles.size == 1) {
|
||||
promptTitles.add("翻译助手")
|
||||
promptContents.add("将输入的文本翻译成指定语言")
|
||||
promptTitles.add("代码解释")
|
||||
promptContents.add("解释代码的功能和逻辑")
|
||||
}
|
||||
|
||||
// Add quick action prompts
|
||||
if (!promptTitles.contains("检查错别字")) {
|
||||
promptTitles.add("检查错别字")
|
||||
promptContents.add("请检查以下文本中的错别字并纠正:")
|
||||
}
|
||||
if (!promptTitles.contains("总结")) {
|
||||
promptTitles.add("总结")
|
||||
promptContents.add("请用简洁的语言总结以下文本的主要内容:")
|
||||
}
|
||||
if (!promptTitles.contains("翻译")) {
|
||||
promptTitles.add("翻译")
|
||||
promptContents.add("请翻译以下文本:")
|
||||
}
|
||||
if (!promptTitles.contains("润色")) {
|
||||
promptTitles.add("润色")
|
||||
promptContents.add("请润色以下文本,使其更通顺流畅:")
|
||||
}
|
||||
|
||||
// Store prompt contents in a map for easy access
|
||||
val promptMap = mutableMapOf<String, String>()
|
||||
for (i in promptTitles.indices) {
|
||||
promptMap[promptTitles[i]] = promptContents[i]
|
||||
}
|
||||
|
||||
// Save prompt map to use in onItemSelectedListener
|
||||
promptSelector.tag = promptMap
|
||||
|
||||
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, promptTitles)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
promptSelector.adapter = adapter
|
||||
promptSelector.setSelection(0)
|
||||
|
||||
Log.d("MainActivity", "loadPromptsFromConfig: Completed with ${promptTitles.size} prompts")
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "loadPromptsFromConfig: Error", e)
|
||||
// Fallback to default prompts
|
||||
val promptTitles = listOf("无系统提示词", "翻译助手", "代码解释")
|
||||
val promptContents = listOf("无特殊指令", "将输入的文本翻译成指定语言", "解释代码的功能和逻辑")
|
||||
|
||||
val promptMap = mutableMapOf<String, String>()
|
||||
for (i in promptTitles.indices) {
|
||||
promptMap[promptTitles[i]] = promptContents[i]
|
||||
}
|
||||
|
||||
promptSelector.tag = promptMap
|
||||
|
||||
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, promptTitles)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
promptSelector.adapter = adapter
|
||||
promptSelector.setSelection(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initQuickButtons() {
|
||||
val btnCheckTypos = findViewById<LinearLayout>(R.id.btnCheckTypos)
|
||||
val btnSummarize = findViewById<LinearLayout>(R.id.btnSummarize)
|
||||
val btnTranslate = findViewById<LinearLayout>(R.id.btnTranslate)
|
||||
val btnPolishing = findViewById<LinearLayout>(R.id.btnPolishing)
|
||||
|
||||
btnCheckTypos.setOnClickListener {
|
||||
selectPrompt("检查错别字")
|
||||
}
|
||||
|
||||
btnSummarize.setOnClickListener {
|
||||
selectPrompt("总结")
|
||||
}
|
||||
|
||||
btnTranslate.setOnClickListener {
|
||||
selectPrompt("翻译")
|
||||
}
|
||||
|
||||
btnPolishing.setOnClickListener {
|
||||
selectPrompt("润色")
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectPrompt(promptName: String) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val promptMap = promptSelector.tag as? MutableMap<String, String>
|
||||
|
||||
val content = promptMap?.get(promptName) ?: when (promptName) {
|
||||
"检查错别字" -> "请检查以下文本中的错别字并纠正:"
|
||||
"总结" -> "请用简洁的语言总结以下文本的主要内容:"
|
||||
"翻译" -> "请翻译以下文本:"
|
||||
"润色" -> "请润色以下文本,使其更通顺流畅:"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
promptContentText.text = content
|
||||
Toast.makeText(this, "已选择: $promptName", Toast.LENGTH_SHORT).show()
|
||||
|
||||
// Auto-trigger send if input is not empty
|
||||
if (inputEditText.text.isNotEmpty()) {
|
||||
findViewById<Button>(R.id.sendButton).performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveToNoteApi(content: String) {
|
||||
try {
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
val json = sharedPrefs.getString("configs", null)
|
||||
|
||||
if (json == null) {
|
||||
Toast.makeText(this, "请先配置笔记API", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val settings = Gson().fromJson(json, NoteSettingsData::class.java)
|
||||
val noteConfig = settings.noteApiConfig
|
||||
|
||||
if (noteConfig == null || noteConfig.apiUrl.isBlank() || noteConfig.apiKey.isBlank()) {
|
||||
Toast.makeText(this, "请先配置笔记API", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
outputStatusLabel.text = "提交中..."
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val result = submitToNoteApi(noteConfig.apiType, noteConfig.apiUrl, noteConfig.apiKey, content)
|
||||
if (result) {
|
||||
outputStatusLabel.text = "已提交"
|
||||
Toast.makeText(this@MainActivity, "笔记已保存", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
outputStatusLabel.text = "提交失败"
|
||||
Toast.makeText(this@MainActivity, "保存失败,请检查配置", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
outputStatusLabel.text = "提交失败"
|
||||
Toast.makeText(this@MainActivity, "保存失败: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
Log.e("MainActivity", "saveToNoteApi error", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "请先配置笔记API", Toast.LENGTH_SHORT).show()
|
||||
Log.e("MainActivity", "saveToNoteApi error", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun submitToNoteApi(apiType: String, apiUrl: String, apiKey: String, content: String): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val client = OkHttpClient()
|
||||
val requestBody = when (apiType) {
|
||||
"Flomo" -> {
|
||||
val json = JSONObject().put("content", content)
|
||||
json.toString().toRequestBody("application/json".toMediaType())
|
||||
}
|
||||
"Notion" -> {
|
||||
val json = JSONObject()
|
||||
.put("parent", JSONObject().put("database_id", apiKey))
|
||||
.put("properties", JSONObject()
|
||||
.put("Name", JSONObject()
|
||||
.put("title", JSONArray()
|
||||
.put(JSONObject().put("text", JSONObject().put("content", "AI优化结果")))
|
||||
)
|
||||
)
|
||||
)
|
||||
json.toString().toRequestBody("application/json".toMediaType())
|
||||
}
|
||||
else -> {
|
||||
val json = JSONObject().put("content", content)
|
||||
json.toString().toRequestBody("application/json".toMediaType())
|
||||
}
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(apiUrl)
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
response.isSuccessful
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "submitToNoteApi error", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class NoteSettingsData(
|
||||
val noteApiConfig: NoteApiConfig?
|
||||
)
|
||||
|
||||
data class NoteApiConfig(
|
||||
val apiType: String,
|
||||
val apiUrl: String,
|
||||
val apiKey: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.example.flomo_ai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
import android.util.Log
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var inputEditText: EditText
|
||||
private lateinit var configButton: Button
|
||||
private lateinit var submitToZhiPuAIButton: Button
|
||||
private lateinit var submitToSparkAIButton: Button
|
||||
private lateinit var tabLayout: TabLayout
|
||||
private lateinit var submitToServerButton: Button
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
@SuppressLint("MissingInflatedId", "CutPasteId", "SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// Initialize views
|
||||
val promptSelector = findViewById<Spinner>(R.id.promptSelector)
|
||||
val inputEditText = findViewById<EditText>(R.id.inputEditText)
|
||||
val sendButton = findViewById<Button>(R.id.sendButton)
|
||||
val stopButton = findViewById<Button>(R.id.stopButton)
|
||||
val outputStatusLabel = findViewById<TextView>(R.id.outputStatusLabel)
|
||||
val outputTextView = findViewById<TextView>(R.id.outputTextView)
|
||||
val copyButton = findViewById<Button>(R.id.copyButton)
|
||||
val btnCopyResult = findViewById<Button>(R.id.btnCopyResult)
|
||||
val headerTitle = findViewById<TextView>(R.id.headerTitle)
|
||||
val headerModelName = findViewById<TextView>(R.id.headerModelName)
|
||||
|
||||
// Set header values from JSON
|
||||
headerTitle.text = "AI优化"
|
||||
headerModelName.text = "gpt-4o"
|
||||
|
||||
// Setup prompt selector
|
||||
val promptOptions = listOf("无系统提示词", "翻译助手", "代码解释")
|
||||
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, promptOptions)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
promptSelector.adapter = adapter
|
||||
promptSelector.setSelection(0) // Default to "无系统提示词"
|
||||
|
||||
// Setup initial output state
|
||||
outputStatusLabel.text = "等待发送"
|
||||
outputTextView.text = "发送消息后结果将在此显示"
|
||||
|
||||
// Send button click listener
|
||||
sendButton.setOnClickListener {
|
||||
val inputText = inputEditText.text.toString()
|
||||
if (inputText.isNotEmpty()) {
|
||||
outputStatusLabel.text = "连接中…"
|
||||
outputTextView.text = "正在生成..."
|
||||
|
||||
// Simulate API call with coroutine
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
// Simulate network delay
|
||||
delay(1000)
|
||||
|
||||
// For demo purposes, we'll show a sample optimized text
|
||||
val optimizedText = "今天阳光明媚,微风拂面,我漫步于公园之中,享受这难得的惬意时光。"
|
||||
|
||||
outputStatusLabel.text = "已完成"
|
||||
outputTextView.text = optimizedText
|
||||
|
||||
// Update selected prompt ID in JSON structure (simulated)
|
||||
val selectedPromptId = when (promptSelector.selectedItemPosition) {
|
||||
0 -> "none"
|
||||
1 -> "default-1"
|
||||
2 -> "default-2"
|
||||
else -> "none"
|
||||
}
|
||||
|
||||
// In a real app, you would update your JSON state here
|
||||
Log.d("MainActivity", "Selected prompt ID: $selectedPromptId")
|
||||
} catch (e: Exception) {
|
||||
outputStatusLabel.text = "发生错误"
|
||||
outputTextView.text = "错误: ${e.message}"
|
||||
Log.e("MainActivity", "Error processing request", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "请输入内容", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop button click listener
|
||||
stopButton.setOnClickListener {
|
||||
outputStatusLabel.text = "已停止"
|
||||
Toast.makeText(this, "生成已停止", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
// Copy button click listener
|
||||
copyButton.setOnClickListener {
|
||||
val textToCopy = outputTextView.text.toString()
|
||||
if (textToCopy.isNotEmpty() && textToCopy != "发送消息后结果将在此显示") {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("优化结果", textToCopy)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(this, "结果已复制到剪贴板", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "没有可复制的内容", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom copy result button
|
||||
btnCopyResult.setOnClickListener {
|
||||
val textToCopy = outputTextView.text.toString()
|
||||
if (textToCopy.isNotEmpty() && textToCopy != "发送消息后结果将在此显示") {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("优化结果", textToCopy)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(this, "结果已复制到剪贴板", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "没有可复制的内容", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
// Keep existing functionality for other buttons (config, etc.)
|
||||
// 点击配置按钮
|
||||
val configButton = findViewById<Button>(R.id.configButton)
|
||||
configButton.setOnClickListener {
|
||||
val intent = Intent(this, SecondActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// 提交到flomo的服务器按钮
|
||||
val submitToServerButton = findViewById<Button>(R.id.submitToServerButton)
|
||||
val inputEditText = findViewById<EditText>(R.id.inputEditText)
|
||||
|
||||
submitToServerButton.setOnClickListener {
|
||||
val textFromEditText = inputEditText.text.toString()
|
||||
submitToServer(textFromEditText)
|
||||
}
|
||||
|
||||
// 创建4个按钮 (保留原有的标签功能)
|
||||
val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
|
||||
// 维持原来的创建标签按钮的代码
|
||||
(1..4).forEach { tabIndex ->
|
||||
tabLayout.newTab().apply {
|
||||
text = "标签示例$tabIndex"
|
||||
tabLayout.addTab(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == SecondActivity.REQUEST_CODE_PICK_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
|
||||
val selectedImageUri = data.getStringExtra("selectedImageUri")
|
||||
if (selectedImageUri != null) {
|
||||
val imageView = findViewById<ImageView>(R.id.imageViewBackground) // 假设你的背景是一个ImageView
|
||||
imageView.setImageURI(Uri.parse(selectedImageUri))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// 获取从其他 Activity 传递过来的按钮颜色值,如果没有传递颜色值,则默认值为透明色。
|
||||
val statusTextView = findViewById<TextView>(R.id.statusTextView)
|
||||
updateStatusTextViewColor(statusTextView)
|
||||
}
|
||||
|
||||
private fun updateStatusTextViewColor(statusTextView: TextView) {
|
||||
try {
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
val colorValue = sharedPrefs.getString("buttonColor", "")
|
||||
Log.d("SharedPrefsDebug", "读取颜色值 MainActivity: $colorValue")
|
||||
|
||||
if (colorValue.isNullOrEmpty()) {
|
||||
statusTextView.setBackgroundResource(android.R.color.holo_blue_dark)
|
||||
} else {
|
||||
// 根据提取的颜色值设置背景颜色,这里只是一个示例,实际应用中可能需要更复杂的逻辑
|
||||
when (colorValue) {
|
||||
"red" -> statusTextView.setBackgroundResource(android.R.color.holo_red_light)
|
||||
"green" -> statusTextView.setBackgroundResource(android.R.color.holo_green_light)
|
||||
"blue" -> statusTextView.setBackgroundResource(android.R.color.holo_blue_light)
|
||||
"orange" -> statusTextView.setBackgroundResource(android.R.color.holo_orange_light)
|
||||
else -> {
|
||||
// 如果颜色值不在预定义的列表中,使用默认颜色
|
||||
statusTextView.setBackgroundResource(android.R.color.holo_blue_dark)
|
||||
Log.e("SharedPrefsError", "Invalid color value: $colorValue")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 捕获并记录任何异常
|
||||
Log.e("SharedPrefsError", "更新状态文本视图颜色时出错\n: ${e.message}", e)
|
||||
// 设置默认颜色
|
||||
statusTextView.setBackgroundResource(android.R.color.holo_blue_dark)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBitmapFromUri(uri: Uri): Bitmap? {
|
||||
val inputStream: InputStream? = contentResolver.openInputStream(uri)
|
||||
return BitmapFactory.decodeStream(inputStream)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun submitToServer(content: String) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
statusText.text = "提交到flomo服务器..."
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
postDataToServer(content)
|
||||
}
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
findViewById<EditText>(R.id.inputEditText).setText("")
|
||||
statusText.text = "提交成功!"
|
||||
}
|
||||
is Result.Error -> {
|
||||
statusText.text = "提交失误: ${result.exception.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交到笔记服务器,flomo服务器
|
||||
private fun postDataToServer(content: String): Result {
|
||||
return try {
|
||||
val client = OkHttpClient()
|
||||
val mediaType = "application/json".toMediaType()
|
||||
val json = JSONObject().apply {
|
||||
put("content", content)
|
||||
}.toString()
|
||||
val body = json.toRequestBody(mediaType)
|
||||
val request = Request.Builder()
|
||||
.url("https://flomoapp.com/iwh/MTY5NTQy/b671d4930ecd1eae63e50cc0cb8ca4ae/")
|
||||
.post(body)
|
||||
.build()
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
Result.Success(response)
|
||||
} else {
|
||||
Result.Error(Exception("服务器返回错误: ${response.code}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data class Success(val response: Response) : Result()
|
||||
data class Error(val exception: Exception) : Result()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
package com.example.flomo_ai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.RadioButton
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.example.flomo_ai.ui.theme.ThemeManager
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
// Data classes for the new settings structure
|
||||
data class HeaderConfig(val key: String, val value: String)
|
||||
data class PromptConfig(val id: String, val title: String, val content: String, val expanded: Boolean = false)
|
||||
data class ButtonConfig(val id: String, val label: String, val action: String, val apiUrl: String? = null, val apiMethod: String? = null, val apiBodyTemplate: String? = null, val expanded: Boolean = false)
|
||||
|
||||
class SecondActivity : AppCompatActivity() {
|
||||
|
||||
// View references
|
||||
private lateinit var etBaseUrl: EditText
|
||||
private lateinit var etApiKey: EditText
|
||||
private lateinit var btnToggleApiKey: ImageButton
|
||||
private lateinit var etModel: EditText
|
||||
private lateinit var llHeadersList: LinearLayout
|
||||
private lateinit var btnAddHeader: Button
|
||||
private lateinit var layoutHeaderContent: LinearLayout
|
||||
private lateinit var ivHeaderArrow: ImageView
|
||||
private lateinit var layoutHeaderToggle: LinearLayout
|
||||
|
||||
// Prompt view references
|
||||
private lateinit var llPromptList: LinearLayout
|
||||
private lateinit var btnAddPrompt: Button
|
||||
private lateinit var layoutPromptContent: LinearLayout
|
||||
private lateinit var ivPromptArrow: ImageView
|
||||
private lateinit var layoutPromptToggle: LinearLayout
|
||||
|
||||
// Theme view references
|
||||
private lateinit var rgThemeMode: RadioGroup
|
||||
private lateinit var rbThemeFollowSystem: RadioButton
|
||||
private lateinit var rbThemeLight: RadioButton
|
||||
private lateinit var rbThemeDark: RadioButton
|
||||
|
||||
// Note API view references
|
||||
private lateinit var spNoteApiType: Spinner
|
||||
private lateinit var etNoteApiUrl: EditText
|
||||
private lateinit var etNoteApiKey: EditText
|
||||
private lateinit var btnToggleNoteApiKey: ImageButton
|
||||
|
||||
// Data storage
|
||||
private var headerConfigs = mutableListOf<HeaderConfig>()
|
||||
private var promptConfigs = mutableListOf<PromptConfig>()
|
||||
private var buttonConfigs = mutableListOf<ButtonConfig>()
|
||||
|
||||
// API config (for backward compatibility with existing API calls)
|
||||
private lateinit var apiConfig: APIConfig
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager.applySavedTheme(this)
|
||||
Log.d("SecondActivity", "onCreate: Starting SecondActivity")
|
||||
try {
|
||||
setContentView(R.layout.activity_second)
|
||||
Log.d("SecondActivity", "onCreate: Layout set")
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecondActivity", "onCreate: Error setting layout", e)
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize views
|
||||
initViews()
|
||||
Log.d("SecondActivity", "onCreate: Views initialized")
|
||||
|
||||
// Load existing configurations
|
||||
loadConfigurations()
|
||||
Log.d("SecondActivity", "onCreate: Configurations loaded")
|
||||
|
||||
// Setup UI based on loaded data
|
||||
setupUI()
|
||||
Log.d("SecondActivity", "onCreate: UI setup completed")
|
||||
|
||||
// Back button functionality
|
||||
findViewById<ImageButton>(R.id.btnBack).setOnClickListener {
|
||||
Log.d("SecondActivity", "Back button clicked")
|
||||
finish()
|
||||
}
|
||||
|
||||
// Home button functionality
|
||||
findViewById<Button>(R.id.btnHome).setOnClickListener {
|
||||
Log.d("SecondActivity", "Home button clicked")
|
||||
// Create intent to go back to MainActivity
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
// Clear the activity stack to start fresh
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
// Add header button
|
||||
btnAddHeader.setOnClickListener {
|
||||
Log.d("SecondActivity", "Add header button clicked")
|
||||
addHeaderEntry()
|
||||
}
|
||||
|
||||
// Add prompt button
|
||||
btnAddPrompt.setOnClickListener {
|
||||
Log.d("SecondActivity", "Add prompt button clicked")
|
||||
addPromptEntry()
|
||||
}
|
||||
|
||||
Log.d("SecondActivity", "onCreate: Completed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecondActivity", "onCreate: Error during initialization", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
Log.d("SecondActivity", "initViews: Starting")
|
||||
try {
|
||||
etBaseUrl = findViewById(R.id.etBaseUrl)
|
||||
etApiKey = findViewById(R.id.etApiKey)
|
||||
btnToggleApiKey = findViewById(R.id.btnToggleApiKey)
|
||||
etModel = findViewById(R.id.etModel)
|
||||
|
||||
// Header Section
|
||||
llHeadersList = findViewById(R.id.llHeadersList)
|
||||
btnAddHeader = findViewById(R.id.btnAddHeader)
|
||||
layoutHeaderContent = findViewById(R.id.layoutHeaderContent)
|
||||
ivHeaderArrow = findViewById(R.id.ivHeaderArrow)
|
||||
layoutHeaderToggle = findViewById(R.id.layoutHeaderToggle)
|
||||
|
||||
// Prompt Section
|
||||
llPromptList = findViewById(R.id.llPromptList)
|
||||
btnAddPrompt = findViewById(R.id.btnAddPrompt)
|
||||
layoutPromptContent = findViewById(R.id.layoutPromptContent)
|
||||
ivPromptArrow = findViewById(R.id.ivPromptArrow)
|
||||
layoutPromptToggle = findViewById(R.id.layoutPromptToggle)
|
||||
|
||||
// Theme Section
|
||||
rgThemeMode = findViewById(R.id.rgThemeMode)
|
||||
rbThemeFollowSystem = findViewById(R.id.rbThemeFollowSystem)
|
||||
rbThemeLight = findViewById(R.id.rbThemeLight)
|
||||
rbThemeDark = findViewById(R.id.rbThemeDark)
|
||||
|
||||
// Note API Section
|
||||
spNoteApiType = findViewById(R.id.spNoteApiType)
|
||||
etNoteApiUrl = findViewById(R.id.etNoteApiUrl)
|
||||
etNoteApiKey = findViewById(R.id.etNoteApiKey)
|
||||
btnToggleNoteApiKey = findViewById(R.id.btnToggleNoteApiKey)
|
||||
|
||||
Log.d("SecondActivity", "initViews: All views found")
|
||||
|
||||
// Setup API key toggle
|
||||
btnToggleApiKey.setOnClickListener {
|
||||
Log.d("SecondActivity", "API key toggle clicked")
|
||||
val isPassword = etApiKey.transformationMethod is PasswordTransformationMethod
|
||||
etApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
|
||||
// Move cursor to end
|
||||
etApiKey.setSelection(etApiKey.text.length)
|
||||
|
||||
// Update icon based on state
|
||||
if (isPassword) {
|
||||
btnToggleApiKey.setImageResource(android.R.drawable.ic_menu_view) // Show eye
|
||||
} else {
|
||||
btnToggleApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock) // Show lock
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Header Toggle (Fold/Unfold)
|
||||
layoutHeaderToggle.setOnClickListener {
|
||||
Log.d("SecondActivity", "Header toggle clicked")
|
||||
val isExpanded = layoutHeaderContent.visibility == View.VISIBLE
|
||||
if (isExpanded) {
|
||||
layoutHeaderContent.visibility = View.GONE
|
||||
ivHeaderArrow.rotation = 0f // Point right
|
||||
} else {
|
||||
layoutHeaderContent.visibility = View.VISIBLE
|
||||
ivHeaderArrow.rotation = 90f // Point down
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Prompt Toggle (Fold/Unfold)
|
||||
layoutPromptToggle.setOnClickListener {
|
||||
Log.d("SecondActivity", "Prompt toggle clicked")
|
||||
val isExpanded = layoutPromptContent.visibility == View.VISIBLE
|
||||
if (isExpanded) {
|
||||
layoutPromptContent.visibility = View.GONE
|
||||
ivPromptArrow.rotation = 0f // Point right
|
||||
} else {
|
||||
layoutPromptContent.visibility = View.VISIBLE
|
||||
ivPromptArrow.rotation = 90f // Point down
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Note API key toggle
|
||||
btnToggleNoteApiKey.setOnClickListener {
|
||||
val isPassword = etNoteApiKey.transformationMethod is PasswordTransformationMethod
|
||||
etNoteApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
|
||||
etNoteApiKey.setSelection(etNoteApiKey.text.length)
|
||||
|
||||
if (isPassword) {
|
||||
btnToggleNoteApiKey.setImageResource(android.R.drawable.ic_menu_view)
|
||||
} else {
|
||||
btnToggleNoteApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("SecondActivity", "initViews: Completed")
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecondActivity", "initViews: Error finding views", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadConfigurations() {
|
||||
Log.d("SecondActivity", "loadConfigurations: Starting")
|
||||
// Load shared preferences
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
val json = sharedPrefs.getString("configs", null)
|
||||
Log.d("SecondActivity", "loadConfigurations: JSON loaded: ${json?.substring(0, minOf(json.length, 100))}")
|
||||
|
||||
if (json != null) {
|
||||
try {
|
||||
// Try to load as new format first
|
||||
Log.d("SecondActivity", "loadConfigurations: Trying to parse as SettingsData")
|
||||
val settings = Gson().fromJson(json, SettingsData::class.java)
|
||||
Log.d("SecondActivity", "loadConfigurations: SettingsData parsed successfully")
|
||||
headerConfigs = settings.headerConfigs?.toMutableList() ?: mutableListOf()
|
||||
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
|
||||
buttonConfigs = settings.buttonConfigs?.toMutableList() ?: mutableListOf()
|
||||
|
||||
// Load LLM config
|
||||
etBaseUrl.setText(settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1")
|
||||
etApiKey.setText(settings.llmConfig?.apiKey ?: "")
|
||||
etModel.setText(settings.llmConfig?.model ?: "gpt-4o")
|
||||
|
||||
// Load Note API config
|
||||
settings.noteApiConfig?.let { noteConfig ->
|
||||
val apiTypes = listOf("Flomo", "Notion", "Joplin", "Custom")
|
||||
val typeIndex = apiTypes.indexOf(noteConfig.apiType)
|
||||
if (typeIndex >= 0) {
|
||||
spNoteApiType.setSelection(typeIndex)
|
||||
}
|
||||
etNoteApiUrl.setText(noteConfig.apiUrl)
|
||||
etNoteApiKey.setText(noteConfig.apiKey)
|
||||
}
|
||||
|
||||
// Update API key visibility based on whether it has text
|
||||
updateApiKeyVisibility()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecondActivity", "loadConfigurations: Error parsing SettingsData", e)
|
||||
// If new format fails, try to load old format for migration
|
||||
try {
|
||||
Log.d("SecondActivity", "loadConfigurations: Trying to parse as List<APIConfig>")
|
||||
val type = object : TypeToken<List<APIConfig>>() {}.type
|
||||
val oldConfigs = Gson().fromJson<List<APIConfig>>(json, type)
|
||||
if (oldConfigs.isNotEmpty()) {
|
||||
val oldConfig = oldConfigs[0]
|
||||
etBaseUrl.setText(oldConfig.url)
|
||||
etApiKey.setText(oldConfig.key)
|
||||
etModel.setText(oldConfig.model)
|
||||
updateApiKeyVisibility()
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
Log.e("SecondActivity", "loadConfigurations: Error parsing List<APIConfig>", e2)
|
||||
// If both fail, use defaults
|
||||
etBaseUrl.setText("https://api.openai.com/v1")
|
||||
etModel.setText("gpt-4o")
|
||||
updateApiKeyVisibility()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No saved config, use defaults
|
||||
Log.d("SecondActivity", "loadConfigurations: No saved config, using defaults")
|
||||
etBaseUrl.setText("https://api.openai.com/v1")
|
||||
etModel.setText("gpt-4o")
|
||||
updateApiKeyVisibility()
|
||||
}
|
||||
Log.d("SecondActivity", "loadConfigurations: Completed")
|
||||
}
|
||||
|
||||
private fun updateApiKeyVisibility() {
|
||||
val isEmpty = etApiKey.text.toString().isEmpty()
|
||||
etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
|
||||
// Keep cursor at end
|
||||
etApiKey.setSelection(etApiKey.text.length)
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
// Setup headers
|
||||
llHeadersList.removeAllViews()
|
||||
if (headerConfigs.isEmpty()) {
|
||||
addHeaderEntry() // Add one empty entry by default
|
||||
} else {
|
||||
for (header in headerConfigs) {
|
||||
addHeaderEntry(header.key, header.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup prompts
|
||||
llPromptList.removeAllViews()
|
||||
if (promptConfigs.isEmpty()) {
|
||||
addPromptEntry() // Add one empty entry by default
|
||||
} else {
|
||||
for (prompt in promptConfigs) {
|
||||
addPromptEntry(prompt.title, prompt.content)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup theme
|
||||
setupTheme()
|
||||
|
||||
// Setup Note API spinner
|
||||
val noteApiTypes = listOf("Flomo", "Notion", "Joplin", "Custom")
|
||||
val noteApiAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, noteApiTypes)
|
||||
noteApiAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spNoteApiType.adapter = noteApiAdapter
|
||||
}
|
||||
|
||||
private fun setupTheme() {
|
||||
// Get saved theme mode
|
||||
val themeMode = ThemeManager.getThemeMode(this)
|
||||
|
||||
// Set the correct radio button
|
||||
when (themeMode) {
|
||||
ThemeManager.THEME_FOLLOW_SYSTEM -> rbThemeFollowSystem.isChecked = true
|
||||
ThemeManager.THEME_LIGHT -> rbThemeLight.isChecked = true
|
||||
ThemeManager.THEME_DARK -> rbThemeDark.isChecked = true
|
||||
else -> rbThemeFollowSystem.isChecked = true
|
||||
}
|
||||
|
||||
// Set up radio group listener
|
||||
rgThemeMode.setOnCheckedChangeListener { _, checkedId ->
|
||||
val newMode = when (checkedId) {
|
||||
R.id.rbThemeFollowSystem -> ThemeManager.THEME_FOLLOW_SYSTEM
|
||||
R.id.rbThemeLight -> ThemeManager.THEME_LIGHT
|
||||
R.id.rbThemeDark -> ThemeManager.THEME_DARK
|
||||
else -> ThemeManager.THEME_FOLLOW_SYSTEM
|
||||
}
|
||||
|
||||
// Save and apply the new theme
|
||||
ThemeManager.setThemeMode(this, newMode)
|
||||
Log.d("SecondActivity", "Theme mode changed to: ${ThemeManager.getThemeModeName(newMode)}")
|
||||
|
||||
// Recreate activity to apply theme changes
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addHeaderEntry(key: String = "", value: String = "") {
|
||||
val view = layoutInflater.inflate(R.layout.header_entry, null)
|
||||
val etKey = view.findViewById<EditText>(R.id.etHeaderKey)
|
||||
val etValue = view.findViewById<EditText>(R.id.etHeaderValue)
|
||||
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemoveHeader)
|
||||
|
||||
etKey.setText(key)
|
||||
etValue.setText(value)
|
||||
|
||||
btnRemove.setOnClickListener {
|
||||
llHeadersList.removeView(view)
|
||||
}
|
||||
|
||||
llHeadersList.addView(view)
|
||||
}
|
||||
|
||||
private fun addPromptEntry(title: String = "", content: String = "") {
|
||||
val view = layoutInflater.inflate(R.layout.prompt_entry, null)
|
||||
val etTitle = view.findViewById<EditText>(R.id.etPromptTitle)
|
||||
val etContent = view.findViewById<EditText>(R.id.etPromptContent)
|
||||
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemovePrompt)
|
||||
|
||||
etTitle.setText(title)
|
||||
etContent.setText(content)
|
||||
|
||||
btnRemove.setOnClickListener {
|
||||
llPromptList.removeView(view)
|
||||
}
|
||||
|
||||
llPromptList.addView(view)
|
||||
}
|
||||
|
||||
// Save all configurations when leaving or explicitly saving
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
saveConfigurations()
|
||||
}
|
||||
|
||||
private fun saveConfigurations() {
|
||||
// Update header configs from UI
|
||||
headerConfigs.clear()
|
||||
for (i in 0 until llHeadersList.childCount) {
|
||||
val view = llHeadersList.getChildAt(i)
|
||||
val key = view.findViewById<EditText>(R.id.etHeaderKey).text.toString()
|
||||
val value = view.findViewById<EditText>(R.id.etHeaderValue).text.toString()
|
||||
if (key.isNotBlank() && value.isNotBlank()) {
|
||||
headerConfigs.add(HeaderConfig(key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// Update prompt configs from UI
|
||||
promptConfigs.clear()
|
||||
for (i in 0 until llPromptList.childCount) {
|
||||
val view = llPromptList.getChildAt(i)
|
||||
val title = view.findViewById<EditText>(R.id.etPromptTitle).text.toString()
|
||||
val content = view.findViewById<EditText>(R.id.etPromptContent).text.toString()
|
||||
if (title.isNotBlank() && content.isNotBlank()) {
|
||||
promptConfigs.add(PromptConfig(id = "prompt_$i", title = title, content = content))
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Buttons are removed from this UI as per new design.
|
||||
// We keep the list empty for data compatibility.
|
||||
buttonConfigs.clear()
|
||||
|
||||
// Save LLM config
|
||||
val llmConfig = LLMConfig(
|
||||
baseUrl = etBaseUrl.text.toString(),
|
||||
apiKey = etApiKey.text.toString(),
|
||||
model = etModel.text.toString()
|
||||
)
|
||||
|
||||
// Save Note API config
|
||||
val noteApiConfig = NoteApiConfig(
|
||||
apiType = spNoteApiType.selectedItem.toString(),
|
||||
apiUrl = etNoteApiUrl.text.toString(),
|
||||
apiKey = etNoteApiKey.text.toString()
|
||||
)
|
||||
|
||||
// Save everything
|
||||
val settingsData = SettingsData(
|
||||
llmConfig = llmConfig,
|
||||
headerConfigs = headerConfigs,
|
||||
promptConfigs = promptConfigs,
|
||||
buttonConfigs = buttonConfigs,
|
||||
noteApiConfig = noteApiConfig
|
||||
)
|
||||
|
||||
val json = Gson().toJson(settingsData)
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
sharedPrefs.edit().putString("configs", json).apply()
|
||||
|
||||
// Also update the legacy APIConfig for backward compatibility
|
||||
apiConfig = APIConfig(
|
||||
System.currentTimeMillis(),
|
||||
"llm-config",
|
||||
etBaseUrl.text.toString(),
|
||||
etApiKey.text.toString(),
|
||||
"",
|
||||
etModel.text.toString()
|
||||
)
|
||||
}
|
||||
|
||||
// Legacy APIConfig class for backward compatibility with existing code
|
||||
data class APIConfig(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val key: String,
|
||||
val secretKey: String,
|
||||
val model: String
|
||||
)
|
||||
|
||||
// New data classes for settings structure
|
||||
data class LLMConfig(
|
||||
val baseUrl: String,
|
||||
val apiKey: String,
|
||||
val model: String
|
||||
)
|
||||
|
||||
data class NoteApiConfig(
|
||||
val apiType: String,
|
||||
val apiUrl: String,
|
||||
val apiKey: String
|
||||
)
|
||||
|
||||
data class SettingsData(
|
||||
val llmConfig: LLMConfig?,
|
||||
val headerConfigs: List<HeaderConfig>?,
|
||||
val promptConfigs: List<PromptConfig>?,
|
||||
val buttonConfigs: List<ButtonConfig>?,
|
||||
val noteApiConfig: NoteApiConfig?
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package com.example.flomo_ai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
// Data classes for the new settings structure
|
||||
data class HeaderConfig(val key: String, val value: String)
|
||||
data class PromptConfig(val id: String, val title: String, val content: String, val expanded: Boolean = false)
|
||||
data class ButtonConfig(val id: String, val label: String, val action: String, val apiUrl: String? = null, val apiMethod: String? = null, val apiBodyTemplate: String? = null, val expanded: Boolean = false)
|
||||
|
||||
class SecondActivity : AppCompatActivity() {
|
||||
|
||||
// View references
|
||||
private lateinit var etBaseUrl: EditText
|
||||
private lateinit var etApiKey: EditText
|
||||
private lateinit var btnToggleApiKey: ImageButton
|
||||
private lateinit var etModel: EditText
|
||||
private lateinit var llHeadersList: LinearLayout
|
||||
private lateinit var btnAddHeader: Button
|
||||
private lateinit var llPromptsList: LinearLayout
|
||||
private lateinit var btnAddPrompt: Button
|
||||
private lateinit var tvEmptyPrompts: TextView
|
||||
private lateinit var llButtonsList: LinearLayout
|
||||
private lateinit var btnAddButton: Button
|
||||
private lateinit var tvEmptyButtons: TextView
|
||||
|
||||
// Data storage
|
||||
private var headerConfigs = mutableListOf<HeaderConfig>()
|
||||
private var promptConfigs = mutableListOf<PromptConfig>()
|
||||
private var buttonConfigs = mutableListOf<ButtonConfig>()
|
||||
|
||||
// API config (for backward compatibility with existing API calls)
|
||||
private lateinit var apiConfig: APIConfig
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_second)
|
||||
|
||||
// Initialize views
|
||||
initViews()
|
||||
|
||||
// Load existing configurations
|
||||
loadConfigurations()
|
||||
|
||||
// Setup UI based on loaded data
|
||||
setupUI()
|
||||
|
||||
// Back button functionality
|
||||
findViewById<Button>(R.id.btnBack).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
// Add header button
|
||||
btnAddHeader.setOnClickListener {
|
||||
addHeaderEntry()
|
||||
}
|
||||
|
||||
// Add prompt button
|
||||
btnAddPrompt.setOnClickListener {
|
||||
addPromptEntry()
|
||||
}
|
||||
|
||||
// Add button
|
||||
btnAddButton.setOnClickListener {
|
||||
addButtonEntry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViews() {
|
||||
etBaseUrl = findViewById(R.id.etBaseUrl)
|
||||
etApiKey = findViewById(R.id.etApiKey)
|
||||
btnToggleApiKey = findViewById(R.id.btnToggleApiKey)
|
||||
etModel = findViewById(R.id.etModel)
|
||||
llHeadersList = findViewById(R.id.llHeadersList)
|
||||
btnAddHeader = findViewById(R.id.btnAddHeader)
|
||||
llPromptsList = findViewById(R.id.llPromptsList)
|
||||
btnAddPrompt = findViewById(R.id.btnAddPrompt)
|
||||
tvEmptyPrompts = findViewById(R.id.tvEmptyPrompts)
|
||||
llButtonsList = findViewById(R.id.llButtonsList)
|
||||
btnAddButton = findViewById(R.id.btnAddButton)
|
||||
tvEmptyButtons = findViewById(R.id.tvEmptyButtons)
|
||||
|
||||
// Setup API key toggle
|
||||
btnToggleApiKey.setOnClickListener {
|
||||
val isPassword = etApiKey.transformationMethod is PasswordTransformationMethod
|
||||
etApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
|
||||
// Move cursor to end
|
||||
etApiKey.setSelection(etApiKey.text.length)
|
||||
// Toggle icon
|
||||
val iconRes = if (isPassword)
|
||||
android.R.drawable.ic_lock_idle_lock else
|
||||
android.R.drawable.ic_lock_idle_unlocked
|
||||
btnToggleApiKey.setImageResource(iconRes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadConfigurations() {
|
||||
// Load shared preferences
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
val json = sharedPrefs.getString("configs", null)
|
||||
|
||||
if (json != null) {
|
||||
try {
|
||||
// Try to load as new format first
|
||||
val settings = Gson().fromJson(json, SettingsData::class.java)
|
||||
headerConfigs = settings.headerConfigs?.toMutableList() ?: mutableListOf()
|
||||
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
|
||||
buttonConfigs = settings.buttonConfigs?.toMutableList() ?: mutableListOf()
|
||||
|
||||
// Load LLM config
|
||||
etBaseUrl.setText(settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1")
|
||||
etApiKey.setText(settings.llmConfig?.apiKey ?: "")
|
||||
etModel.setText(settings.llmConfig?.model ?: "gpt-4o")
|
||||
|
||||
// Update API key visibility based on whether it has text
|
||||
updateApiKeyVisibility()
|
||||
|
||||
} catch (e: Exception) {
|
||||
// If new format fails, try to load old format for migration
|
||||
try {
|
||||
val type = object : TypeToken<List<APIConfig>>() {}.type
|
||||
val oldConfigs = Gson().fromJson<List<APIConfig>>(json, type)
|
||||
if (oldConfigs.isNotEmpty()) {
|
||||
val oldConfig = oldConfigs[0]
|
||||
etBaseUrl.setText(oldConfig.url)
|
||||
etApiKey.setText(oldConfig.key)
|
||||
etModel.setText(oldConfig.model)
|
||||
updateApiKeyVisibility()
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
// If both fail, use defaults
|
||||
etBaseUrl.setText("https://api.openai.com/v1")
|
||||
etModel.setText("gpt-4o")
|
||||
updateApiKeyVisibility()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No saved config, use defaults
|
||||
etBaseUrl.setText("https://api.openai.com/v1")
|
||||
etModel.setText("gpt-4o")
|
||||
updateApiKeyVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateApiKeyVisibility() {
|
||||
val isEmpty = etApiKey.text.toString().isEmpty()
|
||||
etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
|
||||
val iconRes = if (isEmpty || etApiKey.transformationMethod == null)
|
||||
android.R.drawable.ic_lock_idle_lock else
|
||||
android.R.drawable.ic_lock_idle_unlocked
|
||||
btnToggleApiKey.setImageResource(iconRes)
|
||||
// Keep cursor at end
|
||||
etApiKey.setSelection(etApiKey.text.length)
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
// Setup headers
|
||||
llHeadersList.removeAllViews()
|
||||
if (headerConfigs.isEmpty()) {
|
||||
addHeaderEntry() // Add one empty entry by default
|
||||
} else {
|
||||
for (header in headerConfigs) {
|
||||
addHeaderEntry(header.key, header.value)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup prompts
|
||||
llPromptsList.removeAllViews()
|
||||
if (promptConfigs.isEmpty()) {
|
||||
// Add default prompts from JSON
|
||||
promptConfigs.add(PromptConfig("default-1", "翻译助手", "你是一个专业翻译,请将用户输入的内容翻译成中文,保持原意,语言自然流畅。"))
|
||||
promptConfigs.add(PromptConfig("default-2", "代码解释", "你是一个资深程序员,请详细解释用户提供的代码,用中文说明其功能和逻辑。"))
|
||||
}
|
||||
for (prompt in promptConfigs) {
|
||||
addPromptEntry(prompt)
|
||||
}
|
||||
updateEmptyStates()
|
||||
|
||||
// Setup buttons
|
||||
llButtonsList.removeAllViews()
|
||||
if (buttonConfigs.isEmpty()) {
|
||||
// Add default buttons from JSON
|
||||
buttonConfigs.add(ButtonConfig("btn-copy", "复制结果", "copy"))
|
||||
buttonConfigs.add(ButtonConfig(
|
||||
"btn-webhook",
|
||||
"发送到飞书",
|
||||
"api",
|
||||
"https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx",
|
||||
"POST",
|
||||
"{\"msg_type\": \"text\", \"content\": {\"text\": \"{output}\"}}"
|
||||
))
|
||||
}
|
||||
for (button in buttonConfigs) {
|
||||
addButtonEntry(button)
|
||||
}
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
private fun addHeaderEntry(key: String = "", value: String = "") {
|
||||
val view = layoutInflater.inflate(R.layout.header_entry, null)
|
||||
val etKey = view.findViewById<EditText>(R.id.etHeaderKey)
|
||||
val etValue = view.findViewById<EditText>(R.id.etHeaderValue)
|
||||
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemoveHeader)
|
||||
|
||||
etKey.setText(key)
|
||||
etValue.setText(value)
|
||||
|
||||
btnRemove.setOnClickListener {
|
||||
llHeadersList.removeView(view)
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
llHeadersList.addView(view)
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
private fun addPromptEntry(config: PromptConfig = PromptConfig("", "", "")) {
|
||||
val view = layoutInflater.inflate(R.layout.prompt_entry, null)
|
||||
val etTitle = view.findViewById<EditText>(R.id.etPromptTitle)
|
||||
val etContent = view.findViewById<EditText>(R.id.etPromptContent)
|
||||
val btnExpand = view.findViewById<Button>(R.id.btnExpandPrompt)
|
||||
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemovePrompt)
|
||||
val vDivider = view.findViewById<View>(R.id.viewDivider)
|
||||
|
||||
etTitle.setText(config.title)
|
||||
etContent.setText(config.content)
|
||||
// Set expanded state (we'd need to store this in the view tag or similar)
|
||||
|
||||
btnRemove.setOnClickListener {
|
||||
llPromptsList.removeView(view)
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
// For simplicity, we're not implementing expand/collapse here
|
||||
// but in a full implementation we would toggle the content visibility
|
||||
|
||||
llPromptsList.addView(view)
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
private fun addButtonEntry(config: ButtonConfig = ButtonConfig("", "", "")) {
|
||||
val view = layoutInflater.inflate(R.layout.button_entry, null)
|
||||
val etLabel = view.findViewById<EditText>(R.id.etButtonLabel)
|
||||
val btnAction = view.findViewById<Button>(R.id.btnButtonAction)
|
||||
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemoveButton)
|
||||
val llApiFields = view.findViewById<LinearLayout>(R.id.llApiFields)
|
||||
|
||||
etLabel.setText(config.label)
|
||||
btnAction.text = when (config.action) {
|
||||
"copy" -> "复制输出内容"
|
||||
"api" -> "提交到第三方 API"
|
||||
else -> "未知操作"
|
||||
}
|
||||
|
||||
// Show/hide API fields based on action
|
||||
llApiFields.visibility = if (config.action == "api") View.VISIBLE else View.GONE
|
||||
|
||||
btnAction.setOnClickListener {
|
||||
// Cycle through action options
|
||||
val newAction = when (btnAction.text.toString()) {
|
||||
"复制输出内容" -> "提交到第三方 API"
|
||||
"提交到第三方 API" -> "未知操作"
|
||||
else -> "复制输出内容"
|
||||
}
|
||||
btnAction.text = when (newAction) {
|
||||
"复制输出内容" -> "复制输出内容"
|
||||
"提交到第三方 API" -> "提交到第三方 API"
|
||||
else -> "未知操作"
|
||||
}
|
||||
llApiFields.visibility = if (newAction == "提交到第三方 API") View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// If we have existing API config, populate fields
|
||||
if (config.action == "api") {
|
||||
// In a full implementation, we would populate the API fields here
|
||||
}
|
||||
|
||||
btnRemove.setOnClickListener {
|
||||
llButtonsList.removeView(view)
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
llButtonsList.addView(view)
|
||||
updateEmptyStates()
|
||||
}
|
||||
|
||||
private fun updateEmptyStates() {
|
||||
// Update prompts empty state
|
||||
tvEmptyPrompts.visibility = if (llPromptsList.childCount == 0) View.VISIBLE else View.GONE
|
||||
|
||||
// Update buttons empty state
|
||||
tvEmptyButtons.visibility = if (llButtonsList.childCount == 0) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// Save all configurations when leaving or explicitly saving
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
saveConfigurations()
|
||||
}
|
||||
|
||||
private fun saveConfigurations() {
|
||||
// Update header configs from UI
|
||||
headerConfigs.clear()
|
||||
for (i in 0 until llHeadersList.childCount) {
|
||||
val view = llHeadersList.getChildAt(i)
|
||||
val key = view.findViewById<EditText>(R.id.etHeaderKey).text.toString()
|
||||
val value = view.findViewById<EditText>(R.id.etHeaderValue).text.toString()
|
||||
if (key.isNotBlank() && value.isNotBlank()) {
|
||||
headerConfigs.add(HeaderConfig(key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// Update prompt configs from UI
|
||||
promptConfigs.clear()
|
||||
for (i in 0 until llPromptsList.childCount) {
|
||||
val view = llPromptsList.getChildAt(i)
|
||||
val title = view.findViewById<EditText>(R.id.etPromptTitle).text.toString()
|
||||
val content = view.findViewById<EditText>(R.id.etPromptContent).text.toString()
|
||||
if (title.isNotBlank() && content.isNotBlank()) {
|
||||
promptConfigs.add(PromptConfig(
|
||||
if (title.equals("翻译助手", ignoreCase = true)) "default-1"
|
||||
else if (title.equals("代码解释", ignoreCase = true)) "default-2"
|
||||
else UUID.randomUUID().toString(),
|
||||
title,
|
||||
content
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Update button configs from UI
|
||||
buttonConfigs.clear()
|
||||
for (i in 0 until llButtonsList.childCount) {
|
||||
val view = llButtonsList.getChildAt(i)
|
||||
val label = view.findViewById<EditText>(R.id.etButtonLabel).text.toString()
|
||||
val actionText = view.findViewById<Button>(R.id.btnButtonAction).text.toString()
|
||||
val action = when (actionText) {
|
||||
"复制输出内容" -> "copy"
|
||||
"提交到第三方 API" -> "api"
|
||||
else -> "unknown"
|
||||
}
|
||||
if (label.isNotBlank()) {
|
||||
buttonConfigs.add(ButtonConfig(
|
||||
if (label.equals("复制结果", ignoreCase = true)) "btn-copy"
|
||||
else if (label.equals("发送到飞书", ignoreCase = true)) "btn-webhook"
|
||||
else UUID.randomUUID().toString(),
|
||||
label,
|
||||
action
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Save LLM config
|
||||
val llmConfig = LLMConfig(
|
||||
baseUrl = etBaseUrl.text.toString(),
|
||||
apiKey = etApiKey.text.toString(),
|
||||
model = etModel.text.toString()
|
||||
)
|
||||
|
||||
// Save everything
|
||||
val settingsData = SettingsData(
|
||||
llmConfig = llmConfig,
|
||||
headerConfigs = headerConfigs,
|
||||
promptConfigs = promptConfigs,
|
||||
buttonConfigs = buttonConfigs
|
||||
)
|
||||
|
||||
val json = Gson().toJson(settingsData)
|
||||
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
|
||||
sharedPrefs.edit().putString("configs", json).apply()
|
||||
|
||||
// Also update the legacy APIConfig for backward compatibility
|
||||
apiConfig = APIConfig(
|
||||
System.currentTimeMillis(),
|
||||
"llm-config",
|
||||
etBaseUrl.text.toString(),
|
||||
etApiKey.text.toString(),
|
||||
"",
|
||||
etModel.text.toString()
|
||||
)
|
||||
}
|
||||
|
||||
// Legacy APIConfig class for backward compatibility with existing code
|
||||
data class APIConfig(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val key: String,
|
||||
val secretKey: String,
|
||||
val model: String
|
||||
)
|
||||
|
||||
// New data classes for settings structure
|
||||
data class LLMConfig(
|
||||
val baseUrl: String,
|
||||
val apiKey: String,
|
||||
val model: String
|
||||
)
|
||||
|
||||
data class SettingsData(
|
||||
val llmConfig: LLMConfig?,
|
||||
val headerConfigs: List<HeaderConfig>?,
|
||||
val promptConfigs: List<PromptConfig>?,
|
||||
val buttonConfigs: List<ButtonConfig>?
|
||||
)
|
||||
}
|
||||
49
flomo-ai/app/src/main/java/com/example/flomo_ai/kwt.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
import java.util.*
|
||||
import com.nimbusds.jose.JWSAlgorithm
|
||||
import com.nimbusds.jose.JWSHeader
|
||||
import com.nimbusds.jose.crypto.MACSigner
|
||||
import com.nimbusds.jwt.JWTClaimsSet
|
||||
import com.nimbusds.jwt.SignedJWT
|
||||
|
||||
class JWTGenerator {
|
||||
companion object {
|
||||
fun generateJWT(apiKeyId: String, secretStr: String): SignedJWT {
|
||||
// 确保密钥长度至少为 32 字节(256 位)
|
||||
val secret = generateValidSecret(secretStr)
|
||||
// Prepare JWT header
|
||||
val header = JWSHeader.Builder(JWSAlgorithm.HS256)
|
||||
.customParam("sign_type", "SIGN")
|
||||
.build()
|
||||
|
||||
// Prepare JWT payload
|
||||
val currentTimeMillis = System.currentTimeMillis()
|
||||
val claimsSet = JWTClaimsSet.Builder()
|
||||
.claim("api_key", apiKeyId)
|
||||
.expirationTime(Date(currentTimeMillis + 60000))
|
||||
.claim("timestamp", currentTimeMillis)
|
||||
.build()
|
||||
|
||||
// Create HMAC signer
|
||||
val signer = MACSigner(secret)
|
||||
|
||||
// Create signed JWT
|
||||
val signedJWT = SignedJWT(header, claimsSet)
|
||||
signedJWT.sign(signer)
|
||||
|
||||
return signedJWT
|
||||
}
|
||||
|
||||
private fun generateValidSecret(secretStr: String): ByteArray {
|
||||
val originalSecret = secretStr.toByteArray()
|
||||
val desiredLength = 32
|
||||
if (originalSecret.size >= desiredLength) {
|
||||
return originalSecret.copyOfRange(0, desiredLength)
|
||||
}
|
||||
val paddedSecret = ByteArray(desiredLength)
|
||||
for (i in originalSecret.indices) {
|
||||
paddedSecret[i] = originalSecret[i]
|
||||
}
|
||||
return paddedSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.flomo_ai.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.flomo_ai.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun FlomoaiTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.example.flomo_ai.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
|
||||
object ThemeManager {
|
||||
private const val PREFS_NAME = "theme_prefs"
|
||||
private const val KEY_THEME_MODE = "theme_mode"
|
||||
|
||||
const val THEME_FOLLOW_SYSTEM = 0
|
||||
const val THEME_LIGHT = 1
|
||||
const val THEME_DARK = 2
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
fun getThemeMode(context: Context): Int {
|
||||
return getPreferences(context).getInt(KEY_THEME_MODE, THEME_FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
fun setThemeMode(context: Context, mode: Int) {
|
||||
getPreferences(context).edit().putInt(KEY_THEME_MODE, mode).apply()
|
||||
applyTheme(mode)
|
||||
}
|
||||
|
||||
fun applyTheme(mode: Int) {
|
||||
when (mode) {
|
||||
THEME_FOLLOW_SYSTEM -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
THEME_LIGHT -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
THEME_DARK -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applySavedTheme(context: Context) {
|
||||
val mode = getThemeMode(context)
|
||||
applyTheme(mode)
|
||||
}
|
||||
|
||||
fun getThemeModeName(mode: Int): String {
|
||||
return when (mode) {
|
||||
THEME_FOLLOW_SYSTEM -> "跟随系统"
|
||||
THEME_LIGHT -> "浅色模式"
|
||||
THEME_DARK -> "深色模式"
|
||||
else -> "跟随系统"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.flomo_ai.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
BIN
flomo-ai/app/src/main/res/drawable/appbackground.jpeg
Normal file
|
After Width: | Height: | Size: 39 KiB |
6
flomo-ai/app/src/main/res/drawable/button_config_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/secondary" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
9
flomo-ai/app/src/main/res/drawable/button_polish_bg.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/btn_polish_bg" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/btn_polish_border" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
6
flomo-ai/app/src/main/res/drawable/button_primary_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/primary" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
6
flomo-ai/app/src/main/res/drawable/button_quick_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/icon_container_bg"/>
|
||||
<corners android:radius="10dp"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/config_button_border" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/btn_summarize_bg" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/btn_summarize_border" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/btn_translate_bg" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/btn_translate_border" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
9
flomo-ai/app/src/main/res/drawable/button_typos_bg.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/btn_typos_bg" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/btn_typos_border" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
8
flomo-ai/app/src/main/res/drawable/edittext_border.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/surface" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/border_default" />
|
||||
<corners android:radius="14dp" />
|
||||
</shape>
|
||||
BIN
flomo-ai/app/src/main/res/drawable/flomo.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
10
flomo-ai/app/src/main/res/drawable/ic_check_circle.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
||||
10
flomo-ai/app/src/main/res/drawable/ic_copy.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#42A5F5"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
170
flomo-ai/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
7
flomo-ai/app/src/main/res/drawable/icon_with_size.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@drawable/flomo"
|
||||
android:width="18dp"
|
||||
android:height="18dp" />
|
||||
</layer-list>
|
||||
6
flomo-ai/app/src/main/res/drawable/indicator_dot.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/primary"/>
|
||||
<size android:width="6dp" android:height="6dp"/>
|
||||
</shape>
|
||||
7
flomo-ai/app/src/main/res/drawable/input_bg.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/surface"/>
|
||||
<stroke android:width="1dp" android:color="@color/border_default"/>
|
||||
<corners android:radius="22dp"/>
|
||||
</shape>
|
||||
7
flomo-ai/app/src/main/res/drawable/result_card_bg.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/surface"/>
|
||||
<stroke android:width="1dp" android:color="@color/card_border"/>
|
||||
<corners android:radius="14dp"/>
|
||||
</shape>
|
||||
6
flomo-ai/app/src/main/res/drawable/send_button_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/primary"/>
|
||||
<corners android:radius="14dp"/>
|
||||
</shape>
|
||||
7
flomo-ai/app/src/main/res/drawable/stop_button_bg.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/stop_generate_bg"/>
|
||||
<stroke android:width="1dp" android:color="@color/stop_generate"/>
|
||||
<corners android:radius="8dp"/>
|
||||
</shape>
|
||||
368
flomo-ai/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,368 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/mainLinearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/background">
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@color/surface"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/headerLeft"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headerTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="AI优化"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text_primary"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="2dp">
|
||||
|
||||
<View
|
||||
android:layout_width="6dp"
|
||||
android:layout_height="6dp"
|
||||
android:background="@drawable/indicator_dot"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/headerModelName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GPT-4o"
|
||||
android:textSize="11sp"
|
||||
android:textColor="@color/primary"
|
||||
android:layout_marginStart="5dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/configButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="配置"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/primary"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/button_secondary_bg"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="快速操作"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.15"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="18dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/btnCheckTypos"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/button_quick_bg"
|
||||
android:layout_marginEnd="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:text="🔍"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/btnSummarize"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/button_quick_bg"
|
||||
android:layout_marginEnd="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:text="📋"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/btnTranslate"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/button_quick_bg"
|
||||
android:layout_marginEnd="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:text="🌐"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/btnPolishing"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/button_quick_bg">
|
||||
|
||||
<TextView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:text="✨"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 提示词 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="提示词"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.15"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:padding="14dp"
|
||||
android:layout_marginBottom="18dp">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/promptSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:spinnerMode="dropdown"
|
||||
android:padding="4dp"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/promptContentText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_hint"
|
||||
android:minLines="1"
|
||||
android:maxLines="3"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 优化结果 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="优化结果"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.15"
|
||||
android:layout_marginBottom="14dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/result_card_bg"
|
||||
android:padding="16dp">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="@color/primary"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outputStatusLabel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="等待发送"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/primary"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outputTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minLines="3"
|
||||
android:textSize="14sp"
|
||||
android:text="发送消息后结果将在此显示"
|
||||
android:textColor="@color/text_secondary"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="12dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCopyResult"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="复制结果"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/primary"
|
||||
android:background="@drawable/button_secondary_bg"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:layout_marginEnd="8dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSaveNote"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_weight="1"
|
||||
android:text="提交笔记"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/primary"
|
||||
android:background="@drawable/button_secondary_bg"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/surface"
|
||||
android:padding="20dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/inputEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/input_bg"
|
||||
android:hint="输入待发送内容…"
|
||||
android:inputType="textMultiLine"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textColorHint="@color/text_hint"
|
||||
android:gravity="top|start"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/stopButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:text="停止生成"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/stop_generate"
|
||||
android:background="@drawable/stop_button_bg"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCharCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0/4000"
|
||||
android:textSize="11sp"
|
||||
android:textColor="@color/text_hint"
|
||||
android:layout_marginEnd="12dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/sendButton"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:text="➤"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white"
|
||||
android:background="@drawable/send_button_bg"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
627
flomo-ai/app/src/main/res/layout/activity_second.xml
Normal file
@@ -0,0 +1,627 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 1. 顶部导航栏 -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/surface"
|
||||
android:elevation="4dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="返回"
|
||||
android:src="@android:drawable/ic_media_previous"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- 标题和副标题容器 -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tvAutoSaveStatus"
|
||||
app:layout_constraintStart_toEndOf="@id/btnBack"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="设置"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="配置自动保存到本地"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 自动保存状态图标 -->
|
||||
<ImageView
|
||||
android:id="@+id/tvAutoSaveStatus"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_check_circle"
|
||||
android:tint="@color/success"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnHome"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<!-- 返回首页按钮 -->
|
||||
<Button
|
||||
android:id="@+id/btnHome"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:text="首页"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/primary"
|
||||
android:background="@drawable/button_secondary_bg"
|
||||
android:minWidth="0dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- 2. 大模型配置卡片 -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:cardBackgroundColor="@color/surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 卡片标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="大模型配置"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="设置 API 接口地址、密钥与模型"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Base URL -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Base URL"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etBaseUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:hint="https://api.openai.com/v1"
|
||||
android:inputType="textUri"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textColorHint="@color/text_hint"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- API Key -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="API Key"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/edittext_border">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="sk-..."
|
||||
android:inputType="textPassword"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="48dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textColorHint="@color/text_hint"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnToggleApiKey"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="显示/隐藏"
|
||||
android:src="@android:drawable/ic_lock_idle_lock"
|
||||
app:tint="@color/text_hint" />
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Model -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Model"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etModel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:hint="gpt-4o"
|
||||
android:inputType="text"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textColorHint="@color/text_hint"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 3. 主题设置卡片 -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 卡片标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="主题设置"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="设置应用主题和夜间模式"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- 主题选择 -->
|
||||
<RadioGroup
|
||||
android:id="@+id/rgThemeMode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbThemeFollowSystem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:buttonTint="@color/primary"
|
||||
android:text="跟随系统"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbThemeLight"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:buttonTint="@color/primary"
|
||||
android:text="浅色模式"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbThemeDark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:buttonTint="@color/primary"
|
||||
android:text="深色模式"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="16sp" />
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 4. 笔记API配置卡片 -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 卡片标题 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="笔记API配置"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="配置第三方笔记应用API"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- API类型选择 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="API类型"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spNoteApiType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:spinnerMode="dropdown"
|
||||
android:padding="12dp"/>
|
||||
|
||||
<!-- API URL -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="API地址"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etNoteApiUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:hint="https://api.example.com/notes"
|
||||
android:inputType="textUri"
|
||||
android:padding="12dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textColorHint="@color/text_hint"
|
||||
android:textSize="16sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- API Key -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="API密钥"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/edittext_border">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etNoteApiKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="API Key"
|
||||
android:inputType="textPassword"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="48dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textColorHint="@color/text_hint"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnToggleNoteApiKey"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="显示/隐藏"
|
||||
android:src="@android:drawable/ic_lock_idle_lock"
|
||||
app:tint="@color/text_hint" />
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 5. 自定义提示词卡片 -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutPromptToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自定义提示词"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="设置系统级指令和角色扮演"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivPromptArrow"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@android:drawable/ic_media_play"
|
||||
android:tint="@color/primary"
|
||||
android:rotation="0" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 折叠内容区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutPromptContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<Button
|
||||
android:id="@+id/btnAddPrompt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:text="+ 添加提示词"
|
||||
android:textSize="14sp"
|
||||
android:backgroundTint="@color/primary"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<!-- 提示词列表容器 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llPromptList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 5. 自定义请求头折叠卡片 -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 可点击的标题栏 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutHeaderToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="自定义请求头"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="向请求附加额外的HTTP Header"
|
||||
android:textColor="@color/text_hint"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivHeaderArrow"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@android:drawable/ic_media_play"
|
||||
android:tint="@color/primary"
|
||||
android:rotation="0" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 折叠内容区域 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutHeaderContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<Button
|
||||
android:id="@+id/btnAddHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:text="+ 添加请求头"
|
||||
android:textSize="14sp"
|
||||
android:backgroundTint="@color/primary"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<!-- 请求头列表容器 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llHeadersList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
187
flomo-ai/app/src/main/res/layout/activity_second_fixed.xml
Normal file
@@ -0,0 +1,187 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:ignore="ExtraText">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGoBack"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back_to_main" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="文字处理AI区的配置"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiButtonName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="API 按钮显示名称"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="API 名称"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="API URL"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="@string/api_key"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiSecretKey"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="API Secret Key"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiModel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:hint="API 模型类型"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/save_config" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llConfigList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!-- 图片选择区域 -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewImageHint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="点击按钮选择图片作为背景"
|
||||
android:layout_below="@id/buttonChooseImage"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="10dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonChooseImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="选择图片"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="20dp" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- 颜色选择区域 -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statustextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="点击以下按钮,设置主页状态栏的颜色"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/statustextView">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_holo_red_light"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@android:color/holo_red_light"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_holo_green_light"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_holo_green_light"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@android:color/holo_green_light"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_holo_blue_light"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintStart_toEndOf="@+id/button_holo_red_light"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_holo_blue_light"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@android:color/holo_blue_light"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_holo_orange_light"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintStart_toEndOf="@+id/button_holo_green_light"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_holo_orange_light"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:background="@android:color/holo_orange_light"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
app:layout_constraintStart_toEndOf="@+id/button_holo_blue_light"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
93
flomo-ai/app/src/main/res/layout/button_entry.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:padding="12sp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8sp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etButtonLabel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:hint="按钮标签"
|
||||
android:inputType="text"
|
||||
android:padding="4sp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnButtonAction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="复制输出内容"
|
||||
android:layout_marginStart="8sp"
|
||||
android:padding="4sp"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnRemoveButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginStart="8sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- API Configuration Fields (shown when action is "api") -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llApiFields"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:hint="API URL"
|
||||
android:inputType="textUri"
|
||||
android:background="@android:color/white"
|
||||
android:padding="8sp"
|
||||
android:layout_marginBottom="4sp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiMethod"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="HTTP Method (POST/GET)"
|
||||
android:inputType="text"
|
||||
android:background="@android:color/white"
|
||||
android:padding="8sp"
|
||||
android:layout_marginEnd="4sp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etApiBodyTemplate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:hint="请求体模板 (使用 {output} 占位符)"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="2"
|
||||
android:background="@android:color/white"
|
||||
android:padding="8sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
42
flomo-ai/app/src/main/res/layout/header_entry.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:padding="8dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etHeaderKey"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="Header 名称"
|
||||
android:inputType="text"
|
||||
android:padding="4dp"/>
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:layout_marginHorizontal="8dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etHeaderValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:hint="值"
|
||||
android:inputType="text"
|
||||
android:padding="4dp"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnRemoveHeader"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
</LinearLayout>
|
||||
59
flomo-ai/app/src/main/res/layout/item_api_config.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="#EEEEEE">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvButtonName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvKey"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSecretKey"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvApiModel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnEdit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="编辑"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="删除" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
45
flomo-ai/app/src/main/res/layout/prompt_entry.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/edittext_border"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etPromptTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="提示词标题"
|
||||
android:inputType="text"
|
||||
android:padding="4dp"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnRemovePrompt"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etPromptContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="提示词内容"
|
||||
android:inputType="textMultiLine"
|
||||
android:minLines="3"
|
||||
android:padding="4dp"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
flomo-ai/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
flomo-ai/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
flomo-ai/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
65
flomo-ai/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 核心色值 -->
|
||||
<color name="primary">#2DD4A8</color>
|
||||
<color name="primary_variant">#1A1D27</color>
|
||||
<color name="secondary">#2DD4A8</color>
|
||||
<color name="secondary_variant">#1A1D27</color>
|
||||
|
||||
<!-- 背景色 -->
|
||||
<color name="background">#0F1117</color>
|
||||
<color name="surface">#1A1D27</color>
|
||||
<color name="surface_elevated">#232733</color>
|
||||
|
||||
<!-- 主强调色 -->
|
||||
<color name="primary_light">#1A2DD4A8</color>
|
||||
|
||||
<!-- 文字层级 -->
|
||||
<color name="text_primary">#F0F2F5</color>
|
||||
<color name="text_secondary">#C8CDD5</color>
|
||||
<color name="text_hint">#6B7280</color>
|
||||
<color name="text_placeholder">#4B5260</color>
|
||||
|
||||
<!-- 四个操作按钮颜色 -->
|
||||
<!-- 检查错别字 -->
|
||||
<color name="btn_typos_bg">#0E38BDF8</color>
|
||||
<color name="btn_typos_icon">#38BDF8</color>
|
||||
<color name="btn_typos_border">#4D38BDF8</color>
|
||||
|
||||
<!-- 总结 -->
|
||||
<color name="btn_summarize_bg">#0E2DD4A8</color>
|
||||
<color name="btn_summarize_icon">#2DD4A8</color>
|
||||
<color name="btn_summarize_border">#4D2DD4A8</color>
|
||||
|
||||
<!-- 翻译 -->
|
||||
<color name="btn_translate_bg">#0EFBBF24</color>
|
||||
<color name="btn_translate_icon">#FBBF24</color>
|
||||
<color name="btn_translate_border">#4DFBBF24</color>
|
||||
|
||||
<!-- 润色 -->
|
||||
<color name="btn_polish_bg">#0EF472B6</color>
|
||||
<color name="btn_polish_icon">#F472B6</color>
|
||||
<color name="btn_polish_border">#4DF472B6</color>
|
||||
|
||||
<!-- 功能色 -->
|
||||
<color name="stop_generate">#F59E0B</color>
|
||||
<color name="stop_generate_bg">#14F59E0B</color>
|
||||
<color name="config_button_border">#402DD4A8</color>
|
||||
<color name="divider">#0FFFFFFF</color>
|
||||
<color name="card_border">#14FFFFFF</color>
|
||||
|
||||
<!-- 图标容器 -->
|
||||
<color name="icon_container_bg">#0AFFFFFF</color>
|
||||
|
||||
<!-- 边框色 -->
|
||||
<color name="border_default">#14FFFFFF</color>
|
||||
<color name="border_focused">#4D2DD4A8</color>
|
||||
<color name="border_glow">#0F2DD4A8</color>
|
||||
|
||||
<!-- 其他 -->
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="success">#4CAF50</color>
|
||||
<color name="warning">#FF9800</color>
|
||||
<color name="error">#F44336</color>
|
||||
</resources>
|
||||
18
flomo-ai/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">flomo-ai</string>
|
||||
<string name="back_to_main">返回主界面</string>
|
||||
<string name="ai_config_title">文字处理AI区的配置</string>
|
||||
<string name="api_button_name_hint">API按钮显示名称</string>
|
||||
<string name="api_name_hint">API名称</string>
|
||||
<string name="api_url_hint">API URL</string>
|
||||
<string name="api_key_hint">API密钥</string>
|
||||
<string name="api_secret_key_hint">API Secret Key</string>
|
||||
<string name="api_model_type_hint">API模型类型</string>
|
||||
<string name="save_config">保存配置</string>
|
||||
<string name="set_background_image">设置背景图片</string>
|
||||
<string name="choose_image">选择图片</string>
|
||||
<string name="api_key">API密钥</string>
|
||||
<string name="background_image">Background image description</string>
|
||||
|
||||
</resources>
|
||||
8
flomo-ai/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- 定义一个主题 -->
|
||||
<style name="TransparentTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
</resources>
|
||||
5
flomo-ai/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Flomoai" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|
||||
13
flomo-ai/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
flomo-ai/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.flomo_ai
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
37
flomo-ai/build-quick.bat
Normal file
@@ -0,0 +1,37 @@
|
||||
@echo off
|
||||
REM 快速构建脚本 - 使用项目本地配置避免警告
|
||||
|
||||
echo ========================================
|
||||
echo Flomo-AI 项目快速构建脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 检查是否提供了参数
|
||||
if "%1"=="" (
|
||||
echo 用法: build-quick.bat [gradle命令参数]
|
||||
echo 示例: build-quick.bat clean
|
||||
echo build-quick.bat assembleDebug
|
||||
echo build-quick.bat test
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo 正在使用项目本地 Gradle 配置进行构建...
|
||||
echo 命令: %*
|
||||
|
||||
REM 设置使用项目本地初始化脚本
|
||||
set LOCAL_INIT_SCRIPT=%~dp0gradle\init\init.gradle
|
||||
set GRADLE_OPTS=%GRADLE_OPTS% -Dorg.gradle.daemon=false
|
||||
|
||||
echo 初始化脚本路径: %LOCAL_INIT_SCRIPT%
|
||||
echo.
|
||||
|
||||
REM 执行构建
|
||||
call "%~dp0gradlew.bat" %* --init-script="%LOCAL_INIT_SCRIPT%" --warning-mode=summary
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 构建完成
|
||||
echo ========================================
|
||||
pause
|
||||
7
flomo-ai/build.gradle.kts
Normal file
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
}
|
||||
|
||||
// 仓库配置已移至 settings.gradle.kts 中统一管理
|
||||
// 避免重复定义和潜在的配置冲突
|
||||
12
flomo-ai/build.ps1
Normal file
@@ -0,0 +1,12 @@
|
||||
# Local Gradle init script PowerShell wrapper
|
||||
|
||||
Write-Host "Using local Gradle init script..." -ForegroundColor Green
|
||||
|
||||
# Set environment variable
|
||||
$env:GRADLE_OPTS = "-Dorg.gradle.init.script=$PWD\gradle\init\init.gradle"
|
||||
|
||||
# Execute Gradle command
|
||||
& "$PWD\gradlew.bat" clean --warning-mode=all
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build completed with local init script." -ForegroundColor Green
|
||||
33
flomo-ai/gradle.properties
Normal file
@@ -0,0 +1,33 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
|
||||
# 网络连接超时设置
|
||||
org.gradle.internal.http.connectionTimeout=120000
|
||||
org.gradle.internal.http.socketTimeout=120000
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
# Gradle 下载镜像配置
|
||||
# 使用阿里云镜像加速下载
|
||||
org.gradle.wrapper.downloadUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/
|
||||
# 备用腾讯云镜像
|
||||
# org.gradle.wrapper.downloadUrl=https://mirrors.cloud.tencent.com/gradle/
|
||||
52
flomo-ai/gradle/init/init.gradle
Normal file
@@ -0,0 +1,52 @@
|
||||
// 项目本地 Gradle 初始化脚本:阿里云镜像源配置
|
||||
// 用于替代用户全局配置,避免已弃用语法警告
|
||||
|
||||
// 1. 插件管理仓库配置
|
||||
gradle.settingsEvaluated { settings ->
|
||||
settings.pluginManagement {
|
||||
repositories {
|
||||
maven { url = uri('https://maven.aliyun.com/repository/google') }
|
||||
maven { url = uri('https://maven.aliyun.com/repository/gradle-plugin') }
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
// 备用官方源
|
||||
google {
|
||||
content {
|
||||
includeGroupByRegex("com\\.android.*")
|
||||
includeGroupByRegex("com\\.google.*")
|
||||
includeGroupByRegex("androidx.*")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 依赖解析管理配置
|
||||
settings.dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||
repositories {
|
||||
maven { url = uri('https://maven.aliyun.com/repository/google') }
|
||||
maven { url = uri('https://maven.aliyun.com/repository/central') }
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url = uri('https://jitpack.io') }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 兼容旧版 Gradle 的配置
|
||||
allprojects {
|
||||
buildscript {
|
||||
repositories {
|
||||
maven { url = uri('https://maven.aliyun.com/repository/google') }
|
||||
maven { url = uri('https://maven.aliyun.com/repository/gradle-plugin') }
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven { url = uri('https://maven.aliyun.com/repository/google') }
|
||||
maven { url = uri('https://maven.aliyun.com/repository/central') }
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url = uri('https://jitpack.io') }
|
||||
}
|
||||
}
|
||||
59
flomo-ai/gradle/libs.versions.toml
Normal file
@@ -0,0 +1,59 @@
|
||||
[versions]
|
||||
agp = "8.13.2"
|
||||
gradle = "8.13.2"
|
||||
gson = "2.10.1"
|
||||
kjwtJwks = "0.9.0"
|
||||
kotlin = "1.9.0"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.1.5"
|
||||
espressoCore = "3.5.1"
|
||||
kotlinGradlePlugin = "1.6.21"
|
||||
kotlinxCoroutinesAndroid = "1.7.1"
|
||||
kotlinxCoroutinesCore = "1.7.1"
|
||||
lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
composeBom = "2024.04.01"
|
||||
appcompat = "1.7.0-alpha02"
|
||||
appcompatVersion = "1.7.0"
|
||||
loggingInterceptor = "4.9.3"
|
||||
material = "1.12.0"
|
||||
moshiKotlin = "1.12.0"
|
||||
nimbusJoseJwt = "9.40"
|
||||
okhttp = "4.12.0"
|
||||
permissionx = "1.7.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompatVersion" }
|
||||
kjwt-jwks = { module = "io.github.nefilim.kjwt:kjwt-jwks", version.ref = "kjwtJwks" }
|
||||
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
|
||||
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }
|
||||
nimbus-jose-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbusJoseJwt" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
permissionx = { group = "com.permissionx.guolindev", name = "permissionx", version.ref = "permissionx" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
BIN
flomo-ai/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
flomo-ai/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Thu Feb 26 22:34:06 CST 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://repo.huaweicloud.com/gradle/gradle-9.0-milestone-1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
flomo-ai/gradlew
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||