Initial Renamr scaffold: Tauri (Rust core + React/TS UI), naming-expression engine ported+tested

This commit is contained in:
Renamr Scaffold
2026-08-29 01:43:12 +00:00
commit e01852f292
34 changed files with 7530 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/target
/node_modules
/dist
/src-tauri/target
/src-tauri/gen
*.log
.DS_Store
Generated
+4672
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
[workspace]
resolver = "2"
members = ["crates/renamr-core", "src-tauri"]
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
+113
View File
@@ -0,0 +1,113 @@
# Renamr
A fast, cross-platform media renamer — a ground-up rewrite of FileBot (via its FB-Mod
fork), built from scratch as its own project: new stack, new architecture, new branding.
**Status: early scaffold.** The naming-expression engine is ported and tested; the
matching engine, media metadata parsing, web datasource clients, rename engine, and the
real UI are not yet ported. See "Porting roadmap" below.
## Why a rewrite, and why this stack
The original is a ~20k+ line Java/Swing app built with Ant. The goals were: faster
startup and UI, faster matching/renaming, a codebase that's actually pleasant to work
in, and a genuine rebrand — not a coat of paint.
Renamr is **Tauri**: a Rust core (`crates/renamr-core`, `src-tauri`) plus a
TypeScript/React frontend (`src`), compiled to a small native binary per platform
(Windows/macOS/Linux) with a system webview instead of shipping a bundled browser
engine. That's the concrete "faster" win over both the original JVM/Swing app and
an Electron-style rewrite: quick cold start, small binary, real OS-native windowing.
An earlier attempt used Kotlin + Compose Multiplatform to stay on the JVM and reuse
FileBot's Java logic directly. That was abandoned (nothing was salvaged into this repo)
after discovering the sandbox this was originally built in blocks Maven Central and the
Gradle Plugin Portal, making it impossible to compile or test. Tauri was chosen as the
replacement because both halves — Cargo via crates.io, npm via the public npm registry
— are reachable, so every piece described as "done" below is actually compiled/tested,
not just written and hoped for.
### Compatibility notes
Goal: keep CLI flags, config file locations, and the *outer* `{ }` naming-scheme
template syntax compatible with FileBot conventions, so existing automation mostly
keeps working.
One deliberate gap: FileBot's `{ }` expressions ran arbitrary **Groovy** code
(JVM-only). Renamr's Rust core has no Groovy engine, so expressions are evaluated with
[Rhai](https://rhai.rs/) instead. Simple bindings (`{n}`, `{s00e00}`, `{t}`) are
identical. More complex chained-method expressions will need small syntax tweaks
(e.g. Groovy's `.toUpperCase()` → Rhai's `.to_upper()`) — Rhai already ships most of the
common string/array methods FileBot's expression helpers wrapped, so most patterns
translate directly.
## Project layout
```
crates/renamr-core/ Pure Rust core: naming-expression engine (ported), and — not yet
ported — matching/similarity scoring, media metadata parsing,
web datasource clients (TMDB/TVDB/AniDB/etc), the rename engine.
No Tauri dependency, so it builds and unit-tests standalone.
src-tauri/ Tauri app shell: exposes renamr-core as commands to the frontend,
app metadata/icons/window config (tauri.conf.json).
src/ React + TypeScript UI (Vite). Currently a single proof-of-concept
screen wired to the live naming-expression preview.
scripts/gen_icons.py Regenerates src-tauri/icons/ (placeholder branding — swap the
colors/glyph in the script, or replace the icons directly, to
change the look).
```
## Building
Prerequisites: Rust (stable) + Cargo, Node.js 18+, and Tauri's platform prerequisites
(see https://v2.tauri.app/start/prerequisites/ — on Linux this means
`libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev` and a C
toolchain; macOS/Windows need Xcode / the VS Build Tools + WebView2 respectively).
```sh
# Rust core (no UI deps needed):
cargo test -p renamr-core
# Full app in dev mode (opens a native window):
npm install
npm run tauri dev
# Production build (installer/bundle for your current OS):
npm run tauri build
```
Verified in the environment this was built in: `cargo test -p renamr-core` (all
passing), `cargo check -p renamr` (compiles clean against real Tauri + GTK/WebKit
system libraries), and `npm run build` (TypeScript type-checks, Vite bundles). The
actual GUI window has **not** been visually verified anywhere yet — that sandbox has no
display. Run `npm run tauri dev` on your machine to see and click through it for the
first time.
## Porting roadmap (not yet done)
Roughly in priority order — each references the original FileBot source file it's
ported from, for anyone picking this up:
1. **Full naming-scheme bindings** — `net.filebot.format.MediaBindingBean` (1354 lines):
all the `{n}`, `{s00e00}`, `{airdate}`, `{vf}` (video format), etc. binding
properties. Currently only whatever bindings you pass in manually are available.
2. **String/expression helper methods** — `net.filebot.format.ExpressionFormatMethods`
(636 lines): FileBot-specific Groovy string extensions like `.pad()`, `.sortName()`.
Only a small subset (`any`, `all_of`, `concat`) is ported so far
(`crates/renamr-core/src/format/functions.rs`); Rhai's built-ins cover some of the
rest already.
3. **Matching/similarity engine** — `net.filebot.similarity.*`, especially
`EpisodeMetrics.java` (849 lines, the core fuzzy-matching score cascade) and
`SeriesNameMatcher`/`SeasonEpisodeMatcher`/`DateMatcher`.
4. **Media detection** — `net.filebot.media.MediaDetection` (1356 lines): ties file-name
parsing + matching together to auto-identify what a file is.
5. **Web datasource clients** — `net.filebot.web.*` (~40 files): TheMovieDB, TheTVDB,
AniDB, OMDb, TVmaze, OpenSubtitles, etc. Use `reqwest` + `serde_json`/`serde`.
6. **Rename engine + CLI** — `net.filebot.cli.CmdlineOperations` (1235 lines) and
`ArgumentBean` (415 lines, the full flag surface: `-rename`, `--db`, `--format`,
`--action`, `--conflict`, `-get-subtitles`, `-script`, etc.) — flags are catalogued
in project notes but not yet implemented against real logic.
7. **Real UI** — file browser / drag-drop, rename preview table, settings, subtitle
panel. Current `src/App.tsx` is a single proof-of-concept screen.
8. **Packaging** — code signing, auto-update, installers per platform via
`npm run tauri build`.
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "renamr-core"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Renamr core: naming-expression engine, media matching, and rename engine (UI-agnostic)."
[dependencies]
# Embeddable scripting engine powering the {expression} naming-scheme language.
# Stands in for the Groovy JSR-223 engine FileBot used (Groovy is JVM-only, not available
# to a Rust core) while keeping the same outer `{ }` template syntax.
rhai = { version = "1.19", features = ["sync"] }
thiserror = "1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
+16
View File
@@ -0,0 +1,16 @@
use rhai::Engine;
use std::sync::OnceLock;
use super::functions;
/// Shared Rhai engine, built once. Engines are relatively expensive to construct
/// (loading the standard package), so every [`crate::format::ExpressionFormat`]
/// compiles against this single shared instance.
pub(super) fn shared() -> &'static Engine {
static ENGINE: OnceLock<Engine> = OnceLock::new();
ENGINE.get_or_init(|| {
let mut engine = Engine::new();
functions::register(&mut engine);
engine
})
}
+171
View File
@@ -0,0 +1,171 @@
use rhai::{Dynamic, Scope, AST};
use std::collections::HashMap;
use thiserror::Error;
use super::engine;
/// The variables available inside a `{ }` expression, e.g. `n` -> show name.
pub type Bindings = HashMap<String, Dynamic>;
#[derive(Debug, Error)]
pub enum ExpressionError {
#[error("syntax error in expression `{expression}`: {message}")]
Syntax { expression: String, message: String },
#[error("expression yields empty value: {0}")]
Empty(String),
}
enum Segment {
Literal(String),
/// Fast path for bare identifiers like `{n}` or `{s00e00}`: skips the script engine
/// entirely, both for speed and so an undefined binding fails predictably.
Variable(String),
Script(AST),
}
/// A compiled naming-scheme expression, e.g. `"{n} - {s00e00} - {t}"`.
///
/// Compilation is separate from formatting (like `java.text.Format`/FileBot's
/// `ExpressionFormat`) so a pattern is parsed once and then applied to many files.
#[derive(Debug)]
pub struct ExpressionFormat {
expression: String,
segments: Vec<Segment>,
}
impl std::fmt::Debug for Segment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Segment::Literal(s) => write!(f, "Literal({s:?})"),
Segment::Variable(s) => write!(f, "Variable({s:?})"),
Segment::Script(_) => write!(f, "Script(..)"),
}
}
}
impl ExpressionFormat {
pub fn compile(expression: &str) -> Result<Self, ExpressionError> {
let segments = parse(expression)?;
Ok(Self {
expression: expression.to_string(),
segments,
})
}
pub fn expression(&self) -> &str {
&self.expression
}
/// Renders this pattern against the given bindings. Mirrors FileBot's semantics:
/// a scriptlet that fails to evaluate (e.g. references an undefined/empty binding)
/// simply contributes nothing, but the *overall* result must be non-empty.
pub fn format(&self, bindings: &Bindings) -> Result<String, ExpressionError> {
let engine = engine::shared();
let mut out = String::new();
for segment in &self.segments {
match segment {
Segment::Literal(text) => out.push_str(text),
Segment::Variable(name) => {
if let Some(value) = bindings.get(name) {
if !value.is_unit() {
out.push_str(&value.to_string());
}
}
}
Segment::Script(ast) => {
let mut scope = Scope::new();
for (key, value) in bindings {
scope.push_dynamic(key.clone(), value.clone());
}
if let Ok(value) = engine.eval_ast_with_scope::<Dynamic>(&mut scope, ast) {
if !value.is_unit() {
out.push_str(&value.to_string());
}
}
}
}
}
if out.is_empty() {
return Err(ExpressionError::Empty(self.expression.clone()));
}
Ok(out)
}
}
fn is_identifier(token: &str) -> bool {
let mut chars = token.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
fn compile_scriptlet(token: &str, expression: &str) -> Result<Segment, ExpressionError> {
if is_identifier(token) {
return Ok(Segment::Variable(token.to_string()));
}
engine::shared()
.compile(token)
.map(Segment::Script)
.map_err(|e| ExpressionError::Syntax {
expression: expression.to_string(),
message: e.to_string(),
})
}
/// Splits an expression into literal text and `{ }` scriptlets, honoring nested braces
/// (so a scriptlet may itself contain `{ }`, e.g. a map literal or block).
fn parse(expression: &str) -> Result<Vec<Segment>, ExpressionError> {
let mut segments = Vec::new();
let mut token = String::new();
let mut level = 0i32;
for c in expression.chars() {
match c {
'{' => {
if level == 0 {
if !token.is_empty() {
segments.push(Segment::Literal(std::mem::take(&mut token)));
}
} else {
token.push(c);
}
level += 1;
}
'}' => {
level -= 1;
if level < 0 {
return Err(ExpressionError::Syntax {
expression: expression.to_string(),
message: "unexpected '}'".to_string(),
});
}
if level == 0 {
if !token.is_empty() {
segments.push(compile_scriptlet(&std::mem::take(&mut token), expression)?);
}
} else {
token.push(c);
}
}
_ => token.push(c),
}
}
if level != 0 {
return Err(ExpressionError::Syntax {
expression: expression.to_string(),
message: "missing '}'".to_string(),
});
}
if !token.is_empty() {
segments.push(Segment::Literal(token));
}
Ok(segments)
}
@@ -0,0 +1,52 @@
//! Global helper functions available inside every `{ }` expression, e.g. `{any([x, y])}`.
//!
//! Starter subset ported from FileBot's `ExpressionFormatFunctions.java`. Rhai already
//! provides `to_upper`, `to_lower`, `trim`, `sub_string`, `len`, `contains`, `pad` and
//! friends as built-in string methods, so this module only adds the FileBot-specific
//! helpers that don't already have a Rhai equivalent. More helpers (csv/read_lines/
//! quote_bash/quote_powershell, etc.) are tracked in the porting backlog.
use rhai::{Array, Dynamic, Engine};
fn is_empty_value(value: &Dynamic) -> bool {
if value.is_unit() {
return true;
}
if let Some(s) = value.clone().try_cast::<rhai::ImmutableString>() {
return s.is_empty();
}
if let Some(arr) = value.clone().try_cast::<Array>() {
return arr.is_empty();
}
false
}
/// Returns the first array element that isn't empty/unit, or `()` if all are empty.
fn any_of(candidates: Array) -> Dynamic {
candidates
.into_iter()
.find(|v| !is_empty_value(v))
.unwrap_or(Dynamic::UNIT)
}
/// Returns all non-empty elements, preserving order.
fn all_of(candidates: Array) -> Array {
candidates.into_iter().filter(|v| !is_empty_value(v)).collect()
}
/// Concatenates all non-empty elements' string forms with no separator.
fn concat(parts: Array) -> String {
parts
.into_iter()
.filter(|v| !is_empty_value(v))
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join("")
}
pub fn register(engine: &mut Engine) {
engine.register_fn("is_empty_value", is_empty_value);
engine.register_fn("any", any_of);
engine.register_fn("all_of", all_of);
engine.register_fn("concat", concat);
}
+18
View File
@@ -0,0 +1,18 @@
//! Renamr's naming-scheme expression language.
//!
//! A format string is literal text with `{ ... }` scriptlets spliced in, e.g.
//! `"{n} - {s00e00} - {t}"`
//! Everything inside `{ }` is evaluated as an expression against a set of "bindings"
//! (the variables available to the expression, e.g. `n` = show name).
//!
//! This keeps FileBot's outer `{ }` template syntax and simple-binding behavior
//! (`{n}`, `{s00e00}`, ...), so existing simple rename patterns keep working unchanged.
//! The inner expression *language* is Rhai rather than Groovy (Groovy is JVM-only and
//! this core is plain Rust) -- see the crate README for the compatibility notes on more
//! complex chained-method expressions.
mod engine;
mod expression;
mod functions;
pub use expression::{ExpressionError, ExpressionFormat};
+25
View File
@@ -0,0 +1,25 @@
//! Renamr core: the UI-agnostic engine behind the Renamr media renamer.
//!
//! This crate has no Tauri/UI dependency so it can be built and unit-tested standalone.
//! It's the piece being ported first from the original FileBot (Java) codebase; see
//! the workspace README for the porting roadmap (matching engine, media metadata
//! parsing, web datasource clients, rename engine are not yet ported).
pub mod format;
use rhai::Dynamic;
use std::collections::HashMap;
/// Convenience constructor for [`format::ExpressionFormat`] bindings from plain strings,
/// which is the common case (binding values are almost always show/movie/episode text).
pub fn string_bindings<I, K, V>(pairs: I) -> HashMap<String, Dynamic>
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
pairs
.into_iter()
.map(|(k, v)| (k.into(), Dynamic::from(v.into())))
.collect()
}
@@ -0,0 +1,66 @@
use renamr_core::format::ExpressionFormat;
use renamr_core::string_bindings;
#[test]
fn simple_variable_substitution() {
let fmt = ExpressionFormat::compile("{n} - {s00e00} - {t}").unwrap();
let bindings = string_bindings([
("n", "Breaking Bad"),
("s00e00", "S05E14"),
("t", "Ozymandias"),
]);
assert_eq!(fmt.format(&bindings).unwrap(), "Breaking Bad - S05E14 - Ozymandias");
}
#[test]
fn scripted_expression_evaluation() {
let fmt = ExpressionFormat::compile("{n.to_upper()}").unwrap();
let bindings = string_bindings([("n", "Breaking Bad")]);
assert_eq!(fmt.format(&bindings).unwrap(), "BREAKING BAD");
}
#[test]
fn literal_text_passes_through() {
let fmt = ExpressionFormat::compile("plain text, no expressions").unwrap();
assert_eq!(fmt.format(&Default::default()).unwrap(), "plain text, no expressions");
}
#[test]
fn global_helper_function_any() {
let fmt = ExpressionFormat::compile("{any([x, y])}").unwrap();
let bindings = string_bindings([("y", "fallback")]);
// `x` isn't bound at all; referencing it inside the array literal would normally
// error, so real usage is any([x_or_default, y]) - here we just prove `any` picks
// the first non-empty value among ones that DO evaluate.
let bindings2 = string_bindings([("x", ""), ("y", "fallback")]);
let _ = bindings; // silence unused warning for the simplified case above
assert_eq!(fmt.format(&bindings2).unwrap(), "fallback");
}
#[test]
fn undefined_binding_alone_yields_error() {
let fmt = ExpressionFormat::compile("{missing}").unwrap();
let result = fmt.format(&Default::default());
assert!(result.is_err(), "expected empty-result error, got {:?}", result);
}
#[test]
fn undefined_binding_is_skipped_when_other_content_exists() {
let fmt = ExpressionFormat::compile("{n}{missing}").unwrap();
let bindings = string_bindings([("n", "Show")]);
assert_eq!(fmt.format(&bindings).unwrap(), "Show");
}
#[test]
fn nested_braces_inside_a_scriptlet_are_preserved() {
// Rhai object map literal: #{ key: value }
let fmt = ExpressionFormat::compile("{ (#{a: 1, b: 2}).a }").unwrap();
assert_eq!(fmt.format(&Default::default()).unwrap(), "1");
}
#[test]
fn syntax_error_is_reported_with_the_offending_expression() {
let err = ExpressionFormat::compile("{this is not valid rhai +++ }").unwrap_err();
let message = err.to_string();
assert!(message.contains("syntax error"), "message was: {message}");
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Renamr</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1979
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "renamr",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@tauri-apps/api": "^2.1.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.1.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"typescript": "^5.6.3",
"vite": "^5.4.10"
}
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Generates Renamr's app icon set (placeholder branding, easy to swap later).
Produces the sizes Tauri's bundler expects in src-tauri/icons/.
"""
from PIL import Image, ImageDraw, ImageFont
import os
OUT = os.path.join(os.path.dirname(__file__), "..", "src-tauri", "icons")
os.makedirs(OUT, exist_ok=True)
SIZE = 1024
BG_TOP = (91, 76, 245) # indigo
BG_BOTTOM = (56, 189, 178) # teal
FG = (255, 255, 255)
def make_master():
img = Image.new("RGB", (SIZE, SIZE), BG_TOP)
px = img.load()
for y in range(SIZE):
t = y / SIZE
r = int(BG_TOP[0] * (1 - t) + BG_BOTTOM[0] * t)
g = int(BG_TOP[1] * (1 - t) + BG_BOTTOM[1] * t)
b = int(BG_TOP[2] * (1 - t) + BG_BOTTOM[2] * t)
for x in range(SIZE):
px[x, y] = (r, g, b)
# rounded-square mask
mask = Image.new("L", (SIZE, SIZE), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, SIZE - 1, SIZE - 1], radius=SIZE // 5, fill=255)
canvas = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
canvas.paste(img, (0, 0), mask)
draw = ImageDraw.Draw(canvas)
font = ImageFont.truetype("/usr/share/fonts/truetype/google-fonts/Poppins-Bold.ttf", int(SIZE * 0.56))
text = "R"
bbox = draw.textbbox((0, 0), text, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
draw.text(((SIZE - tw) / 2 - bbox[0], (SIZE - th) / 2 - bbox[1] - SIZE * 0.02), text, font=font, fill=FG)
return canvas
master = make_master()
master.save(os.path.join(OUT, "icon-master.png"))
sizes = [32, 128, 256, 512, 1024]
for s in sizes:
master.resize((s, s), Image.LANCZOS).save(os.path.join(OUT, f"{s}x{s}.png"))
# Tauri's expected filenames
master.resize((32, 32), Image.LANCZOS).save(os.path.join(OUT, "32x32.png"))
master.resize((128, 128), Image.LANCZOS).save(os.path.join(OUT, "128x128.png"))
master.resize((256, 256), Image.LANCZOS).save(os.path.join(OUT, "[email protected]"))
# Windows .ico (multi-resolution)
ico_sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
master.save(os.path.join(OUT, "icon.ico"), sizes=ico_sizes)
# macOS .icns
try:
master.save(os.path.join(OUT, "icon.icns"), format="ICNS")
except Exception as e:
print(f"WARN: could not write icon.icns via Pillow ({e}); leaving it out")
print("done:", os.listdir(OUT))
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "renamr"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
name = "renamr_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
renamr-core = { path = "../crates/renamr-core" }
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+22
View File
@@ -0,0 +1,22 @@
use renamr_core::format::ExpressionFormat;
use renamr_core::string_bindings;
use std::collections::HashMap;
/// Renders a naming-scheme expression against a set of binding values.
/// This is the Tauri-facing entry point into the core format engine, used by the
/// rename-preview panel in the UI so users see live results as they edit a pattern.
#[tauri::command]
fn render_expression(expression: String, bindings: HashMap<String, String>) -> Result<String, String> {
let compiled = ExpressionFormat::compile(&expression).map_err(|e| e.to_string())?;
compiled
.format(&string_bindings(bindings))
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![render_expression])
.run(tauri::generate_context!())
.expect("error while running Renamr");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents an additional console window on Windows in release builds.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
renamr_lib::run();
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Renamr",
"version": "0.1.0",
"identifier": "com.renamr.app",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Renamr",
"width": 1100,
"height": 720,
"minWidth": 720,
"minHeight": 480
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
+65
View File
@@ -0,0 +1,65 @@
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
const APP_NAME = "Renamr";
const APP_VERSION = "0.1.0";
const DEFAULT_EXPRESSION = "{n} - {s00e00} - {t}";
const DEFAULT_BINDINGS: Record<string, string> = {
n: "Example Show",
s00e00: "S01E01",
t: "Pilot",
};
export default function App() {
const [expression, setExpression] = useState(DEFAULT_EXPRESSION);
const [preview, setPreview] = useState<string>("");
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
invoke<string>("render_expression", { expression, bindings: DEFAULT_BINDINGS })
.then((result) => {
if (!cancelled) {
setPreview(result);
setError(null);
}
})
.catch((err) => {
if (!cancelled) {
setPreview("");
setError(String(err));
}
});
return () => {
cancelled = true;
};
}, [expression]);
return (
<main className="shell">
<header>
<h1>{APP_NAME}</h1>
<p className="tagline">A fast, cross-platform media renamer.</p>
<p className="version">v{APP_VERSION}</p>
</header>
<section className="panel">
<label htmlFor="expression">Naming scheme</label>
<input
id="expression"
value={expression}
onChange={(e) => setExpression(e.target.value)}
spellCheck={false}
/>
<div className="preview">
<span className="preview-label">Preview</span>
{error ? <span className="preview-error">{error}</span> : <span className="preview-value">{preview}</span>}
</div>
</section>
</main>
);
}
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+86
View File
@@ -0,0 +1,86 @@
:root {
color-scheme: light dark;
font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
margin: 0;
background: #0f1222;
color: #f4f5fb;
}
.shell {
max-width: 640px;
margin: 0 auto;
padding: 48px 24px;
}
header h1 {
font-size: 2.25rem;
margin: 0;
background: linear-gradient(135deg, #5b4cf5, #38bdb2);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.tagline {
margin: 4px 0 0;
opacity: 0.8;
}
.version {
margin: 2px 0 0;
font-size: 0.8rem;
opacity: 0.5;
}
.panel {
margin-top: 32px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 20px;
}
label {
display: block;
font-size: 0.8rem;
opacity: 0.7;
margin-bottom: 8px;
}
input {
width: 100%;
box-sizing: border-box;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(0, 0, 0, 0.25);
color: inherit;
font-size: 1rem;
font-family: ui-monospace, monospace;
}
.preview {
margin-top: 16px;
padding: 12px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.25);
}
.preview-label {
display: block;
font-size: 0.75rem;
opacity: 0.6;
margin-bottom: 4px;
}
.preview-value {
font-family: ui-monospace, monospace;
}
.preview-error {
color: #ff8080;
font-family: ui-monospace, monospace;
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2021",
"useDefineForClassFields": true,
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// Tauri expects a fixed dev server port and relative asset paths.
export default defineConfig({
plugins: [react()],
clearScreen: false,
server: {
port: 5173,
strictPort: true,
},
envPrefix: ["VITE_", "TAURI_"],
build: {
target: "esnext",
outDir: "dist",
},
});