Move to tailwind-build instead of CDNs

This commit is contained in:
Mukhtar Akere
2025-07-10 02:17:35 +01:00
parent c72867ff57
commit cf61546bec
32 changed files with 4149 additions and 20 deletions

View File

@@ -11,3 +11,27 @@ torrents.json
*.json
.ven/**
docs/**
# Don't copy built assets to avoid conflicts
pkg/web/assets/build/
# Don't copy node modules
node_modules/
# Don't copy development files
.git/
.gitignore
*.md
.env*
*.log
# Don't copy source assets to the Go builder (they're copied from asset-builder)
pkg/web/assets/css/
pkg/web/assets/js/
pkg/web/assets/images/
# Build artifacts
decypharr
healthcheck
*.exe

View File

@@ -7,6 +7,7 @@ on:
permissions:
contents: write
packages: write
jobs:
goreleaser:
@@ -22,6 +23,29 @@ jobs:
with:
go-version: '1.24'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install Node.js dependencies
run: npm ci
- name: Build and minify assets
run: npm run build-assets
- name: Verify assets were built
run: |
echo "🔍 Verifying built assets..."
ls -la pkg/web/assets/build/
echo ""
echo "📊 Asset sizes:"
du -sh pkg/web/assets/build/* 2>/dev/null || echo "No assets found"
echo ""
echo "📁 Total build directory size:"
du -sh pkg/web/assets/build/
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:

4
.gitignore vendored
View File

@@ -12,4 +12,6 @@ tmp/**
torrents.json
logs/**
auth.json
.ven/
.ven/
.env
node_modules/

1624
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "decypharr",
"version": "1.0.0",
"description": "Media management tool",
"scripts": {
"build-css": "tailwindcss -i ./pkg/web/assets/styles.css -o ./pkg/web/assets/build/css/styles.css --minify",
"minify-js": "node scripts/minify-js.js",
"minify-css": "node scripts/minify-css.js",
"download-assets": "node scripts/download-assets.js",
"build": "npm run build-css && npm run minify-js && npm run minify-css && npm run download-assets",
"dev": "npm run build-assets && air"
},
"devDependencies": {
"tailwindcss": "^3.4.0",
"daisyui": "^4.12.10",
"terser": "^5.24.0",
"clean-css": "^5.3.3"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2078
pkg/web/assets/css/bootstrap-icons.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 284 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 665 B

After

Width:  |  Height:  |  Size: 665 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

View File

@@ -1,4 +1,7 @@
/* Custom Styles for Decypharr */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Smooth transitions for all interactive elements */
* {
@@ -6,7 +9,6 @@
}
/* Context menu styles */
.context-menu {
position: absolute;

View File

@@ -9,12 +9,18 @@ import (
func (wb *Web) Routes() http.Handler {
r := chi.NewRouter()
staticFS, err := fs.Sub(content, "assets")
// Load static files from embedded filesystem
staticFS, err := fs.Sub(assetsEmbed, "assets/build")
if err != nil {
panic(err)
}
imagesFS, err := fs.Sub(imagesEmbed, "assets/images")
if err != nil {
panic(err)
}
r.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServer(http.FS(staticFS))))
r.Handle("/images/*", http.StripPrefix("/images/", http.FileServer(http.FS(imagesFS))))
r.Get("/login", wb.LoginHandler)
r.Post("/login", wb.LoginHandler)

View File

@@ -6,20 +6,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Decypharr - {{.Title}}</title>
<!-- DaisyUI and Tailwind CSS -->
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link href="{{.URLBase}}assets/css/styles.css" rel="stylesheet" type="text/css" />
<link href="{{.URLBase}}assets/css/bootstrap-icons.css" rel="stylesheet" type="text/css" />
<!-- Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom Styles -->
<link href="{{.URLBase}}assets/css/styles.css" rel="stylesheet">
<link rel="apple-touch-icon" sizes="180x180" href="{{.URLBase}}assetsfavicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{.URLBase}}assets/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{.URLBase}}assets/favicon/favicon-16x16.png">
<link rel="manifest" href="{{.URLBase}}assets/favicon/site.webmanifest">
<link rel="apple-touch-icon" sizes="180x180" href="{{.URLBase}}images/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{.URLBase}}images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{.URLBase}}images/favicon/favicon-16x16.png">
<link rel="manifest" href="{{.URLBase}}images/favicon/site.webmanifest">
<!-- Preload JavaScript -->
<link rel="preload" href="{{.URLBase}}assets/js/common.js" as="script">
@@ -79,7 +72,7 @@
</div>
<a class="btn btn-ghost text-xl font-bold text-primary group" href="{{.URLBase}}">
<!-- Logo -->
<img src="{{.URLBase}}assets/logo.svg" alt="Decypharr Logo" class="w-8 h-8 inline-block mr-2">
<img src="{{.URLBase}}images/logo.svg" alt="Decypharr Logo" class="w-8 h-8 inline-block mr-2">
<span class="hidden sm:inline bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Decypharr</span>
</a>
</div>
@@ -188,7 +181,7 @@
</footer>
<!-- Scripts -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="{{.URLBase}}assets/js/jquery-3.7.1.min.js"></script>
<script src="{{.URLBase}}assets/js/common.js"></script>
<!-- Page-specific scripts -->

View File

@@ -47,9 +47,15 @@ type RepairRequest struct {
AutoProcess bool `json:"autoProcess"`
}
//go:embed templates/* assets/*
//go:embed templates/*
var content embed.FS
//go:embed assets/build
var assetsEmbed embed.FS
//go:embed assets/images
var imagesEmbed embed.FS
type Web struct {
logger zerolog.Logger
cookie *sessions.CookieStore

112
scripts/download-assets.js Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const https = require('https');
const buildDir = {
css: './pkg/web/assets/build/css',
js: './pkg/web/assets/build/js',
fonts: './pkg/web/assets/build/fonts'
};
// Create directories
Object.values(buildDir).forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// Download function
function downloadFile(url, filepath) {
return new Promise((resolve, reject) => {
console.log(`📥 Downloading ${path.basename(filepath)}...`);
const file = fs.createWriteStream(filepath);
https.get(url, (response) => {
if (response.statusCode === 200) {
response.pipe(file);
file.on('finish', () => {
file.close();
const stats = fs.statSync(filepath);
const size = (stats.size / 1024).toFixed(1) + 'KB';
console.log(` ✓ Downloaded ${path.basename(filepath)} (${size})`);
resolve();
});
} else if (response.statusCode === 302 || response.statusCode === 301) {
downloadFile(response.headers.location, filepath).then(resolve).catch(reject);
} else {
reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
}
}).on('error', reject);
});
}
// Download text content
function downloadText(url) {
return new Promise((resolve, reject) => {
https.get(url, (response) => {
let data = '';
response.on('data', chunk => data += chunk);
response.on('end', () => {
if (response.statusCode === 200) {
resolve(data);
} else {
reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
}
});
}).on('error', reject);
});
}
// Files to download
const downloads = [
{
url: 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/fonts/bootstrap-icons.woff',
path: path.join(buildDir.fonts, 'bootstrap-icons.woff')
},
{
url: 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/fonts/bootstrap-icons.woff2',
path: path.join(buildDir.fonts, 'bootstrap-icons.woff2')
},
{
url: 'https://code.jquery.com/jquery-3.7.1.min.js',
path: path.join(buildDir.js, 'jquery-3.7.1.min.js')
}
];
// Download all files
async function downloadAssets() {
console.log('📦 Downloading external assets...\n');
try {
// Download Bootstrap Icons CSS and fix paths
console.log('📥 Downloading Bootstrap Icons CSS...');
const biCSS = await downloadText('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css');
// Fix font paths to point to our local fonts
const fixedCSS = biCSS.replace(
/url\("\.\/fonts\//g,
'url("../fonts/'
);
// Write fixed CSS to source directory so it can be minified
const biCSSSourcePath = path.join('./pkg/web/assets/css', 'bootstrap-icons.css');
fs.writeFileSync(biCSSSourcePath, fixedCSS);
console.log(` ✓ Downloaded Bootstrap Icons CSS (${(fixedCSS.length/1024).toFixed(1)}KB)`);
// Download other assets
for (const download of downloads) {
await downloadFile(download.url, download.path);
}
console.log('\n✅ External assets downloaded successfully!');
} catch (error) {
console.error('💥 Error downloading assets:', error);
process.exit(1);
}
}
downloadAssets();

96
scripts/minify-css.js Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const CleanCSS = require('clean-css');
const sourceDir = './pkg/web/assets/css';
const buildDir = './pkg/web/assets/build/css';
// Create build directory
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Create source directory if it doesn't exist
if (!fs.existsSync(sourceDir)) {
fs.mkdirSync(sourceDir, { recursive: true });
}
const cleanCSS = new CleanCSS({
level: 2, // Aggressive optimization
returnPromise: false
});
function minifyFile(inputPath, outputPath) {
try {
console.log(`🎨 Minifying ${path.basename(inputPath)}...`);
const css = fs.readFileSync(inputPath, 'utf8');
const result = cleanCSS.minify(css);
if (result.errors.length > 0) {
throw new Error(result.errors.join('\n'));
}
fs.writeFileSync(outputPath, result.styles);
// Show size reduction
const originalSize = Buffer.byteLength(css, 'utf8');
const minifiedSize = Buffer.byteLength(result.styles, 'utf8');
const reduction = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
console.log(`${path.basename(inputPath)}: ${(originalSize/1024).toFixed(1)}KB → ${(minifiedSize/1024).toFixed(1)}KB (${reduction}% reduction)`);
return { original: originalSize, minified: minifiedSize };
} catch (error) {
console.error(` ✗ Error minifying ${inputPath}:`, error.message);
return null;
}
}
function minifyAllCSS() {
console.log('🎨 Minifying additional CSS files...\n');
try {
// Get all CSS files from source directory (excluding the main styles.css which is built by Tailwind)
const cssFiles = fs.readdirSync(sourceDir).filter(file =>
file.endsWith('.css') && file !== 'styles.css'
);
if (cssFiles.length === 0) {
console.log(' No additional CSS files found to minify');
return;
}
let totalOriginal = 0;
let totalMinified = 0;
let processedFiles = 0;
// Minify each file
cssFiles.forEach(file => {
const inputPath = path.join(sourceDir, file);
const outputPath = path.join(buildDir, file);
const result = minifyFile(inputPath, outputPath);
if (result) {
totalOriginal += result.original;
totalMinified += result.minified;
processedFiles++;
}
});
if (processedFiles > 0) {
const totalReduction = ((totalOriginal - totalMinified) / totalOriginal * 100).toFixed(1);
console.log(`\n✅ Successfully minified ${processedFiles}/${cssFiles.length} additional CSS file(s)`);
console.log(`📊 Total: ${(totalOriginal/1024).toFixed(1)}KB → ${(totalMinified/1024).toFixed(1)}KB (${totalReduction}% reduction)`);
}
} catch (error) {
console.error('💥 Error during CSS minification:', error);
process.exit(1);
}
}
minifyAllCSS();

118
scripts/minify-js.js Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { minify } = require('terser');
const sourceDir = './pkg/web/assets/js';
const buildDir = './pkg/web/assets/build/js';
// Create build directory
if (!fs.existsSync(buildDir)) {
fs.mkdirSync(buildDir, { recursive: true });
}
// Minify options
const minifyOptions = {
compress: {
drop_console: false, // Keep console.log for debugging
drop_debugger: true,
dead_code: true,
unused: true,
sequences: true,
conditionals: true,
booleans: true,
if_return: true,
join_vars: true,
},
mangle: {
toplevel: false,
reserved: [
'$', 'jQuery', 'decypharrUtils', 'configManager', 'repairManager',
'RepairManager', 'RepairUtils', 'ConfigManager', 'window', 'document'
]
},
format: {
comments: false,
beautify: false
}
};
async function minifyFile(inputPath, outputPath) {
try {
console.log(`🗜️ Minifying ${path.basename(inputPath)}...`);
const code = fs.readFileSync(inputPath, 'utf8');
const result = await minify(code, minifyOptions);
if (result.error) {
throw result.error;
}
fs.writeFileSync(outputPath, result.code);
// Show size reduction
const originalSize = fs.statSync(inputPath).size;
const minifiedSize = fs.statSync(outputPath).size;
const reduction = ((originalSize - minifiedSize) / originalSize * 100).toFixed(1);
console.log(`${path.basename(inputPath)}: ${(originalSize/1024).toFixed(1)}KB → ${(minifiedSize/1024).toFixed(1)}KB (${reduction}% reduction)`);
return { original: originalSize, minified: minifiedSize };
} catch (error) {
console.error(` ✗ Error minifying ${inputPath}:`, error.message);
return null;
}
}
async function minifyAllJS() {
console.log('📦 Minifying JavaScript files...\n');
try {
// Check if source directory exists
if (!fs.existsSync(sourceDir)) {
console.log(`Creating source directory ${sourceDir}...`);
fs.mkdirSync(sourceDir, { recursive: true });
console.log(' No JavaScript files found to minify');
return;
}
// Get all JS files from source directory
const jsFiles = fs.readdirSync(sourceDir).filter(file => file.endsWith('.js'));
if (jsFiles.length === 0) {
console.log(' No JavaScript files found to minify');
return;
}
let totalOriginal = 0;
let totalMinified = 0;
let processedFiles = 0;
// Minify each file
for (const file of jsFiles) {
const inputPath = path.join(sourceDir, file);
const outputPath = path.join(buildDir, file);
const result = await minifyFile(inputPath, outputPath);
if (result) {
totalOriginal += result.original;
totalMinified += result.minified;
processedFiles++;
}
}
if (processedFiles > 0) {
const totalReduction = ((totalOriginal - totalMinified) / totalOriginal * 100).toFixed(1);
console.log(`\n✅ Successfully minified ${processedFiles}/${jsFiles.length} JavaScript file(s)`);
console.log(`📊 Total: ${(totalOriginal/1024).toFixed(1)}KB → ${(totalMinified/1024).toFixed(1)}KB (${totalReduction}% reduction)`);
}
} catch (error) {
console.error('💥 Error during JavaScript minification:', error);
process.exit(1);
}
}
minifyAllJS();

12
tailwind.config.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
content: [
"./pkg/web/templates/**/*.html",
"./pkg/web/assets/**/*.js"
],
theme: {
extend: {},
},
plugins: [
require('daisyui'),
],
};