diff --git a/frontend/.gitignore b/frontend/.gitignore index c10a00946..0fd28e3aa 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -5,6 +5,8 @@ /tmp /out-tsc server.run.js +.theme-build +theme-manifest.json # docker Dockerfile diff --git a/frontend/angular.json b/frontend/angular.json index d7acbeeaa..9c1580767 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -171,25 +171,15 @@ "src/robots.txt", "src/config.js", "src/customize.js", - "src/config.template.js" + "src/config.template.js", + { + "glob": "*.css", + "input": ".theme-build", + "output": "/" + } ], "styles": [ "src/styles.scss", - { - "input": "src/theme-contrast.scss", - "bundleName": "contrast", - "inject": false - }, - { - "input": "src/theme-softsimon.scss", - "bundleName": "softsimon", - "inject": false - }, - { - "input": "src/theme-bukele.scss", - "bundleName": "bukele", - "inject": false - }, "node_modules/@fortawesome/fontawesome-svg-core/styles.css" ], "vendorChunk": true, diff --git a/frontend/generate-config.js b/frontend/generate-config.js index 3ae3870e5..4485c2a95 100644 --- a/frontend/generate-config.js +++ b/frontend/generate-config.js @@ -46,6 +46,22 @@ try { throw new Error(e); } +// Inject theme manifest if it exists (created by generate-themes.js) +const THEME_MANIFEST_FILE = 'theme-manifest.json'; +try { + const themeManifest = fs.readFileSync(THEME_MANIFEST_FILE, 'utf-8'); + const themeFiles = JSON.parse(themeManifest); + let indexHtml = fs.readFileSync('src/index.html', 'utf-8'); + const script = ` `; + indexHtml = indexHtml.replace('', `${script}\n`); + fs.writeFileSync('src/index.html', indexHtml); + console.log('Injected theme manifest into src/index.html:', themeFiles); +} catch (e) { + if (e.code !== 'ENOENT') { + console.log('Warning: Could not inject theme manifest:', e.message); + } +} + try { const packageJson = fs.readFileSync('package.json'); packetJsonVersion = JSON.parse(packageJson).version; diff --git a/frontend/generate-themes.js b/frontend/generate-themes.js new file mode 100644 index 000000000..da7f06c20 --- /dev/null +++ b/frontend/generate-themes.js @@ -0,0 +1,57 @@ +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const THEMES = ['contrast', 'softsimon', 'bukele']; +const STAGING_DIR = path.join(__dirname, '.theme-build'); +const DIST_DIR = path.join(__dirname, 'dist/mempool/browser'); +const MANIFEST_FILE = path.join(__dirname, 'theme-manifest.json'); + +const command = process.argv[2]; + +if (command === 'copy') { + const themeFiles = fs.readdirSync(STAGING_DIR).filter(f => f.endsWith('.css')); + for (const dir of fs.readdirSync(DIST_DIR, { withFileTypes: true })) { + if (dir.isDirectory()) { + for (const file of themeFiles) { + fs.copyFileSync(path.join(STAGING_DIR, file), path.join(DIST_DIR, dir.name, file)); + } + } + } + console.log(`Copied ${themeFiles.length} theme files to all locale directories`); +} else { + fs.rmSync(STAGING_DIR, { recursive: true, force: true }); + fs.mkdirSync(STAGING_DIR, { recursive: true }); + + const manifest = {}; + + for (const theme of THEMES) { + const inputFile = path.join(__dirname, `src/theme-${theme}.scss`); + const tempOutput = path.join(STAGING_DIR, `${theme}.tmp.css`); + + try { + execSync(`npx sass --style=compressed --no-source-map "${inputFile}" "${tempOutput}"`, { + stdio: 'pipe' + }); + } catch (e) { + console.error(`Failed to compile theme-${theme}.scss:`, e.message); + process.exit(1); + } + + const css = fs.readFileSync(tempOutput); + const hash = crypto.createHash('md5').update(css).digest('hex').slice(0, 16); + + const nonHashedFilename = `${theme}.css`; + fs.copyFileSync(tempOutput, path.join(STAGING_DIR, nonHashedFilename)); + + const hashedFilename = `${theme}.${hash}.css`; + fs.renameSync(tempOutput, path.join(STAGING_DIR, hashedFilename)); + + manifest[theme] = hashedFilename; + console.log(`Built ${nonHashedFilename} and ${hashedFilename}`); + } + + fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2)); + console.log('Theme manifest written to theme-manifest.json'); +} diff --git a/frontend/package.json b/frontend/package.json index f5db71ef0..389efdd9c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,16 +24,18 @@ "tsc": "./node_modules/typescript/bin/tsc", "i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf", "i18n-pull-from-transifex": "tx pull -a --minimum-perc 1 --force", - "serve": "npm run generate-config && npm run ng -- serve -c local", - "serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod", - "serve:parameterized": "npm run generate-config && npm run ng -- serve -c parameterized", - "start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local", - "start:parameterized": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized", - "start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora", - "start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod", - "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", - "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets", - "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'", + "serve": "npm run generate-themes && npm run generate-config && npm run ng -- serve -c local", + "serve:local-prod": "npm run generate-themes && npm run generate-config && npm run ng -- serve -c local-prod", + "serve:parameterized": "npm run generate-themes && npm run generate-config && npm run ng -- serve -c parameterized", + "start": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local", + "start:parameterized": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized", + "start:local-esplora": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora", + "start:local-prod": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod", + "start:mixed": "npm run generate-themes && npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed", + "build": "npm run generate-themes && npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets", + "generate-themes": "node generate-themes.js", + "copy-themes": "node generate-themes.js copy", + "sync-assets": "npm run copy-themes && rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'", "sync-assets-dev": "node sync-assets.js 'src/resources/'", "generate-config": "node generate-config.js", "test": "npm run ng -- test", diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts index 4a6e2dfae..0f184e9d2 100644 --- a/frontend/src/app/services/theme.service.ts +++ b/frontend/src/app/services/theme.service.ts @@ -68,7 +68,7 @@ export class ThemeService { this.themeState$.next({ theme, loading: false }); }; this.style.onerror = () => this.apply('default'); - this.style.href = `${theme}.css`; + this.style.href = this.getThemeFile(theme); if (!this.stateService.env.customize?.theme) { this.storageService.setValue('theme-preference', theme); @@ -78,4 +78,12 @@ export class ThemeService { this.apply('default'); } } + + private getThemeFile(theme: string): string { + const themeFiles = (window as any).__env?.THEME_FILES; + if (themeFiles?.[theme]) { + return themeFiles[theme]; + } + return `${theme}.css`; + } }