Compare commits

..
2 Commits
Author SHA1 Message Date
Lncvrt fb44650382 Fix bg 2026-04-15 13:27:27 -07:00
Lncvrt 71b1986180 Update binaries 2026-04-15 13:27:27 -07:00
9 changed files with 75 additions and 149 deletions

No files matched your search

-3
View File
@@ -15,7 +15,6 @@
"@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-shell": "~2", "@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-window-state": "~2",
"pretty-bytes": "^7.1.0", "pretty-bytes": "^7.1.0",
"react": "19.2.5", "react": "19.2.5",
"react-dom": "19.2.5", "react-dom": "19.2.5",
@@ -159,8 +158,6 @@
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="],
"@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
-1
View File
@@ -20,7 +20,6 @@
"@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "~2", "@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-shell": "~2", "@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-window-state": "~2",
"pretty-bytes": "^7.1.0", "pretty-bytes": "^7.1.0",
"react": "19.2.5", "react": "19.2.5",
"react-dom": "19.2.5", "react-dom": "19.2.5",
-5
View File
@@ -20,7 +20,6 @@ walkdir = "2.5.0"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
tauri-plugin-prevent-default = "4.0.4"
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
window-vibrancy = "0.7.1" window-vibrancy = "0.7.1"
@@ -28,10 +27,6 @@ window-vibrancy = "0.7.1"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
windows-version = "0.1.7" windows-version = "0.1.7"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2"
tauri-plugin-window-state = "2"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
lto = "thin" lto = "thin"
Binary file not shown.
Binary file not shown.
+5 -11
View File
@@ -2,19 +2,13 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": [
"main"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"shell:default", "shell:default",
"dialog:default", "dialog:default",
"opener:default", "opener:default"
{
"identifier": "opener:allow-open-path",
"allow": [
{
"path": "**"
}
]
}
] ]
} }
-14
View File
@@ -1,14 +0,0 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"window-state:default"
]
}
+36 -61
View File
@@ -1,17 +1,13 @@
use std::{collections::HashMap, fs, path::PathBuf, sync::Mutex}; use std::{fs, path::PathBuf};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sysinfo::Disks; use sysinfo::Disks;
use tauri::{Manager, State, WebviewUrl, WebviewWindowBuilder}; use tauri::{Emitter, WebviewUrl, WebviewWindowBuilder};
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use window_vibrancy::{NSVisualEffectMaterial, apply_vibrancy}; use window_vibrancy::{NSVisualEffectMaterial, apply_vibrancy};
struct AppState {
files: Mutex<HashMap<String, Vec<FileInfo>>>,
}
#[derive(Serialize)] #[derive(Serialize)]
struct DiskInfo { struct DiskInfo {
name: String, name: String,
@@ -29,6 +25,13 @@ pub struct FileInfo {
pub is_dir: bool, pub is_dir: bool,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct DirScanResult {
pub items: Vec<FileInfo>,
pub total_size: u64,
pub error: Option<String>,
}
#[tauri::command] #[tauri::command]
fn list_disks() -> Vec<DiskInfo> { fn list_disks() -> Vec<DiskInfo> {
Disks::new_with_refreshed_list() Disks::new_with_refreshed_list()
@@ -50,7 +53,6 @@ fn list_disks() -> Vec<DiskInfo> {
#[tauri::command] #[tauri::command]
async fn get_file_info_from_dirs( async fn get_file_info_from_dirs(
app: tauri::AppHandle, app: tauri::AppHandle,
state: State<'_, AppState>,
path: String, path: String,
min_ratio: String, min_ratio: String,
) -> Result<(), String> { ) -> Result<(), String> {
@@ -76,18 +78,20 @@ async fn get_file_info_from_dirs(
let json: serde_json::Value = let json: serde_json::Value =
serde_json::from_slice(&output.stdout).map_err(|e| e.to_string())?; serde_json::from_slice(&output.stdout).map_err(|e| e.to_string())?;
let mut local_map: HashMap<String, Vec<FileInfo>> = HashMap::new();
if let Some(tree) = json.get("tree") { if let Some(tree) = json.get("tree") {
stream(tree, "", &mut local_map); let mut batch = Vec::new();
stream(tree, "", &mut batch, &app);
} }
*state.files.lock().unwrap() = local_map;
Ok(()) Ok(())
} }
fn stream(node: &serde_json::Value, parent: &str, map: &mut HashMap<String, Vec<FileInfo>>) { fn stream(
node: &serde_json::Value,
parent: &str,
batch: &mut Vec<FileInfo>,
app: &tauri::AppHandle,
) {
let name = match node.get("name").and_then(|v| v.as_str()) { let name = match node.get("name").and_then(|v| v.as_str()) {
Some(n) => n, Some(n) => n,
None => return, None => return,
@@ -102,86 +106,58 @@ fn stream(node: &serde_json::Value, parent: &str, map: &mut HashMap<String, Vec<
format!("{}/{}", parent, name) format!("{}/{}", parent, name)
}; };
let is_dir = children.map_or(false, |c| !c.is_empty()); let is_dir = children.as_ref().map(|c| !c.is_empty()).unwrap_or(false);
map.entry(parent.to_string()).or_default().push(FileInfo { batch.push(FileInfo {
name: name.to_string(), name: name.to_string(),
path: path.clone(), path: path.clone(),
size, size,
is_dir, is_dir,
}); });
if batch.len() >= 200 {
let _ = app.emit("scan-batch", batch.clone());
batch.clear();
}
if let Some(children) = children { if let Some(children) = children {
for c in children { for c in children {
stream(c, &path, map); stream(c, &path, batch, app);
} }
} }
if parent.is_empty() && !batch.is_empty() {
let _ = app.emit("scan-batch", batch.clone());
}
} }
#[tauri::command] #[tauri::command]
fn get_files_in_dir(path: String, state: State<'_, AppState>) -> Vec<FileInfo> { fn remove_item(path: String) -> Result<(), String> {
state let path = PathBuf::from(path);
.files
.lock()
.unwrap()
.get(&path)
.cloned()
.unwrap_or_default()
}
#[tauri::command] if !path.exists() {
fn remove_item(path: String, state: State<'_, AppState>) -> Result<(), String> {
let path_buf = PathBuf::from(&path);
if !path_buf.exists() {
return Err("path does not exist".to_string()); return Err("path does not exist".to_string());
} }
if path_buf.is_dir() { if path.is_dir() {
fs::remove_dir_all(&path_buf).map_err(|e| e.to_string())?; fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
} else { } else {
fs::remove_file(&path_buf).map_err(|e| e.to_string())?; fs::remove_file(&path).map_err(|e| e.to_string())?;
}
let parent = path_buf
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
if let Some(files) = state.files.lock().unwrap().get_mut(&parent) {
files.retain(|f| f.path != path);
} }
Ok(()) Ok(())
} }
#[tauri::command]
fn reset_scan(state: State<'_, AppState>) {
state.files.lock().unwrap().clear();
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
}
}))
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(tauri_plugin_prevent_default::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.manage(AppState {
files: Mutex::new(HashMap::new()),
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
list_disks, list_disks,
get_file_info_from_dirs, get_file_info_from_dirs,
get_files_in_dir, remove_item
remove_item,
reset_scan
]) ])
.setup(|app| { .setup(|app| {
#[allow(unused)] #[allow(unused)]
@@ -215,7 +191,6 @@ pub fn run() {
) )
.title("DiskSense") .title("DiskSense")
.inner_size(800.0, 600.0) .inner_size(800.0, 600.0)
.min_inner_size(600.0, 400.0)
.transparent(transparency) .transparent(transparency)
.visible(false) .visible(false)
.build()?; .build()?;
+34 -54
View File
@@ -12,7 +12,8 @@ import {
import { v5 as uuidv5 } from 'uuid' import { v5 as uuidv5 } from 'uuid'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { ask, message, open } from '@tauri-apps/plugin-dialog' import { ask, message, open } from '@tauri-apps/plugin-dialog'
import { openPath, revealItemInDir } from '@tauri-apps/plugin-opener' import { listen } from '@tauri-apps/api/event'
import { revealItemInDir } from '@tauri-apps/plugin-opener'
interface Disk { interface Disk {
name: string name: string
@@ -43,23 +44,16 @@ export default function App () {
})() })()
}, []) }, [])
useEffect(() => {
;(async () => {
if (currentPath && !scanning) {
const result = await invoke<FileInfo[]>('get_files_in_dir', {
path: currentPath
})
setFiles(result)
}
})()
}, [currentPath, scanning])
const handleScan = async (path: string) => { const handleScan = async (path: string) => {
setRootPath(path) setRootPath(path)
setCurrentPath(path) setCurrentPath(path)
setFiles([]) setFiles([])
setScanning(true) setScanning(true)
const unlisten = await listen<FileInfo[]>('scan-batch', e => {
setFiles(prev => [...prev, ...e.payload])
})
try { try {
await invoke('get_file_info_from_dirs', { await invoke('get_file_info_from_dirs', {
path, path,
@@ -71,6 +65,7 @@ export default function App () {
} }
setScanning(false) setScanning(false)
unlisten()
} }
const breadcrumbs = useMemo(() => { const breadcrumbs = useMemo(() => {
@@ -83,13 +78,13 @@ export default function App () {
] ]
}, [rootPath, currentPath]) }, [rootPath, currentPath])
const goBackToDisks = async () => { const visibleFiles = useMemo(() => {
setScanning(false) if (!rootPath) return []
setRootPath(null) return files.filter(f => {
setCurrentPath('') const parent = f.path.split('/').slice(0, -1).join('/')
setFiles([]) return parent === currentPath
await invoke('reset_scan') })
} }, [files, currentPath, rootPath])
return !rootPath ? ( return !rootPath ? (
<> <>
@@ -109,10 +104,9 @@ export default function App () {
className='icon-large -ml-2' className='icon-large -ml-2'
/> />
<div className='flex flex-col -ml-2 w-full'> <div className='flex flex-col -ml-2 w-full'>
<p hidden={d.name.length == 0}> <p>
{d.name} <span className='text-sm'>({d.mount_point})</span> {d.name} <span className='text-sm'>({d.mount_point})</span>
</p> </p>
<p hidden={d.name.length != 0}>{d.mount_point}</p>
<p>{prettyBytes(d.total_space)} total</p> <p>{prettyBytes(d.total_space)} total</p>
<div className='w-full h-2 bg-(--col6) rounded overflow-hidden my-1'> <div className='w-full h-2 bg-(--col6) rounded overflow-hidden my-1'>
<div <div
@@ -160,64 +154,48 @@ export default function App () {
</p> </p>
) : ( ) : (
<div className='p-3'> <div className='p-3'>
<div className='flex flex-wrap gap-1 mb-3 relative'> <div className='flex flex-wrap gap-1 mb-3'>
<div className='flex flex-nowrap gap-1 overflow-x-auto mr-14'> {breadcrumbs.map((p, i) => (
{breadcrumbs.map((p, i) => ( <button
<button key={p + i}
key={p + i} className='cursor-pointer bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 px-2 py-1 rounded-lg transition-colors text-white/60 hover:text-white/75'
className='cursor-pointer bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 px-2 py-1 rounded-lg transition-colors text-white' onClick={() => setCurrentPath(p)}
onClick={() => setCurrentPath(p)} >
> {i == 0 ? p : p.split('/').pop()}
{i == 0 ? p : p.split('/').pop()} </button>
</button> ))}
))}
</div>
<button
className='cursor-pointer bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 px-2 py-1 rounded-lg transition-colors text-white absolute right-0'
onClick={() => goBackToDisks()}
>
Back
</button>
</div> </div>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
{files.map(f => ( {visibleFiles.map(f => (
<div <div
key={f.path} key={f.path}
className='flex justify-between px-2 py-1 cursor-pointer bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 transition-colors rounded-lg' className='flex justify-between px-2 py-1 cursor-pointer bg-(--col2)/50 hover:bg-(--col4)/50 border border-(--col4)/50 hover:border-(--col6)/50 transition-colors rounded-lg'
onClick={() => { onClick={() => {
if (f.is_dir) setCurrentPath(f.path) if (f.is_dir) setCurrentPath(f.path)
else openPath(f.path)
}} }}
title={f.is_dir ? undefined : 'Click to open the file'}
> >
<p> <p>
<FontAwesomeIcon <FontAwesomeIcon
icon={f.is_dir ? faFolder : faFile} icon={f.is_dir ? faFolder : faFile}
className='text-white transition-all hover:scale-110' className='text-white/60 hover:text-white/75 transition-all hover:scale-110'
/>{' '} />{' '}
{f.name} {f.name}
</p> </p>
<div className='flex flex-row gap-2 items-center'> <div className='flex flex-row gap-2 items-center'>
<div <div className='w-4 h-6 flex justify-center items-center'>
className='w-4 h-6 flex justify-center items-center'
title={'Click to show the ' + f.is_dir ? 'folder' : 'file'}
>
<FontAwesomeIcon <FontAwesomeIcon
icon={faFolderOpen} icon={faFolderOpen}
className='text-white transition-all hover:scale-110' className='text-white/60 hover:text-white/75 transition-all hover:scale-110'
onClick={async e => { onClick={async e => {
e.stopPropagation() e.stopPropagation()
revealItemInDir(f.path) revealItemInDir(f.path)
}} }}
/> />
</div> </div>
<div <div className='w-4 h-6 flex justify-center items-center'>
className='w-4 h-6 flex justify-center items-center'
title={'Click to delete the ' + f.is_dir ? 'folder' : 'file'}
>
<FontAwesomeIcon <FontAwesomeIcon
icon={faTrashCan} icon={faTrashCan}
className='text-red-500 transition-all hover:scale-110' className='text-red-500/60 hover:text-red-500/75 transition-all hover:scale-110'
onClick={async e => { onClick={async e => {
e.stopPropagation() e.stopPropagation()
@@ -237,7 +215,9 @@ export default function App () {
if (!response) return if (!response) return
await invoke('remove_item', { path: f.path }) await invoke('remove_item', { path: f.path })
setFiles(prev => prev.filter(x => x.path !== f.path)) setFiles(prev =>
prev.filter(x => !x.path.startsWith(f.path))
)
}} }}
/> />
</div> </div>