Compare commits
2
Commits
main
..
fb44650382
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb44650382
|
||
|
|
71b1986180
|
No files matched your search
@@ -15,7 +15,6 @@
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-window-state": "~2",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"react": "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-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=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-window-state": "~2",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"react": "19.2.5",
|
||||
"react-dom": "19.2.5",
|
||||
|
||||
@@ -20,7 +20,6 @@ walkdir = "2.5.0"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-prevent-default = "4.0.4"
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
window-vibrancy = "0.7.1"
|
||||
@@ -28,10 +27,6 @@ window-vibrancy = "0.7.1"
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
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]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,19 +2,13 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:default",
|
||||
"dialog:default",
|
||||
"opener:default",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
{
|
||||
"path": "**"
|
||||
}
|
||||
]
|
||||
}
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"window-state:default"
|
||||
]
|
||||
}
|
||||
+36
-61
@@ -1,17 +1,13 @@
|
||||
use std::{collections::HashMap, fs, path::PathBuf, sync::Mutex};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sysinfo::Disks;
|
||||
use tauri::{Manager, State, WebviewUrl, WebviewWindowBuilder};
|
||||
use tauri::{Emitter, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
#[cfg(target_os = "macos")]
|
||||
use window_vibrancy::{NSVisualEffectMaterial, apply_vibrancy};
|
||||
|
||||
struct AppState {
|
||||
files: Mutex<HashMap<String, Vec<FileInfo>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DiskInfo {
|
||||
name: String,
|
||||
@@ -29,6 +25,13 @@ pub struct FileInfo {
|
||||
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]
|
||||
fn list_disks() -> Vec<DiskInfo> {
|
||||
Disks::new_with_refreshed_list()
|
||||
@@ -50,7 +53,6 @@ fn list_disks() -> Vec<DiskInfo> {
|
||||
#[tauri::command]
|
||||
async fn get_file_info_from_dirs(
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
path: String,
|
||||
min_ratio: String,
|
||||
) -> Result<(), String> {
|
||||
@@ -76,18 +78,20 @@ async fn get_file_info_from_dirs(
|
||||
let json: serde_json::Value =
|
||||
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") {
|
||||
stream(tree, "", &mut local_map);
|
||||
let mut batch = Vec::new();
|
||||
stream(tree, "", &mut batch, &app);
|
||||
}
|
||||
|
||||
*state.files.lock().unwrap() = local_map;
|
||||
|
||||
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()) {
|
||||
Some(n) => n,
|
||||
None => return,
|
||||
@@ -102,86 +106,58 @@ fn stream(node: &serde_json::Value, parent: &str, map: &mut HashMap<String, Vec<
|
||||
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(),
|
||||
path: path.clone(),
|
||||
size,
|
||||
is_dir,
|
||||
});
|
||||
|
||||
if batch.len() >= 200 {
|
||||
let _ = app.emit("scan-batch", batch.clone());
|
||||
batch.clear();
|
||||
}
|
||||
|
||||
if let Some(children) = 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]
|
||||
fn get_files_in_dir(path: String, state: State<'_, AppState>) -> Vec<FileInfo> {
|
||||
state
|
||||
.files
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&path)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
fn remove_item(path: String) -> Result<(), String> {
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
#[tauri::command]
|
||||
fn remove_item(path: String, state: State<'_, AppState>) -> Result<(), String> {
|
||||
let path_buf = PathBuf::from(&path);
|
||||
|
||||
if !path_buf.exists() {
|
||||
if !path.exists() {
|
||||
return Err("path does not exist".to_string());
|
||||
}
|
||||
|
||||
if path_buf.is_dir() {
|
||||
fs::remove_dir_all(&path_buf).map_err(|e| e.to_string())?;
|
||||
if path.is_dir() {
|
||||
fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
fs::remove_file(&path_buf).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);
|
||||
fs::remove_file(&path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn reset_scan(state: State<'_, AppState>) {
|
||||
state.files.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
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_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(AppState {
|
||||
files: Mutex::new(HashMap::new()),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_disks,
|
||||
get_file_info_from_dirs,
|
||||
get_files_in_dir,
|
||||
remove_item,
|
||||
reset_scan
|
||||
remove_item
|
||||
])
|
||||
.setup(|app| {
|
||||
#[allow(unused)]
|
||||
@@ -215,7 +191,6 @@ pub fn run() {
|
||||
)
|
||||
.title("DiskSense")
|
||||
.inner_size(800.0, 600.0)
|
||||
.min_inner_size(600.0, 400.0)
|
||||
.transparent(transparency)
|
||||
.visible(false)
|
||||
.build()?;
|
||||
|
||||
+34
-54
@@ -12,7 +12,8 @@ import {
|
||||
import { v5 as uuidv5 } from 'uuid'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
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 {
|
||||
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) => {
|
||||
setRootPath(path)
|
||||
setCurrentPath(path)
|
||||
setFiles([])
|
||||
setScanning(true)
|
||||
|
||||
const unlisten = await listen<FileInfo[]>('scan-batch', e => {
|
||||
setFiles(prev => [...prev, ...e.payload])
|
||||
})
|
||||
|
||||
try {
|
||||
await invoke('get_file_info_from_dirs', {
|
||||
path,
|
||||
@@ -71,6 +65,7 @@ export default function App () {
|
||||
}
|
||||
|
||||
setScanning(false)
|
||||
unlisten()
|
||||
}
|
||||
|
||||
const breadcrumbs = useMemo(() => {
|
||||
@@ -83,13 +78,13 @@ export default function App () {
|
||||
]
|
||||
}, [rootPath, currentPath])
|
||||
|
||||
const goBackToDisks = async () => {
|
||||
setScanning(false)
|
||||
setRootPath(null)
|
||||
setCurrentPath('')
|
||||
setFiles([])
|
||||
await invoke('reset_scan')
|
||||
}
|
||||
const visibleFiles = useMemo(() => {
|
||||
if (!rootPath) return []
|
||||
return files.filter(f => {
|
||||
const parent = f.path.split('/').slice(0, -1).join('/')
|
||||
return parent === currentPath
|
||||
})
|
||||
}, [files, currentPath, rootPath])
|
||||
|
||||
return !rootPath ? (
|
||||
<>
|
||||
@@ -109,10 +104,9 @@ export default function App () {
|
||||
className='icon-large -ml-2'
|
||||
/>
|
||||
<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>
|
||||
</p>
|
||||
<p hidden={d.name.length != 0}>{d.mount_point}</p>
|
||||
<p>{prettyBytes(d.total_space)} total</p>
|
||||
<div className='w-full h-2 bg-(--col6) rounded overflow-hidden my-1'>
|
||||
<div
|
||||
@@ -160,64 +154,48 @@ export default function App () {
|
||||
</p>
|
||||
) : (
|
||||
<div className='p-3'>
|
||||
<div className='flex flex-wrap gap-1 mb-3 relative'>
|
||||
<div className='flex flex-nowrap gap-1 overflow-x-auto mr-14'>
|
||||
{breadcrumbs.map((p, i) => (
|
||||
<button
|
||||
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'
|
||||
onClick={() => setCurrentPath(p)}
|
||||
>
|
||||
{i == 0 ? p : p.split('/').pop()}
|
||||
</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 className='flex flex-wrap gap-1 mb-3'>
|
||||
{breadcrumbs.map((p, i) => (
|
||||
<button
|
||||
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'
|
||||
onClick={() => setCurrentPath(p)}
|
||||
>
|
||||
{i == 0 ? p : p.split('/').pop()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
{files.map(f => (
|
||||
{visibleFiles.map(f => (
|
||||
<div
|
||||
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'
|
||||
onClick={() => {
|
||||
if (f.is_dir) setCurrentPath(f.path)
|
||||
else openPath(f.path)
|
||||
}}
|
||||
title={f.is_dir ? undefined : 'Click to open the file'}
|
||||
>
|
||||
<p>
|
||||
<FontAwesomeIcon
|
||||
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}
|
||||
</p>
|
||||
<div className='flex flex-row gap-2 items-center'>
|
||||
<div
|
||||
className='w-4 h-6 flex justify-center items-center'
|
||||
title={'Click to show the ' + f.is_dir ? 'folder' : 'file'}
|
||||
>
|
||||
<div className='w-4 h-6 flex justify-center items-center'>
|
||||
<FontAwesomeIcon
|
||||
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 => {
|
||||
e.stopPropagation()
|
||||
revealItemInDir(f.path)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='w-4 h-6 flex justify-center items-center'
|
||||
title={'Click to delete the ' + f.is_dir ? 'folder' : 'file'}
|
||||
>
|
||||
<div className='w-4 h-6 flex justify-center items-center'>
|
||||
<FontAwesomeIcon
|
||||
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 => {
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -237,7 +215,9 @@ export default function App () {
|
||||
if (!response) return
|
||||
|
||||
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>
|
||||
|
||||
Reference in new issue
Block a user