pub mod api; pub mod components; pub mod config; pub mod dialogs; pub mod settings; pub mod util; use api::{community::default_community, post::default_post, user::default_person}; use components::{ communities_page::{CommunitiesPage, CommunitiesPageInput}, community_page::{self, CommunityPage}, inbox_page::{InboxInput, InboxPage}, instances_page::{InstancesPage, InstancesPageInput}, post_page::{self, PostPage}, post_row::PostRow, profile_page::{self, ProfilePage}, }; use dialogs::about::AboutDialog; use gtk::prelude::*; use lemmy_api_common::{ community::GetCommunityResponse, lemmy_db_schema::{ newtypes::{CommunityId, PersonId, PostId}, ListingType, }, lemmy_db_views::structs::PostView, person::GetPersonDetailsResponse, post::GetPostResponse, }; use relm4::{ actions::{RelmAction, RelmActionGroup}, factory::FactoryVecDeque, prelude::*, set_global_css, }; use crate::components::login_page::LoginPage; #[derive(Debug, Clone, Copy)] pub enum AppState { Loading, Posts, ChooseInstance, Communities, Community, Person, Post, Login, Message, Inbox, } struct App { state: AppState, message: Option, back_queue: Vec, posts: FactoryVecDeque, instances_page: Controller, profile_page: Controller, community_page: Controller, communities_page: Controller, post_page: Controller, inbox_page: Controller, login_page: Controller, logged_in: bool, current_posts_type: Option, current_posts_page: i64, about_dialog: Controller, } #[derive(Debug, Clone)] pub enum AppMsg { ChooseInstance, ShowLogin, LoggedIn, Logout, ShowMessage(String), StartFetchPosts(Option, bool), DoneFetchPosts(Vec), OpenCommunity(CommunityId), DoneFetchCommunity(GetCommunityResponse), OpenPerson(PersonId), DoneFetchPerson(GetPersonDetailsResponse), OpenPost(PostId), DoneFetchPost(GetPostResponse), OpenInbox, OpenCommunities, PopBackStack, UpdateState(AppState), } #[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: (1400, 800), #[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_icon_name: "go-previous", connect_clicked => AppMsg::PopBackStack, #[watch] set_visible: model.back_queue.len() > 1, }, pack_start = >k::Button { set_label: "Home", connect_clicked => AppMsg::StartFetchPosts(None, true), }, pack_start = >k::Button { set_label: "Communities", connect_clicked => AppMsg::OpenCommunities, }, pack_start = >k::Button { set_label: "Recommended", connect_clicked => AppMsg::StartFetchPosts(Some(ListingType::Subscribed), true), #[watch] set_visible: model.logged_in, }, pack_start = >k::Button { set_label: "Inbox", connect_clicked => AppMsg::OpenInbox, #[watch] set_visible: model.logged_in, }, }, match model.state { AppState::Posts => gtk::ScrolledWindow { set_hexpand: true, gtk::Box { set_orientation: gtk::Orientation::Vertical, #[local_ref] posts_box -> gtk::Box { set_orientation: gtk::Orientation::Vertical, set_spacing: 5, }, gtk::Button { set_label: "More", connect_clicked => AppMsg::StartFetchPosts(model.current_posts_type, false), set_margin_all: 10, } } }, 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 { #[local_ref] instances_page -> gtk::Box {} }, AppState::Login => gtk::Box { #[local_ref] login_page -> gtk::Box {} }, AppState::Communities => gtk::Box { #[local_ref] communities_page -> gtk::Box {} } 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 back", connect_clicked => AppMsg::PopBackStack, } } } AppState::Inbox => { gtk::ScrolledWindow { #[local_ref] inbox_page -> gtk::Box {} } } } } } menu! { menu_model: { "Choose Instance" => ChangeInstanceAction, "Profile" => ProfileAction, "Login" => LoginAction, "Logout" => LogoutAction, "About" => AboutAction } } // Initialize the component. fn init( _init: Self::Init, root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { let current_account = settings::get_current_account(); let state = if current_account.instance_url.is_empty() { AppState::ChooseInstance } else { AppState::Loading }; let logged_in = current_account.jwt.is_some(); // initialize all controllers and factories let posts = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender()); let instances_page = InstancesPage::builder() .launch(()) .forward(sender.input_sender(), |msg| msg); 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 inbox_page = InboxPage::builder() .launch(()) .forward(sender.input_sender(), |msg| msg); let communities_page = CommunitiesPage::builder() .launch(()) .forward(sender.input_sender(), |msg| msg); let about_dialog = AboutDialog::builder() .launch(root.toplevel_window().unwrap()) .detach(); let login_page = LoginPage::builder() .launch(()) .forward(sender.input_sender(), |msg| msg); let model = App { state, back_queue: vec![], logged_in, posts, instances_page, profile_page, community_page, post_page, inbox_page, communities_page, login_page, message: None, current_posts_type: None, current_posts_page: 1, about_dialog, }; // fetch posts if that's the initial page if !current_account.instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts(None, true)) }; // setup all widgets and different stack pages let posts_box = model.posts.widget(); let instances_page = model.instances_page.widget(); let profile_page = model.profile_page.widget(); let community_page = model.community_page.widget(); let post_page = model.post_page.widget(); let inbox_page = model.inbox_page.widget(); let communities_page = model.communities_page.widget(); let login_page = model.login_page.widget(); let widgets = view_output!(); // create the header bar menu and its actions let instance_sender = sender.clone(); let instance_action: RelmAction = RelmAction::new_stateless(move |_| { instance_sender.input(AppMsg::ChooseInstance); }); let profile_sender = sender.clone(); let profile_action: RelmAction = RelmAction::new_stateless(move |_| { let person = settings::get_current_account(); if !person.name.is_empty() { profile_sender.input(AppMsg::OpenPerson(PersonId(person.id))); } }); let login_sender = sender.clone(); let login_action: RelmAction = RelmAction::new_stateless(move |_| { login_sender.input(AppMsg::ShowLogin); }); let logout_action: RelmAction = RelmAction::new_stateless(move |_| { sender.input(AppMsg::Logout); }); let about_action = { let sender = model.about_dialog.sender().clone(); RelmAction::::new_stateless(move |_| { sender.send(()).unwrap_or_default(); }) }; let mut group = RelmActionGroup::::new(); group.add_action(instance_action); group.add_action(profile_action); group.add_action(login_action); group.add_action(logout_action); group.add_action(about_action); group.register_for_widget(&widgets.main_window); ComponentParts { model, widgets } } fn update(&mut self, msg: Self::Input, sender: ComponentSender) { // save the back queue match msg { AppMsg::OpenCommunities | AppMsg::DoneFetchCommunity(_) | AppMsg::DoneFetchPerson(_) | AppMsg::DoneFetchPost(_) | AppMsg::DoneFetchPosts(_) | AppMsg::ShowMessage(_) => self.back_queue.push(msg.clone()), _ => {} } match msg { AppMsg::ChooseInstance => { self.state = AppState::ChooseInstance; self.instances_page .sender() .emit(InstancesPageInput::FetchInstances); } AppMsg::StartFetchPosts(type_, remove_previous) => { self.current_posts_type = type_; let page = if remove_previous { 1 } else { self.current_posts_page + 1 }; // show the loading indicator if it's the first page if page == 1 { self.state = AppState::Loading; } self.current_posts_page = page; std::thread::spawn(move || { let message = match api::posts::list_posts(page, None, type_) { Ok(posts) => AppMsg::DoneFetchPosts(posts), Err(err) => AppMsg::ShowMessage(err.to_string()), }; sender.input(message); }); } AppMsg::DoneFetchPosts(posts) => { self.state = AppState::Posts; if self.current_posts_page == 1 { self.posts.guard().clear(); } for post in posts { self.posts.guard().push_back(post); } } AppMsg::OpenCommunities => { self.state = AppState::Communities; self.communities_page .sender() .emit(CommunitiesPageInput::FetchCommunities( ListingType::Local, true, )); } AppMsg::OpenPerson(person_id) => { self.state = AppState::Loading; std::thread::spawn(move || { let message = match api::user::get_user(person_id, 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_id) => { self.state = AppState::Loading; std::thread::spawn(move || { let message = match api::community::get_community(community_id) { 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::PostPageInput::UpdatePost(post)); self.state = AppState::Post; } AppMsg::ShowLogin => { self.state = AppState::Login; } AppMsg::Logout => { let mut account = settings::get_current_account(); account.jwt = None; settings::update_current_account(account); self.logged_in = false; } AppMsg::ShowMessage(message) => { self.message = Some(message); self.state = AppState::Message; } AppMsg::OpenInbox => { self.state = AppState::Inbox; self.inbox_page.sender().emit(InboxInput::FetchInbox); } AppMsg::LoggedIn => { self.logged_in = true; self.back_queue.clear(); sender.input(AppMsg::StartFetchPosts(None, true)); } AppMsg::PopBackStack => { let action = self.back_queue.get(self.back_queue.len() - 2); if let Some(action) = action { sender.input(action.clone()); } for _ in 0..2 { self.back_queue.remove(self.back_queue.len() - 1); } } AppMsg::UpdateState(state) => { self.state = state; } } } } relm4::new_action_group!(WindowActionGroup, "win"); relm4::new_stateless_action!(ChangeInstanceAction, WindowActionGroup, "instance"); relm4::new_stateless_action!(ProfileAction, WindowActionGroup, "profile"); relm4::new_stateless_action!(LoginAction, WindowActionGroup, "login"); relm4::new_stateless_action!(LogoutAction, WindowActionGroup, "logout"); relm4::new_stateless_action!(AboutAction, WindowActionGroup, "about"); fn main() { let app = RelmApp::new(config::APP_ID); set_global_css(include_str!("style.css")); app.run::(()); }