Initial commit

This commit is contained in:
Bnyro
2023-06-18 16:53:53 +02:00
commit 6b8eae051e
25 changed files with 3959 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
[build]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
+1
View File
@@ -0,0 +1 @@
/target
Generated
+2655
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -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" }
+20
View File
@@ -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(&params).send()?.json::<ListCommunitiesResponse>()?.communities)
} else {
Ok(search::fetch_search(page, query.unwrap(), Some(SearchType::Communities))?.communities)
}
}
+17
View File
@@ -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(&params).send()?.json()
}
pub fn default_community() -> GetCommunityResponse {
serde_json::from_str(include_str!("../examples/community.json")).unwrap()
}
+14
View File
@@ -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()
}
+34
View File
@@ -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(&params).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(&params).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()
}
+14
View File
@@ -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(&params).send()?.json::<GetPostsResponse>()?.posts)
}
+16
View File
@@ -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(&params).send()?.json()
}
+17
View File
@@ -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(&params).send()?.json()
}
pub fn default_person() -> GetPersonDetailsResponse {
serde_json::from_str(include_str!("../examples/person.json")).unwrap()
}
+101
View File
@@ -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()))
}
}
}
}
+123
View File
@@ -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);
}
}
}
}
}
+102
View File
@@ -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()))
}
}
}
}
+13
View File
@@ -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()
});
+204
View File
@@ -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);
}
}
}
}
+140
View File
@@ -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()))
}
}
}
}
+106
View File
@@ -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
+311
View File
@@ -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 = &gtk::HeaderBar {
pack_end = match model.state {
AppState::ChooseInstance => {
&gtk::Box {}
}
_ => {
&gtk::Button {
set_label: "Reset",
connect_clicked => AppMsg::ChooseInstance,
}
}
},
pack_start = &gtk::Button {
set_label: "Posts",
connect_clicked => AppMsg::StartFetchPosts,
},
pack_start = &gtk::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>(());
}
+33
View File
@@ -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();
}
+7
View File
@@ -0,0 +1,7 @@
.font-bold {
font-size: 1.2rem;
}
.font-very-bold {
font-size: 2rem;
}
+14
View File
@@ -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("") }
}