Skip to content

Commit a7941ce

Browse files
committed
refactor: Add Details To User Accounts
Leverage `/api/user/profile` endpoint to fetch crucial information regarding the user. Individual users control if they support sharing bookmarks, we should respect this option, and act accordingly. Add database migrations that add new columns. Update accounts, and bookmarks forms to indicate wether sharing bookmarks is possible.
1 parent ce4017d commit a7941ce

File tree

9 files changed

+178
-34
lines changed

9 files changed

+178
-34
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Features:
2121
- Add/Edit/Remove bookmarks.
2222
- Search bookmarks based on title, URL, tags, desscription, and notes.
2323

24+
cosmicding was tested against linkding releases `1.31.0`, and `1.36.0`.
25+
2426
## Dependencies
2527

2628
- `cargo`
@@ -60,6 +62,7 @@ Potential improvements:
6062
- [Application] Allow user-provided TLS certificate.
6163
- [UI] Visual indicator for last sync status.
6264
- [UI] Indicators for `archived`, `unread`, `shared` bookmarks.
65+
- [UI] Display account information in accounts page.
6366
- [Distribution] Flatpack release.
6467
- [Distribution] compiled binary in GitHub release.
6568
- [UI] Sort bookmarks.

i18n/en/cosmicding.ftl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ display-name = Display Name
1919
edit = Edit
2020
edit-account = Edit Account
2121
edit-bookmark = Edit Bookmark
22+
disabled-public-sharing = Public bookmarks sharing disabled
23+
disabled-sharing = Bookmarks sharing disabled
24+
enabled-public-sharing = Public bookmarks sharing enabled
25+
enabled-sharing = Bookmarks sharing enabled
2226
failed-to-find-linkding-api-endpoint = Failed to find linkding API endpoint
2327
failed-to-parse-response = Failed to parse response
2428
file = File
@@ -46,8 +50,10 @@ removed-account = Removed account {$acc}
4650
removed-bookmark-from-account = Removed bookmark from account {$acc}
4751
save = Save
4852
search = Search
53+
setting-managed-externally = This setting can only be managed from Linkding web UI
4954
settings = Settings
5055
shared = Shared
56+
shared-disabled = Shared (Disabled)
5157
tags = Tags
5258
tags-helper = Enter any number of tags separated by space.
5359
theme = Theme
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE UserAccounts ADD COLUMN enable_sharing INTEGER default 0;
2+
ALTER TABLE UserAccounts ADD COLUMN enable_public_sharing INTEGER default 0;

src/app.rs

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::db::{self, SqliteDatabase};
33
use crate::fl;
44
use crate::http::{self};
55
use crate::key_binds::key_binds;
6-
use crate::models::account::Account;
6+
use crate::models::account::{Account, AccountApiResponse};
77
use crate::models::bookmarks::Bookmark;
88
use crate::nav::NavPage;
99
use crate::pages::accounts::{add_account, edit_account, AccountsMessage, AccountsView};
@@ -73,6 +73,7 @@ pub enum Message {
7373
CompleteRemoveDialog(Account, Option<Bookmark>),
7474
DialogCancel,
7575
DialogUpdate(DialogPage),
76+
DoneRefreshAccountProfile(Account, AccountApiResponse),
7677
DoneRefreshBookmarksForAccount(Account, Vec<Bookmark>),
7778
DoneRefreshBookmarksForAllAccounts(Vec<Bookmark>),
7879
EditAccount(Account),
@@ -101,6 +102,7 @@ pub enum Message {
101102
SetBookmarkTitle(String),
102103
SetBookmarkURL(String),
103104
SetBookmarkUnread(bool),
105+
StartRefreshAccountProfile(Account),
104106
StartRefreshBookmarksForAccount(Account),
105107
StartRefreshBookmarksForAllAccounts,
106108
StartupCompleted,
@@ -420,11 +422,13 @@ impl Application for Cosmicding {
420422
.map(cosmic::app::Message::App),
421423
);
422424
}
423-
Message::CompleteAddAccount(account) => {
425+
Message::CompleteAddAccount(mut account) => {
424426
let mut valid_account = false;
425427
block_on(async {
426-
match http::check_instance(&account).await {
427-
Ok(()) => {
428+
match http::check_account_on_instance(&account).await {
429+
Ok(value) => {
430+
account.enable_sharing = value.enable_sharing;
431+
account.enable_public_sharing = value.enable_public_sharing;
428432
valid_account = true;
429433
}
430434
Err(e) => {
@@ -465,11 +469,13 @@ impl Application for Cosmicding {
465469
}
466470
self.core.window.show_context = false;
467471
}
468-
Message::UpdateAccount(account) => {
472+
Message::UpdateAccount(mut account) => {
469473
let mut valid_account = false;
470474
block_on(async {
471-
match http::check_instance(&account).await {
472-
Ok(()) => {
475+
match http::check_account_on_instance(&account).await {
476+
Ok(value) => {
477+
account.enable_sharing = value.enable_sharing;
478+
account.enable_public_sharing = value.enable_public_sharing;
473479
valid_account = true;
474480
}
475481
Err(e) => {
@@ -524,7 +530,7 @@ impl Application for Cosmicding {
524530
};
525531
if !self.accounts_view.accounts.is_empty() {
526532
commands.push(Command::perform(
527-
http::fetch_all_bookmarks_from_accounts(
533+
http::fetch_bookmarks_from_all_accounts(
528534
self.accounts_view.accounts.clone(),
529535
),
530536
message,
@@ -553,9 +559,10 @@ impl Application for Cosmicding {
553559
x,
554560
))
555561
};
562+
commands.push(self.update(Message::StartRefreshAccountProfile(account.clone())));
556563
if !self.accounts_view.accounts.is_empty() {
557564
commands.push(Command::perform(
558-
http::fetch_all_bookmarks_from_accounts(acc_vec.clone()),
565+
http::fetch_bookmarks_from_all_accounts(acc_vec.clone()),
559566
message,
560567
));
561568
commands.push(
@@ -579,6 +586,27 @@ impl Application for Cosmicding {
579586
});
580587
commands.push(self.update(Message::LoadBookmarks));
581588
}
589+
Message::StartRefreshAccountProfile(account) => {
590+
let borrowed_acc = account.clone();
591+
let message = |r: Option<AccountApiResponse>| {
592+
cosmic::app::Message::App(Message::DoneRefreshAccountProfile(
593+
borrowed_acc,
594+
r.unwrap(),
595+
))
596+
};
597+
commands.push(Command::perform(
598+
http::fetch_account_details(account),
599+
message,
600+
));
601+
}
602+
Message::DoneRefreshAccountProfile(mut account, account_details) => {
603+
account.enable_sharing = account_details.enable_sharing;
604+
account.enable_public_sharing = account_details.enable_public_sharing;
605+
block_on(async {
606+
db::SqliteDatabase::update_account(&mut self.db, &account).await
607+
});
608+
commands.push(self.update(Message::LoadAccounts));
609+
}
582610
Message::AddBookmarkForm => {
583611
self.placeholder_bookmark = Some(Bookmark::new(
584612
None,
@@ -853,14 +881,17 @@ impl Application for Cosmicding {
853881
block_on(async { db::SqliteDatabase::fetch_bookmarks(&mut self.db).await });
854882
}
855883
Message::StartupCompleted => {
856-
self.startup_completed = true;
884+
for account in self.accounts_view.accounts.clone() {
885+
commands.push(self.update(Message::StartRefreshAccountProfile(account)));
886+
}
857887
commands.push(Command::perform(
858888
async {
859889
tokio::time::sleep(Duration::from_secs(1)).await;
860890
crate::app::Message::StartRefreshBookmarksForAllAccounts
861891
},
862892
|msg| cosmic::app::Message::App(msg),
863-
))
893+
));
894+
self.startup_completed = true;
864895
}
865896
Message::EmpptyMessage => {
866897
commands.push(Command::none());

src/db/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ impl SqliteDatabase {
7070
last_sync_status: row.get("last_sync_status"),
7171
last_sync_timestamp: row.get("last_sync_timestamp"),
7272
tls: row.get("tls"),
73+
enable_sharing: row.get("enable_sharing"),
74+
enable_public_sharing: row.get("enable_public_sharing"),
7375
})
7476
.collect();
7577
return data;
@@ -83,24 +85,28 @@ impl SqliteDatabase {
8385
.unwrap();
8486
}
8587
pub async fn update_account(&mut self, account: &Account) {
86-
let query: &str = "UPDATE UserAccounts SET display_name=$2, instance=$3, api_token=$4, tls=$5 WHERE id=$1;";
88+
let query: &str = "UPDATE UserAccounts SET display_name=$2, instance=$3, api_token=$4, tls=$5, enable_sharing=$6, enable_public_sharing=$7 WHERE id=$1;";
8789
sqlx::query(query)
8890
.bind(&account.id)
8991
.bind(&account.display_name)
9092
.bind(&account.instance)
9193
.bind(&account.api_token)
9294
.bind(&account.tls)
95+
.bind(&account.enable_sharing)
96+
.bind(&account.enable_public_sharing)
9397
.execute(&mut self.conn)
9498
.await
9599
.unwrap();
96100
}
97101
pub async fn create_account(&mut self, account: &Account) {
98-
let query: &str = "INSERT INTO UserAccounts (display_name, instance, api_token, last_sync_status, last_sync_timestamp, tls) VALUES ($1, $2, $3, 0, 0, $4);";
102+
let query: &str = "INSERT INTO UserAccounts (display_name, instance, api_token, last_sync_status, last_sync_timestamp, tls, enable_sharing, enable_public_sharing) VALUES ($1, $2, $3, 0, 0, $4, $5, $6);";
99103
sqlx::query(query)
100104
.bind(&account.display_name)
101105
.bind(&account.instance)
102106
.bind(&account.api_token)
103107
.bind(&account.tls)
108+
.bind(&account.enable_sharing)
109+
.bind(&account.enable_public_sharing)
104110
.execute(&mut self.conn)
105111
.await
106112
.unwrap();

src/http/mod.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::fl;
2-
use crate::models::account::Account;
2+
use crate::models::account::{Account, AccountApiResponse};
33
use crate::models::bookmarks::{Bookmark, BookmarksApiResponse};
44
use anyhow::Result;
55
use reqwest::{
@@ -9,7 +9,7 @@ use reqwest::{
99
use serde_json::Value;
1010
use std::fmt::Write;
1111

12-
pub async fn fetch_all_bookmarks_from_accounts(accounts: Vec<Account>) -> Vec<Bookmark> {
12+
pub async fn fetch_bookmarks_from_all_accounts(accounts: Vec<Account>) -> Vec<Bookmark> {
1313
let mut all_bookmarks: Vec<Bookmark> = Vec::new();
1414
for account in accounts {
1515
match fetch_bookmarks_for_account(&account).await {
@@ -251,14 +251,28 @@ pub async fn edit_bookmark(
251251
}
252252
}
253253

254-
pub async fn check_instance(account: &Account) -> Result<(), Box<dyn std::error::Error>> {
254+
pub async fn fetch_account_details(account: Account) -> Option<AccountApiResponse> {
255+
let mut account_details: Option<AccountApiResponse> = None;
256+
match check_account_on_instance(&account).await {
257+
Ok(details) => {
258+
account_details = Some(details);
259+
}
260+
Err(e) => {
261+
log::error!(
262+
"Error fetching account {} details: {}",
263+
account.display_name,
264+
e
265+
);
266+
}
267+
}
268+
return account_details;
269+
}
270+
271+
pub async fn check_account_on_instance(
272+
account: &Account,
273+
) -> Result<AccountApiResponse, Box<dyn std::error::Error>> {
255274
let mut rest_api_url: String = "".to_owned();
256-
write!(
257-
&mut rest_api_url,
258-
"{}/api/bookmarks/?limit=1",
259-
account.instance,
260-
)
261-
.unwrap();
275+
write!(&mut rest_api_url, "{}/api/user/profile/", account.instance).unwrap();
262276
let mut headers = HeaderMap::new();
263277
let http_client = ClientBuilder::new()
264278
.danger_accept_invalid_certs(account.tls)
@@ -273,8 +287,8 @@ pub async fn check_instance(account: &Account) -> Result<(), Box<dyn std::error:
273287
.send()
274288
.await?;
275289
match response.status() {
276-
StatusCode::OK => match response.json::<BookmarksApiResponse>().await {
277-
Ok(_value) => Ok(()),
290+
StatusCode::OK => match response.json::<AccountApiResponse>().await {
291+
Ok(_value) => Ok(_value),
278292
Err(_e) => Err(Box::new(std::io::Error::new(
279293
std::io::ErrorKind::Other,
280294
fl!("failed-to-find-linkding-api-endpoint"),

src/models/account.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
use serde::{Deserialize, Serialize};
12
use sqlx::FromRow;
2-
#[derive(Debug, Clone, FromRow, Eq, PartialEq)]
3+
#[derive(Serialize, Deserialize, Debug, Clone, FromRow, Eq, PartialEq)]
34
pub struct Account {
45
pub id: Option<i64>,
56
pub api_token: String,
@@ -8,6 +9,8 @@ pub struct Account {
89
pub last_sync_status: bool,
910
pub last_sync_timestamp: i64,
1011
pub tls: bool,
12+
pub enable_sharing: bool,
13+
pub enable_public_sharing: bool,
1114
}
1215

1316
impl AsRef<str> for Account {
@@ -26,6 +29,30 @@ impl Account {
2629
last_sync_status: false,
2730
last_sync_timestamp: 0,
2831
tls: true,
32+
enable_sharing: false,
33+
enable_public_sharing: false,
2934
}
3035
}
3136
}
37+
38+
// NOTE: (vkhitrin) we do not use these preferences as part of the application
39+
#[derive(Debug, Clone, Serialize, Deserialize)]
40+
pub struct SearchPreferences {
41+
pub sort: Option<String>,
42+
pub shared: Option<String>,
43+
pub unread: Option<String>,
44+
}
45+
46+
#[derive(Debug, Clone, Serialize, Deserialize)]
47+
pub struct AccountApiResponse {
48+
pub theme: String,
49+
pub bookmark_date_display: String,
50+
pub web_archive_integration: String,
51+
pub tag_search: String,
52+
pub enable_sharing: bool,
53+
pub enable_public_sharing: bool,
54+
pub enable_favicons: bool,
55+
pub display_url: bool,
56+
pub permanent_notes: bool,
57+
pub search_preferences: SearchPreferences,
58+
}

src/pages/accounts.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::app::Message;
22
use crate::fl;
33
use crate::models::account::Account;
44
use cosmic::iced::Length;
5+
use cosmic::iced_widget::tooltip;
56
use cosmic::{
67
cosmic_theme,
78
iced::{self, Alignment},
@@ -223,6 +224,7 @@ pub fn add_account<'a>(account: Account) -> Element<'a, Message> {
223224
}
224225

225226
pub fn edit_account<'a>(account: Account) -> Element<'a, Message> {
227+
let spacing = theme::active().cosmic().spacing;
226228
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
227229
let display_name_widget_title = widget::text::body(fl!("display-name"));
228230
let display_name_widget_text_input = widget::text_input("Name", account.display_name.clone())
@@ -235,6 +237,49 @@ pub fn edit_account<'a>(account: Account) -> Element<'a, Message> {
235237
widget::secure_input("Token", account.api_token.clone(), None, true)
236238
.on_input(Message::SetAccountAPIKey);
237239
let tls_widget_checkbox = widget::checkbox(fl!("tls"), account.tls, Message::SetAccountTLS);
240+
//let enable_shared_widget_text = widget::text(if account.enable_sharing {
241+
// fl!("enabled-sharing")
242+
//} else {
243+
// "".to_owned()
244+
//});
245+
let enable_shared_widget_text = if account.enable_sharing {
246+
widget::tooltip(
247+
widget::row::with_capacity(2)
248+
.spacing(spacing.space_xxs)
249+
.push(widget::text::body(fl!("enabled-sharing")))
250+
.push(widget::icon::from_name("dialog-information-symbolic").size(18)),
251+
fl!("setting-managed-externally"),
252+
tooltip::Position::Top,
253+
)
254+
} else {
255+
widget::tooltip(
256+
widget::row::with_capacity(2)
257+
.spacing(spacing.space_xxs)
258+
.push(widget::text::body(fl!("disabled-sharing")))
259+
.push(widget::icon::from_name("dialog-information-symbolic").size(18)),
260+
fl!("setting-managed-externally"),
261+
tooltip::Position::Top,
262+
)
263+
};
264+
let enable_public_shared_widget_text = if account.enable_sharing {
265+
widget::tooltip(
266+
widget::row::with_capacity(2)
267+
.spacing(spacing.space_xxs)
268+
.push(widget::text::body(fl!("enabled-public-sharing")))
269+
.push(widget::icon::from_name("dialog-information-symbolic").size(18)),
270+
fl!("setting-managed-externally"),
271+
tooltip::Position::Top,
272+
)
273+
} else {
274+
widget::tooltip(
275+
widget::row::with_capacity(2)
276+
.spacing(spacing.space_xxs)
277+
.push(widget::text::body(fl!("disabled-public-sharing")))
278+
.push(widget::icon::from_name("dialog-information-symbolic").size(18)),
279+
fl!("setting-managed-externally"),
280+
tooltip::Position::Top,
281+
)
282+
};
238283
let buttons_widget_container = widget::container(
239284
widget::button::text(fl!("save"))
240285
.style(widget::button::Style::Standard)
@@ -254,6 +299,9 @@ pub fn edit_account<'a>(account: Account) -> Element<'a, Message> {
254299
.push(widget::vertical_space(Length::from(10)))
255300
.push(tls_widget_checkbox)
256301
.push(widget::vertical_space(Length::from(10)))
302+
.push(enable_shared_widget_text)
303+
.push(enable_public_shared_widget_text)
304+
.push(widget::vertical_space(Length::from(10)))
257305
.push(buttons_widget_container)
258306
.into()
259307
}

0 commit comments

Comments
 (0)