diff --git a/package.json b/package.json index 937d380..cb115a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fanslysync-desktop", - "version": "0.1.7", + "version": "0.2.0", "private": true, "scripts": { "dev": "vite dev", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 851ac6b..627fa5b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,7 @@ tauri-build = { version = "2.0.0", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "2.4.1", features = [] } +tauri = { version = "2.4.1", features = ["tray-icon"] } dirs = "5.0.1" reqwest = { version = "0.11.18", features = ["json", "multipart"] } lazy_static = "1.5.0" @@ -28,7 +28,7 @@ tauri-plugin-dialog = { version = "2.2.1" } tauri-plugin-clipboard-manager = { version = "2.2.1" } tauri-plugin-notification = { version = "2.2.1" } tauri-plugin-updater = { version = "2.2.1" } -tauri-plugin-log = { version = "2.2.1" } +tauri-plugin-log = { version = "2.2.1" } log = "0.4.27" thiserror = "2.0.12" @@ -36,7 +36,7 @@ thiserror = "2.0.12" # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes. # DO NOT REMOVE!! -custom-protocol = [ "tauri/custom-protocol" ] +custom-protocol = ["tauri/custom-protocol"] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2.3.0" diff --git a/src-tauri/src/handlers/fansly/mod.rs b/src-tauri/src/handlers/fansly/mod.rs index 760106c..108a94c 100644 --- a/src-tauri/src/handlers/fansly/mod.rs +++ b/src-tauri/src/handlers/fansly/mod.rs @@ -1,4 +1,3 @@ -use lazy_static::lazy::Lazy; use lazy_static::lazy_static; // Create a simple module for handling the Fansly API, using reqwest to make requests to the API. // This module will contain a struct Fansly, which will have a method to get the user's profile information. @@ -9,8 +8,8 @@ use crate::structs::{ use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use tokio::sync::Mutex; use thiserror::Error; +use tokio::sync::Mutex; // Create a PROGRESS mutex to hold the current sync progress, lazy initialized lazy_static! { @@ -39,6 +38,11 @@ pub struct PasteResponse { payload: PasteData, } +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +struct PasteRequest { + content: String, +} + pub struct Fansly { client: reqwest::Client, token: Option, @@ -48,9 +52,6 @@ pub struct Fansly { pub enum UploadError { #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), - - #[error("Failed to get UUID from paste.hep.gg URL")] - MissingUuid, } impl Fansly { @@ -230,44 +231,58 @@ impl Fansly { p.complete = complete; } - async fn upload_sync_data(&self, data: SyncDataResponse) -> Result { - let url = "https://paste.hep.gg/"; + let url = "https://paste.hep.gg/api/"; - // Convert passed data to bytes - let json_string = serde_json::to_string(&data).unwrap(); + // Make an JSON object with our raw data + let paste_data = PasteRequest { + content: serde_json::to_string(&data).unwrap(), + }; - let form = reqwest::multipart::Form::new() - .text("content", json_string); + let paste_data_str = serde_json::to_string(&paste_data).unwrap(); + let est_upload_size = paste_data_str.len() / 1024; // in KB + + log::info!( + "Uploading sync data to paste.hep.gg (size: {} KB)", + est_upload_size + ); // Create a new client and POST let response = self .client .post(url) - .multipart(form) + .body(paste_data_str) + .header("Content-Type", "application/json") .send() .await?; if !response.status().is_success() { - log::error!("Failed to upload sync data..."); - log::info!("Response: {:?}", response); - return Err(UploadError::from(response.error_for_status().unwrap_err())); + let status_code = response.status(); + let err = response.error_for_status_ref().unwrap_err(); + let response_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + + log::error!( + "Failed to upload sync data to paste.hep.gg. Status code: {}, Response: {}", + status_code, + response_text + ); + + return Err(UploadError::Http(err)); } log::info!("Uploaded sync data successfully."); - // Get the response URL from the response - let url = response.url(); + // Parse the response + let paste_response: PasteResponse = response.json().await?; - // Grab the UUID from the URL - let uuid = url.path_segments() - .and_then(|segments| segments.last()) - .ok_or(UploadError::MissingUuid)?; + // Return the paste URL + let paste_url = format!("https://paste.hep.gg/api/{}/raw", paste_response.payload.id); + log::info!("Paste URL: {}", paste_url); - log::info!("Sync data uploaded to paste.hep.gg with UUID: {}", uuid); - - // Return the URL of the uploaded data - Ok(format!("https://paste.hep.gg/api/{}/raw", uuid)) + Ok(paste_url) } pub async fn upload_auto_sync_data( @@ -341,7 +356,8 @@ impl Fansly { pub async fn sync(&mut self, auto: bool) -> Result { // Reset progress - self.update_progress("Starting Sync".to_string(), 0, 100, false).await; + self.update_progress("Starting Sync".to_string(), 0, 100, false) + .await; // Fetch profile log::info!("[sync::process] Fetching profile..."); @@ -357,10 +373,14 @@ impl Fansly { let total_followers = account.follow_count; let total_subscribers = account.subscriber_count; - log::info!("[sync::process] Account ID: {}, Followers: {}, Subscribers: {}", - account.id, total_followers, total_subscribers); + log::info!( + "[sync::process] Account ID: {}, Followers: {}, Subscribers: {}", + account.id, + total_followers, + total_subscribers + ); - let mut followers: Vec = Vec::new(); + let mut followers: Vec = Vec::new(); let mut subscribers: Vec = Vec::new(); log::info!("[sync::process] Fetching followers..."); @@ -371,7 +391,9 @@ impl Fansly { while followers.len() < total_followers as usize { log::info!( "[sync::process] Fetching followers for account {} with offset {} (total: {})", - account.id, offset, total_followers + account.id, + offset, + total_followers ); let response = self .fetch_followers(&account.id, &self.token.as_ref().unwrap(), offset) @@ -382,7 +404,13 @@ impl Fansly { "[sync::process] Got {} followers from API.", response.response.len() ); - followers.extend(response.response.clone()); + + + // Collect followers + for follower in response.response.clone() { + followers.push(follower.follower_id); + } + offset += 100; total_requests += 1; @@ -391,8 +419,9 @@ impl Fansly { "Fetching Followers".to_string(), followers.len() as u32, total_followers as u32, - false - ).await; + false, + ) + .await; // Every 10 requests, sleep for a bit to avoid rate limiting if total_requests % 10 == 0 { @@ -400,7 +429,7 @@ impl Fansly { } // If we've received no followers, break the loop - if response.response.is_empty() { + if response.clone().response.is_empty() { log::info!("[sync::process] No more followers found, breaking the loop."); break; } @@ -411,7 +440,9 @@ impl Fansly { while subscribers.len() < total_subscribers as usize { log::info!( "[sync::process] Fetching subscribers with offset {} for account {} (total: {})", - offset, account.id, total_subscribers + offset, + account.id, + total_subscribers ); let response = self @@ -428,8 +459,9 @@ impl Fansly { "Fetching Subscribers".to_string(), subscribers.len() as u32, total_subscribers as u32, - false - ).await; + false, + ) + .await; // Every 10 requests, sleep for a bit to avoid rate limiting if total_requests % 10 == 0 { @@ -452,7 +484,8 @@ impl Fansly { log::info!("[sync::process] Sync complete."); // Reset progress - self.update_progress("Sync Complete".to_string(), 100, 100, true).await; + self.update_progress("Sync Complete".to_string(), 100, 100, true) + .await; log::info!("[sync::process] Uploading sync data to paste.hep.gg for processing..."); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9ace7c5..1b412ab 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -10,10 +10,15 @@ use std::io; use commands::config::{get_config, init_config, save_config}; use commands::fansly::{ - fansly_check_sync_token, fansly_get_me, fansly_set_token, fansly_sync, - fansly_upload_auto_sync_data, fansly_get_sync_status + fansly_check_sync_token, fansly_get_me, fansly_get_sync_status, fansly_set_token, fansly_sync, + fansly_upload_auto_sync_data, }; use commands::utils::quit; +use tauri::menu::Menu; +use tauri::menu::MenuItem; +use tauri::tray::TrayIconBuilder; +use tauri::AppHandle; +use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_log::{Target, TargetKind}; @@ -32,9 +37,55 @@ fn get_log_path() -> io::Result { Ok(config_dir.to_string_lossy().to_string()) } +fn handle_menu(app: &tauri::AppHandle, event: &tauri::menu::MenuEvent) { + match event.id().as_ref() { + "quit" => { + app.exit(0); + } + "show_window" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + _ => {} + } +} + #[tokio::main] async fn main() { tauri::Builder::default() + .setup(|app| { + // Setup menu items for the tray + let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let show_window_i = + MenuItem::with_id(app, "show_window", "Show Window", true, None::<&str>)?; + + // Create our Menu and add the items to it + let menu = Menu::with_items(app, &[&quit_i, &show_window_i])?; + + // Create our Tray using TrayIconBuilder and add the menu to it + TrayIconBuilder::new() + .icon(app.default_window_icon().unwrap().clone()) + .title("FanslySync") + .tooltip("FanslySync") + .menu(&menu) + .show_menu_on_left_click(true) + .on_menu_event(|app: &AppHandle, event: tauri::menu::MenuEvent| { + handle_menu(app, &event) + }) + .build(app)?; + + Ok(()) + }) + .on_window_event(|app, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + if let Some(window) = app.get_webview_window("main") { + let _ = window.hide(); + api.prevent_close(); + } + } + }) .plugin(tauri_plugin_autostart::init( MacosLauncher::LaunchAgent, None, diff --git a/src-tauri/src/structs/mod.rs b/src-tauri/src/structs/mod.rs index 17c7b0a..7bdabb9 100644 --- a/src-tauri/src/structs/mod.rs +++ b/src-tauri/src/structs/mod.rs @@ -3,7 +3,7 @@ use serde_json::Value; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SyncDataResponse { - pub followers: Vec, + pub followers: Vec, pub subscribers: Vec, pub sync_data_url: String, } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 504fbb0..753dd78 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -42,7 +42,7 @@ "createUpdaterArtifacts": true }, "productName": "FanslySync", - "version": "0.1.7", + "version": "0.2.0", "identifier": "com.fanslycreatorbot.fanslysync", "plugins": { "updater": { diff --git a/src/routes/home/+page.svelte b/src/routes/home/+page.svelte index 3d9c3cf..26c5f57 100644 --- a/src/routes/home/+page.svelte +++ b/src/routes/home/+page.svelte @@ -1,930 +1,573 @@ -
- - {#if isAutoSyncConfigModalOpen} -
+ +
+
+ FanslySync +
+

FanslySync

+ + v{versionData.appVersion} · Tauri {versionData.tauriVersion} + +
+
+
+ + +
+ + {#if loadingSync} +
+ + -

- How often should the app sync data automatically? Please enter a number in hours. The - minimum value is 1 hour. -

- -
- - - {#if !autoSyncConfigState.validatingToken && autoSyncConfigState.tokenValid} - - - - {:else if !autoSyncConfigState.validatingToken && !autoSyncConfigState.tokenValid} - - - - {:else if autoSyncConfigState.validatingToken} - - - - {/if} -
-

- Enter your sync token here. A green tick will be displayed if the token is valid, a red - cross if it's invalid, and a spinner if it's validating. Please ensure you have a valid - sync token set or automatic sync will not work. -

-
-
+ {:else if config === null} +
+ + + + We couldn't load your config or your configuration doesn't exist. +

+ Please restart the app or use the button below to reconfigure. +

+ +
+ {:else if config !== null && !loadingSync} + +
+ +
+
+

Automatic Sync

+ - {canSave ? 'Save' : 'Please enter a valid sync token'} + {config?.auto_sync_enabled ? 'Enabled' : 'Disabled'} + +
+

+ We'll automatically sync your data {config?.sync_interval} + {config?.sync_interval === 1 ? 'hour' : 'hours'} to your configured discord server. +

+
+
+ + +
+

Manual Sync

+

Trigger a sync immediately.

+ +
+
+ {/if} +
+ + + {#if syncState.show} +
+
+ +
+ + +
+ + +
+ {#if syncState.syncing} + + + +
+

Syncing… {syncProgress.percentage}%

+

+ {syncProgress.current_count} / {syncProgress.total_count} • {syncProgress.currentStep} +

+
+ {:else if syncState.success} +
+

Sync Successful!

+

Use Copy to grab URL.

+
+
+ + +
+ {:else} +
+

Sync Failed!

+

{syncState.message}

+
+ + {/if} +
{/if} - -
-
- FanslySync -

FanslySync

- v{versionData.appVersion} (runtime: {versionData.tauriVersion}) - {#if upToDate === false} - - {:else if upToDate === true} - Up to date! - {/if} -
- -
- - -
-

Sync

-

Manage automatic sync options and manual sync here.

-
- {#if loadingSync} -
-
- - Loading... -
- -

Loading sync options, one moment...

-
- {:else} -
- -
-
-
-

Automatic Sync

- - - {config?.auto_sync_enabled ? 'Enabled' : 'Disabled'} - -
- -

- Sync content automatically every {config?.sync_interval} - {(config?.sync_interval ?? 0 > 1) ? 'hour' : 'hours'}. Please ensure you have a - stable internet connection. -

-
- - -
-
-
- - -
-

Manual Sync

-

- Trigger a manual sync now, instead of waiting for an automatic sync. -

-
- -
-
-
- {/if} -
-
- - - {#if syncState.show} + + {#if isAutoSyncConfigModalOpen}
-
-
- -
- {#if !syncState.success && !syncState.error} - + class="bg-zinc-800 p-6 rounded-lg w-full max-w-md" + in:fly={{ y: 20 }} + out:fly={{ y: 20 }} + > +

+ + + Auto Sync Settings +

- -
-

Syncing...

-

- We are currently {syncProgress.currentStep}. We've processed {syncProgress.current_count} of {syncProgress.total_count} items so far. We - are about {syncProgress.percentage}% done with this step. -

-
- {:else if syncState.success} - -
-

Sync Successful!

-

- Data synced successfully. Use the copy button to copy the sync URL to your clipboard. +

+ +
+ + +

+ How often (in hours) the app will automatically sync.

- -
- - -
- {:else} - -
-

Sync Failed!

-

- An error occurred while syncing your data. Details: {syncState.message} + +

+ +
+ + + {#if autoSyncConfigState.validatingToken} + + + + + {:else if autoSyncConfigState.tokenValid} + + + + + {:else} + + + + + {/if} +
+

+ Enter your Fansly sync-token. We’ll validate it before saving.

+
- +
- {/if} + +
{/if} diff --git a/src/routes/home/debug.log b/src/routes/home/debug.log deleted file mode 100644 index 47dc41e..0000000 --- a/src/routes/home/debug.log +++ /dev/null @@ -1,2 +0,0 @@ -[0813/143825.367:ERROR:registration_protocol_win.cc(108)] CreateFile: The system cannot find the file specified. (0x2) -[0813/143918.868:ERROR:registration_protocol_win.cc(108)] CreateFile: The system cannot find the file specified. (0x2) diff --git a/src/routes/setup/+page.svelte b/src/routes/setup/+page.svelte index aa8d70c..31c506a 100644 --- a/src/routes/setup/+page.svelte +++ b/src/routes/setup/+page.svelte @@ -1,265 +1,194 @@ -
- -
+
+
{#if loading} -
-
+ + + - Loading... +

Loading setup...

- -

Setup is loading...

- {/if} - - {#if errored} - - - - -

Oops!

-

- An error was encountered while initializing the setup, please try closing and reopening - FanslySync. -

- {/if} - - {#if !loading && !errored} + {:else if errored} +
+ + + +

Oops!

+

An error occurred during setup. Please restart the application.

+
+ {:else} + {#if step === 0} -

Welcome to FanslySync!

-

- Because this is your first time running FanslySync, we need to set up the connection to - your Fansly account. Click the button below to get started. -

- - {#if errored} -

- An error occurred while initializing the setup. Please try again. +

+

Welcome to FanslySync!

+

+ Since this is your first time running FanslySync, we need to connect to your Fansly + account.

- {/if} - - {#if !loading} - {/if} - {:else if step === 1} -

Authenticate with Fansly

-

- To establish a secure connection with Fansly, we require your Fansly Authentication Token. - We do not transmit this token to our servers and it is only used to authenticate with - Fansly and fetch data locally.

- For more information on how to get your Fansly Authentication Token, please visit our documentation, - or join our Discord server if you need help or have any questions. -

- - - - - {#if validationErrors.fanslyToken !== ''} -

{validationErrors.fanslyToken}

- {/if} - - {#if !loading} - - {/if} - {:else if step == 2} -
- - Loading...
-

We are working our magic!

-

- We are now authenticating with Fansly and fetching your data. This may take a few moments. -

-

- {status} -

- {:else if step === 3} - - - -

Setup Complete!

-

- You're all set! FanslySync is now connected to your Fansly account and is ready to use. -

+ + {:else if step === 1} +
+

Authenticate with Fansly

+

+ Enter your Fansly Authentication Token. We use it only locally to fetch data. +

+
+ + + {#if validationErrors.fanslyToken} +

{validationErrors.fanslyToken}

+ {/if} +
+ +
- + + {:else if step === 2} +
+ + + +

Processing…

+

{status}

+
+ + + {:else if step === 3} +
+ + + +

Setup Complete!

+

You’re now connected. Ready to go!

+ +
{/if} {/if}