Initial commit
This commit is contained in:
commit
6b8eae051e
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "lemmy-gtk"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
relm4 = "0.6.0"
|
||||||
|
relm4-components = { version = "0.6.0", features = ["web"] }
|
||||||
|
reqwest = { version = "0.11.16", features = ["json", "blocking"] }
|
||||||
|
serde = { version = "1.0.160", features = ["derive"] }
|
||||||
|
serde_json = "1.0.96"
|
||||||
|
lemmy_api_common = { git = "https://github.com/LemmyNet/lemmy.git", tag = "0.17.2" }
|
|
@ -0,0 +1,20 @@
|
||||||
|
use lemmy_api_common::{community::{ListCommunities, ListCommunitiesResponse}, lemmy_db_schema::{SortType, SearchType}, lemmy_db_views_actor::structs::CommunityView};
|
||||||
|
|
||||||
|
use crate::components::CLIENT;
|
||||||
|
|
||||||
|
use super::search;
|
||||||
|
|
||||||
|
pub fn fetch_communities(page: i64, query: Option<String>) -> std::result::Result<Vec<CommunityView>, reqwest::Error> {
|
||||||
|
if query.is_none() || query.clone().unwrap().trim().is_empty() {
|
||||||
|
let params = ListCommunities {
|
||||||
|
sort: Some(SortType::TopMonth),
|
||||||
|
page: Some(page),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/community/list", super::get_api_url());
|
||||||
|
Ok(CLIENT.get(&url).query(¶ms).send()?.json::<ListCommunitiesResponse>()?.communities)
|
||||||
|
} else {
|
||||||
|
Ok(search::fetch_search(page, query.unwrap(), Some(SearchType::Communities))?.communities)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
use lemmy_api_common::community::{GetCommunity, GetCommunityResponse};
|
||||||
|
|
||||||
|
use crate::components::CLIENT;
|
||||||
|
|
||||||
|
pub fn get_community(name: String) -> std::result::Result<GetCommunityResponse, reqwest::Error> {
|
||||||
|
let params = GetCommunity {
|
||||||
|
name: Some(name),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/community", super::get_api_url());
|
||||||
|
CLIENT.get(&url).query(¶ms).send()?.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_community() -> GetCommunityResponse {
|
||||||
|
serde_json::from_str(include_str!("../examples/community.json")).unwrap()
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::settings::get_prefs;
|
||||||
|
|
||||||
|
pub mod communities;
|
||||||
|
pub mod community;
|
||||||
|
pub mod post;
|
||||||
|
pub mod posts;
|
||||||
|
pub mod search;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
|
static API_VERSION: &str = "v3";
|
||||||
|
|
||||||
|
pub fn get_api_url() -> String {
|
||||||
|
format!("{}/api/{}", get_prefs().instance_url, API_VERSION).to_string()
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
use lemmy_api_common::{post::{GetPost, GetPostResponse}, lemmy_db_schema::{newtypes::PostId, CommentSortType, ListingType}, comment::{GetComments, GetCommentsResponse}, lemmy_db_views::structs::CommentView};
|
||||||
|
|
||||||
|
use crate::components::CLIENT;
|
||||||
|
|
||||||
|
pub fn get_post(id: PostId) -> std::result::Result<GetPostResponse, reqwest::Error> {
|
||||||
|
let params = GetPost {
|
||||||
|
id: Some(id),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/post", super::get_api_url());
|
||||||
|
CLIENT.get(&url).query(¶ms).send()?.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_comments(post_id: PostId) -> std::result::Result<Vec<CommentView>, reqwest::Error> {
|
||||||
|
let params = GetComments {
|
||||||
|
post_id: Some(post_id),
|
||||||
|
sort: Some(CommentSortType::Hot),
|
||||||
|
type_: Some(ListingType::All),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/comment/list", super::get_api_url());
|
||||||
|
let mut comments = CLIENT.get(&url).query(¶ms).send()?.json::<GetCommentsResponse>()?.comments;
|
||||||
|
|
||||||
|
// hide removed comments
|
||||||
|
comments.retain(|c| !c.comment.deleted && !c.comment.removed);
|
||||||
|
|
||||||
|
Ok(comments)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_post() -> GetPostResponse {
|
||||||
|
serde_json::from_str(include_str!("../examples/post.json")).unwrap()
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use lemmy_api_common::{post::{GetPostsResponse, GetPosts}, lemmy_db_views::structs::PostView};
|
||||||
|
|
||||||
|
use crate::components::CLIENT;
|
||||||
|
|
||||||
|
pub fn list_posts(page: i64, community_name: Option<String>) -> std::result::Result<Vec<PostView>, reqwest::Error> {
|
||||||
|
let params = GetPosts {
|
||||||
|
page: Some(page),
|
||||||
|
community_name,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/post/list", super::get_api_url());
|
||||||
|
Ok(CLIENT.get(&url).query(¶ms).send()?.json::<GetPostsResponse>()?.posts)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
use lemmy_api_common::{site::{SearchResponse, Search}, lemmy_db_schema::{SortType, SearchType}};
|
||||||
|
|
||||||
|
use crate::components::CLIENT;
|
||||||
|
|
||||||
|
pub fn fetch_search(page: i64, query: String, search_type: Option<SearchType>) -> std::result::Result<SearchResponse, reqwest::Error> {
|
||||||
|
let params = Search {
|
||||||
|
q: query,
|
||||||
|
sort: Some(SortType::TopMonth),
|
||||||
|
page: Some(page),
|
||||||
|
type_: search_type,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/search", super::get_api_url());
|
||||||
|
CLIENT.get(&url).query(¶ms).send()?.json()
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
use lemmy_api_common::{person::{GetPersonDetailsResponse, GetPersonDetails}};
|
||||||
|
use crate::components::CLIENT;
|
||||||
|
|
||||||
|
pub fn get_user(username: String, page: i64) -> std::result::Result<GetPersonDetailsResponse, reqwest::Error> {
|
||||||
|
let params = GetPersonDetails {
|
||||||
|
page: Some(page),
|
||||||
|
username: Some(username),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}/user", super::get_api_url());
|
||||||
|
CLIENT.get(&url).query(¶ms).send()?.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_person() -> GetPersonDetailsResponse {
|
||||||
|
serde_json::from_str(include_str!("../examples/person.json")).unwrap()
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
use lemmy_api_common::lemmy_db_views::structs::CommentView;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4_components::web_image::WebImage;
|
||||||
|
|
||||||
|
use crate::util::get_web_image_url;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommentRow {
|
||||||
|
comment: CommentView,
|
||||||
|
avatar: Controller<WebImage>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CommentRowMsg {
|
||||||
|
OpenPerson,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::factory(pub)]
|
||||||
|
impl FactoryComponent for CommentRow {
|
||||||
|
type Init = CommentView;
|
||||||
|
type Input = CommentRowMsg;
|
||||||
|
type Output = crate::AppMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
type Widgets = PostViewWidgets;
|
||||||
|
type ParentInput = crate::AppMsg;
|
||||||
|
type ParentWidget = gtk::Box;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
root = gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_spacing: 10,
|
||||||
|
set_margin_end: 10,
|
||||||
|
set_margin_start: 10,
|
||||||
|
set_margin_top: 10,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_spacing: 10,
|
||||||
|
set_vexpand: false,
|
||||||
|
|
||||||
|
if self.comment.creator.avatar.is_some() {
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: false,
|
||||||
|
#[local_ref]
|
||||||
|
community_image -> gtk::Box {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gtk::Box {}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: &self.comment.creator.name,
|
||||||
|
connect_clicked => CommentRowMsg::OpenPerson,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_label: &self.comment.comment.content,
|
||||||
|
set_halign: gtk::Align::Start,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_label: &format!("{} score", self.comment.counts.score),
|
||||||
|
set_halign: gtk::Align::Start,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Separator {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
|
||||||
|
Some(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
|
||||||
|
let avatar = WebImage::builder().launch(get_web_image_url(value.community.clone().icon)).detach();
|
||||||
|
|
||||||
|
Self { comment: value, avatar }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_widgets(
|
||||||
|
&mut self,
|
||||||
|
_index: &Self::Index,
|
||||||
|
root: &Self::Root,
|
||||||
|
_returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
|
||||||
|
sender: FactorySender<Self>,
|
||||||
|
) -> Self::Widgets {
|
||||||
|
let community_image = self.avatar.widget();
|
||||||
|
let widgets = view_output!();
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
||||||
|
match message {
|
||||||
|
CommentRowMsg::OpenPerson => {
|
||||||
|
sender.output(crate::AppMsg::OpenPerson(self.comment.creator.name.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
use lemmy_api_common::{community::GetCommunityResponse, lemmy_db_views::structs::PostView};
|
||||||
|
use relm4::{prelude::*, factory::FactoryVecDeque};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4_components::web_image::WebImage;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use crate::{api, util::get_web_image_msg};
|
||||||
|
|
||||||
|
use super::post_row::PostRow;
|
||||||
|
|
||||||
|
pub struct CommunityPage {
|
||||||
|
info: RefCell<GetCommunityResponse>,
|
||||||
|
avatar: Controller<WebImage>,
|
||||||
|
posts: FactoryVecDeque<PostRow>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CommunityInput {
|
||||||
|
UpdateCommunity(GetCommunityResponse),
|
||||||
|
DoneFetchPosts(Vec<PostView>)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub)]
|
||||||
|
impl SimpleComponent for CommunityPage {
|
||||||
|
type Init = GetCommunityResponse;
|
||||||
|
type Input = CommunityInput;
|
||||||
|
type Output = crate::AppMsg;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
gtk::ScrolledWindow {
|
||||||
|
set_vexpand: false,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_vexpand: false,
|
||||||
|
set_margin_all: 10,
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
avatar -> gtk::Box {
|
||||||
|
set_size_request: (100, 100),
|
||||||
|
set_margin_bottom: 20,
|
||||||
|
set_margin_top: 20,
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &model.info.borrow().community_view.community.name,
|
||||||
|
add_css_class: "font-very-bold",
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &model.info.borrow().clone().community_view.community.description.unwrap_or("".to_string()),
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} subscribers, ", model.info.borrow().community_view.counts.subscribers),
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_top: 10,
|
||||||
|
set_margin_bottom: 10,
|
||||||
|
set_hexpand: false,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} posts, ", model.info.borrow().community_view.counts.posts),
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} comments", model.info.borrow().clone().community_view.counts.comments),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Separator {},
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
posts -> gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: &Self::Root,
|
||||||
|
sender: relm4::ComponentSender<Self>,
|
||||||
|
) -> relm4::ComponentParts<Self> {
|
||||||
|
let avatar = WebImage::builder().launch("".to_string()).detach();
|
||||||
|
let posts = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender());
|
||||||
|
let model = CommunityPage { info: RefCell::new(init), avatar, posts };
|
||||||
|
let avatar = model.avatar.widget();
|
||||||
|
let posts = model.posts.widget();
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
ComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||||
|
match message {
|
||||||
|
CommunityInput::UpdateCommunity(community) => {
|
||||||
|
*self.info.borrow_mut() = community.clone();
|
||||||
|
self.avatar.emit(get_web_image_msg(community.community_view.community.icon));
|
||||||
|
self.posts.guard().clear();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if community.community_view.counts.posts == 0 { return; }
|
||||||
|
let community_posts = api::posts::list_posts(1, Some(community.community_view.community.name));
|
||||||
|
if let Ok(community_posts) = community_posts {
|
||||||
|
sender.input(CommunityInput::DoneFetchPosts(community_posts));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CommunityInput::DoneFetchPosts(posts) => {
|
||||||
|
for post in posts {
|
||||||
|
self.posts.guard().push_back(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
use lemmy_api_common::lemmy_db_views_actor::structs::CommunityView;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4_components::web_image::WebImage;
|
||||||
|
|
||||||
|
use crate::util::get_web_image_url;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommunityRow {
|
||||||
|
community: CommunityView,
|
||||||
|
community_image: Controller<WebImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CommunityRowMsg {
|
||||||
|
OpenCommunity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::factory(pub)]
|
||||||
|
impl FactoryComponent for CommunityRow {
|
||||||
|
type Init = CommunityView;
|
||||||
|
type Input = CommunityRowMsg;
|
||||||
|
type Output = crate::AppMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
type Widgets = PostViewWidgets;
|
||||||
|
type ParentInput = crate::AppMsg;
|
||||||
|
type ParentWidget = gtk::Box;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
root = gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_spacing: 10,
|
||||||
|
set_margin_end: 10,
|
||||||
|
set_margin_start: 10,
|
||||||
|
set_vexpand: false,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_spacing: 10,
|
||||||
|
|
||||||
|
if self.community.community.icon.is_some() {
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: false,
|
||||||
|
#[local_ref]
|
||||||
|
community_image -> gtk::Box {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gtk::Box {}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_label: &self.community.community.title,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_label: &format!("{} subscribers, {} posts", self.community.counts.subscribers, self.community.counts.posts),
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: "View",
|
||||||
|
connect_clicked => CommunityRowMsg::OpenCommunity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Separator {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> {
|
||||||
|
Some(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
|
||||||
|
let community_image= WebImage::builder().launch(get_web_image_url(value.community.clone().icon)).detach();
|
||||||
|
|
||||||
|
Self { community: value, community_image }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_widgets(
|
||||||
|
&mut self,
|
||||||
|
_index: &Self::Index,
|
||||||
|
root: &Self::Root,
|
||||||
|
_returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
|
||||||
|
sender: FactorySender<Self>,
|
||||||
|
) -> Self::Widgets {
|
||||||
|
let community_image = self.community_image.widget();
|
||||||
|
let widgets = view_output!();
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
||||||
|
match message {
|
||||||
|
CommunityRowMsg::OpenCommunity => {
|
||||||
|
sender.output(crate::AppMsg::OpenCommunity(self.community.community.name.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
pub mod post_row;
|
||||||
|
pub mod community_row;
|
||||||
|
pub mod profile_page;
|
||||||
|
pub mod community_page;
|
||||||
|
pub mod post_page;
|
||||||
|
pub mod comment_row;
|
||||||
|
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use relm4::once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
pub static CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||||
|
Client::new()
|
||||||
|
});
|
|
@ -0,0 +1,204 @@
|
||||||
|
use lemmy_api_common::{lemmy_db_views::structs::{CommentView}, post::GetPostResponse};
|
||||||
|
use relm4::{prelude::*, factory::FactoryVecDeque};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4_components::web_image::WebImage;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use crate::{api, util::{get_web_image_msg, get_web_image_url}};
|
||||||
|
|
||||||
|
use super::comment_row::CommentRow;
|
||||||
|
|
||||||
|
pub struct PostPage {
|
||||||
|
info: RefCell<GetPostResponse>,
|
||||||
|
image: Controller<WebImage>,
|
||||||
|
creator_avatar: Controller<WebImage>,
|
||||||
|
community_avatar: Controller<WebImage>,
|
||||||
|
comments: FactoryVecDeque<CommentRow>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PostInput {
|
||||||
|
UpdatePost(GetPostResponse),
|
||||||
|
DoneFetchComments(Vec<CommentView>),
|
||||||
|
OpenPerson,
|
||||||
|
OpenCommunity,
|
||||||
|
OpenLink
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub)]
|
||||||
|
impl SimpleComponent for PostPage {
|
||||||
|
type Init = GetPostResponse;
|
||||||
|
type Input = PostInput;
|
||||||
|
type Output = crate::AppMsg;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
gtk::ScrolledWindow {
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_vexpand: false,
|
||||||
|
set_margin_all: 10,
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
image -> gtk::Box {
|
||||||
|
set_height_request: 100,
|
||||||
|
set_margin_bottom: 20,
|
||||||
|
set_margin_top: 20,
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &model.info.borrow().post_view.post.name,
|
||||||
|
add_css_class: "font-very-bold",
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &model.info.borrow().clone().post_view.post.body.unwrap_or("".to_string()),
|
||||||
|
set_margin_top: 10,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_top: 10,
|
||||||
|
set_spacing: 10,
|
||||||
|
set_vexpand: false,
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_text: "posted by "
|
||||||
|
},
|
||||||
|
|
||||||
|
if model.info.borrow().post_view.creator.avatar.is_some() {
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: false,
|
||||||
|
set_margin_start: 10,
|
||||||
|
#[local_ref]
|
||||||
|
creator_avatar -> gtk::Box {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gtk::Box {}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: &model.info.borrow().post_view.creator.name,
|
||||||
|
connect_clicked => PostInput::OpenPerson,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_text: " in "
|
||||||
|
},
|
||||||
|
|
||||||
|
if model.info.borrow().community_view.community.icon.is_some() {
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: false,
|
||||||
|
#[local_ref]
|
||||||
|
community_avatar -> gtk::Box {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gtk::Box {}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: &model.info.borrow().community_view.community.title,
|
||||||
|
connect_clicked => PostInput::OpenCommunity,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: "View",
|
||||||
|
connect_clicked => PostInput::OpenLink,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_top: 10,
|
||||||
|
set_margin_bottom: 10,
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} comments, ", model.info.borrow().post_view.counts.comments),
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} score", model.info.borrow().post_view.counts.score),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Separator {},
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
comments -> gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: &Self::Root,
|
||||||
|
sender: relm4::ComponentSender<Self>,
|
||||||
|
) -> relm4::ComponentParts<Self> {
|
||||||
|
let image = WebImage::builder().launch("".to_string()).detach();
|
||||||
|
let comments = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender());
|
||||||
|
let creator_avatar = WebImage::builder().launch("".to_string()).detach();
|
||||||
|
let community_avatar = WebImage::builder().launch("".to_string()).detach();
|
||||||
|
let model = PostPage { info: RefCell::new(init), image, comments, creator_avatar, community_avatar };
|
||||||
|
|
||||||
|
let image = model.image.widget();
|
||||||
|
let comments = model.comments.widget();
|
||||||
|
let creator_avatar = model.creator_avatar.widget();
|
||||||
|
let community_avatar = model.community_avatar.widget();
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
ComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||||
|
match message {
|
||||||
|
PostInput::UpdatePost(post) => {
|
||||||
|
*self.info.borrow_mut() = post.clone();
|
||||||
|
|
||||||
|
self.image.emit(get_web_image_msg(post.post_view.post.thumbnail_url));
|
||||||
|
self.community_avatar.emit(get_web_image_msg(post.community_view.community.icon));
|
||||||
|
self.creator_avatar.emit(get_web_image_msg(post.post_view.creator.avatar));
|
||||||
|
|
||||||
|
self.comments.guard().clear();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if post.post_view.counts.comments == 0 { return; }
|
||||||
|
let comments = api::post::get_comments(post.post_view.post.id);
|
||||||
|
if let Ok(comments) = comments {
|
||||||
|
sender.input(PostInput::DoneFetchComments(comments));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PostInput::DoneFetchComments(comments) => {
|
||||||
|
for comment in comments {
|
||||||
|
self.comments.guard().push_back(comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PostInput::OpenPerson => {
|
||||||
|
let name = self.info.borrow().post_view.creator.name.clone();
|
||||||
|
let _ = sender.output(crate::AppMsg::OpenPerson(name));
|
||||||
|
}
|
||||||
|
PostInput::OpenCommunity => {
|
||||||
|
let community_name = self.info.borrow().community_view.community.name.clone();
|
||||||
|
let _ = sender.output(crate::AppMsg::OpenCommunity(community_name));
|
||||||
|
}
|
||||||
|
PostInput::OpenLink => {
|
||||||
|
let post = self.info.borrow().post_view.post.clone();
|
||||||
|
let mut link = get_web_image_url(post.url);
|
||||||
|
if link.is_empty() {
|
||||||
|
link = get_web_image_url(post.thumbnail_url);
|
||||||
|
}
|
||||||
|
if link.is_empty() {
|
||||||
|
link = get_web_image_url(post.embed_video_url);
|
||||||
|
}
|
||||||
|
if link.is_empty() { return; }
|
||||||
|
gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
use lemmy_api_common::lemmy_db_views::structs::PostView;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4_components::web_image::WebImage;
|
||||||
|
|
||||||
|
use crate::util::get_web_image_url;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PostRow {
|
||||||
|
post: PostView,
|
||||||
|
author_image: Controller<WebImage>,
|
||||||
|
community_image: Controller<WebImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PostViewMsg {
|
||||||
|
OpenPost,
|
||||||
|
OpenCommunity,
|
||||||
|
OpenPerson
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::factory(pub)]
|
||||||
|
impl FactoryComponent for PostRow {
|
||||||
|
type Init = PostView;
|
||||||
|
type Input = PostViewMsg;
|
||||||
|
type Output = crate::AppMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
type Widgets = PostViewWidgets;
|
||||||
|
type ParentInput = crate::AppMsg;
|
||||||
|
type ParentWidget = gtk::Box;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
root = gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_spacing: 10,
|
||||||
|
set_margin_end: 10,
|
||||||
|
set_margin_start: 10,
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_top: 10,
|
||||||
|
set_spacing: 10,
|
||||||
|
set_vexpand: false,
|
||||||
|
set_hexpand: true,
|
||||||
|
|
||||||
|
if self.post.community.icon.clone().is_some() {
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: false,
|
||||||
|
#[local_ref]
|
||||||
|
community_image -> gtk::Box {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gtk::Box {}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: &self.post.community.title,
|
||||||
|
connect_clicked => PostViewMsg::OpenCommunity,
|
||||||
|
},
|
||||||
|
|
||||||
|
if self.post.creator.avatar.clone().is_some() {
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: false,
|
||||||
|
set_margin_start: 10,
|
||||||
|
#[local_ref]
|
||||||
|
author_image -> gtk::Box {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gtk::Box {}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: &self.post.creator.name,
|
||||||
|
connect_clicked => PostViewMsg::OpenPerson,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_hexpand: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Button {
|
||||||
|
set_label: "View",
|
||||||
|
set_margin_end: 10,
|
||||||
|
connect_clicked => PostViewMsg::OpenPost,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_halign: gtk::Align::Start,
|
||||||
|
set_text: &self.post.post.name,
|
||||||
|
add_css_class: "font-bold",
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
set_halign: gtk::Align::Start,
|
||||||
|
set_text: &format!("{} score, {} comments", self.post.counts.score, self.post.clone().counts.comments),
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Separator {
|
||||||
|
set_margin_top: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> { Some(output) }
|
||||||
|
|
||||||
|
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
|
||||||
|
let author_image= WebImage::builder().launch(get_web_image_url(value.creator.clone().avatar)).detach();
|
||||||
|
let community_image= WebImage::builder().launch(get_web_image_url(value.creator.clone().avatar)).detach();
|
||||||
|
|
||||||
|
Self { post: value, author_image, community_image }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_widgets(
|
||||||
|
&mut self,
|
||||||
|
_index: &Self::Index,
|
||||||
|
root: &Self::Root,
|
||||||
|
_returned_widget: &<Self::ParentWidget as relm4::factory::FactoryView>::ReturnedWidget,
|
||||||
|
sender: FactorySender<Self>,
|
||||||
|
) -> Self::Widgets {
|
||||||
|
let author_image = self.author_image.widget();
|
||||||
|
let community_image = self.community_image.widget();
|
||||||
|
let widgets = view_output!();
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
||||||
|
match message {
|
||||||
|
PostViewMsg::OpenCommunity => {
|
||||||
|
sender.output(crate::AppMsg::OpenCommunity(self.post.community.name.clone()))
|
||||||
|
}
|
||||||
|
PostViewMsg::OpenPerson => {
|
||||||
|
sender.output(crate::AppMsg::OpenPerson(self.post.creator.name.clone()))
|
||||||
|
}
|
||||||
|
PostViewMsg::OpenPost => {
|
||||||
|
sender.output(crate::AppMsg::OpenPost(self.post.post.id.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
use lemmy_api_common::person::GetPersonDetailsResponse;
|
||||||
|
use relm4::{prelude::*, factory::FactoryVecDeque};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use relm4_components::web_image::WebImage;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use crate::util::get_web_image_msg;
|
||||||
|
|
||||||
|
use super::post_row::PostRow;
|
||||||
|
|
||||||
|
pub struct ProfilePage {
|
||||||
|
info: RefCell<GetPersonDetailsResponse>,
|
||||||
|
avatar: Controller<WebImage>,
|
||||||
|
posts: FactoryVecDeque<PostRow>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ProfileInput {
|
||||||
|
UpdatePerson(GetPersonDetailsResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(pub)]
|
||||||
|
impl SimpleComponent for ProfilePage {
|
||||||
|
type Init = GetPersonDetailsResponse;
|
||||||
|
type Input = ProfileInput;
|
||||||
|
type Output = crate::AppMsg;
|
||||||
|
|
||||||
|
view! {
|
||||||
|
gtk::ScrolledWindow {
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_vexpand: false,
|
||||||
|
set_margin_all: 10,
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
avatar -> gtk::Box {
|
||||||
|
set_size_request: (100, 100),
|
||||||
|
set_margin_bottom: 20,
|
||||||
|
set_margin_top: 20,
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &model.info.borrow().person_view.person.name,
|
||||||
|
add_css_class: "font-very-bold",
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &model.info.borrow().clone().person_view.person.bio.unwrap_or("".to_string()),
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
|
set_margin_top: 10,
|
||||||
|
set_margin_bottom: 10,
|
||||||
|
set_hexpand: false,
|
||||||
|
set_halign: gtk::Align::Center,
|
||||||
|
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} posts, ", model.info.borrow().person_view.counts.post_count),
|
||||||
|
},
|
||||||
|
gtk::Label {
|
||||||
|
#[watch]
|
||||||
|
set_text: &format!("{} comments", model.info.borrow().person_view.counts.comment_count),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
gtk::Separator {},
|
||||||
|
|
||||||
|
#[local_ref]
|
||||||
|
posts -> gtk::Box {
|
||||||
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: &Self::Root,
|
||||||
|
sender: relm4::ComponentSender<Self>,
|
||||||
|
) -> relm4::ComponentParts<Self> {
|
||||||
|
let avatar = WebImage::builder().launch("".to_string()).detach();
|
||||||
|
let posts = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender());
|
||||||
|
let model = ProfilePage { info: RefCell::new(init), avatar, posts };
|
||||||
|
let avatar = model.avatar.widget();
|
||||||
|
let posts = model.posts.widget();
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
ComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
|
||||||
|
match message {
|
||||||
|
ProfileInput::UpdatePerson(person) => {
|
||||||
|
*self.info.borrow_mut() = person.clone();
|
||||||
|
self.avatar.emit(get_web_image_msg(person.person_view.person.avatar));
|
||||||
|
self.posts.guard().clear();
|
||||||
|
for post in person.posts {
|
||||||
|
self.posts.guard().push_back(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,311 @@
|
||||||
|
pub mod settings;
|
||||||
|
pub mod api;
|
||||||
|
pub mod components;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
|
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, post::GetPostResponse, community::GetCommunityResponse};
|
||||||
|
use relm4::{prelude::*, factory::FactoryVecDeque, set_global_css};
|
||||||
|
|
||||||
|
static APP_ID: &str = "com.lemmy-gtk.lemoa";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum AppState {
|
||||||
|
Loading,
|
||||||
|
Posts,
|
||||||
|
ChooseInstance,
|
||||||
|
Communities,
|
||||||
|
Community,
|
||||||
|
Person,
|
||||||
|
Post
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
state: AppState,
|
||||||
|
posts: FactoryVecDeque<PostRow>,
|
||||||
|
communities: FactoryVecDeque<CommunityRow>,
|
||||||
|
profile_page: Controller<ProfilePage>,
|
||||||
|
community_page: Controller<CommunityPage>,
|
||||||
|
post_page: Controller<PostPage>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppMsg {
|
||||||
|
ChooseInstance,
|
||||||
|
DoneChoosingInstance(String),
|
||||||
|
StartFetchPosts,
|
||||||
|
DoneFetchPosts(Result<Vec<PostView>, reqwest::Error>),
|
||||||
|
DoneFetchCommunities(Result<Vec<CommunityView>, reqwest::Error>),
|
||||||
|
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! {
|
||||||
|
gtk::Window {
|
||||||
|
set_title: Some("Lemoa"),
|
||||||
|
set_default_size: (300, 100),
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_titlebar = >k::HeaderBar {
|
||||||
|
pack_end = match model.state {
|
||||||
|
AppState::ChooseInstance => {
|
||||||
|
>k::Box {}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
>k::Button {
|
||||||
|
set_label: "Reset",
|
||||||
|
connect_clicked => AppMsg::ChooseInstance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pack_start = >k::Button {
|
||||||
|
set_label: "Posts",
|
||||||
|
connect_clicked => AppMsg::StartFetchPosts,
|
||||||
|
},
|
||||||
|
pack_start = >k::Button {
|
||||||
|
set_label: "Communities",
|
||||||
|
connect_clicked => AppMsg::ViewCommunities(None),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
#[name(stack)]
|
||||||
|
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.buffer().text().as_str().to_string();
|
||||||
|
instance_url.buffer().set_text("");
|
||||||
|
sender.input(AppMsg::DoneChoosingInstance(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.buffer().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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
|
||||||
|
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()).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 };
|
||||||
|
|
||||||
|
if !instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts) };
|
||||||
|
|
||||||
|
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!();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
AppMsg::ChooseInstance => {
|
||||||
|
self.state = AppState::ChooseInstance;
|
||||||
|
}
|
||||||
|
AppMsg::StartFetchPosts => {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let posts = api::posts::list_posts(1, None);
|
||||||
|
sender.input(AppMsg::DoneFetchPosts(posts));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
AppMsg::DoneFetchPosts(posts) => {
|
||||||
|
self.state = AppState::Posts;
|
||||||
|
if let Ok(posts) = 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 communities = api::communities::fetch_communities(1, query);
|
||||||
|
sender.input(AppMsg::DoneFetchCommunities(communities));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
AppMsg::DoneFetchCommunities(communities) => {
|
||||||
|
self.state = AppState::Communities;
|
||||||
|
if let Ok(communities) = 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 person = api::user::get_user(person_name, 1);
|
||||||
|
if let Ok(person) = person {
|
||||||
|
sender.input(AppMsg::DoneFetchPerson(person));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 community = api::community::get_community(community_name);
|
||||||
|
if let Ok(community) = community {
|
||||||
|
sender.input(AppMsg::DoneFetchCommunity(community));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
AppMsg::DoneFetchCommunity(community) => {
|
||||||
|
self.community_page.sender().emit(community_page::CommunityInput::UpdateCommunity(community));
|
||||||
|
self.state = AppState::Community;
|
||||||
|
}
|
||||||
|
AppMsg::OpenPost(post_id) => {
|
||||||
|
self.state = AppState::Loading;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let post = api::post::get_post(post_id);
|
||||||
|
if let Ok(post) = post {
|
||||||
|
sender.input(AppMsg::DoneFetchPost(post));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
AppMsg::DoneFetchPost(post) => {
|
||||||
|
self.post_page.sender().emit(post_page::PostInput::UpdatePost(post));
|
||||||
|
self.state = AppState::Post;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let app = RelmApp::new(APP_ID);
|
||||||
|
set_global_css(include_str!("style.css"));
|
||||||
|
app.run::<App>(());
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
use std::{fs::File, path::PathBuf};
|
||||||
|
use crate::gtk::glib;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::APP_ID;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Default)]
|
||||||
|
pub struct Preferences {
|
||||||
|
pub instance_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_path() -> PathBuf {
|
||||||
|
let mut path = glib::user_data_dir();
|
||||||
|
path.push(APP_ID);
|
||||||
|
std::fs::create_dir_all(&path).expect("Could not create directory.");
|
||||||
|
path.push("data.json");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_prefs(prefs: &Preferences) {
|
||||||
|
let file = File::create(data_path()).expect("Could not create json file.");
|
||||||
|
serde_json::to_writer(file, &prefs).expect("Could not write data to json file");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_prefs() -> Preferences {
|
||||||
|
if let Ok(file) = File::open(data_path()) {
|
||||||
|
// Deserialize data from file to vector
|
||||||
|
let prefs: Result<Preferences, serde_json::Error> = serde_json::from_reader(file);
|
||||||
|
if prefs.is_ok() {
|
||||||
|
return prefs.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Preferences::default();
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
.font-bold {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-very-bold {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
use lemmy_api_common::lemmy_db_schema::newtypes::DbUrl;
|
||||||
|
use relm4_components::web_image::WebImageMsg;
|
||||||
|
|
||||||
|
pub fn get_web_image_msg(url: Option<DbUrl>) -> WebImageMsg {
|
||||||
|
return if let Some(url) = url {
|
||||||
|
WebImageMsg::LoadImage(url.to_string())
|
||||||
|
} else { WebImageMsg::Unload };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_web_image_url(url: Option<DbUrl>) -> String {
|
||||||
|
return if let Some(url) = url {
|
||||||
|
url.to_string()
|
||||||
|
} else { String::from("") }
|
||||||
|
}
|
Loading…
Reference in New Issue