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.
This commit is contained in:
Ian Wijma 2023-11-27 16:08:17 +11:00
commit f276f686a9
40 changed files with 6679 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -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

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
}

7
README.md Normal file
View File

@ -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)

7
assets/input.css Normal file
View File

@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
dialog:not([open]) {
display: none;
}

1527
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@ -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"
}
}

4
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/

4497
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
src-tauri/Cargo.toml Normal file
View File

@ -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"]

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

25
src-tauri/src/main.rs Normal file
View File

@ -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");
}

68
src-tauri/tauri.conf.json Normal file
View File

@ -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
}
]
}
}

55
src/index.html Normal file
View File

@ -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>

8
src/index.js Normal file
View File

@ -0,0 +1,8 @@
import {renderFavourite} from "./parts/favourites.js";
async function Main() {
renderFavourite();
}
Main();

124
src/libs/config.js Normal file
View File

@ -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;
}

62
src/libs/element.js Normal file
View File

@ -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]
}

18
src/libs/fs.js Normal file
View File

@ -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;

29
src/libs/navigation.js Normal file
View File

@ -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);

39
src/libs/path.js Normal file
View File

@ -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

23
src/libs/tauri.js Normal file
View File

@ -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__;

26
src/libs/template.js Normal file
View File

@ -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
}
}

1
src/libs/typeChecker.js Normal file
View File

@ -0,0 +1 @@
export const isPromise = (value) => typeof value === 'object' && typeof value.then === 'function';

77
src/parts/favourites.js Normal file
View File

@ -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();
}
});
});
}

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{html,js}"],
theme: {
extend: {},
},
plugins: [],
}