Add initial project setup for a File Browser system
This commit includes the setup of a new File Browser system. The system uses Tailwind CSS for styling alongside Tauri for a Rust backend. It also includes a new HTML index file, several JavaScript files for application logic e.g. favourite handling as well as new CSS files for custom styles. Git ignore file is added to exclude unnecessary files to be tracked by git. This setup is necessary to start developing File Browsing features and provides a base project structure to work on.
|
@ -0,0 +1,27 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Project directories and files
|
||||
src/index.css
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# Tauri + Vanilla
|
||||
|
||||
This template should help get you started developing with Tauri in vanilla HTML, CSS and Javascript.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
|
@ -0,0 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
dialog:not([open]) {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "file-browser",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others \"npm run dev:tauri\" \"npm run dev:tailwind\"",
|
||||
"dev:tauri": "RUST_BACKTRACE=1 tauri dev",
|
||||
"dev:tailwind": "tailwindcss -i ./assets/input.css -o ./src/index.css --watch",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"concurrently": "^8.2.2",
|
||||
"tailwindcss": "^3.3.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "file-browser"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.5", features = [ "fs-all", "dialog-all", "clipboard-all", "path-all", "window-all", "notification-all", "global-shortcut-all", "shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 974 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 903 B |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,25 @@
|
|||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
use tauri::Manager;
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
#[cfg(debug_assertions)] // only include this code on debug builds
|
||||
{
|
||||
let window = app.get_window("main").unwrap();
|
||||
window.open_devtools();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"devPath": "../src",
|
||||
"distDir": "../src",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "file-browser",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
},
|
||||
"fs": {
|
||||
"all": true,
|
||||
"scope": ["**"]
|
||||
},
|
||||
"dialog": {
|
||||
"all": true
|
||||
},
|
||||
"globalShortcut": {
|
||||
"all": true
|
||||
},
|
||||
"clipboard": {
|
||||
"all": true
|
||||
},
|
||||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"window": {
|
||||
"all": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "com.tauri.dev",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"title": "file-browser",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script type="module" src="./index.js" defer></script>
|
||||
<title>File Browser</title>
|
||||
</head>
|
||||
|
||||
<body class="h-screen max-h-screen flex flex-col">
|
||||
<header class="bg-amber-500 w-screen h-12 flex items-center justify-between">
|
||||
<span class="flex">
|
||||
<button>Back</button>
|
||||
<button>Forward</button>
|
||||
</span>
|
||||
|
||||
<span class="flex">
|
||||
<div>
|
||||
path / to / memes
|
||||
</div>
|
||||
<button>Search</button>
|
||||
</span>
|
||||
|
||||
<span class="flex">
|
||||
<button>List</button>
|
||||
<button>Panel</button>
|
||||
<button>Column</button>
|
||||
</span>
|
||||
</header>
|
||||
<div class="h-full flex">
|
||||
<dialog data-ui="favourite-dialog" class="flex flex-col gap-2 bg-green-500 backdrop-blur-lg mt-16 w-44">
|
||||
<input type="text" aria-label="Name" data-ui="favourite-name" />
|
||||
<input type="text" aria-label="Path" data-ui="favourite-path" />
|
||||
<div class="w-44 flex">
|
||||
<button data-ui="favourite-submit" class="bg-white w-1/2">Add</button>
|
||||
<button data-ui="favourite-close" class="bg-white w-1/2">Close</button>
|
||||
</div>
|
||||
</dialog>
|
||||
<menu class="bg-blue-500 h-full min-w-[250px] flex flex-col" data-ui="favourites">
|
||||
<li class="order-last">
|
||||
<button data-ui="add-favourite">+ Add favourite</button>
|
||||
</li>
|
||||
</menu>
|
||||
<main class="bg-red-500 h-full w-full" id="ui-files"></main>
|
||||
</div>
|
||||
|
||||
<template data-template="favourites">
|
||||
<li data-el="added" class="h-8 flex">
|
||||
<button data-el="action" class="w-full text-left"></button>
|
||||
<button data-el="remove" class="w-8">x</button>
|
||||
</li>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
import {renderFavourite} from "./parts/favourites.js";
|
||||
|
||||
|
||||
async function Main() {
|
||||
renderFavourite();
|
||||
}
|
||||
|
||||
Main();
|
|
@ -0,0 +1,124 @@
|
|||
import {audioDir, desktopDir, documentDir, downloadDir, homeDir, pictureDir, videoDir} from "./path.js";
|
||||
|
||||
/**
|
||||
* @type {Promise<Favourite[]>}
|
||||
*/
|
||||
const getDefaultFavourites = async () => {
|
||||
return [
|
||||
{
|
||||
name: 'Music',
|
||||
path: await audioDir(),
|
||||
system: true
|
||||
},
|
||||
{
|
||||
name: 'Documents',
|
||||
path: await documentDir(),
|
||||
system: true
|
||||
},
|
||||
{
|
||||
name: 'Desktop',
|
||||
path: await desktopDir(),
|
||||
system: true
|
||||
},
|
||||
{
|
||||
name: 'Downloads',
|
||||
path: await downloadDir(),
|
||||
system: true
|
||||
},
|
||||
{
|
||||
name: 'Home',
|
||||
path: await homeDir(),
|
||||
system: true
|
||||
},
|
||||
{
|
||||
name: 'Pictures',
|
||||
path: await pictureDir(),
|
||||
system: true
|
||||
},
|
||||
{
|
||||
name: 'Videos',
|
||||
path: await videoDir(),
|
||||
system: true
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const defaultConfig = JSON.stringify({
|
||||
favourites: await getDefaultFavourites()
|
||||
});
|
||||
|
||||
let config = null;
|
||||
const persist = () => localStorage.setItem('app:config', JSON.stringify(config));
|
||||
const load = () => config = JSON.parse(localStorage.getItem('app:config') ?? defaultConfig);
|
||||
const ensureConfig = () => config ? '' : load();
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
*/
|
||||
export const setConfig = (key, value) => {
|
||||
config[key] = value;
|
||||
|
||||
persist();
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {any} fallback
|
||||
* @returns {any}
|
||||
*/
|
||||
export const getConfig = (key, fallback) => {
|
||||
ensureConfig();
|
||||
|
||||
if (key in config) {
|
||||
return config[key];
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} Favourite
|
||||
* @property {string} name
|
||||
* @property {string} path
|
||||
* @property {number} index
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param favourites
|
||||
* @returns {Favourite[]}
|
||||
*/
|
||||
export const setFavourites = (favourites) => setConfig('favourites', favourites);
|
||||
|
||||
/**
|
||||
* @returns {Favourite[]}
|
||||
*/
|
||||
export const getFavourites = () => getConfig('favourites', []);
|
||||
|
||||
/**
|
||||
* @param {Favourite} favourite
|
||||
*
|
||||
* @return {Favourite[]}
|
||||
*/
|
||||
export const addFavourite = (favourite) => {
|
||||
const favourites = getFavourites();
|
||||
|
||||
favourites.push(favourite);
|
||||
|
||||
setFavourites(favourites);
|
||||
|
||||
return favourites;
|
||||
}
|
||||
|
||||
export const removeFavouriteByIndex = (index) => {
|
||||
const favourites = getFavourites();
|
||||
|
||||
favourites.splice(index, 1)
|
||||
|
||||
setFavourites(favourites);
|
||||
|
||||
return favourites;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* @param {ParentNode|Element} source
|
||||
* @param {string} selector
|
||||
* @returns {HTMLElement|null}
|
||||
*/
|
||||
export const getElement = (source, selector) => source.querySelector(selector);
|
||||
|
||||
/**
|
||||
* @param {ParentNode|Element} source
|
||||
* @param {string} selector
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
export const getElements = (source, selector) => {
|
||||
const list = source.querySelectorAll(selector);
|
||||
|
||||
return [...list]
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ParentNode|Element} source
|
||||
* @param {string} selector
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export const getElementOrThrow = (source, selector) => {
|
||||
const element = getElement(source, selector);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`Element ${selector} not found in ${source}`);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ParentNode|Element} source
|
||||
* @param {string} selector
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
export const getElementsOrThrow = (source, selector) => {
|
||||
const elements = getElements(source, selector);
|
||||
|
||||
if (elements.length <= 0) {
|
||||
throw new Error(`No elements found with ${selector} in ${source}`);
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ParentNode|Element} source
|
||||
* @param {string} selector
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export const getOneElementOrThrow = (source, selector) => {
|
||||
const elements = getElementsOrThrow(source, selector);
|
||||
|
||||
if (elements.length > 1) {
|
||||
throw new Error(`Found more then 1 element with ${selector} in ${source}`);
|
||||
}
|
||||
|
||||
return elements[0]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { tauriFs } from "./tauri.js";
|
||||
|
||||
export const {
|
||||
BaseDirectory,
|
||||
Dir,
|
||||
copyFile,
|
||||
createDir,
|
||||
exists,
|
||||
readBinaryFile,
|
||||
readDir,
|
||||
readTextFile,
|
||||
removeDir,
|
||||
removeFile,
|
||||
renameFile,
|
||||
writeBinaryFile,
|
||||
writeFile,
|
||||
writeTextFile,
|
||||
} = tauriFs;
|
|
@ -0,0 +1,29 @@
|
|||
import {homeDir} from "./path.js";
|
||||
import {readDir} from "./fs.js";
|
||||
|
||||
const eventBus = new Comment();
|
||||
|
||||
const NAVIGATE_EVENT = 'navigate';
|
||||
|
||||
let currentDir = await homeDir();
|
||||
|
||||
export const onNavigate = (callback) => eventBus.addEventListener(
|
||||
NAVIGATE_EVENT,
|
||||
() => callback(currentDir)
|
||||
);
|
||||
export const onceNavigate = (callback) => eventBus.addEventListener(
|
||||
NAVIGATE_EVENT,
|
||||
() => callback(currentDir),
|
||||
{ once: true }
|
||||
);
|
||||
export const navigate = (toDir) => {
|
||||
currentDir = toDir;
|
||||
eventBus.dispatchEvent(new CustomEvent(NAVIGATE_EVENT))
|
||||
}
|
||||
|
||||
const logDir = async () => console.log({
|
||||
currentDir,
|
||||
items: await readDir(currentDir)
|
||||
});
|
||||
|
||||
onNavigate(logDir);
|
|
@ -0,0 +1,39 @@
|
|||
import {tauriPath} from "./tauri.js";
|
||||
|
||||
export const {
|
||||
BaseDirectory,
|
||||
appCacheDir,
|
||||
appConfigDir,
|
||||
appDataDir,
|
||||
appDir,
|
||||
appLocalDataDir,
|
||||
appLogDir,
|
||||
audioDir,
|
||||
basename,
|
||||
cacheDir,
|
||||
configDir,
|
||||
dataDir,
|
||||
delimiter,
|
||||
desktopDir,
|
||||
dirname,
|
||||
documentDir,
|
||||
downloadDir,
|
||||
executableDir,
|
||||
extname,
|
||||
fontDir,
|
||||
homeDir,
|
||||
isAbsolute,
|
||||
join,
|
||||
localDataDir,
|
||||
logDir,
|
||||
normalize,
|
||||
pictureDir,
|
||||
publicDir,
|
||||
resolve,
|
||||
resolveResource,
|
||||
resourceDir,
|
||||
runtimeDir,
|
||||
sep,
|
||||
templateDir,
|
||||
videoDir
|
||||
} = tauriPath
|
|
@ -0,0 +1,23 @@
|
|||
console.log(window.__TAURI__);
|
||||
|
||||
export const {
|
||||
app: tauriApp,
|
||||
cli: tauriCli,
|
||||
clipboard: tauriClipboard,
|
||||
convertFileSrc,
|
||||
dialog: tauriDialog,
|
||||
event: tauriEvent,
|
||||
fs: tauriFs,
|
||||
globalShortcut: tauriGlobalShortcut,
|
||||
http: tauriHttp,
|
||||
invoke: tauriInvoke,
|
||||
notification: tauriNotification,
|
||||
os: tauriOs,
|
||||
path: tauriPath,
|
||||
process: tauriProcess,
|
||||
shell: tauriShell,
|
||||
tauri,
|
||||
transformCallback,
|
||||
updater: tauriUpdated,
|
||||
window: tauriWindow,
|
||||
} = window.__TAURI__;
|
|
@ -0,0 +1,26 @@
|
|||
import {getElementOrThrow, getOneElementOrThrow} from "./element.js";
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
export const getTemplate = (name) => {
|
||||
/**
|
||||
* @type {HTMLTemplateElement}
|
||||
*/
|
||||
const template = getOneElementOrThrow(document, `[data-template=${name}]`);
|
||||
const element = template.content.cloneNode(true);
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
const el = (selector) => getElementOrThrow(element, `[data-el=${selector}]`);
|
||||
|
||||
const render = () => element;
|
||||
|
||||
return {
|
||||
el,
|
||||
render
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const isPromise = (value) => typeof value === 'object' && typeof value.then === 'function';
|
|
@ -0,0 +1,77 @@
|
|||
import {getElements, getOneElementOrThrow} from "../libs/element.js";
|
||||
import {addFavourite, getFavourites, removeFavouriteByIndex} from "../libs/config.js";
|
||||
import {getTemplate} from "../libs/template.js";
|
||||
import {navigate} from "../libs/navigation.js";
|
||||
|
||||
export const renderFavourite = () => {
|
||||
const updateFavourites = (favouritesArray) => {
|
||||
const favouriteList = getOneElementOrThrow(document, '[data-ui=favourites]');
|
||||
|
||||
const addedItems = getElements(favouriteList, '[data-el=added]');
|
||||
addedItems.forEach(addedItems => addedItems.remove());
|
||||
|
||||
favouritesArray.forEach(({ name, path, system }, index) => {
|
||||
const template = getTemplate('favourites');
|
||||
const actionEl = template.el('action');
|
||||
actionEl.textContent = name;
|
||||
actionEl.addEventListener('click', () => navigate(path));
|
||||
|
||||
const removeEl = template.el('remove');
|
||||
if (system) {
|
||||
removeEl.remove();
|
||||
} else {
|
||||
removeEl.addEventListener('click', () => {
|
||||
removeFavouriteByIndex(index);
|
||||
|
||||
renderFavourite();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
favouriteList.appendChild(template.render());
|
||||
});
|
||||
}
|
||||
|
||||
const favourites = getFavourites();
|
||||
updateFavourites(favourites);
|
||||
|
||||
const addEl = getOneElementOrThrow(document, '[data-ui=add-favourite]');
|
||||
|
||||
/**
|
||||
* @type {HTMLDialogElement}
|
||||
*/
|
||||
const dialog = getOneElementOrThrow(document, '[data-ui=favourite-dialog]');
|
||||
dialog.close();
|
||||
addEl.addEventListener('click', () => {
|
||||
const nameEl = getOneElementOrThrow(dialog, '[data-ui=favourite-name]');
|
||||
const pathEl = getOneElementOrThrow(dialog, '[data-ui=favourite-path]');
|
||||
const submitEl = getOneElementOrThrow(dialog, '[data-ui=favourite-submit]');
|
||||
const closeEl = getOneElementOrThrow(dialog, '[data-ui=favourite-close]');
|
||||
|
||||
dialog.showModal();
|
||||
|
||||
const handleClose = () => {
|
||||
nameEl.value = '';
|
||||
pathEl.value = '';
|
||||
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
closeEl.addEventListener('click', () => handleClose());
|
||||
|
||||
submitEl.addEventListener('click', () => {
|
||||
const { value: name } = nameEl;
|
||||
const { value: path } = pathEl;
|
||||
|
||||
if (name.trim() && path.trim()) {
|
||||
const favourites = getFavourites();
|
||||
|
||||
addFavourite({ name, path, index: favourites.length });
|
||||
|
||||
renderFavourite();
|
||||
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|