Reformat project and change package id

This commit is contained in:
Bnyro 2023-06-26 10:58:21 +02:00
parent d71bbcd3f3
commit fbf4b8dfa5
31 changed files with 644 additions and 284 deletions

6
.gitignore vendored
View File

@ -1,3 +1,7 @@
/target
/.cargo
/_build
/_build
/builddir
/.flatpak
/.flatpak-builder
/.vscode

15
.vscode/settings.json vendored
View File

@ -1,3 +1,16 @@
{
"C_Cpp.default.compileCommands": "builddir/vscode_compile_commands.json"
"C_Cpp.default.compileCommands": "builddir/vscode_compile_commands.json",
"files.watcherExclude": {
"**/.dart_tool": true,
".flatpak/**": true,
"_build/**": true
},
"mesonbuild.configureOnOpen": false,
"mesonbuild.buildFolder": "_build",
"mesonbuild.mesonPath": "${workspaceFolder}/.flatpak/meson.sh",
"rust-analyzer.server.path": "${workspaceFolder}/.flatpak/rust-analyzer.sh",
"rust-analyzer.runnables.command": "${workspaceFolder}/.flatpak/cargo.sh",
"rust-analyzer.files.excludeDirs": [
".flatpak"
]
}

View File

@ -1,5 +1,5 @@
{
"id": "com.lemmy-gtk.lemoa",
"id": "com.lemmygtk.lemoa",
"runtime": "org.gnome.Platform",
"runtime-version": "44",
"sdk": "org.gnome.Sdk",

View File

@ -8,7 +8,7 @@ project(
gnome = import('gnome')
application_id = 'com.lemmy-gtk.lemoa'
application_id = 'com.lemmygtk.lemoa'
dependency('glib-2.0', version: '>= 2.70')
dependency('gio-2.0', version: '>= 2.70')

View File

@ -1,6 +1,10 @@
use lemmy_api_common::{person::Login, sensitive::Sensitive};
pub fn login(username_or_email: String, password: String, totp_token: Option<String>) -> std::result::Result<lemmy_api_common::person::LoginResponse, reqwest::Error> {
pub fn login(
username_or_email: String,
password: String,
totp_token: Option<String>,
) -> std::result::Result<lemmy_api_common::person::LoginResponse, reqwest::Error> {
let params = Login {
username_or_email: Sensitive::new(username_or_email),
password: Sensitive::new(password),

View File

@ -1,4 +1,7 @@
use lemmy_api_common::{comment::{CommentResponse, CreateComment, CreateCommentLike, DeleteComment, EditComment}, lemmy_db_schema::newtypes::{PostId, CommentId}};
use lemmy_api_common::{
comment::{CommentResponse, CreateComment, CreateCommentLike, DeleteComment, EditComment},
lemmy_db_schema::newtypes::{CommentId, PostId},
};
use crate::settings;
@ -27,10 +30,7 @@ pub fn like_comment(comment_id: CommentId, score: i16) -> Result<CommentResponse
super::post("/comment/like", &params)
}
pub fn edit_comment(
body: String,
comment_id: i32
) -> Result<CommentResponse, reqwest::Error> {
pub fn edit_comment(body: String, comment_id: i32) -> Result<CommentResponse, reqwest::Error> {
let params = EditComment {
content: Some(body),
comment_id: CommentId(comment_id),

View File

@ -1,9 +1,17 @@
use lemmy_api_common::{community::{ListCommunities, ListCommunitiesResponse}, lemmy_db_schema::{SortType, SearchType, ListingType}, lemmy_db_views_actor::structs::CommunityView};
use lemmy_api_common::{
community::{ListCommunities, ListCommunitiesResponse},
lemmy_db_schema::{ListingType, SearchType, SortType},
lemmy_db_views_actor::structs::CommunityView,
};
use crate::settings;
use super::search;
use crate::settings;
pub fn fetch_communities(page: i64, query: Option<String>,listing_type: Option<ListingType>) -> std::result::Result<Vec<CommunityView>, reqwest::Error> {
pub fn fetch_communities(
page: i64,
query: Option<String>,
listing_type: Option<ListingType>,
) -> std::result::Result<Vec<CommunityView>, reqwest::Error> {
if query.is_none() || query.clone().unwrap().trim().is_empty() {
let params = ListCommunities {
type_: listing_type,

View File

@ -1,4 +1,7 @@
use lemmy_api_common::{community::{GetCommunity, GetCommunityResponse, CommunityResponse, FollowCommunity}, lemmy_db_schema::newtypes::CommunityId};
use lemmy_api_common::{
community::{CommunityResponse, FollowCommunity, GetCommunity, GetCommunityResponse},
lemmy_db_schema::newtypes::CommunityId,
};
use crate::settings;

View File

@ -1,9 +1,9 @@
use reqwest::blocking::multipart::Part;
use std::io::Read;
use std::fs::File;
use crate::settings;
use serde::Deserialize;
use rand::distributions::{Alphanumeric, DistString};
use reqwest::blocking::multipart::Part;
use serde::Deserialize;
use std::fs::File;
use std::io::Read;
use super::CLIENT;
@ -21,9 +21,7 @@ struct UploadImageFile {
pub delete_token: String,
}
pub fn upload_image(
image: std::path::PathBuf,
) -> Result<String, reqwest::Error> {
pub fn upload_image(image: std::path::PathBuf) -> Result<String, reqwest::Error> {
let mime_type = mime_guess::from_path(image.clone()).first();
let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
@ -31,17 +29,22 @@ pub fn upload_image(
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
let part = Part::bytes(data).file_name(file_name).mime_str(&mime_type.unwrap().essence_str())?;
let part = Part::bytes(data)
.file_name(file_name)
.mime_str(&mime_type.unwrap().essence_str())?;
let form = reqwest::blocking::multipart::Form::new().part("images[]", part);
let account = settings::get_current_account();
let base_url = account.instance_url;
let path = format!("{}/pictrs/image", base_url);
let res: UploadImageResponse = CLIENT
.post(&path)
.header("cookie", format!("jwt={}", account.jwt.unwrap().to_string()))
.header(
"cookie",
format!("jwt={}", account.jwt.unwrap().to_string()),
)
.multipart(form)
.send()?
.json()?;
Ok(format!("{}/pictrs/image/{}", base_url, res.files[0].file))
}
}

View File

@ -2,26 +2,24 @@ use serde::{de::DeserializeOwned, Serialize};
use crate::settings::get_current_account;
pub mod auth;
pub mod comment;
pub mod communities;
pub mod community;
pub mod image;
pub mod moderation;
pub mod post;
pub mod posts;
pub mod search;
pub mod user;
pub mod auth;
pub mod moderation;
pub mod comment;
pub mod site;
pub mod image;
pub mod user;
static API_VERSION: &str = "v3";
use reqwest::blocking::Client;
use relm4::once_cell::sync::Lazy;
use reqwest::blocking::Client;
pub static CLIENT: Lazy<Client> = Lazy::new(|| {
Client::new()
});
pub static CLIENT: Lazy<Client> = Lazy::new(|| Client::new());
fn get_api_url() -> String {
format!("{}/api/{}", get_current_account().instance_url, API_VERSION).to_string()
@ -36,11 +34,7 @@ where
T: DeserializeOwned,
Params: Serialize + std::fmt::Debug,
{
CLIENT
.get(&get_url(path))
.query(&params)
.send()?
.json()
CLIENT.get(&get_url(path)).query(&params).send()?.json()
}
fn post<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error>
@ -48,11 +42,7 @@ where
T: DeserializeOwned,
Params: Serialize + std::fmt::Debug,
{
CLIENT
.post(&get_url(path))
.json(&params)
.send()?
.json()
CLIENT.post(&get_url(path)).json(&params).send()?.json()
}
fn put<T, Params>(path: &str, params: &Params) -> Result<T, reqwest::Error>
@ -60,9 +50,5 @@ where
T: DeserializeOwned,
Params: Serialize + std::fmt::Debug,
{
CLIENT
.put(&get_url(path))
.json(&params)
.send()?
.json()
CLIENT.put(&get_url(path)).json(&params).send()?.json()
}

View File

@ -1,4 +1,9 @@
use lemmy_api_common::{lemmy_db_schema::newtypes::{CommentId, PostId}, comment::{RemoveComment, CommentResponse}, sensitive::Sensitive, post::{PostResponse, RemovePost}};
use lemmy_api_common::{
comment::{CommentResponse, RemoveComment},
lemmy_db_schema::newtypes::{CommentId, PostId},
post::{PostResponse, RemovePost},
sensitive::Sensitive,
};
pub fn remove_post(
post_id: i32,

View File

@ -1,4 +1,14 @@
use lemmy_api_common::{post::{GetPost, GetPostResponse, PostResponse, CreatePost, CreatePostLike, DeletePost, EditPost}, lemmy_db_schema::{newtypes::{PostId, CommunityId}, CommentSortType, ListingType}, comment::{GetComments, GetCommentsResponse}, lemmy_db_views::structs::CommentView};
use lemmy_api_common::{
comment::{GetComments, GetCommentsResponse},
lemmy_db_schema::{
newtypes::{CommunityId, PostId},
CommentSortType, ListingType,
},
lemmy_db_views::structs::CommentView,
post::{
CreatePost, CreatePostLike, DeletePost, EditPost, GetPost, GetPostResponse, PostResponse,
},
};
use crate::settings;
@ -6,7 +16,7 @@ pub fn get_post(id: PostId) -> Result<GetPostResponse, reqwest::Error> {
let params = GetPost {
id: Some(id),
comment_id: None,
auth: settings::get_current_account().jwt
auth: settings::get_current_account().jwt,
};
super::get("/post", &params)
@ -54,7 +64,7 @@ pub fn edit_post(
name: String,
url: Option<reqwest::Url>,
body: String,
post_id: i32
post_id: i32,
) -> Result<PostResponse, reqwest::Error> {
let params = EditPost {
name: Some(name),

View File

@ -1,8 +1,16 @@
use lemmy_api_common::{post::{GetPostsResponse, GetPosts}, lemmy_db_views::structs::PostView, lemmy_db_schema::ListingType};
use lemmy_api_common::{
lemmy_db_schema::ListingType,
lemmy_db_views::structs::PostView,
post::{GetPosts, GetPostsResponse},
};
use crate::settings;
pub fn list_posts(page: i64, community_name: Option<String>, listing_type: Option<ListingType>) -> std::result::Result<Vec<PostView>, reqwest::Error> {
pub fn list_posts(
page: i64,
community_name: Option<String>,
listing_type: Option<ListingType>,
) -> std::result::Result<Vec<PostView>, reqwest::Error> {
let params = GetPosts {
page: Some(page),
type_: listing_type,

View File

@ -1,8 +1,15 @@
use lemmy_api_common::{site::{SearchResponse, Search}, lemmy_db_schema::{SortType, SearchType}};
use lemmy_api_common::{
lemmy_db_schema::{SearchType, SortType},
site::{Search, SearchResponse},
};
use crate::settings;
pub fn fetch_search(page: i64, query: String, search_type: Option<SearchType>) -> std::result::Result<SearchResponse, reqwest::Error> {
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),
@ -13,4 +20,4 @@ pub fn fetch_search(page: i64, query: String, search_type: Option<SearchType>) -
};
super::get("/search", &params)
}
}

View File

@ -1,4 +1,4 @@
use lemmy_api_common::site::{GetSiteResponse, GetSite};
use lemmy_api_common::site::{GetSite, GetSiteResponse};
use crate::settings;

View File

@ -1,8 +1,17 @@
use lemmy_api_common::{person::{GetPersonDetailsResponse, GetPersonDetails, GetPersonMentionsResponse, GetRepliesResponse, MarkAllAsRead, GetReplies, GetPersonMentions}, lemmy_db_schema::{CommentSortType, newtypes::PersonId}};
use lemmy_api_common::{
lemmy_db_schema::{newtypes::PersonId, CommentSortType},
person::{
GetPersonDetails, GetPersonDetailsResponse, GetPersonMentions, GetPersonMentionsResponse,
GetReplies, GetRepliesResponse, MarkAllAsRead,
},
};
use crate::settings;
pub fn get_user(id: PersonId, page: i64) -> std::result::Result<GetPersonDetailsResponse, reqwest::Error> {
pub fn get_user(
id: PersonId,
page: i64,
) -> std::result::Result<GetPersonDetailsResponse, reqwest::Error> {
let params = GetPersonDetails {
page: Some(page),
person_id: Some(id),
@ -17,7 +26,10 @@ pub fn default_person() -> GetPersonDetailsResponse {
serde_json::from_str(include_str!("../examples/person.json")).unwrap()
}
pub fn get_mentions(page: i64, unread_only: bool) -> std::result::Result<GetPersonMentionsResponse, reqwest::Error> {
pub fn get_mentions(
page: i64,
unread_only: bool,
) -> std::result::Result<GetPersonMentionsResponse, reqwest::Error> {
let params = GetPersonMentions {
auth: settings::get_current_account().jwt.unwrap(),
unread_only: Some(unread_only),
@ -28,7 +40,10 @@ pub fn get_mentions(page: i64, unread_only: bool) -> std::result::Result<GetPers
super::get("/user/mention", &params)
}
pub fn get_replies(page: i64, unread_only: bool) -> std::result::Result<GetRepliesResponse, reqwest::Error> {
pub fn get_replies(
page: i64,
unread_only: bool,
) -> std::result::Result<GetRepliesResponse, reqwest::Error> {
let params = GetReplies {
auth: settings::get_current_account().jwt.unwrap(),
page: Some(page),

View File

@ -1,13 +1,13 @@
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views::structs::CommentView;
use relm4::prelude::*;
use gtk::prelude::*;
use relm4_components::web_image::WebImage;
use crate::api;
use crate::dialogs::editor::EditorData;
use crate::settings;
use crate::util::get_web_image_url;
use crate::util::markdown_to_pango_markup;
use crate::settings;
use super::post_page::PostInput;
use super::voting_row::VotingRowModel;
@ -17,14 +17,14 @@ use super::voting_row::VotingStats;
pub struct CommentRow {
pub comment: CommentView,
avatar: Controller<WebImage>,
voting_row: Controller<VotingRowModel>
voting_row: Controller<VotingRowModel>,
}
#[derive(Debug)]
pub enum CommentRowMsg {
OpenPerson,
DeleteComment,
OpenEditCommentDialog
OpenEditCommentDialog,
}
#[relm4::factory(pub)]
@ -65,7 +65,7 @@ impl FactoryComponent for CommentRow {
connect_clicked => CommentRowMsg::OpenPerson,
},
},
gtk::Label {
#[watch]
set_markup: &markdown_to_pango_markup(self.comment.comment.content.clone()),
@ -99,7 +99,7 @@ impl FactoryComponent for CommentRow {
gtk::Box {}
}
},
gtk::Separator {}
}
}
@ -109,19 +109,30 @@ impl FactoryComponent for CommentRow {
}
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
let avatar = WebImage::builder().launch(get_web_image_url(value.creator.avatar.clone())).detach();
let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.counts.clone(), value.my_vote)).detach();
let avatar = WebImage::builder()
.launch(get_web_image_url(value.creator.avatar.clone()))
.detach();
let voting_row = VotingRowModel::builder()
.launch(VotingStats::from_comment(
value.counts.clone(),
value.my_vote,
))
.detach();
Self { comment: value, avatar, voting_row }
Self {
comment: value,
avatar,
voting_row,
}
}
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 {
&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 voting_row = self.voting_row.widget();
let widgets = view_output!();
@ -131,13 +142,17 @@ impl FactoryComponent for CommentRow {
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message {
CommentRowMsg::OpenPerson => {
sender.output(PostInput::PassAppMessage(crate::AppMsg::OpenPerson(self.comment.creator.id.clone())));
sender.output(PostInput::PassAppMessage(crate::AppMsg::OpenPerson(
self.comment.creator.id.clone(),
)));
}
CommentRowMsg::DeleteComment => {
let comment_id = self.comment.comment.id;
std::thread::spawn(move || {
let _ = api::comment::delete_comment(comment_id);
let _ = sender.output(PostInput::PassAppMessage(crate::AppMsg::StartFetchPosts(None, true)));
let _ = sender.output(PostInput::PassAppMessage(
crate::AppMsg::StartFetchPosts(None, true),
));
});
}
CommentRowMsg::OpenEditCommentDialog => {
@ -151,4 +166,4 @@ impl FactoryComponent for CommentRow {
}
}
}
}
}

View File

@ -1,10 +1,16 @@
use crate::{util::markdown_to_pango_markup, dialogs::editor::{EditorDialog, DialogMsg, EditorOutput, EditorType, EditorData}};
use lemmy_api_common::{lemmy_db_views::structs::PostView, lemmy_db_views_actor::structs::CommunityView, lemmy_db_schema::SubscribedType};
use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker};
use crate::{
dialogs::editor::{DialogMsg, EditorData, EditorDialog, EditorOutput, EditorType},
util::markdown_to_pango_markup,
};
use gtk::prelude::*;
use lemmy_api_common::{
lemmy_db_schema::SubscribedType, lemmy_db_views::structs::PostView,
lemmy_db_views_actor::structs::CommunityView,
};
use relm4::{factory::FactoryVecDeque, prelude::*, MessageBroker};
use relm4_components::web_image::WebImage;
use crate::{api, util::get_web_image_msg, settings};
use crate::{api, settings, util::get_web_image_msg};
use super::post_row::PostRow;
@ -16,7 +22,7 @@ pub struct CommunityPage {
posts: FactoryVecDeque<PostRow>,
#[allow(dead_code)]
create_post_dialog: Controller<EditorDialog>,
current_posts_page: i64
current_posts_page: i64,
}
#[derive(Debug)]
@ -29,7 +35,7 @@ pub enum CommunityInput {
CreatedPost(PostView),
ToggleSubscription,
UpdateSubscriptionState(SubscribedType),
None
None,
}
#[relm4::component(pub)]
@ -46,7 +52,7 @@ impl SimpleComponent for CommunityPage {
set_orientation: gtk::Orientation::Vertical,
set_vexpand: false,
set_margin_all: 10,
#[local_ref]
avatar -> gtk::Box {
set_size_request: (100, 100),
@ -137,12 +143,18 @@ impl SimpleComponent for CommunityPage {
let dialog = EditorDialog::builder()
.transient_for(root)
.launch_with_broker(EditorType::Post, &COMMUNITY_PAGE_BROKER)
.forward(sender.input_sender(), |msg| match msg {
.forward(sender.input_sender(), |msg| match msg {
EditorOutput::CreateRequest(post, _) => CommunityInput::CreatePostRequest(post),
_ => CommunityInput::None
_ => CommunityInput::None,
});
let model = CommunityPage { info: init, avatar, posts, create_post_dialog: dialog, current_posts_page: 0 };
let model = CommunityPage {
info: init,
avatar,
posts,
create_post_dialog: dialog,
current_posts_page: 0,
};
let avatar = model.avatar.widget();
let posts = model.posts.widget();
let widgets = view_output!();
@ -154,10 +166,13 @@ impl SimpleComponent for CommunityPage {
match message {
CommunityInput::UpdateCommunity(community) => {
self.info = community.clone();
self.avatar.emit(get_web_image_msg(community.community.icon));
self.avatar
.emit(get_web_image_msg(community.community.icon));
self.posts.guard().clear();
self.current_posts_page = 0;
if community.counts.posts == 0 { return; }
if community.counts.posts == 0 {
return;
}
sender.input(CommunityInput::FetchPosts);
}
CommunityInput::FetchPosts => {
@ -176,9 +191,7 @@ impl SimpleComponent for CommunityPage {
self.posts.guard().push_back(post);
}
}
CommunityInput::OpenCreatePostDialog => {
COMMUNITY_PAGE_BROKER.send(DialogMsg::Show)
}
CommunityInput::OpenCreatePostDialog => COMMUNITY_PAGE_BROKER.send(DialogMsg::Show),
CommunityInput::CreatedPost(post) => {
self.posts.guard().push_front(post);
}
@ -187,23 +200,35 @@ impl SimpleComponent for CommunityPage {
std::thread::spawn(move || {
let message = match api::post::create_post(post.name, post.body, post.url, id) {
Ok(post) => Some(CommunityInput::CreatedPost(post.post_view)),
Err(err) => { println!("{}", err.to_string()); None }
Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
};
if let Some(message) = message { sender.input(message) };
});
}
CommunityInput::ToggleSubscription => {
let community_id = self.info.community.id.0;
let new_state = match self.info.subscribed {
SubscribedType::NotSubscribed => true,
_ => false
_ => false,
};
std::thread::spawn(move || {
let message = match api::community::follow_community(community_id, new_state) {
Ok(community) => Some(CommunityInput::UpdateSubscriptionState(community.community_view.subscribed)),
Err(err) => { println!("{}", err.to_string()); None }
Ok(community) => Some(CommunityInput::UpdateSubscriptionState(
community.community_view.subscribed,
)),
Err(err) => {
println!("{}", err.to_string());
None
}
};
if message.is_some() {
sender.input(message.unwrap())
};
if message.is_some() { sender.input(message.unwrap()) };
});
}
CommunityInput::UpdateSubscriptionState(state) => {

View File

@ -1,6 +1,6 @@
use gtk::prelude::*;
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;
@ -75,18 +75,23 @@ impl FactoryComponent for CommunityRow {
}
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();
let community_image = WebImage::builder()
.launch(get_web_image_url(value.community.clone().icon))
.detach();
Self { community: value, community_image }
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 {
&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
@ -94,9 +99,9 @@ impl FactoryComponent for CommunityRow {
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
match message {
CommunityRowMsg::OpenCommunity => {
sender.output(crate::AppMsg::OpenCommunity(self.community.community.id.clone()))
}
CommunityRowMsg::OpenCommunity => sender.output(crate::AppMsg::OpenCommunity(
self.community.community.id.clone(),
)),
}
}
}
}

View File

@ -1,6 +1,6 @@
use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView;
use relm4::{prelude::*, factory::FactoryVecDeque};
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView;
use relm4::{factory::FactoryVecDeque, prelude::*};
use crate::api;
@ -16,7 +16,7 @@ pub struct InboxPage {
mentions: FactoryVecDeque<MentionRow>,
page: i64,
unread_only: bool,
type_: InboxType
type_: InboxType,
}
#[derive(Debug)]
@ -70,12 +70,17 @@ impl SimpleComponent for InboxPage {
}
fn init(
_init: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
_init: Self::Init,
root: &Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mentions = FactoryVecDeque::new(gtk::Box::default(), sender.output_sender());
let model = Self { mentions, page: 1, unread_only: false, type_: InboxType::Mentions };
let model = Self {
mentions,
page: 1,
unread_only: false,
type_: InboxType::Mentions,
};
let mentions = model.mentions.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
@ -94,12 +99,16 @@ impl SimpleComponent for InboxPage {
// It's just a different object, but its contents are exactly the same
let serialised = serde_json::to_string(&response.mentions).unwrap();
serde_json::from_str(&serialised).ok()
} else { None }
} else {
None
}
}
InboxType::Replies => {
if let Ok(response) = api::user::get_replies(page, unread_only) {
Some(response.replies)
} else { None }
} else {
None
}
}
};
if let Some(comments) = comments {

View File

@ -1,6 +1,6 @@
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_views_actor::structs::CommentReplyView;
use relm4::prelude::*;
use gtk::prelude::*;
use relm4_components::web_image::WebImage;
use crate::util::get_web_image_url;
@ -14,7 +14,7 @@ pub struct MentionRow {
comment: CommentReplyView,
creator_image: Controller<WebImage>,
community_image: Controller<WebImage>,
voting_row: Controller<VotingRowModel>
voting_row: Controller<VotingRowModel>,
}
#[derive(Debug)]
@ -93,7 +93,7 @@ impl FactoryComponent for MentionRow {
connect_clicked => MentionRowMsg::OpenPerson,
},
},
gtk::Label {
#[watch]
set_markup: &markdown_to_pango_markup(self.comment.comment.content.clone()),
@ -104,7 +104,7 @@ impl FactoryComponent for MentionRow {
#[local_ref]
voting_row -> gtk::Box {},
gtk::Separator {}
}
}
@ -114,11 +114,25 @@ impl FactoryComponent for MentionRow {
}
fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender<Self>) -> Self {
let creator_image = WebImage::builder().launch(get_web_image_url(value.creator.avatar.clone())).detach();
let community_image = WebImage::builder().launch(get_web_image_url(value.community.icon.clone())).detach();
let voting_row = VotingRowModel::builder().launch(VotingStats::from_comment(value.counts.clone(), value.my_vote)).detach();
let creator_image = WebImage::builder()
.launch(get_web_image_url(value.creator.avatar.clone()))
.detach();
let community_image = WebImage::builder()
.launch(get_web_image_url(value.community.icon.clone()))
.detach();
let voting_row = VotingRowModel::builder()
.launch(VotingStats::from_comment(
value.counts.clone(),
value.my_vote,
))
.detach();
Self { comment: value, creator_image, community_image, voting_row }
Self {
comment: value,
creator_image,
community_image,
voting_row,
}
}
fn init_widgets(
@ -144,8 +158,10 @@ impl FactoryComponent for MentionRow {
sender.output(crate::AppMsg::OpenPost(self.comment.post.id.clone()));
}
MentionRowMsg::OpenCommunity => {
sender.output(crate::AppMsg::OpenCommunity(self.comment.community.id.clone()));
sender.output(crate::AppMsg::OpenCommunity(
self.comment.community.id.clone(),
));
}
}
}
}
}

View File

@ -1,9 +1,9 @@
pub mod post_row;
pub mod community_row;
pub mod profile_page;
pub mod community_page;
pub mod post_page;
pub mod comment_row;
pub mod voting_row;
pub mod community_page;
pub mod community_row;
pub mod inbox_page;
pub mod mention_row;
pub mod mention_row;
pub mod post_page;
pub mod post_row;
pub mod profile_page;
pub mod voting_row;

View File

@ -1,11 +1,22 @@
use lemmy_api_common::{lemmy_db_views::structs::{CommentView, PostView}, post::GetPostResponse};
use relm4::{prelude::*, factory::FactoryVecDeque, MessageBroker};
use gtk::prelude::*;
use lemmy_api_common::{
lemmy_db_views::structs::{CommentView, PostView},
post::GetPostResponse,
};
use relm4::{factory::FactoryVecDeque, prelude::*, MessageBroker};
use relm4_components::web_image::WebImage;
use crate::{api, util::{get_web_image_msg, get_web_image_url, markdown_to_pango_markup}, dialogs::editor::{EditorDialog, EditorOutput, DialogMsg, EditorType, EditorData}, settings};
use crate::{
api,
dialogs::editor::{DialogMsg, EditorData, EditorDialog, EditorOutput, EditorType},
settings,
util::{get_web_image_msg, get_web_image_url, markdown_to_pango_markup},
};
use super::{comment_row::CommentRow, voting_row::{VotingRowModel, VotingStats, VotingRowInput}};
use super::{
comment_row::CommentRow,
voting_row::{VotingRowInput, VotingRowModel, VotingStats},
};
pub static POST_PAGE_BROKER: MessageBroker<DialogMsg> = MessageBroker::new();
@ -17,7 +28,7 @@ pub struct PostPage {
comments: FactoryVecDeque<CommentRow>,
#[allow(dead_code)]
create_comment_dialog: Controller<EditorDialog>,
voting_row: Controller<VotingRowModel>
voting_row: Controller<VotingRowModel>,
}
#[derive(Debug)]
@ -52,7 +63,7 @@ impl SimpleComponent for PostPage {
set_orientation: gtk::Orientation::Vertical,
set_vexpand: false,
set_margin_all: 10,
#[local_ref]
image -> gtk::Box {
set_height_request: 100,
@ -196,16 +207,26 @@ impl SimpleComponent for PostPage {
let dialog = EditorDialog::builder()
.transient_for(root)
.launch_with_broker(EditorType::Comment, &POST_PAGE_BROKER)
.forward(sender.input_sender(), |msg| match msg {
.forward(sender.input_sender(), |msg| match msg {
EditorOutput::CreateRequest(comment, _) => PostInput::CreateCommentRequest(comment),
EditorOutput::EditRequest(post, type_) => match type_ {
EditorType::Post => PostInput::EditPostRequest(post),
EditorType::Comment => PostInput::EditCommentRequest(post)
}
EditorType::Comment => PostInput::EditCommentRequest(post),
},
});
let voting_row = VotingRowModel::builder().launch(VotingStats::default()).detach();
let model = PostPage { info: init, image, comments, creator_avatar, community_avatar, create_comment_dialog: dialog, voting_row };
let voting_row = VotingRowModel::builder()
.launch(VotingStats::default())
.detach();
let model = PostPage {
info: init,
image,
comments,
creator_avatar,
community_avatar,
create_comment_dialog: dialog,
voting_row,
};
let image = model.image.widget();
let comments = model.comments.widget();
let creator_avatar = model.creator_avatar.widget();
@ -220,16 +241,25 @@ impl SimpleComponent for PostPage {
match message {
PostInput::UpdatePost(post) => {
self.info = 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.voting_row.emit(VotingRowInput::UpdateStats(VotingStats::from_post(post.post_view.counts.clone(), post.post_view.my_vote)));
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.voting_row
.emit(VotingRowInput::UpdateStats(VotingStats::from_post(
post.post_view.counts.clone(),
post.post_view.my_vote,
)));
self.comments.guard().clear();
std::thread::spawn(move || {
if post.post_view.counts.comments == 0 { return; }
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));
@ -258,7 +288,9 @@ impl SimpleComponent for PostPage {
if link.is_empty() {
link = get_web_image_url(post.embed_video_url);
}
if link.is_empty() { return; }
if link.is_empty() {
return;
}
gtk::show_uri(None::<&relm4::gtk::Window>, &link, 0);
}
PostInput::OpenCreateCommentDialog => {
@ -273,9 +305,14 @@ impl SimpleComponent for PostPage {
std::thread::spawn(move || {
let message = match api::comment::create_comment(id, post.body, None) {
Ok(comment) => Some(PostInput::CreatedComment(comment.comment_view)),
Err(err) => { println!("{}", err.to_string()); None }
Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
};
if let Some(message) = message { sender.input(message) };
});
}
PostInput::DeletePost => {
@ -288,11 +325,17 @@ impl SimpleComponent for PostPage {
PostInput::OpenEditPostDialog => {
let url = match self.info.post_view.post.url.clone() {
Some(url) => url.to_string(),
None => String::from("")
None => String::from(""),
};
let data = EditorData {
name: self.info.post_view.post.name.clone(),
body: self.info.post_view.post.body.clone().unwrap_or(String::from("")),
body: self
.info
.post_view
.post
.body
.clone()
.unwrap_or(String::from("")),
url: reqwest::Url::parse(&url).ok(),
id: None,
};
@ -305,9 +348,14 @@ impl SimpleComponent for PostPage {
std::thread::spawn(move || {
let message = match api::post::edit_post(post.name, post.url, post.body, id) {
Ok(post) => Some(PostInput::DoneEditPost(post.post_view)),
Err(err) => { println!("{}", err.to_string()); None }
Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
};
if let Some(message) = message { sender.input(message) };
});
}
PostInput::DoneEditPost(post) => {
@ -322,9 +370,14 @@ impl SimpleComponent for PostPage {
std::thread::spawn(move || {
let message = match api::comment::edit_comment(data.body, data.id.unwrap()) {
Ok(comment) => Some(PostInput::UpdateComment(comment.comment_view)),
Err(err) => { println!("{}", err.to_string()); None }
Err(err) => {
println!("{}", err.to_string());
None
}
};
if let Some(message) = message {
sender.input(message)
};
if let Some(message) = message { sender.input(message) };
});
}
PostInput::UpdateComment(comment) => {

View File

@ -1,10 +1,10 @@
use gtk::prelude::*;
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, api};
use crate::settings;
use crate::{api, util::get_web_image_url};
use super::voting_row::{VotingRowModel, VotingStats};
@ -13,7 +13,7 @@ pub struct PostRow {
post: PostView,
author_image: Controller<WebImage>,
community_image: Controller<WebImage>,
voting_row: Controller<VotingRowModel>
voting_row: Controller<VotingRowModel>,
}
#[derive(Debug)]
@ -21,7 +21,7 @@ pub enum PostViewMsg {
OpenPost,
OpenCommunity,
OpenPerson,
DeletePost
DeletePost,
}
#[relm4::factory(pub)]
@ -123,23 +123,36 @@ impl FactoryComponent for PostRow {
}
}
fn forward_to_parent(output: Self::Output) -> Option<Self::ParentInput> { Some(output) }
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.avatar.clone())).detach();
let community_image= WebImage::builder().launch(get_web_image_url(value.community.icon.clone())).detach();
let voting_row = VotingRowModel::builder().launch(VotingStats::from_post(value.counts.clone(), value.my_vote)).detach();
let author_image = WebImage::builder()
.launch(get_web_image_url(value.creator.avatar.clone()))
.detach();
let community_image = WebImage::builder()
.launch(get_web_image_url(value.community.icon.clone()))
.detach();
let voting_row = VotingRowModel::builder()
.launch(VotingStats::from_post(value.counts.clone(), value.my_vote))
.detach();
Self { post: value, author_image, community_image, voting_row }
Self {
post: value,
author_image,
community_image,
voting_row,
}
}
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 {
&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 voting_row = self.voting_row.widget();
@ -167,4 +180,4 @@ impl FactoryComponent for PostRow {
}
}
}
}
}

View File

@ -1,6 +1,6 @@
use lemmy_api_common::person::GetPersonDetailsResponse;
use relm4::{prelude::*, factory::FactoryVecDeque};
use gtk::prelude::*;
use lemmy_api_common::person::GetPersonDetailsResponse;
use relm4::{factory::FactoryVecDeque, prelude::*};
use relm4_components::web_image::WebImage;
use crate::util::get_web_image_msg;
@ -11,7 +11,7 @@ use super::post_row::PostRow;
pub struct ProfilePage {
info: GetPersonDetailsResponse,
avatar: Controller<WebImage>,
posts: FactoryVecDeque<PostRow>
posts: FactoryVecDeque<PostRow>,
}
#[derive(Debug)]
@ -31,7 +31,7 @@ impl SimpleComponent for ProfilePage {
set_orientation: gtk::Orientation::Vertical,
set_vexpand: false,
set_margin_all: 10,
#[local_ref]
avatar -> gtk::Box {
set_size_request: (100, 100),
@ -84,7 +84,11 @@ impl SimpleComponent for ProfilePage {
) -> 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: init, avatar, posts };
let model = ProfilePage {
info: init,
avatar,
posts,
};
let avatar = model.avatar.widget();
let posts = model.posts.widget();
let widgets = view_output!();
@ -96,7 +100,8 @@ impl SimpleComponent for ProfilePage {
match message {
ProfileInput::UpdatePerson(person) => {
self.info = person.clone();
self.avatar.emit(get_web_image_msg(person.person_view.person.avatar));
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);

View File

@ -1,6 +1,9 @@
use lemmy_api_common::{lemmy_db_schema::{aggregates::structs::{PostAggregates, CommentAggregates}, newtypes::{PostId, CommentId}}};
use relm4::{SimpleComponent, ComponentParts, gtk};
use gtk::prelude::*;
use lemmy_api_common::lemmy_db_schema::{
aggregates::structs::{CommentAggregates, PostAggregates},
newtypes::{CommentId, PostId},
};
use relm4::{gtk, ComponentParts, SimpleComponent};
use crate::{api, settings};
@ -15,7 +18,7 @@ pub struct VotingStats {
#[allow(dead_code)]
id: i32,
post_id: Option<i32>,
comment_id: Option<i32>
comment_id: Option<i32>,
}
impl VotingStats {
@ -46,7 +49,7 @@ impl VotingStats {
#[derive(Debug)]
pub struct VotingRowModel {
stats: VotingStats
stats: VotingStats,
}
#[derive(Debug)]
@ -56,9 +59,7 @@ pub enum VotingRowInput {
}
#[derive(Debug)]
pub enum VotingRowOutput {
}
pub enum VotingRowOutput {}
#[relm4::component(pub)]
impl SimpleComponent for VotingRowModel {
@ -105,24 +106,52 @@ impl SimpleComponent for VotingRowModel {
match message {
VotingRowInput::Vote(vote) => {
let mut score = self.stats.own_vote.unwrap_or(0) + vote;
if score < -1 || score > 1 { score = 0 };
if settings::get_current_account().jwt.is_none() { return; }
if score < -1 || score > 1 {
score = 0
};
if settings::get_current_account().jwt.is_none() {
return;
}
let stats = self.stats.clone();
std::thread::spawn(move || {
let info = if stats.post_id.is_some() {
let response = api::post::like_post(PostId { 0: stats.post_id.unwrap() }, score);
let response = api::post::like_post(
PostId {
0: stats.post_id.unwrap(),
},
score,
);
match response {
Ok(post) => Some(VotingStats::from_post(post.post_view.counts, post.post_view.my_vote)),
Err(err) => { println!("{}", err.to_string()); None }
Ok(post) => Some(VotingStats::from_post(
post.post_view.counts,
post.post_view.my_vote,
)),
Err(err) => {
println!("{}", err.to_string());
None
}
}
} else {
let response = api::comment::like_comment(CommentId { 0: stats.comment_id.unwrap() }, score);
let response = api::comment::like_comment(
CommentId {
0: stats.comment_id.unwrap(),
},
score,
);
match response {
Ok(comment) => Some(VotingStats::from_comment(comment.comment_view.counts, comment.comment_view.my_vote)),
Err(err) => { println!("{}", err.to_string()); None }
Ok(comment) => Some(VotingStats::from_comment(
comment.comment_view.counts,
comment.comment_view.my_vote,
)),
Err(err) => {
println!("{}", err.to_string());
None
}
}
};
if let Some(info) = info { sender.input(VotingRowInput::UpdateStats(info)) };
if let Some(info) = info {
sender.input(VotingRowInput::UpdateStats(info))
};
});
}
VotingRowInput::UpdateStats(stats) => {

View File

@ -1,5 +1,8 @@
use relm4::{prelude::*, gtk::{ResponseType, FileFilter}};
use gtk::prelude::*;
use relm4::{
gtk::{FileFilter, ResponseType},
prelude::*,
};
use crate::api;
@ -8,7 +11,7 @@ pub struct EditorData {
pub name: String,
pub body: String,
pub url: Option<reqwest::Url>,
pub id: Option<i32>
pub id: Option<i32>,
}
pub struct EditorDialog {
@ -20,13 +23,13 @@ pub struct EditorDialog {
body_buffer: gtk::TextBuffer,
// Optional field to temporarily store the post or comment id
id: Option<i32>,
window: gtk::Window
window: gtk::Window,
}
#[derive(Debug, Clone, Copy)]
pub enum EditorType {
Post,
Comment
Comment,
}
#[derive(Debug)]
@ -38,13 +41,13 @@ pub enum DialogMsg {
Okay,
ChooseImage,
UploadImage(std::path::PathBuf),
AppendBody(String)
AppendBody(String),
}
#[derive(Debug)]
pub enum EditorOutput {
CreateRequest(EditorData, EditorType),
EditRequest(EditorData, EditorType)
EditRequest(EditorData, EditorType),
}
#[relm4::component(pub)]
@ -76,13 +79,13 @@ impl SimpleComponent for EditorDialog {
set_label: "Post content",
add_css_class: "font-bold"
},
gtk::Entry {
gtk::Entry {
set_placeholder_text: Some("Title"),
set_margin_top: 10,
set_margin_bottom: 10,
set_buffer: &model.name_buffer,
},
gtk::Entry {
gtk::Entry {
set_placeholder_text: Some("Url"),
set_margin_top: 10,
set_margin_bottom: 10,
@ -151,7 +154,16 @@ impl SimpleComponent for EditorDialog {
let url_buffer = gtk::EntryBuffer::builder().build();
let body_buffer = gtk::TextBuffer::builder().build();
let window = root.toplevel_window().unwrap();
let model = EditorDialog { type_: init, visible: false, is_new: true, name_buffer, url_buffer, body_buffer, id: None, window };
let model = EditorDialog {
type_: init,
visible: false,
is_new: true,
name_buffer,
url_buffer,
body_buffer,
id: None,
window,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
@ -164,17 +176,22 @@ impl SimpleComponent for EditorDialog {
self.url_buffer.set_text("");
self.body_buffer.set_text("");
self.visible = false;
},
}
DialogMsg::Okay => {
let name = self.name_buffer.text().to_string();
let url = self.url_buffer.text().to_string();
let (start, end) = &self.body_buffer.bounds();
let body = self.body_buffer.text(start, end, true).to_string();
let url = reqwest::Url::parse(&url).ok();
let post = EditorData { name, body, url, id: self.id };
let post = EditorData {
name,
body,
url,
id: self.id,
};
let message = match self.is_new {
true => EditorOutput::CreateRequest(post, self.type_),
false => EditorOutput::EditRequest(post, self.type_)
false => EditorOutput::EditRequest(post, self.type_),
};
let _ = sender.output(message);
self.visible = false;
@ -185,22 +202,32 @@ impl SimpleComponent for EditorDialog {
}
DialogMsg::UpdateData(data) => {
self.name_buffer.set_text(data.name);
if let Some(url) = data.url { self.url_buffer.set_text(url.to_string()); }
if let Some(url) = data.url {
self.url_buffer.set_text(url.to_string());
}
self.body_buffer.set_text(&data.body.clone());
}
DialogMsg::ChooseImage => {
let buttons = [("_Cancel", ResponseType::Cancel), ("_Okay", ResponseType::Accept)];
let dialog = gtk::FileChooserDialog::new(Some("Upload image"), None::<&gtk::ApplicationWindow>, gtk::FileChooserAction::Open, &buttons);
let buttons = [
("_Cancel", ResponseType::Cancel),
("_Okay", ResponseType::Accept),
];
let dialog = gtk::FileChooserDialog::new(
Some("Upload image"),
None::<&gtk::ApplicationWindow>,
gtk::FileChooserAction::Open,
&buttons,
);
dialog.set_transient_for(Some(&self.window));
let image_filter = FileFilter::new();
image_filter.add_pattern("image/*");
dialog.add_filter(&image_filter);
dialog.run_async(move |dialog, result | {
dialog.run_async(move |dialog, result| {
match result {
ResponseType::Accept => {
let path = dialog.file().unwrap().path();
sender.input(DialogMsg::UploadImage(path.unwrap()))
},
}
_ => dialog.hide(),
}
dialog.destroy();
@ -217,8 +244,9 @@ impl SimpleComponent for EditorDialog {
DialogMsg::AppendBody(new_text) => {
let (start, end) = &self.body_buffer.bounds();
let body = self.body_buffer.text(start, end, true).to_string();
self.body_buffer.set_text(&format!("{}\n{}", body, new_text));
self.body_buffer
.set_text(&format!("{}\n{}", body, new_text));
}
}
}
}
}

View File

@ -1 +1 @@
pub mod editor;
pub mod editor;

View File

@ -1,18 +1,39 @@
pub mod settings;
pub mod api;
pub mod components;
pub mod util;
pub mod config;
pub mod dialogs;
pub mod settings;
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}, inbox_page::{InboxPage, InboxInput}};
use api::{community::default_community, post::default_post, user::default_person};
use components::{
community_page::{self, CommunityPage},
community_row::CommunityRow,
inbox_page::{InboxInput, InboxPage},
post_page::{self, PostPage},
post_row::PostRow,
profile_page::{self, ProfilePage},
};
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, CommunityId, PersonId}, ListingType}, post::GetPostResponse, community::GetCommunityResponse};
use relm4::{prelude::*, factory::FactoryVecDeque, set_global_css, actions::{RelmAction, RelmActionGroup}};
use lemmy_api_common::{
community::GetCommunityResponse,
lemmy_db_schema::{
newtypes::{CommunityId, PersonId, PostId},
ListingType,
},
lemmy_db_views::structs::PostView,
lemmy_db_views_actor::structs::CommunityView,
person::GetPersonDetailsResponse,
post::GetPostResponse,
};
use relm4::{
actions::{RelmAction, RelmActionGroup},
factory::FactoryVecDeque,
prelude::*,
set_global_css,
};
use settings::get_current_account;
static APP_ID: &str = "com.lemmy-gtk.lemoa";
#[derive(Debug, Clone, Copy)]
enum AppState {
Loading,
@ -24,7 +45,7 @@ enum AppState {
Post,
Login,
Message,
Inbox
Inbox,
}
struct App {
@ -65,7 +86,7 @@ pub enum AppMsg {
OpenPost(PostId),
DoneFetchPost(GetPostResponse),
OpenInbox,
PopBackStack
PopBackStack,
}
#[relm4::component]
@ -123,7 +144,7 @@ impl SimpleComponent for App {
match model.state {
AppState::Posts => gtk::ScrolledWindow {
set_hexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
@ -229,11 +250,11 @@ impl SimpleComponent for App {
gtk::ScrolledWindow {
set_vexpand: true,
set_hexpand: true,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
gtk::Box {
set_margin_all: 10,
@ -322,22 +343,52 @@ impl SimpleComponent for App {
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let current_account = settings::get_current_account();
let state = if current_account.instance_url.is_empty() { AppState::ChooseInstance } else { AppState::Loading };
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 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 inbox_page = InboxPage::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 community_search_buffer = gtk::EntryBuffer::builder().build();
let model = App { state, back_queue: vec![], logged_in, posts, communities, profile_page, community_page, post_page, inbox_page, message: None, current_communities_type: None, current_posts_type: None, current_communities_page: 1, current_posts_page: 1, community_search_buffer };
let model = App {
state,
back_queue: vec![],
logged_in,
posts,
communities,
profile_page,
community_page,
post_page,
inbox_page,
message: None,
current_communities_type: None,
current_posts_type: None,
current_communities_page: 1,
current_posts_page: 1,
community_search_buffer,
};
// fetch posts if that's the initial page
if !current_account.instance_url.is_empty() { sender.input(AppMsg::StartFetchPosts(None, true)) };
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();
@ -346,18 +397,21 @@ impl SimpleComponent for App {
let community_page = model.community_page.widget();
let post_page = model.post_page.widget();
let inbox_page = model.inbox_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 instance_action: RelmAction<ChangeInstanceAction> =
RelmAction::new_stateless(move |_| {
instance_sender.input(AppMsg::ChooseInstance);
});
let profile_sender = sender.clone();
let profile_action: RelmAction<ProfileAction> = RelmAction::new_stateless(move |_| {
let person = settings::get_current_account();
if !person.name.is_empty() { profile_sender.input(AppMsg::OpenPerson(PersonId(person.id))); }
if !person.name.is_empty() {
profile_sender.input(AppMsg::OpenPerson(PersonId(person.id)));
}
});
let login_sender = sender.clone();
let login_action: RelmAction<LoginAction> = RelmAction::new_stateless(move |_| {
@ -380,15 +434,20 @@ impl SimpleComponent for App {
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
// save the back queue
match msg {
AppMsg::DoneFetchCommunities(_) | AppMsg::DoneFetchCommunity(_) | AppMsg::DoneFetchPerson(_) | AppMsg::DoneFetchPost(_) | AppMsg::DoneFetchPosts(_) | AppMsg::ShowMessage(_) => {
self.back_queue.push(msg.clone())
}
_ => {}
AppMsg::DoneFetchCommunities(_)
| AppMsg::DoneFetchCommunity(_)
| AppMsg::DoneFetchPerson(_)
| AppMsg::DoneFetchPost(_)
| AppMsg::DoneFetchPosts(_)
| AppMsg::ShowMessage(_) => self.back_queue.push(msg.clone()),
_ => {}
}
match msg {
AppMsg::DoneChoosingInstance(instance_url) => {
if instance_url.trim().is_empty() { return; }
if instance_url.trim().is_empty() {
return;
}
let mut current_account = settings::get_current_account();
current_account.instance_url = instance_url;
settings::update_current_account(current_account);
@ -400,41 +459,58 @@ impl SimpleComponent for App {
}
AppMsg::StartFetchPosts(type_, remove_previous) => {
self.current_posts_type = type_;
let page = if remove_previous { 1 } else { self.current_posts_page + 1 };
let page = if remove_previous {
1
} else {
self.current_posts_page + 1
};
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())
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(); }
if self.current_posts_page == 1 {
self.posts.guard().clear();
}
for post in posts {
self.posts.guard().push_back(post);
}
}
AppMsg::FetchCommunities(listing_type, remove_previous) => {
let query_text = self.community_search_buffer.text().as_str().to_owned();
let query = if query_text.is_empty() { None } else { Some(query_text) };
let query = if query_text.is_empty() {
None
} else {
Some(query_text)
};
self.state = AppState::Communities;
let page = if remove_previous { 1 } else { self.current_communities_page + 1 };
let page = if remove_previous {
1
} else {
self.current_communities_page + 1
};
self.current_communities_page = page;
self.current_communities_type = listing_type;
std::thread::spawn(move || {
let message = match api::communities::fetch_communities(page, query, listing_type) {
Ok(communities) => AppMsg::DoneFetchCommunities(communities),
Err(err) => AppMsg::ShowMessage(err.to_string())
};
let message =
match api::communities::fetch_communities(page, query, listing_type) {
Ok(communities) => AppMsg::DoneFetchCommunities(communities),
Err(err) => AppMsg::ShowMessage(err.to_string()),
};
sender.input(message);
});
}
AppMsg::DoneFetchCommunities(communities) => {
self.state = AppState::Communities;
if self.current_communities_page == 1 { self.communities.guard().clear(); }
if self.current_communities_page == 1 {
self.communities.guard().clear();
}
for community in communities {
self.communities.guard().push_back(community);
}
@ -444,13 +520,15 @@ impl SimpleComponent for App {
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())
Err(err) => AppMsg::ShowMessage(err.to_string()),
};
sender.input(message);
});
}
AppMsg::DoneFetchPerson(person) => {
self.profile_page.sender().emit(profile_page::ProfileInput::UpdatePerson(person));
self.profile_page
.sender()
.emit(profile_page::ProfileInput::UpdatePerson(person));
self.state = AppState::Person;
}
AppMsg::OpenCommunity(community_id) => {
@ -458,13 +536,17 @@ impl SimpleComponent for App {
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())
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.community_page
.sender()
.emit(community_page::CommunityInput::UpdateCommunity(
community.community_view,
));
self.state = AppState::Community;
}
AppMsg::OpenPost(post_id) => {
@ -472,21 +554,29 @@ impl SimpleComponent for App {
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())
Err(err) => AppMsg::ShowMessage(err.to_string()),
};
sender.input(message);
});
}
AppMsg::DoneFetchPost(post) => {
self.post_page.sender().emit(post_page::PostInput::UpdatePost(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, totp_token) => {
if get_current_account().instance_url.is_empty() { return; }
let token = if totp_token.is_empty() { None } else { Some(totp_token) };
if get_current_account().instance_url.is_empty() {
return;
}
let token = if totp_token.is_empty() {
None
} else {
Some(totp_token)
};
self.state = AppState::Loading;
std::thread::spawn(move || {
let message = match api::auth::login(username, password, token) {
@ -506,7 +596,7 @@ impl SimpleComponent for App {
AppMsg::ShowMessage("Wrong credentials!".to_string())
}
}
Err(err) => AppMsg::ShowMessage(err.to_string())
Err(err) => AppMsg::ShowMessage(err.to_string()),
};
sender.input(message);
});
@ -531,7 +621,9 @@ impl SimpleComponent for App {
}
AppMsg::PopBackStack => {
let action = self.back_queue.get(self.back_queue.len() - 2);
if let Some(action) = action { sender.input(action.clone()); }
if let Some(action) = action {
sender.input(action.clone());
}
for _ in 0..2 {
self.back_queue.remove(self.back_queue.len() - 1);
}
@ -547,7 +639,7 @@ relm4::new_stateless_action!(LoginAction, WindowActionGroup, "login");
relm4::new_stateless_action!(LogoutAction, WindowActionGroup, "logout");
fn main() {
let app = RelmApp::new(APP_ID);
let app = RelmApp::new(config::APP_ID);
set_global_css(include_str!("style.css"));
app.run::<App>(());
}

View File

@ -1,8 +1,8 @@
use std::{fs::File, path::PathBuf};
use crate::config::APP_ID;
use crate::gtk::glib;
use lemmy_api_common::sensitive::Sensitive;
use serde::{Deserialize, Serialize};
use crate::APP_ID;
use std::{fs::File, path::PathBuf};
#[derive(Deserialize, Serialize, Default, Clone)]
pub struct Account {
@ -15,7 +15,7 @@ pub struct Account {
#[derive(Deserialize, Serialize, Default)]
pub struct Preferences {
pub accounts: Vec<Account>,
pub current_account_index: u32
pub current_account_index: u32,
}
pub fn data_path() -> PathBuf {

View File

@ -1,18 +1,22 @@
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 {
pub fn get_web_image_msg(url: Option<DbUrl>) -> WebImageMsg {
return if let Some(url) = url {
WebImageMsg::LoadImage(url.to_string())
} else { WebImageMsg::Unload };
} 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("") }
} else {
String::from("")
};
}
pub fn markdown_to_pango_markup(text: String) -> String {
return html2pango::markup_html(&markdown::to_html(&text)).unwrap_or(text)
return html2pango::markup_html(&markdown::to_html(&text)).unwrap_or(text);
}