Skip to content

Commit

Permalink
feat: add anki importing
Browse files Browse the repository at this point in the history
  • Loading branch information
mariinkys committed Jan 27, 2025
1 parent 97ebdfd commit 8da93d1
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 3 deletions.
122 changes: 122 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ sqlx = { version = "0.8.3", features = [
futures = "0.3.31"
dirs = "6.0.0"
serde = { version = "1.0.217", features = ["derive"] }
rand = "0.8.5"
rand = "0.8.5" #needed for flashcard selection
ashpd = { version = "0.10.2", features = ["wayland"] } #needed for file dialogs
percent-encoding = "2.3.1" #needed for correct anki file importing

[dependencies.i18n-embed]
version = "0.15"
Expand Down
3 changes: 2 additions & 1 deletion info/ANKI_IMPORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ The file once exported should be a TXT file that looks similar to this:
```
#separator:tab
#html:false
Front Content Sample Back Content Sample
Front Content Sample Back Content Sample
Cat Gato
Dog Perro
```
Expand Down
34 changes: 34 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::oboete::pages::folder_content::{self, FolderContent};
use crate::oboete::pages::homepage::{self, HomePage};
use crate::oboete::pages::study_page::{self, StudyPage};
use crate::{fl, icons};
use ashpd::desktop::file_chooser::{FileFilter, SelectedFiles};
use cosmic::app::{context_drawer, Core, Task};
use cosmic::cosmic_config::{self, CosmicConfigEntry};
use cosmic::iced::{Alignment, Event, Length, Subscription};
Expand Down Expand Up @@ -688,6 +689,39 @@ impl Application for Oboete {
));
}

// Opens the file selection dialog for anki importing and executes a callback with the result
folder_content::FolderContentTask::OpenAnkiFileSelection => {
tasks.push(Task::perform(
async move {
let result = SelectedFiles::open_file()
.title("Open Anki File")
.accept_label("Open")
.modal(true)
.multiple(false)
.filter(FileFilter::new("TXT File").glob("*.txt"))
.send()
.await
.unwrap()
.response();

if let Ok(result) = result {
result
.uris()
.iter()
.map(|file| file.path().to_string())
.collect::<Vec<String>>()
} else {
Vec::new()
}
},
|files| {
cosmic::app::message::app(Message::FolderContent(
folder_content::Message::OpenAnkiFileResult(files),
))
},
));
}

// Retrieves the flashcard of a folder and gives it to the studypage
folder_content::FolderContentTask::StudyFolder(folder_id) => {
tasks.push(Task::perform(
Expand Down
61 changes: 60 additions & 1 deletion src/oboete/pages/folder_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ use cosmic::{

use crate::{
fl, icons,
oboete::{models::flashcard::Flashcard, utils::parse_import_content},
oboete::{
models::flashcard::Flashcard,
utils::{parse_ankifile, parse_import_content},
},
};

pub struct FolderContent {
Expand Down Expand Up @@ -84,6 +87,9 @@ pub enum Message {
FolderOptionsImportContentInput(String),
ImportContent,
ContentImported,
OpenAnkiFileSelection,
OpenAnkiFileResult(Vec<String>),
LaunchUrl(String),

// Change to Study Page
StudyFolder(i32),
Expand All @@ -100,6 +106,7 @@ pub enum FolderContentTask {

OpenFolderOptionsContextPage,
ImportContent(Vec<Flashcard>),
OpenAnkiFileSelection,

StudyFolder(i32),
}
Expand Down Expand Up @@ -256,6 +263,34 @@ impl FolderContent {
tasks.push(FolderContentTask::CloseContextPage);
}

// Asks for the app selection dialog to be opened
Message::OpenAnkiFileSelection => {
tasks.push(FolderContentTask::OpenAnkiFileSelection);
}

// Callback after anki file import dialog, tries to parse the content and asks to import it
Message::OpenAnkiFileResult(result) => {
if !result.is_empty() {
for path in result {
let flashcards = parse_ankifile(&path);
match flashcards {
Ok(flashcards) => {
tasks.push(FolderContentTask::ImportContent(flashcards))
}
Err(err) => eprintln!("{:?}", err),
}
}
}
}

// Opens the given URL
Message::LaunchUrl(url) => match open::that_detached(&url) {
Ok(()) => {}
Err(err) => {
eprintln!("failed to open {url:?}: {err}");
}
},

// Asks for the study mode for a given folder (page change)
Message::StudyFolder(folder_id) => {
tasks.push(FolderContentTask::StudyFolder(folder_id));
Expand Down Expand Up @@ -493,9 +528,33 @@ impl FolderContent {
)
.into()]);

let anki_import_col = widget::settings::view_column(vec![widget::settings::section()
.title(fl!("import-anki-title"))
.add(
widget::column::with_children(vec![widget::button::link(fl!(
"about-anki-importing"
))
.on_press(Message::LaunchUrl(String::from(
"https://github.com/mariinkys/oboete/blob/main/info/ANKI_IMPORTING.md",
)))
.into()])
.spacing(spacing.space_xxs),
)
.into()]);

let anki_import_button = widget::Row::new()
.push(Space::new(Length::Fill, Length::Shrink))
.push(
widget::button::text(fl!("import-button"))
.on_press(Message::OpenAnkiFileSelection)
.class(theme::Button::Suggested),
);

widget::Column::new()
.push(folder_import_col)
.push(folder_import_btn)
.push(anki_import_col)
.push(anki_import_button)
.spacing(spacing.space_xs)
.into()
}
Expand Down
37 changes: 37 additions & 0 deletions src/oboete/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-only

use std::{
fs::File,
io::{self, BufRead},
path::Path,
};

use percent_encoding::percent_decode_str;
use rand::{seq::SliceRandom, thread_rng};

use super::models::flashcard::Flashcard;
Expand Down Expand Up @@ -47,3 +54,33 @@ pub fn parse_import_content(
})
.collect()
}

pub fn parse_ankifile(file_path: &str) -> Result<Vec<Flashcard>, io::Error> {
let decoded_path = percent_decode_str(file_path)
.decode_utf8_lossy()
.to_string();
let path = Path::new(&decoded_path);
let file = File::open(path)?;
let reader = io::BufReader::new(file);

let mut flashcards = Vec::new();

for (index, line) in reader.lines().enumerate() {
let line = line?;
// Skip the first three lines which are metadata
if index < 3 {
continue;
}
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() == 2 {
flashcards.push(Flashcard {
id: None,
front: parts[0].to_string(),
back: parts[1].to_string(),
status: 0,
});
}
}

Ok(flashcards)
}

0 comments on commit 8da93d1

Please sign in to comment.