Add file browsing, viewing functionalities and enhance navigation
Introduced new functionalities in the file manager application for browsing, listing and opening files. We have added a new command 'get_files' in 'main.rs', which reads entries in a directory and fetches associated metadata. This allows users to view files in a selected directory. Additionally, the command 'open_file' has been added for opening files. Simultaneously, frontend changes have been made such as: new templates for file lists, handlers for view switching in 'files.js', and rendered files in each mode. The 'navigation.js' error handling has been improved for invalid navigation. Improved user experience by adding a breadcrumbs feature for better navigation. This is performed by the newly introduced 'breadcrumbs.js' file which maintains the history of navigated directories. Overall, these enhanced functionalities improve navigation and file management efficiency for end-users.
This commit is contained in:
parent
e3716d7e89
commit
e8969a4306
|
@ -1,12 +1,120 @@
|
|||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::time::{UNIX_EPOCH};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct EntryMetaData {
|
||||
name: String,
|
||||
path: String,
|
||||
size: u64,
|
||||
is_directory: bool,
|
||||
is_file: bool,
|
||||
is_symlink: bool,
|
||||
directory_item_count: u64,
|
||||
permission: String,
|
||||
created: u64,
|
||||
modified: u64,
|
||||
accessed: u64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_files(
|
||||
_window: tauri::Window,
|
||||
directory: &str,
|
||||
) -> Vec<EntryMetaData> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let path = Path::new(directory);
|
||||
|
||||
// Read the entries in the directory
|
||||
let entries = fs::read_dir(path).unwrap();
|
||||
|
||||
let mut data: Vec<EntryMetaData> = vec![];
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let metadata = entry.metadata().unwrap();
|
||||
|
||||
let mode = metadata.permissions().mode();
|
||||
let permission = format!(
|
||||
"{:04o} ({}{}{})",
|
||||
mode,
|
||||
if mode & 0o400 != 0 { 'r' } else { '-' },
|
||||
if mode & 0o200 != 0 { 'w' } else { '-' },
|
||||
if mode & 0o100 != 0 { 'x' } else { '-' },
|
||||
);
|
||||
|
||||
let mut directory_item_count = 0 as u64;
|
||||
if metadata.is_dir() {
|
||||
if let Ok(dir_entries) = fs::read_dir(entry.path()) {
|
||||
directory_item_count = dir_entries.count() as u64;
|
||||
}
|
||||
}
|
||||
|
||||
data.push(EntryMetaData{
|
||||
name: entry.file_name().to_string_lossy().to_string(),
|
||||
path: entry.path().to_string_lossy().to_string(),
|
||||
size: metadata.len(),
|
||||
is_directory: metadata.is_dir(),
|
||||
is_file: metadata.is_file(),
|
||||
is_symlink: metadata.is_symlink(),
|
||||
directory_item_count,
|
||||
permission,
|
||||
created: metadata.created().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||
modified: metadata.modified().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||
accessed: metadata.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_file(
|
||||
_window: tauri::Window,
|
||||
path: &str,
|
||||
) -> bool {
|
||||
use std::process::Command;
|
||||
use std::env;
|
||||
|
||||
let os = env::consts::OS;
|
||||
|
||||
match os {
|
||||
"linux" => {
|
||||
Command::new("xdg-open").arg(path).output()
|
||||
.expect("Unable to open file");
|
||||
|
||||
return true;
|
||||
},
|
||||
"macos" => {
|
||||
Command::new("open").arg(path).output()
|
||||
.expect("Unable to open file");
|
||||
|
||||
return true;
|
||||
},
|
||||
"windows" => {
|
||||
Command::new("cmd").arg("/C").arg("start").arg(path).output()
|
||||
.expect("Unable to open file");
|
||||
|
||||
return true;
|
||||
},
|
||||
_ => {
|
||||
println!("Unsupported operating system: {}", os);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
use tauri::Manager;
|
||||
tauri::Builder::default()
|
||||
|
@ -18,7 +126,10 @@ fn main() {
|
|||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_files,
|
||||
open_file
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
|
|
|
@ -19,7 +19,19 @@
|
|||
},
|
||||
"fs": {
|
||||
"all": true,
|
||||
"scope": ["**"]
|
||||
"scope": [
|
||||
"**",
|
||||
"$APP",
|
||||
"$APP/**",
|
||||
"$CACHE",
|
||||
"$CACHE/**",
|
||||
"$CONFIG",
|
||||
"$CONFIG/**",
|
||||
"$LOG",
|
||||
"$LOG/**",
|
||||
"$HOME/**",
|
||||
"$HOME/.*"
|
||||
]
|
||||
},
|
||||
"dialog": {
|
||||
"all": true
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<title>File Browser</title>
|
||||
</head>
|
||||
|
||||
<body class="h-screen max-h-screen flex flex-col">
|
||||
<body class="h-screen max-h-screen flex flex-col overflow-hidden">
|
||||
<header class="bg-amber-500 w-screen h-12 flex items-center justify-between">
|
||||
<span class="flex">
|
||||
<button>Back</button>
|
||||
|
@ -53,5 +53,44 @@
|
|||
<template data-template="breadcrumbs">
|
||||
<button data-el="action" class="after:content-['/']"></button>
|
||||
</template>
|
||||
|
||||
<template data-template="files-list">
|
||||
<div class="w-full h-full overflow-y-scroll">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="sticky top-0 bg-red-500">
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Permissions</th>
|
||||
<th>Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-el="body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<template data-template="files-list-item">
|
||||
<tr data-el="action">
|
||||
<td data-el="name"></td>
|
||||
<td data-el="size"></td>
|
||||
<td data-el="permission"></td>
|
||||
<td data-el="modified"></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
||||
<template data-template="files-panel">
|
||||
|
||||
</template>
|
||||
<template data-template="files-panel-item">
|
||||
|
||||
</template>
|
||||
|
||||
<template data-template="files-column">
|
||||
|
||||
</template>
|
||||
<template data-template="files-column-item">
|
||||
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import {renderFavourite} from "./parts/favourites.js";
|
||||
import {renderBreadcrumbs} from "./parts/breadcrumbs.js";
|
||||
import {renderFiles} from "./parts/files.js";
|
||||
|
||||
|
||||
async function Main() {
|
||||
renderBreadcrumbs();
|
||||
renderFavourite();
|
||||
await renderFiles();
|
||||
}
|
||||
|
||||
Main();
|
||||
|
|
|
@ -48,7 +48,3 @@ const logDir = async () => console.log({
|
|||
currentDir,
|
||||
items: await readDir(currentDir)
|
||||
});
|
||||
|
||||
onNavigate(logDir);
|
||||
|
||||
logDir();
|
|
@ -1,5 +1,10 @@
|
|||
import {getElementOrThrow, getOneElementOrThrow} from "./element.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Template
|
||||
* @property {function} el
|
||||
* @property {function} render
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import {getOneElementOrThrow} from "../libs/element.js";
|
||||
import {tauriInvoke} from "../libs/tauri.js";
|
||||
import {renderPanelFiles} from "./files/panel.js";
|
||||
import {renderColumnFiles} from "./files/column.js";
|
||||
import {renderListFiles} from "./files/list.js";
|
||||
import {onceNavigate} from "../libs/navigation.js";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Entry
|
||||
* @property {string} name
|
||||
* @property {string} path
|
||||
* @property {number} size
|
||||
* @property {boolean} is_directory
|
||||
* @property {boolean} is_file
|
||||
* @property {boolean} is_symlink
|
||||
* @property {string} permission
|
||||
* @property {number} directory_item_count
|
||||
* @property {number} created
|
||||
* @property {number} modified
|
||||
* @property {number} accessed
|
||||
*/
|
||||
|
||||
let currentView = 'list';
|
||||
|
||||
export const renderFiles = async () => {
|
||||
const listEl = getOneElementOrThrow(document, '[data-ui=view-list]');
|
||||
const panelEl = getOneElementOrThrow(document, '[data-ui=view-panel]');
|
||||
const columnEl = getOneElementOrThrow(document, '[data-ui=view-column]');
|
||||
|
||||
const handleClick = (toView) => () => {
|
||||
if (currentView !== toView) {
|
||||
currentView = toView;
|
||||
renderFiles();
|
||||
}
|
||||
}
|
||||
|
||||
const once = true;
|
||||
listEl.addEventListener('click', handleClick('list'), {once});
|
||||
panelEl.addEventListener('click', handleClick('panel'), {once});
|
||||
columnEl.addEventListener('click', handleClick('column'), {once});
|
||||
|
||||
switch (currentView) {
|
||||
case 'panel':
|
||||
renderPanelFiles();
|
||||
break;
|
||||
case 'column':
|
||||
renderColumnFiles();
|
||||
break;
|
||||
default:
|
||||
renderListFiles();
|
||||
break;
|
||||
}
|
||||
|
||||
onceNavigate(() => renderFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} directory
|
||||
* @returns {Promise<Entry[]>}
|
||||
*/
|
||||
export const getEntries = async (directory) => {
|
||||
return await tauriInvoke('get_files', { directory });
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to a human-readable size format.
|
||||
*
|
||||
* @param {number} bytes - The number of bytes to convert.
|
||||
* @param {number} decimals - The number of decimal places to round to (optional, default is 2).
|
||||
* @returns {string} - A string representing the human-readable size.
|
||||
*/
|
||||
export const formatBytes = (bytes, decimals = 2) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export const formatDirectoryItemCount = (directoryItemCount) => {
|
||||
if (directoryItemCount === 1) {
|
||||
return '1 item'
|
||||
}
|
||||
|
||||
return `${directoryItemCount} items`
|
||||
}
|
||||
|
||||
export const openFile = async (path) => {
|
||||
return await tauriInvoke('open_file', { path });
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import {getOneElementOrThrow} from "../../libs/element.js";
|
||||
|
||||
export const renderColumnFiles = () => {
|
||||
const fileEl = getOneElementOrThrow(document, '[data-ui=files]');
|
||||
fileEl.innerHTML = '';
|
||||
console.log('column');
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import {getTemplate} from "../../libs/template.js";
|
||||
import {formatBytes, formatDirectoryItemCount, getEntries, openFile} from "../files.js";
|
||||
import {getCurrentDir, navigate} from "../../libs/navigation.js";
|
||||
import {getOneElementOrThrow} from "../../libs/element.js";
|
||||
import {sep} from "../../libs/path.js";
|
||||
|
||||
export const renderListFiles = () => {
|
||||
const fileEl = getOneElementOrThrow(document, '[data-ui=files]');
|
||||
const containerTemplate = getTemplate('files-list');
|
||||
|
||||
const body = containerTemplate.el('body');
|
||||
|
||||
/**
|
||||
* @callback ValueFormat
|
||||
* @param {any} value
|
||||
* @param {Entry} entry
|
||||
* @return any
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Entry} entry
|
||||
* @param {Template} parentEl
|
||||
* @param {string} name
|
||||
* @param {ValueFormat} valueFormat
|
||||
*/
|
||||
const applyValue = (entry, parentEl, name, valueFormat = (v, e) => v) => {
|
||||
if (name in entry) {
|
||||
const targetEl = parentEl.el(name);
|
||||
targetEl.innerText = valueFormat(entry[name], entry);
|
||||
}
|
||||
}
|
||||
|
||||
const renderList = async () => {
|
||||
const currentDirectory = getCurrentDir();
|
||||
const entries = await getEntries(currentDirectory);
|
||||
|
||||
for (const entry of entries) {
|
||||
const itemTemplate = getTemplate('files-list-item');
|
||||
|
||||
applyValue(entry, itemTemplate, 'name', (value, entry) => {
|
||||
let prefix = '';
|
||||
if (entry.is_directory) prefix = sep;
|
||||
return `${prefix}${value}`;
|
||||
});
|
||||
applyValue(entry, itemTemplate, 'permission')
|
||||
applyValue(entry, itemTemplate, 'modified')
|
||||
applyValue(
|
||||
entry,
|
||||
itemTemplate,
|
||||
'size',
|
||||
(value, entry) => {
|
||||
if (entry.is_directory) {
|
||||
return formatDirectoryItemCount(entry.directory_item_count)
|
||||
} else {
|
||||
return formatBytes(value);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const actionEl = itemTemplate.el('action');
|
||||
actionEl.addEventListener('click', ({currentTarget}) => {
|
||||
fileEl.querySelectorAll('[data-el=body] .bg-blue-500')
|
||||
.forEach(el => el.classList.remove('bg-blue-500'))
|
||||
|
||||
currentTarget.classList.add('bg-blue-500');
|
||||
});
|
||||
|
||||
actionEl.addEventListener('dblclick', () => {
|
||||
if (entry.is_directory) {
|
||||
navigate(entry.path);
|
||||
} else {
|
||||
openFile(entry.path);
|
||||
}
|
||||
})
|
||||
|
||||
body.appendChild(itemTemplate.render());
|
||||
}
|
||||
|
||||
fileEl.innerHTML = '';
|
||||
fileEl.appendChild(containerTemplate.render());
|
||||
}
|
||||
|
||||
renderList();
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import {getOneElementOrThrow} from "../../libs/element.js";
|
||||
|
||||
export const renderPanelFiles = () => {
|
||||
const fileEl = getOneElementOrThrow(document, '[data-ui=files]');
|
||||
fileEl.innerHTML = '';
|
||||
console.log('panel');
|
||||
}
|
Loading…
Reference in New Issue