2024-11-05 20:39:21 +00:00
use std ::sync ::Mutex ;
use std ::time ::Duration ;
2025-11-24 11:37:18 +00:00
use log ::{ debug , error , info , trace , warn } ;
2024-11-05 20:39:21 +00:00
use once_cell ::sync ::Lazy ;
2025-01-01 14:49:27 +00:00
use rocket ::{ get , post } ;
2025-11-24 11:37:18 +00:00
use rocket ::response ::stream ::TextStream ;
2024-11-05 20:39:21 +00:00
use rocket ::serde ::json ::Json ;
use rocket ::serde ::Serialize ;
2025-01-01 14:49:27 +00:00
use serde ::Deserialize ;
2024-11-05 20:39:21 +00:00
use tauri ::updater ::UpdateResponse ;
2025-11-24 11:37:18 +00:00
use tauri ::{ FileDropEvent , UpdaterEvent , RunEvent , Manager , PathResolver , Window , WindowEvent } ;
2025-01-01 14:49:27 +00:00
use tauri ::api ::dialog ::blocking ::FileDialogBuilder ;
2025-11-24 11:37:18 +00:00
use tokio ::sync ::broadcast ;
2024-11-05 20:39:21 +00:00
use tokio ::time ;
use crate ::api_token ::APIToken ;
use crate ::dotnet ::stop_dotnet_server ;
2025-09-25 17:47:18 +00:00
use crate ::environment ::{ is_prod , is_dev , CONFIG_DIRECTORY , DATA_DIRECTORY } ;
2024-11-05 20:39:21 +00:00
use crate ::log ::switch_to_file_logging ;
2025-05-03 10:20:22 +00:00
use crate ::pdfium ::PDFIUM_LIB_PATH ;
2024-11-05 20:39:21 +00:00
/// The Tauri main window.
static MAIN_WINDOW : Lazy < Mutex < Option < Window > > > = Lazy ::new ( | | Mutex ::new ( None ) ) ;
/// The update response coming from the Tauri updater.
static CHECK_UPDATE_RESPONSE : Lazy < Mutex < Option < UpdateResponse < tauri ::Wry > > > > = Lazy ::new ( | | Mutex ::new ( None ) ) ;
2025-11-24 11:37:18 +00:00
/// The event broadcast sender for Tauri events.
static EVENT_BROADCAST : Lazy < Mutex < Option < broadcast ::Sender < Event > > > > = Lazy ::new ( | | Mutex ::new ( None ) ) ;
2024-11-05 20:39:21 +00:00
/// Starts the Tauri app.
pub fn start_tauri ( ) {
info! ( " Starting Tauri app... " ) ;
2025-11-24 11:37:18 +00:00
// Create the event broadcast channel:
let ( event_sender , root_event_receiver ) = broadcast ::channel ( 100 ) ;
// Save a copy of the event broadcast sender for later use:
* EVENT_BROADCAST . lock ( ) . unwrap ( ) = Some ( event_sender . clone ( ) ) ;
// When the last receiver is dropped, we lose the ability to send events.
// Therefore, we spawn a task that keeps the root receiver alive:
tauri ::async_runtime ::spawn ( async move {
let mut root_receiver = root_event_receiver ;
loop {
match root_receiver . recv ( ) . await {
Ok ( event ) = > {
debug! ( Source = " Tauri " ; " Tauri event received: location=root receiver , event={event:?} " ) ;
} ,
Err ( broadcast ::error ::RecvError ::Lagged ( skipped ) ) = > {
warn! ( Source = " Tauri " ; " Root event receiver lagged, skipped {skipped} messages. " ) ;
} ,
Err ( broadcast ::error ::RecvError ::Closed ) = > {
warn! ( Source = " Tauri " ; " Root event receiver channel closed. " ) ;
return ;
} ,
}
}
} ) ;
2024-11-05 20:39:21 +00:00
let app = tauri ::Builder ::default ( )
. setup ( move | app | {
2025-11-24 11:37:18 +00:00
// Get the main window:
2024-11-05 20:39:21 +00:00
let window = app . get_window ( " main " ) . expect ( " Failed to get main window. " ) ;
2025-11-24 11:37:18 +00:00
// Register a callback for window events, such as file drops. We have to use
// this handler in addition to the app event handler, because file drop events
// are only available in the window event handler (is a bug, cf. https://github.com/tauri-apps/tauri/issues/14338):
window . on_window_event ( move | event | {
debug! ( Source = " Tauri " ; " Tauri event received: location=window event handler, event={event:?} " ) ;
let event_to_send = Event ::from_window_event ( event ) ;
let sender = event_sender . clone ( ) ;
tauri ::async_runtime ::spawn ( async move {
match sender . send ( event_to_send ) {
Ok ( _ ) = > { } ,
Err ( error ) = > error! ( Source = " Tauri " ; " Failed to channel window event: {error} " ) ,
}
} ) ;
} ) ;
// Save the main window for later access:
2024-11-05 20:39:21 +00:00
* MAIN_WINDOW . lock ( ) . unwrap ( ) = Some ( window ) ;
info! ( Source = " Bootloader Tauri " ; " Setup is running. " ) ;
2025-03-03 14:14:09 +00:00
let data_path = app . path_resolver ( ) . app_local_data_dir ( ) . unwrap ( ) ;
let data_path = data_path . join ( " data " ) ;
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
// Get and store the data and config directories:
2025-03-03 14:14:09 +00:00
DATA_DIRECTORY . set ( data_path . to_str ( ) . unwrap ( ) . to_string ( ) ) . map_err ( | _ | error! ( " Was not abe to set the data directory. " ) ) . unwrap ( ) ;
2024-11-05 20:39:21 +00:00
CONFIG_DIRECTORY . set ( app . path_resolver ( ) . app_config_dir ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) . to_string ( ) ) . map_err ( | _ | error! ( " Was not able to set the config directory. " ) ) . unwrap ( ) ;
2025-03-03 14:14:09 +00:00
info! ( Source = " Bootloader Tauri " ; " Reconfigure the file logger to use the app data directory {data_path:?} " ) ;
switch_to_file_logging ( data_path ) . map_err ( | e | error! ( " Failed to switch logging to file: {e} " ) ) . unwrap ( ) ;
2025-05-03 10:20:22 +00:00
set_pdfium_path ( app . path_resolver ( ) ) ;
2024-11-05 20:39:21 +00:00
Ok ( ( ) )
} )
. plugin ( tauri_plugin_window_state ::Builder ::default ( ) . build ( ) )
. build ( tauri ::generate_context! ( ) )
. expect ( " Error while running Tauri application " ) ;
2025-11-24 11:37:18 +00:00
// The app event handler:
app . run ( | app_handle , event | {
if ! matches! ( event , RunEvent ::MainEventsCleared ) {
debug! ( Source = " Tauri " ; " Tauri event received: location=app event handler , event={event:?} " ) ;
}
match event {
RunEvent ::WindowEvent { event , label , .. } = > {
match event {
WindowEvent ::CloseRequested { .. } = > {
warn! ( Source = " Tauri " ; " Window '{label}': close was requested. " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
WindowEvent ::Destroyed = > {
warn! ( Source = " Tauri " ; " Window '{label}': was destroyed. " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
_ = > ( ) ,
2024-11-05 20:39:21 +00:00
}
}
2025-11-24 11:37:18 +00:00
RunEvent ::Updater ( updater_event ) = > {
match updater_event {
UpdaterEvent ::UpdateAvailable { body , date , version } = > {
let body_len = body . len ( ) ;
info! ( Source = " Tauri " ; " Updater: update available: body size={body_len} time={date:?} version={version} " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
UpdaterEvent ::Pending = > {
info! ( Source = " Tauri " ; " Updater: update is pending! " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
UpdaterEvent ::DownloadProgress { chunk_length , content_length : _ } = > {
trace! ( Source = " Tauri " ; " Updater: downloading chunk of {chunk_length} bytes " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
UpdaterEvent ::Downloaded = > {
info! ( Source = " Tauri " ; " Updater: update has been downloaded! " ) ;
warn! ( Source = " Tauri " ; " Try to stop the .NET server now... " ) ;
2025-09-25 17:47:18 +00:00
2025-11-24 11:37:18 +00:00
if is_prod ( ) {
stop_dotnet_server ( ) ;
} else {
warn! ( Source = " Tauri " ; " Development environment detected; do not stop the .NET server. " ) ;
}
2025-09-25 17:47:18 +00:00
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
UpdaterEvent ::Updated = > {
info! ( Source = " Tauri " ; " Updater: app has been updated " ) ;
warn! ( Source = " Tauri " ; " Try to restart the app now... " ) ;
2025-09-25 17:47:18 +00:00
2025-11-24 11:37:18 +00:00
if is_prod ( ) {
app_handle . restart ( ) ;
} else {
warn! ( Source = " Tauri " ; " Development environment detected; do not restart the app. " ) ;
}
2025-09-25 17:47:18 +00:00
}
2025-11-24 11:37:18 +00:00
UpdaterEvent ::AlreadyUpToDate = > {
info! ( Source = " Tauri " ; " Updater: app is already up to date " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
UpdaterEvent ::Error ( error ) = > {
warn! ( Source = " Tauri " ; " Updater: failed to update: {error} " ) ;
}
2024-11-05 20:39:21 +00:00
}
2025-11-24 11:37:18 +00:00
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
RunEvent ::ExitRequested { .. } = > {
warn! ( Source = " Tauri " ; " Run event: exit was requested. " ) ;
2024-11-05 20:39:21 +00:00
}
2025-11-24 11:37:18 +00:00
RunEvent ::Ready = > {
info! ( Source = " Tauri " ; " Run event: Tauri app is ready. " ) ;
}
2024-11-05 20:39:21 +00:00
2025-11-24 11:37:18 +00:00
_ = > { }
2024-11-05 20:39:21 +00:00
}
} ) ;
warn! ( Source = " Tauri " ; " Tauri app was stopped. " ) ;
if is_prod ( ) {
warn! ( " Try to stop the .NET server as well... " ) ;
stop_dotnet_server ( ) ;
}
}
2025-11-24 11:37:18 +00:00
/// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client.
/// If no events are available for a certain time, we send a ping event to keep the connection alive.
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
/// The client is expected to reconnect automatically when the connection is closed and continue
/// listening for events.
#[ get( " /events " ) ]
pub async fn get_event_stream ( _token : APIToken ) -> TextStream ! [ String ] {
// Get the lock to the event broadcast sender:
let event_broadcast_lock = EVENT_BROADCAST . lock ( ) . unwrap ( ) ;
// Get and subscribe to the event receiver:
let mut event_receiver = event_broadcast_lock . as_ref ( )
. expect ( " Event sender not initialized. " )
. subscribe ( ) ;
// Drop the lock to allow other access to the sender:
drop ( event_broadcast_lock ) ;
// Create the event stream:
TextStream! {
loop {
// Wait at most 3 seconds for an event:
match time ::timeout ( Duration ::from_secs ( 3 ) , event_receiver . recv ( ) ) . await {
// Case: we received an event
Ok ( Ok ( event ) ) = > {
// Serialize the event to JSON. Important is that the entire event
// is serialized as a single line so that the client can parse it
// correctly:
let event_json = serde_json ::to_string ( & event ) . unwrap ( ) ;
yield event_json ;
// The client expects a newline after each event because we are using
// a method to read the stream line-by-line:
yield " \n " . to_string ( ) ;
} ,
// Case: we lagged behind and missed some events
Ok ( Err ( broadcast ::error ::RecvError ::Lagged ( skipped ) ) ) = > {
warn! ( Source = " Tauri " ; " Event receiver lagged, skipped {skipped} messages. " ) ;
} ,
// Case: the event channel was closed
Ok ( Err ( broadcast ::error ::RecvError ::Closed ) ) = > {
warn! ( Source = " Tauri " ; " Event receiver channel closed. " ) ;
return ;
} ,
// Case: timeout. We will send a ping event to keep the connection alive.
Err ( _ ) = > {
let ping_event = Event ::new ( TauriEventType ::Ping , Vec ::new ( ) ) ;
// Again, we have to serialize the event as a single line:
let event_json = serde_json ::to_string ( & ping_event ) . unwrap ( ) ;
yield event_json ;
// The client expects a newline after each event because we are using
// a method to read the stream line-by-line:
yield " \n " . to_string ( ) ;
} ,
}
}
}
}
/// Data structure representing a Tauri event for our event API.
#[ derive(Debug, Clone, Serialize) ]
pub struct Event {
pub event_type : TauriEventType ,
pub payload : Vec < String > ,
}
/// Implementation of the Event struct.
impl Event {
/// Creates a new Event instance.
pub fn new ( event_type : TauriEventType , payload : Vec < String > ) -> Self {
Event {
payload ,
event_type ,
}
}
/// Creates an Event instance from a Tauri WindowEvent.
pub fn from_window_event ( window_event : & WindowEvent ) -> Self {
match window_event {
WindowEvent ::FileDrop ( drop_event ) = > {
match drop_event {
FileDropEvent ::Hovered ( files ) = > Event ::new ( TauriEventType ::FileDropHovered ,
files . iter ( ) . map ( | f | f . to_string_lossy ( ) . to_string ( ) ) . collect ( ) ,
) ,
FileDropEvent ::Dropped ( files ) = > Event ::new ( TauriEventType ::FileDropDropped ,
files . iter ( ) . map ( | f | f . to_string_lossy ( ) . to_string ( ) ) . collect ( ) ,
) ,
FileDropEvent ::Cancelled = > Event ::new ( TauriEventType ::FileDropCanceled ,
Vec ::new ( ) ,
) ,
_ = > Event ::new ( TauriEventType ::Unknown ,
Vec ::new ( ) ,
) ,
}
} ,
WindowEvent ::Focused ( state ) = > if * state {
Event ::new ( TauriEventType ::WindowFocused ,
Vec ::new ( ) ,
)
} else {
Event ::new ( TauriEventType ::WindowNotFocused ,
Vec ::new ( ) ,
)
} ,
_ = > Event ::new ( TauriEventType ::Unknown ,
Vec ::new ( ) ,
) ,
}
}
}
/// The types of Tauri events we can send through our event API.
#[ derive(Debug, Serialize, Clone) ]
pub enum TauriEventType {
None ,
Ping ,
Unknown ,
WindowFocused ,
WindowNotFocused ,
FileDropHovered ,
FileDropDropped ,
FileDropCanceled ,
}
2024-11-05 20:39:21 +00:00
/// Changes the location of the main window to the given URL.
pub async fn change_location_to ( url : & str ) {
// Try to get the main window. If it is not available yet, wait for it:
let mut main_window_ready = false ;
let mut main_window_status_reported = false ;
let main_window_spawn_clone = & MAIN_WINDOW ;
while ! main_window_ready
{
main_window_ready = {
let main_window = main_window_spawn_clone . lock ( ) . unwrap ( ) ;
main_window . is_some ( )
} ;
if ! main_window_ready {
if ! main_window_status_reported {
info! ( " Waiting for main window to be ready, because .NET was faster than Tauri. " ) ;
main_window_status_reported = true ;
}
time ::sleep ( Duration ::from_millis ( 100 ) ) . await ;
}
}
let js_location_change = format! ( " window.location = ' {url} '; " ) ;
let main_window = main_window_spawn_clone . lock ( ) . unwrap ( ) ;
let location_change_result = main_window . as_ref ( ) . unwrap ( ) . eval ( js_location_change . as_str ( ) ) ;
match location_change_result {
Ok ( _ ) = > info! ( " The app location was changed to {url}. " ) ,
Err ( e ) = > error! ( " Failed to change the app location to {url}: {e}. " ) ,
}
}
/// Checks for updates.
#[ get( " /updates/check " ) ]
pub async fn check_for_update ( _token : APIToken ) -> Json < CheckUpdateResponse > {
2025-09-25 17:47:18 +00:00
if is_dev ( ) {
warn! ( Source = " Updater " ; " The app is running in development mode; skipping update check. " ) ;
return Json ( CheckUpdateResponse {
update_is_available : false ,
error : false ,
new_version : String ::from ( " " ) ,
changelog : String ::from ( " " ) ,
} ) ;
}
2024-11-05 20:39:21 +00:00
let app_handle = MAIN_WINDOW . lock ( ) . unwrap ( ) . as_ref ( ) . unwrap ( ) . app_handle ( ) ;
let response = app_handle . updater ( ) . check ( ) . await ;
match response {
Ok ( update_response ) = > match update_response . is_update_available ( ) {
true = > {
* CHECK_UPDATE_RESPONSE . lock ( ) . unwrap ( ) = Some ( update_response . clone ( ) ) ;
let new_version = update_response . latest_version ( ) ;
info! ( Source = " Updater " ; " An update to version '{new_version}' is available. " ) ;
let changelog = update_response . body ( ) ;
Json ( CheckUpdateResponse {
update_is_available : true ,
error : false ,
new_version : new_version . to_string ( ) ,
changelog : match changelog {
Some ( c ) = > c . to_string ( ) ,
None = > String ::from ( " " ) ,
} ,
} )
} ,
false = > {
info! ( Source = " Updater " ; " No updates are available. " ) ;
Json ( CheckUpdateResponse {
update_is_available : false ,
error : false ,
new_version : String ::from ( " " ) ,
changelog : String ::from ( " " ) ,
} )
} ,
} ,
Err ( e ) = > {
warn! ( Source = " Updater " ; " Failed to check for updates: {e}. " ) ;
Json ( CheckUpdateResponse {
update_is_available : false ,
error : true ,
new_version : String ::from ( " " ) ,
changelog : String ::from ( " " ) ,
} )
} ,
}
}
/// The response to the check for update request.
#[ derive(Serialize) ]
pub struct CheckUpdateResponse {
update_is_available : bool ,
error : bool ,
new_version : String ,
changelog : String ,
}
/// Installs the update.
#[ get( " /updates/install " ) ]
pub async fn install_update ( _token : APIToken ) {
2025-09-25 17:47:18 +00:00
if is_dev ( ) {
warn! ( Source = " Updater " ; " The app is running in development mode; skipping update installation. " ) ;
return ;
}
2024-11-05 20:39:21 +00:00
let cloned_response_option = CHECK_UPDATE_RESPONSE . lock ( ) . unwrap ( ) . clone ( ) ;
match cloned_response_option {
Some ( update_response ) = > {
update_response . download_and_install ( ) . await . unwrap ( ) ;
} ,
None = > {
error! ( Source = " Updater " ; " No update available to install. Did you check for updates first? " ) ;
} ,
}
2025-01-01 14:49:27 +00:00
}
/// Let the user select a directory.
#[ post( " /select/directory?<title> " , data = " <previous_directory> " ) ]
pub fn select_directory ( _token : APIToken , title : & str , previous_directory : Option < Json < PreviousDirectory > > ) -> Json < DirectorySelectionResponse > {
let folder_path = match previous_directory {
Some ( previous ) = > {
let previous_path = previous . path . as_str ( ) ;
FileDialogBuilder ::new ( )
. set_title ( title )
. set_directory ( previous_path )
. pick_folder ( )
} ,
None = > {
FileDialogBuilder ::new ( )
. set_title ( title )
. pick_folder ( )
} ,
} ;
match folder_path {
Some ( path ) = > {
info! ( " User selected directory: {path:?} " ) ;
Json ( DirectorySelectionResponse {
user_cancelled : false ,
selected_directory : path . to_str ( ) . unwrap ( ) . to_string ( ) ,
} )
} ,
None = > {
info! ( " User cancelled directory selection. " ) ;
Json ( DirectorySelectionResponse {
user_cancelled : true ,
selected_directory : String ::from ( " " ) ,
} )
} ,
}
}
#[ derive(Clone, Deserialize) ]
pub struct PreviousDirectory {
path : String ,
}
2025-05-03 13:43:12 +00:00
#[ derive(Clone, Deserialize) ]
pub struct FileTypeFilter {
filter_name : String ,
filter_extensions : Vec < String > ,
}
#[ derive(Clone, Deserialize) ]
pub struct SelectFileOptions {
title : String ,
previous_file : Option < PreviousFile > ,
filter : Option < FileTypeFilter > ,
}
2025-11-11 14:30:17 +00:00
#[ derive(Clone, Deserialize) ]
pub struct SaveFileOptions {
title : String ,
name_file : Option < PreviousFile > ,
filter : Option < FileTypeFilter > ,
}
2025-01-01 14:49:27 +00:00
#[ derive(Serialize) ]
pub struct DirectorySelectionResponse {
user_cancelled : bool ,
selected_directory : String ,
2025-01-13 18:51:26 +00:00
}
/// Let the user select a file.
2025-05-03 13:43:12 +00:00
#[ post( " /select/file " , data = " <payload> " ) ]
pub fn select_file ( _token : APIToken , payload : Json < SelectFileOptions > ) -> Json < FileSelectionResponse > {
// Create a new file dialog builder:
let file_dialog = FileDialogBuilder ::new ( ) ;
// Set the title of the file dialog:
let file_dialog = file_dialog . set_title ( & payload . title ) ;
// Set the file type filter if provided:
let file_dialog = match & payload . filter {
Some ( filter ) = > {
file_dialog . add_filter ( & filter . filter_name , & filter . filter_extensions . iter ( ) . map ( | s | s . as_str ( ) ) . collect ::< Vec < & str > > ( ) )
} ,
None = > file_dialog ,
} ;
// Set the previous file path if provided:
let file_dialog = match & payload . previous_file {
2025-01-13 18:51:26 +00:00
Some ( previous ) = > {
let previous_path = previous . file_path . as_str ( ) ;
2025-05-03 13:43:12 +00:00
file_dialog . set_directory ( previous_path )
2025-01-13 18:51:26 +00:00
} ,
2025-05-03 13:43:12 +00:00
None = > file_dialog ,
2025-01-13 18:51:26 +00:00
} ;
2025-05-03 13:43:12 +00:00
// Show the file dialog and get the selected file path:
let file_path = file_dialog . pick_file ( ) ;
2025-01-13 18:51:26 +00:00
match file_path {
Some ( path ) = > {
info! ( " User selected file: {path:?} " ) ;
Json ( FileSelectionResponse {
user_cancelled : false ,
selected_file_path : path . to_str ( ) . unwrap ( ) . to_string ( ) ,
} )
} ,
None = > {
info! ( " User cancelled file selection. " ) ;
Json ( FileSelectionResponse {
user_cancelled : true ,
selected_file_path : String ::from ( " " ) ,
} )
} ,
}
}
2025-12-16 18:14:27 +00:00
/// Let the user select some files.
#[ post( " /select/files " , data = " <payload> " ) ]
pub fn select_files ( _token : APIToken , payload : Json < SelectFileOptions > ) -> Json < FilesSelectionResponse > {
// Create a new file dialog builder:
let file_dialog = FileDialogBuilder ::new ( ) ;
// Set the title of the file dialog:
let file_dialog = file_dialog . set_title ( & payload . title ) ;
// Set the file type filter if provided:
let file_dialog = match & payload . filter {
Some ( filter ) = > {
file_dialog . add_filter ( & filter . filter_name , & filter . filter_extensions . iter ( ) . map ( | s | s . as_str ( ) ) . collect ::< Vec < & str > > ( ) )
} ,
None = > file_dialog ,
} ;
// Set the previous file path if provided:
let file_dialog = match & payload . previous_file {
Some ( previous ) = > {
let previous_path = previous . file_path . as_str ( ) ;
file_dialog . set_directory ( previous_path )
} ,
None = > file_dialog ,
} ;
// Show the file dialog and get the selected file path:
let file_paths = file_dialog . pick_files ( ) ;
match file_paths {
Some ( paths ) = > {
info! ( " User selected {} files. " , paths . len ( ) ) ;
Json ( FilesSelectionResponse {
user_cancelled : false ,
selected_file_paths : paths . iter ( ) . map ( | p | p . to_str ( ) . unwrap ( ) . to_string ( ) ) . collect ( ) ,
} )
}
None = > {
info! ( " User cancelled file selection. " ) ;
Json ( FilesSelectionResponse {
user_cancelled : true ,
selected_file_paths : Vec ::new ( ) ,
} )
} ,
}
}
2025-11-11 14:30:17 +00:00
#[ post( " /save/file " , data = " <payload> " ) ]
pub fn save_file ( _token : APIToken , payload : Json < SaveFileOptions > ) -> Json < FileSaveResponse > {
// Create a new file dialog builder:
let file_dialog = FileDialogBuilder ::new ( ) ;
// Set the title of the file dialog:
let file_dialog = file_dialog . set_title ( & payload . title ) ;
// Set the file type filter if provided:
let file_dialog = match & payload . filter {
Some ( filter ) = > {
file_dialog . add_filter ( & filter . filter_name , & filter . filter_extensions . iter ( ) . map ( | s | s . as_str ( ) ) . collect ::< Vec < & str > > ( ) )
} ,
None = > file_dialog ,
} ;
// Set the previous file path if provided:
let file_dialog = match & payload . name_file {
Some ( previous ) = > {
let previous_path = previous . file_path . as_str ( ) ;
file_dialog . set_directory ( previous_path )
} ,
None = > file_dialog ,
} ;
// Displays the file dialogue box and select the file:
let file_path = file_dialog . save_file ( ) ;
match file_path {
Some ( path ) = > {
info! ( " User selected file for writing operation: {path:?} " ) ;
Json ( FileSaveResponse {
user_cancelled : false ,
save_file_path : path . to_str ( ) . unwrap ( ) . to_string ( ) ,
} )
} ,
None = > {
info! ( " User cancelled file selection. " ) ;
Json ( FileSaveResponse {
user_cancelled : true ,
save_file_path : String ::from ( " " ) ,
} )
} ,
}
}
2025-01-13 18:51:26 +00:00
#[ derive(Clone, Deserialize) ]
pub struct PreviousFile {
file_path : String ,
}
#[ derive(Serialize) ]
pub struct FileSelectionResponse {
user_cancelled : bool ,
selected_file_path : String ,
2025-05-02 21:09:50 +00:00
}
2025-12-16 18:14:27 +00:00
#[ derive(Serialize) ]
pub struct FilesSelectionResponse {
user_cancelled : bool ,
selected_file_paths : Vec < String > ,
}
2025-11-11 14:30:17 +00:00
#[ derive(Serialize) ]
pub struct FileSaveResponse {
user_cancelled : bool ,
save_file_path : String ,
}
2025-05-02 21:09:50 +00:00
2025-05-03 10:20:22 +00:00
fn set_pdfium_path ( path_resolver : PathResolver ) {
let pdfium_relative_source_path = String ::from ( " resources/libraries/ " ) ;
2025-05-02 21:09:50 +00:00
let pdfium_source_path = path_resolver . resolve_resource ( pdfium_relative_source_path ) ;
if pdfium_source_path . is_none ( ) {
2025-05-03 10:20:22 +00:00
error! ( Source = " Bootloader Tauri " ; " Failed to set the PDFium library path. " ) ;
2025-05-02 21:09:50 +00:00
return ;
}
let pdfium_source_path = pdfium_source_path . unwrap ( ) ;
2025-05-03 10:20:22 +00:00
let pdfium_source_path = pdfium_source_path . to_str ( ) . unwrap ( ) . to_string ( ) ;
* PDFIUM_LIB_PATH . lock ( ) . unwrap ( ) = Some ( pdfium_source_path . clone ( ) ) ;
2024-11-05 20:39:21 +00:00
}