feat: 包含 Kotlin 原始代码和 Rust Windows 桌面版

- flomo-ai/: Android Kotlin 原始项目
- flomo-ai-desktop/: Rust + egui Windows 桌面移植版
  - LLM API 调用、提示词管理、主题切换、配置持久化
  - MinGW 工具链编译,无控制台窗口
This commit is contained in:
xiaji
2026-04-04 17:32:49 +08:00
commit b4908fcc2d
138 changed files with 6815 additions and 0 deletions

8
.gitignore vendored Normal file
View 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/

View 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
View File

@@ -0,0 +1,3 @@
/target/
*.pdb
Cargo.lock

View 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

View 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())
}

View File

@@ -0,0 +1,2 @@
pub mod llm_client;
pub use llm_client::*;

465
flomo-ai-desktop/src/app.rs Normal file
View 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();
}
});
}
}

View File

@@ -0,0 +1,2 @@
pub mod store;
pub use store::*;

View 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))
}

View 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)))),
)
}

View 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);
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod main_page;
pub mod settings_page;
pub use main_page::OutputStatus;

View 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;
}
}

View 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(),
}
}
}

View 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::*;

View 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
}

View 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);
}
}
});
}

View 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
View 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

View 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
View 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
View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
flomo-ai/apkkey.jks Normal file

Binary file not shown.

1
flomo-ai/app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
}

View File

@@ -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)
}
}

View 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>

View 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
)
}

View File

@@ -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()
}
}

View File

@@ -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?
)
}

View File

@@ -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>?
)
}

View 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
}
}
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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 -> "跟随系统"
}
}
}

View File

@@ -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
)
*/
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View 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>

View 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>

View 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>

View 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>

View 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="@android:color/transparent" />
<stroke
android:width="1dp"
android:color="@color/config_button_border" />
<corners android:radius="12dp" />
</shape>

View 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_summarize_bg" />
<stroke
android:width="1dp"
android:color="@color/btn_summarize_border" />
<corners android:radius="14dp" />
</shape>

View 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_translate_bg" />
<stroke
android:width="1dp"
android:color="@color/btn_translate_border" />
<corners android:radius="14dp" />
</shape>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Flomoai" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View 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>

View 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>

View File

@@ -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
View 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

View 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
View 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

View 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/

View 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') }
}
}

View 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" }

Binary file not shown.

View 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
View 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" "$@"

Some files were not shown because too many files have changed in this diff Show More