447 lines
18 KiB
Rust
447 lines
18 KiB
Rust
pub mod settings;
|
|
pub mod api;
|
|
pub mod components;
|
|
pub mod util;
|
|
pub mod dialogs;
|
|
|
|
use api::{user::default_person, community::default_community, post::default_post};
|
|
use components::{post_row::PostRow, community_row::CommunityRow, profile_page::{ProfilePage, self}, community_page::{CommunityPage, self}, post_page::{PostPage, self}};
|
|
use gtk::prelude::*;
|
|
use lemmy_api_common::{lemmy_db_views_actor::structs::CommunityView, lemmy_db_views::structs::PostView, person::GetPersonDetailsResponse, lemmy_db_schema::{newtypes::PostId, ListingType}, post::GetPostResponse, community::GetCommunityResponse};
|
|
use relm4::{prelude::*, factory::FactoryVecDeque, set_global_css, actions::{RelmAction, RelmActionGroup}};
|
|
|
|
static APP_ID: &str = "com.lemmy-gtk.lemoa";
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum AppState {
|
|
Loading,
|
|
Posts,
|
|
ChooseInstance,
|
|
Communities,
|
|
Community,
|
|
Person,
|
|
Post,
|
|
Login,
|
|
Message
|
|
}
|
|
|
|
struct App {
|
|
state: AppState,
|
|
message: Option<String>,
|
|
latest_action: Option<AppMsg>,
|
|
posts: FactoryVecDeque<PostRow>,
|
|
communities: FactoryVecDeque<CommunityRow>,
|
|
profile_page: Controller<ProfilePage>,
|
|
community_page: Controller<CommunityPage>,
|
|
post_page: Controller<PostPage>
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum AppMsg {
|
|
ChooseInstance,
|
|
ShowLogin,
|
|
Login(String, String),
|
|
Logout,
|
|
Retry,
|
|
ShowMessage(String),
|
|
DoneChoosingInstance(String),
|
|
StartFetchPosts(Option<ListingType>),
|
|
DoneFetchPosts(Vec<PostView>),
|
|
DoneFetchCommunities(Vec<CommunityView>),
|
|
ViewCommunities(Option<String>),
|
|
OpenCommunity(String),
|
|
DoneFetchCommunity(GetCommunityResponse),
|
|
OpenPerson(String),
|
|
DoneFetchPerson(GetPersonDetailsResponse),
|
|
OpenPost(PostId),
|
|
DoneFetchPost(GetPostResponse)
|
|
}
|
|
|
|
#[relm4::component]
|
|
impl SimpleComponent for App {
|
|
type Init = ();
|
|
type Input = AppMsg;
|
|
type Output = ();
|
|
|
|
view! {
|
|
#[root]
|
|
main_window = gtk::ApplicationWindow {
|
|
set_title: Some("Lemoa"),
|
|
set_default_size: (300, 100),
|
|
|
|
#[wrap(Some)]
|
|
set_titlebar = >k::HeaderBar {
|
|
pack_end = >k::MenuButton {
|
|
set_icon_name: "view-more",
|
|
set_menu_model: Some(&menu_model),
|
|
},
|
|
pack_start = >k::Button {
|
|
set_label: "Recommended",
|
|
connect_clicked => AppMsg::StartFetchPosts(None),
|
|
},
|
|
pack_start = >k::Button {
|
|
set_label: "Subscribed",
|
|
connect_clicked => AppMsg::StartFetchPosts(Some(ListingType::Subscribed)),
|
|
},
|
|
pack_start = >k::Button {
|
|
set_label: "Communities",
|
|
connect_clicked => AppMsg::ViewCommunities(None),
|
|
},
|
|
},
|
|
|
|
match model.state {
|
|
AppState::Posts => gtk::ScrolledWindow {
|
|
set_vexpand: true,
|
|
set_hexpand: true,
|
|
|
|
#[local_ref]
|
|
posts_box -> gtk::Box {
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_spacing: 5,
|
|
}
|
|
},
|
|
AppState::Loading => gtk::Box {
|
|
set_hexpand: true,
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_spacing: 12,
|
|
set_valign: gtk::Align::Center,
|
|
set_halign: gtk::Align::Center,
|
|
gtk::Spinner {
|
|
set_spinning: true,
|
|
set_height_request: 80,
|
|
},
|
|
gtk::Label {
|
|
set_text: "Loading",
|
|
},
|
|
},
|
|
AppState::ChooseInstance => gtk::Box {
|
|
set_hexpand: true,
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_spacing: 12,
|
|
set_margin_all: 20,
|
|
set_valign: gtk::Align::Center,
|
|
set_halign: gtk::Align::Center,
|
|
gtk::Label {
|
|
set_text: "Please enter the URL of a valid lemmy instance",
|
|
},
|
|
#[name(instance_url)]
|
|
gtk::Entry {
|
|
set_tooltip_text: Some("Instance"),
|
|
},
|
|
gtk::Button {
|
|
set_label: "Done",
|
|
connect_clicked[sender, instance_url] => move |_| {
|
|
let text = instance_url.text().as_str().to_string();
|
|
instance_url.set_text("");
|
|
sender.input(AppMsg::DoneChoosingInstance(text));
|
|
},
|
|
}
|
|
},
|
|
AppState::Login => gtk::Box {
|
|
set_hexpand: true,
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_spacing: 12,
|
|
set_margin_all: 20,
|
|
set_valign: gtk::Align::Center,
|
|
set_hexpand: true,
|
|
|
|
gtk::Label {
|
|
set_text: "Login",
|
|
add_css_class: "font-bold",
|
|
},
|
|
#[name(username)]
|
|
gtk::Entry {
|
|
set_placeholder_text: Some("Username or E-Mail"),
|
|
},
|
|
#[name(password)]
|
|
gtk::PasswordEntry {
|
|
set_placeholder_text: Some("Password"),
|
|
set_show_peek_icon: true,
|
|
},
|
|
gtk::Box {
|
|
set_orientation: gtk::Orientation::Horizontal,
|
|
set_halign: gtk::Align::End,
|
|
|
|
gtk::Button {
|
|
set_label: "Cancel",
|
|
connect_clicked => AppMsg::StartFetchPosts(None),
|
|
set_margin_end: 10,
|
|
},
|
|
gtk::Button {
|
|
set_label: "Login",
|
|
connect_clicked[sender, username, password] => move |_| {
|
|
let username_text = username.text().as_str().to_string();
|
|
username.set_text("");
|
|
let password_text = password.text().as_str().to_string();
|
|
password.set_text("");
|
|
sender.input(AppMsg::Login(username_text, password_text));
|
|
},
|
|
},
|
|
}
|
|
},
|
|
AppState::Communities => gtk::Box {
|
|
gtk::ScrolledWindow {
|
|
set_vexpand: true,
|
|
set_hexpand: true,
|
|
|
|
gtk::Box {
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_spacing: 10,
|
|
|
|
gtk::Box {
|
|
set_margin_all: 10,
|
|
|
|
#[name(community_search_query)]
|
|
gtk::Entry {
|
|
set_hexpand: true,
|
|
set_tooltip_text: Some("Search"),
|
|
set_margin_end: 10,
|
|
},
|
|
gtk::Button {
|
|
set_label: "Search",
|
|
connect_clicked[sender, community_search_query] => move |_| {
|
|
let text = community_search_query.text().as_str().to_string();
|
|
sender.input(AppMsg::ViewCommunities(Some(text)));
|
|
},
|
|
}
|
|
},
|
|
|
|
#[local_ref]
|
|
communities_box -> gtk::Box {
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_spacing: 5,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
AppState::Person => {
|
|
gtk::Box {
|
|
#[local_ref]
|
|
profile_page -> gtk::ScrolledWindow {}
|
|
}
|
|
}
|
|
AppState::Community => {
|
|
gtk::Box {
|
|
#[local_ref]
|
|
community_page -> gtk::ScrolledWindow {}
|
|
}
|
|
}
|
|
AppState::Post => {
|
|
gtk::Box {
|
|
#[local_ref]
|
|
post_page -> gtk::ScrolledWindow {}
|
|
}
|
|
}
|
|
AppState::Message => {
|
|
gtk::Box {
|
|
set_orientation: gtk::Orientation::Vertical,
|
|
set_margin_all: 40,
|
|
|
|
gtk::Label {
|
|
#[watch]
|
|
set_text: &model.message.clone().unwrap_or("".to_string()),
|
|
},
|
|
gtk::Button {
|
|
set_label: "Go Home",
|
|
connect_clicked => AppMsg::Retry,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
menu! {
|
|
menu_model: {
|
|
"Choose Instance" => ChangeInstanceAction,
|
|
"Login" => LoginAction,
|
|
"Logout" => LogoutAction
|
|
}
|
|
}
|
|
|
|
// Initialize the component.
|
|
fn init(
|
|
_init: Self::Init,
|
|
root: &Self::Root,
|
|
sender: ComponentSender<Self>,
|
|
) -> ComponentParts<Self> {
|
|
let instance_url = settings::get_prefs().instance_url;
|
|
let state = if instance_url.is_empty() { AppState::ChooseInstance } else { AppState::Loading };
|
|
|
|
// initialize all controllers and factories
|
|
let posts = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
|
|
let communities = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
|
|
let profile_page = ProfilePage::builder().launch(default_person()).forward(sender.input_sender(), |msg| msg);
|
|
let community_page = CommunityPage::builder().launch(default_community().community_view).forward(sender.input_sender(), |msg| msg);
|
|
let post_page = PostPage::builder().launch(default_post()).forward(sender.input_sender(), |msg| msg);
|
|
|
|
let model = App { state, posts, communities, profile_page, community_page, post_page, message: None, latest_action: None };
|
|
|
|
// fetch posts if that's the initial page
|
|
if !instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts(None)) };
|
|
|
|
// setup all widgets and different stack pages
|
|
let posts_box = model.posts.widget();
|
|
let communities_box = model.communities.widget();
|
|
let profile_page = model.profile_page.widget();
|
|
let community_page = model.community_page.widget();
|
|
let post_page = model.post_page.widget();
|
|
|
|
let widgets = view_output!();
|
|
|
|
// create the header bar menu and its actions
|
|
let instance_sender = sender.clone();
|
|
let instance_action: RelmAction<ChangeInstanceAction> = RelmAction::new_stateless(move |_| {
|
|
instance_sender.input(AppMsg::ChooseInstance);
|
|
});
|
|
let login_sender = sender.clone();
|
|
let login_action: RelmAction<LoginAction> = RelmAction::new_stateless(move |_| {
|
|
login_sender.input(AppMsg::ShowLogin);
|
|
});
|
|
let logout_action: RelmAction<LogoutAction> = RelmAction::new_stateless(move |_| {
|
|
sender.input(AppMsg::ChooseInstance);
|
|
});
|
|
|
|
let mut group = RelmActionGroup::<WindowActionGroup>::new();
|
|
group.add_action(instance_action);
|
|
group.add_action(login_action);
|
|
group.add_action(logout_action);
|
|
group.register_for_widget(&widgets.main_window);
|
|
|
|
ComponentParts { model, widgets }
|
|
}
|
|
|
|
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
|
|
match msg {
|
|
AppMsg::DoneChoosingInstance(instance_url) => {
|
|
if instance_url.trim().is_empty() { return; }
|
|
let mut preferences = settings::get_prefs();
|
|
preferences.instance_url = instance_url;
|
|
settings::save_prefs(&preferences);
|
|
self.state = AppState::Loading;
|
|
sender.input(AppMsg::StartFetchPosts(None));
|
|
}
|
|
AppMsg::ChooseInstance => {
|
|
self.state = AppState::ChooseInstance;
|
|
}
|
|
AppMsg::StartFetchPosts(type_) => {
|
|
std::thread::spawn(move || {
|
|
let message = match api::posts::list_posts(1, None, type_) {
|
|
Ok(posts) => AppMsg::DoneFetchPosts(posts),
|
|
Err(err) => AppMsg::ShowMessage(err.to_string())
|
|
};
|
|
sender.input(message);
|
|
});
|
|
}
|
|
AppMsg::DoneFetchPosts(posts) => {
|
|
self.state = AppState::Posts;
|
|
self.posts.guard().clear();
|
|
for post in posts {
|
|
self.posts.guard().push_back(post);
|
|
}
|
|
}
|
|
AppMsg::ViewCommunities(query) => {
|
|
self.state = AppState::Communities;
|
|
if (query.is_none() || query.clone().unwrap().trim().is_empty()) && !self.communities.is_empty() { return; }
|
|
std::thread::spawn(move || {
|
|
let message = match api::communities::fetch_communities(1, query) {
|
|
Ok(communities) => AppMsg::DoneFetchCommunities(communities),
|
|
Err(err) => AppMsg::ShowMessage(err.to_string())
|
|
};
|
|
sender.input(message);
|
|
});
|
|
}
|
|
AppMsg::DoneFetchCommunities(communities) => {
|
|
self.state = AppState::Communities;
|
|
self.communities.guard().clear();
|
|
for community in communities {
|
|
self.communities.guard().push_back(community);
|
|
}
|
|
}
|
|
AppMsg::OpenPerson(person_name) => {
|
|
self.state = AppState::Loading;
|
|
std::thread::spawn(move || {
|
|
let message = match api::user::get_user(person_name, 1) {
|
|
Ok(person) => AppMsg::DoneFetchPerson(person),
|
|
Err(err) => AppMsg::ShowMessage(err.to_string())
|
|
};
|
|
sender.input(message);
|
|
});
|
|
}
|
|
AppMsg::DoneFetchPerson(person) => {
|
|
self.profile_page.sender().emit(profile_page::ProfileInput::UpdatePerson(person));
|
|
self.state = AppState::Person;
|
|
}
|
|
AppMsg::OpenCommunity(community_name) => {
|
|
self.state = AppState::Loading;
|
|
std::thread::spawn(move || {
|
|
let message = match api::community::get_community(community_name) {
|
|
Ok(community) => AppMsg::DoneFetchCommunity(community),
|
|
Err(err) => AppMsg::ShowMessage(err.to_string())
|
|
};
|
|
sender.input(message);
|
|
});
|
|
}
|
|
AppMsg::DoneFetchCommunity(community) => {
|
|
self.community_page.sender().emit(community_page::CommunityInput::UpdateCommunity(community.community_view));
|
|
self.state = AppState::Community;
|
|
}
|
|
AppMsg::OpenPost(post_id) => {
|
|
self.state = AppState::Loading;
|
|
std::thread::spawn(move || {
|
|
let message = match api::post::get_post(post_id) {
|
|
Ok(post) => AppMsg::DoneFetchPost(post),
|
|
Err(err) => AppMsg::ShowMessage(err.to_string())
|
|
};
|
|
sender.input(message);
|
|
});
|
|
}
|
|
AppMsg::DoneFetchPost(post) => {
|
|
self.post_page.sender().emit(post_page::PostInput::UpdatePost(post));
|
|
self.state = AppState::Post;
|
|
}
|
|
AppMsg::ShowLogin => {
|
|
self.state = AppState::Login;
|
|
}
|
|
AppMsg::Login(username, password) => {
|
|
self.state = AppState::Loading;
|
|
std::thread::spawn(move || {
|
|
let message = match api::auth::login(username, password) {
|
|
Ok(login) => {
|
|
if let Some(token) = login.jwt {
|
|
util::set_auth_token(Some(token));
|
|
AppMsg::StartFetchPosts(None)
|
|
} else {
|
|
AppMsg::ShowMessage("Wrong credentials!".to_string())
|
|
}
|
|
}
|
|
Err(err) => AppMsg::ShowMessage(err.to_string())
|
|
};
|
|
sender.input(message);
|
|
});
|
|
}
|
|
AppMsg::Logout => {
|
|
util::set_auth_token(None);
|
|
}
|
|
AppMsg::ShowMessage(message) => {
|
|
self.message = Some(message);
|
|
self.state = AppState::Message;
|
|
}
|
|
AppMsg::Retry => {
|
|
sender.input(self.latest_action.clone().unwrap_or(AppMsg::StartFetchPosts(None)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
relm4::new_action_group!(WindowActionGroup, "win");
|
|
relm4::new_stateless_action!(ChangeInstanceAction, WindowActionGroup, "instance");
|
|
relm4::new_stateless_action!(LoginAction, WindowActionGroup, "login");
|
|
relm4::new_stateless_action!(LogoutAction, WindowActionGroup, "logout");
|
|
|
|
fn main() {
|
|
let app = RelmApp::new(APP_ID);
|
|
set_global_css(include_str!("style.css"));
|
|
app.run::<App>(());
|
|
}
|