@mtldev514/retro-portfolio-maker 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +409 -0
- package/bin/cli.js +103 -0
- package/engine/admin/admin_api.py +221 -0
- package/engine/admin/scripts/backup.sh +116 -0
- package/engine/admin/scripts/config_loader.py +180 -0
- package/engine/admin/scripts/init.sh +141 -0
- package/engine/admin/scripts/manager.py +308 -0
- package/engine/admin/scripts/restore.sh +121 -0
- package/engine/admin/scripts/server.py +41 -0
- package/engine/admin/scripts/update.sh +321 -0
- package/engine/admin/scripts/validate_json.py +62 -0
- package/engine/admin.css +720 -0
- package/engine/admin.html +801 -0
- package/engine/fonts.css +37 -0
- package/engine/index.html +186 -0
- package/engine/js/config-loader.js +370 -0
- package/engine/js/config.js +173 -0
- package/engine/js/counter.js +17 -0
- package/engine/js/effects.js +97 -0
- package/engine/js/i18n.js +68 -0
- package/engine/js/init.js +107 -0
- package/engine/js/media.js +264 -0
- package/engine/js/render.js +282 -0
- package/engine/js/router.js +133 -0
- package/engine/js/sparkle.js +123 -0
- package/engine/js/themes.js +607 -0
- package/engine/style.css +2037 -0
- package/index.js +35 -0
- package/package.json +48 -0
- package/scripts/admin.js +63 -0
- package/scripts/build.js +142 -0
- package/scripts/init.js +369 -0
- package/scripts/init.js.bak +331 -0
- package/scripts/init.js.bak2 +331 -0
- package/scripts/init.js.bak3 +331 -0
- package/scripts/post-install.js +16 -0
- package/scripts/serve.js +54 -0
- package/templates/.env.example +10 -0
- package/templates/user-portfolio/.github/workflows/deploy.yml +57 -0
- package/templates/user-portfolio/config/app.json +36 -0
- package/templates/user-portfolio/config/categories.json +241 -0
- package/templates/user-portfolio/config/languages.json +15 -0
- package/templates/user-portfolio/config/media-types.json +59 -0
- package/templates/user-portfolio/data/painting.json +3 -0
- package/templates/user-portfolio/data/projects.json +3 -0
- package/templates/user-portfolio/lang/en.json +114 -0
- package/templates/user-portfolio/lang/fr.json +114 -0
package/engine/fonts.css
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* ═══ Typography — independent of theme/style ═══ */
|
|
2
|
+
|
|
3
|
+
/* Base */
|
|
4
|
+
body {
|
|
5
|
+
font-size: 14px;
|
|
6
|
+
line-height: 1.5;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Gallery cards */
|
|
10
|
+
.gallery-item h3 {
|
|
11
|
+
font-size: 0.95em;
|
|
12
|
+
margin: 4px 5px 0;
|
|
13
|
+
line-height: 1.3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.gallery-item .gallery-subtitle {
|
|
17
|
+
font-size: 0.85em;
|
|
18
|
+
margin: 1px 5px 0;
|
|
19
|
+
line-height: 1.3;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Dates — switch from monospace to primary font */
|
|
23
|
+
.item-date {
|
|
24
|
+
font-size: 0.75em;
|
|
25
|
+
font-family: var(--font-primary);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* UI controls — bump from below-legibility sizes */
|
|
29
|
+
.sort-btn { font-size: 11px; }
|
|
30
|
+
|
|
31
|
+
/* Winamp — minimal bumps to preserve skin authenticity */
|
|
32
|
+
.winamp-title { font-size: 10px; }
|
|
33
|
+
.winamp-pl-item { font-size: 11px; }
|
|
34
|
+
|
|
35
|
+
/* Terminal — sharper text */
|
|
36
|
+
.terminal-body { font-size: 12px; }
|
|
37
|
+
.terminal-line { text-shadow: 0 0 2px var(--term-glow, rgba(0,255,0,0.2)); }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title data-i18n="header_title">alex a montreal</title>
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
<link rel="stylesheet" href="fonts.css">
|
|
10
|
+
|
|
11
|
+
<!-- Load configuration first (supports remote/local sources) -->
|
|
12
|
+
<script src="js/config-loader.js"></script>
|
|
13
|
+
<script src="js/i18n.js" defer></script>
|
|
14
|
+
<script src="js/themes.js" defer></script>
|
|
15
|
+
<script src="js/render.js" defer></script>
|
|
16
|
+
<script src="js/router.js" defer></script>
|
|
17
|
+
<script src="js/media.js" defer></script>
|
|
18
|
+
<script src="js/sparkle.js" defer></script>
|
|
19
|
+
<script src="js/effects.js" defer></script>
|
|
20
|
+
<!-- Initialize app after all modules loaded -->
|
|
21
|
+
<script src="js/init.js" defer></script>
|
|
22
|
+
</head>
|
|
23
|
+
|
|
24
|
+
<body>
|
|
25
|
+
<div id="rotate-overlay">
|
|
26
|
+
<div class="rotate-content">
|
|
27
|
+
<div class="rotate-icon">📺↩️</div>
|
|
28
|
+
<h2>ROTATE YOUR DEVICE!</h2>
|
|
29
|
+
<p>This website was designed for<br><b>DESKTOP COMPUTERS</b></p>
|
|
30
|
+
<p class="rotate-sub">Please turn your phone sideways<br>for the full retro experience!</p>
|
|
31
|
+
<div class="rotate-ascii">
|
|
32
|
+
┌─────────────────┐<br>
|
|
33
|
+
│ ╔══════════╗ │<br>
|
|
34
|
+
│ ║ TURN ║ │<br>
|
|
35
|
+
│ ║ ME! ║ │<br>
|
|
36
|
+
│ ╚══════════╝ │<br>
|
|
37
|
+
└─────────────────┘<br>
|
|
38
|
+
⟲
|
|
39
|
+
</div>
|
|
40
|
+
<p class="rotate-blink">*** LANDSCAPE MODE REQUIRED ***</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="container">
|
|
44
|
+
<header>
|
|
45
|
+
<div class="settings-switcher">
|
|
46
|
+
<button class="settings-btn">⚙</button>
|
|
47
|
+
<div class="settings-dropdown">
|
|
48
|
+
<div class="settings-section-label">Effects</div>
|
|
49
|
+
<div class="settings-option" onclick="togglePartyMode()">
|
|
50
|
+
<span id="party-mode-indicator">✨</span> Party Mode
|
|
51
|
+
</div>
|
|
52
|
+
<div class="settings-divider"></div>
|
|
53
|
+
<div class="settings-section-label">Theme</div>
|
|
54
|
+
<div class="settings-option" onclick="themes.changeTheme('jr16')"><span class="theme-icon">🌿</span>
|
|
55
|
+
JR-16</div>
|
|
56
|
+
<div class="settings-option" onclick="themes.changeTheme('beton')"><span
|
|
57
|
+
class="theme-icon">🌫️</span> Béton</div>
|
|
58
|
+
<div class="settings-option" onclick="themes.changeTheme('ciment')"><span
|
|
59
|
+
class="theme-icon">🪨</span> Ciment</div>
|
|
60
|
+
<div class="settings-option" onclick="themes.changeTheme('bubblegum')"><span
|
|
61
|
+
class="theme-icon">🍬</span> Bubble Gum</div>
|
|
62
|
+
<div class="settings-divider"></div>
|
|
63
|
+
<div class="settings-section-label">Language</div>
|
|
64
|
+
<div class="settings-option" onclick="i18n.changeLang('en')"><span class="lang-flag">🇬🇧🇨🇦</span>
|
|
65
|
+
English</div>
|
|
66
|
+
<div class="settings-option" onclick="i18n.changeLang('fr')"><span class="lang-flag">⚜️🇨🇦</span>
|
|
67
|
+
French</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<h1 id="page-title" data-i18n="header_title">alex a montreal</h1>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
<div class="marquee-container">
|
|
74
|
+
<marquee scrollamount="5" data-i18n="marquee"></marquee>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="content">
|
|
78
|
+
<aside class="sidebar">
|
|
79
|
+
<div class="winamp">
|
|
80
|
+
<div class="winamp-titlebar">
|
|
81
|
+
<span class="winamp-grip"></span>
|
|
82
|
+
<span class="winamp-title">Radyo</span>
|
|
83
|
+
<span class="winamp-grip"></span>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="winamp-display">
|
|
86
|
+
<div class="winamp-time-row">
|
|
87
|
+
<span class="winamp-time" id="winamp-time">00:00</span>
|
|
88
|
+
<span class="winamp-time-sep">/</span>
|
|
89
|
+
<span class="winamp-duration" id="winamp-duration">00:00</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="winamp-ticker">
|
|
92
|
+
<span class="radio-track-name" data-i18n="sidebar_radio_title">Your Radio</span> - Winamp 2.91
|
|
93
|
+
</div>
|
|
94
|
+
<div class="winamp-viz" id="winamp-viz"></div>
|
|
95
|
+
<div class="winamp-info">
|
|
96
|
+
<div class="winamp-bitrate"><span class="winamp-kbps">192</span>kbps</div>
|
|
97
|
+
<div class="winamp-freq"><span class="winamp-khz">44</span>kHz</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="winamp-transport">
|
|
101
|
+
<button class="winamp-btn radio-prev" title="Previous"><svg class="icon" viewBox="0 0 24 24">
|
|
102
|
+
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" transform="scale(-1,1) translate(-24,0)" />
|
|
103
|
+
</svg></button>
|
|
104
|
+
<button class="winamp-btn radio-playpause" title="Play"><svg class="icon" viewBox="0 0 24 24">
|
|
105
|
+
<path d="M8 5v14l11-7z" />
|
|
106
|
+
</svg></button>
|
|
107
|
+
<button class="winamp-btn radio-next" title="Next"><svg class="icon" viewBox="0 0 24 24">
|
|
108
|
+
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
|
|
109
|
+
</svg></button>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="winamp-volume-row">
|
|
112
|
+
<svg class="icon winamp-vol-icon" viewBox="0 0 24 24">
|
|
113
|
+
<path d="M3 9v6h4l5 5V4L7 9H3z" />
|
|
114
|
+
</svg>
|
|
115
|
+
<input type="range" class="winamp-volume radio-volume" value="80" min="0" max="100">
|
|
116
|
+
<svg class="icon winamp-vol-icon" viewBox="0 0 24 24">
|
|
117
|
+
<path
|
|
118
|
+
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
|
|
119
|
+
</svg>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="winamp-seek-row">
|
|
122
|
+
<input type="range" class="winamp-seek" id="winamp-seek" value="0" min="0" max="100">
|
|
123
|
+
</div>
|
|
124
|
+
<div class="winamp-playlist-titlebar">
|
|
125
|
+
<span class="winamp-grip-sm"></span>
|
|
126
|
+
<span class="winamp-pl-title">PLAYLIST</span>
|
|
127
|
+
<span class="winamp-grip-sm"></span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="winamp-playlist" id="radio-tracklist">
|
|
130
|
+
<div class="winamp-pl-item winamp-pl-empty" data-i18n="sidebar_radio_loading">Loading tracks...
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</aside>
|
|
135
|
+
|
|
136
|
+
<main id="app">
|
|
137
|
+
<div id="filter-nav" class="filter-bar">
|
|
138
|
+
<button class="filter-btn active" data-filter="all" data-i18n="filter_all">All</button>
|
|
139
|
+
<button class="filter-btn" data-filter="painting" data-i18n="nav_painting">Painting</button>
|
|
140
|
+
<button class="filter-btn" data-filter="drawing" data-i18n="nav_drawing">Drawing</button>
|
|
141
|
+
<button class="filter-btn" data-filter="photography" data-i18n="nav_photo">Photography</button>
|
|
142
|
+
<button class="filter-btn" data-filter="sculpting" data-i18n="nav_sculpting">Sculpting</button>
|
|
143
|
+
<button class="filter-btn" data-filter="music" data-i18n="nav_music">Music</button>
|
|
144
|
+
<button class="filter-btn" data-filter="projects" data-i18n="nav_projects">Code</button>
|
|
145
|
+
<span class="sort-controls">
|
|
146
|
+
<button class="sort-btn active" data-sort="desc" title="Newest first">▼</button>
|
|
147
|
+
<button class="sort-btn" data-sort="asc" title="Oldest first">▲</button>
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
<!-- Gallery grid loaded by render.js -->
|
|
151
|
+
</main>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<footer class="footer-terminal">
|
|
155
|
+
<div class="terminal-window">
|
|
156
|
+
<div class="terminal-titlebar">
|
|
157
|
+
<div class="terminal-titlebar-btns">
|
|
158
|
+
<span class="terminal-tbtn terminal-tbtn-close"></span>
|
|
159
|
+
<span class="terminal-tbtn terminal-tbtn-min"></span>
|
|
160
|
+
<span class="terminal-tbtn terminal-tbtn-max"></span>
|
|
161
|
+
</div>
|
|
162
|
+
<span class="terminal-titlebar-text">user@host:~</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="terminal-body">
|
|
165
|
+
<div class="terminal-line"><span class="terminal-prompt">$</span> <span
|
|
166
|
+
data-i18n="footer_copy">© 2026 alex a montreal</span> — <a
|
|
167
|
+
href="https://github.com/mtldev514" target="_blank" title="GitHub"><svg class="icon"
|
|
168
|
+
viewBox="0 0 24 24">
|
|
169
|
+
<path
|
|
170
|
+
d="M12 .3a12 12 0 00-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1.1-.8.1-.7.1-.7 1.2.1 1.9 1.3 1.9 1.3 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-5.9 0-1.3.5-2.4 1.2-3.2-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 016 0c2.3-1.5 3.3-1.2 3.3-1.2.7 1.7.3 2.9.1 3.2.8.8 1.2 1.9 1.2 3.2 0 4.6-2.8 5.6-5.5 5.9.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0012 .3z" />
|
|
171
|
+
</svg> github</a> · <a href="https://www.linkedin.com/in/alexcatus/?locale=fr_FR"
|
|
172
|
+
target="_blank" title="LinkedIn"><svg class="icon" viewBox="0 0 24 24">
|
|
173
|
+
<path
|
|
174
|
+
d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z" />
|
|
175
|
+
</svg> linkedin</a> — <span class="terminal-dim"><span
|
|
176
|
+
data-i18n="footer_visitors">Visits:</span> <span
|
|
177
|
+
class="visitor-counter">......</span></span> <span class="terminal-cursor">█</span>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</footer>
|
|
182
|
+
</div>
|
|
183
|
+
<script src="js/counter.js"></script>
|
|
184
|
+
</body>
|
|
185
|
+
|
|
186
|
+
</html>
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Configuration Loader
|
|
3
|
+
* Supports loading from local directories OR remote GitHub repository
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ConfigLoader = {
|
|
7
|
+
source: null,
|
|
8
|
+
cache: {},
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize and load configuration source
|
|
12
|
+
*/
|
|
13
|
+
async init() {
|
|
14
|
+
try {
|
|
15
|
+
// Check for override version from global config or query params
|
|
16
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
17
|
+
const overrideMode = urlParams.get('mode') || window.RETRO_CONFIG?.mode;
|
|
18
|
+
const overrideData = urlParams.get('dataDir') || window.RETRO_CONFIG?.dataDir;
|
|
19
|
+
|
|
20
|
+
// Load config-source.json to determine where to load from
|
|
21
|
+
let response;
|
|
22
|
+
try {
|
|
23
|
+
response = await fetch('config-source.json');
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.warn('⚠️ No config-source.json found, using defaults');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.source = response ? await response.json() : {
|
|
29
|
+
mode: overrideMode || 'local',
|
|
30
|
+
local: { configDir: 'config', dataDir: overrideData || 'data', langDir: 'lang' }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (overrideMode) this.source.mode = overrideMode;
|
|
34
|
+
if (overrideData) {
|
|
35
|
+
if (!this.source.local) this.source.local = {};
|
|
36
|
+
this.source.local.dataDir = overrideData;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`📦 Config mode: ${this.source.mode}`);
|
|
40
|
+
|
|
41
|
+
if (this.source.mode === 'remote' && this.source.remote.enabled) {
|
|
42
|
+
console.log(`🌐 Loading from remote: ${this.source.remote.repo}`);
|
|
43
|
+
return this.loadRemote();
|
|
44
|
+
} else if (this.source.mode === 'hybrid') {
|
|
45
|
+
console.log('🔄 Trying remote, fallback to local');
|
|
46
|
+
try {
|
|
47
|
+
return await this.loadRemote();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn('⚠️ Remote failed, using local:', error.message);
|
|
50
|
+
return this.loadLocal();
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
console.log('💾 Loading from local directories');
|
|
54
|
+
return this.loadLocal();
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('❌ Error loading config-source.json:', error);
|
|
58
|
+
console.log('🔄 Falling back to local mode');
|
|
59
|
+
return this.loadLocal();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load configuration from local directories
|
|
65
|
+
*/
|
|
66
|
+
async loadLocal() {
|
|
67
|
+
const configDir = this.source?.local?.configDir || 'config';
|
|
68
|
+
const dataDir = this.source?.local?.dataDir || 'data';
|
|
69
|
+
const langDir = this.source?.local?.langDir || 'lang';
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const [app, languages, categories, mediaTypes] = await Promise.all([
|
|
73
|
+
fetch(`${configDir}/app.json`).then(r => r.json()),
|
|
74
|
+
fetch(`${configDir}/languages.json`).then(r => r.json()),
|
|
75
|
+
fetch(`${configDir}/categories.json`).then(r => r.json()),
|
|
76
|
+
fetch(`${configDir}/media-types.json`).then(r => r.json())
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
app,
|
|
81
|
+
languages,
|
|
82
|
+
categories,
|
|
83
|
+
mediaTypes,
|
|
84
|
+
source: 'local',
|
|
85
|
+
paths: { configDir, dataDir, langDir }
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('❌ Failed to load local config:', error);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load configuration from remote GitHub repository
|
|
95
|
+
*/
|
|
96
|
+
async loadRemote() {
|
|
97
|
+
if (!this.source?.remote?.enabled) {
|
|
98
|
+
throw new Error('Remote config not enabled');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const baseUrl = this.source.remote.baseUrl;
|
|
102
|
+
|
|
103
|
+
// Check cache first
|
|
104
|
+
if (this.source.cache?.enabled && this.isCacheValid()) {
|
|
105
|
+
console.log('💨 Using cached remote config');
|
|
106
|
+
return this.cache.data;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const [app, languages, categories, mediaTypes] = await Promise.all([
|
|
111
|
+
this.fetchRemote(`${baseUrl}config/app.json`),
|
|
112
|
+
this.fetchRemote(`${baseUrl}config/languages.json`),
|
|
113
|
+
this.fetchRemote(`${baseUrl}config/categories.json`),
|
|
114
|
+
this.fetchRemote(`${baseUrl}config/media-types.json`)
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const data = {
|
|
118
|
+
app,
|
|
119
|
+
languages,
|
|
120
|
+
categories,
|
|
121
|
+
mediaTypes,
|
|
122
|
+
source: 'remote',
|
|
123
|
+
paths: {
|
|
124
|
+
configDir: baseUrl + 'config',
|
|
125
|
+
dataDir: baseUrl + 'data',
|
|
126
|
+
langDir: baseUrl + 'lang'
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Cache the result
|
|
131
|
+
if (this.source.cache?.enabled) {
|
|
132
|
+
this.cache = {
|
|
133
|
+
data,
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
duration: this.source.cache.duration * 1000
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return data;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('❌ Failed to load remote config:', error);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fetch from remote with error handling
|
|
148
|
+
*/
|
|
149
|
+
async fetchRemote(url) {
|
|
150
|
+
const response = await fetch(url);
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`HTTP ${response.status}: ${url}`);
|
|
153
|
+
}
|
|
154
|
+
return response.json();
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if cache is still valid
|
|
159
|
+
*/
|
|
160
|
+
isCacheValid() {
|
|
161
|
+
if (!this.cache.timestamp) return false;
|
|
162
|
+
const age = Date.now() - this.cache.timestamp;
|
|
163
|
+
return age < this.cache.duration;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear cache
|
|
168
|
+
*/
|
|
169
|
+
clearCache() {
|
|
170
|
+
this.cache = {};
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get data file path (adjusts for local vs remote)
|
|
175
|
+
*/
|
|
176
|
+
getDataPath(filename) {
|
|
177
|
+
if (!this.cache.data) return `data/${filename}`;
|
|
178
|
+
return `${this.cache.data.paths.dataDir}/${filename}`;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get language file path
|
|
183
|
+
*/
|
|
184
|
+
getLangPath(langCode) {
|
|
185
|
+
if (!this.cache.data) return `lang/${langCode}.json`;
|
|
186
|
+
return `${this.cache.data.paths.langDir}/${langCode}.json`;
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get engine asset path
|
|
191
|
+
*/
|
|
192
|
+
getEnginePath(path) {
|
|
193
|
+
const baseUrl = window.RETRO_ENGINE_URL || '';
|
|
194
|
+
return baseUrl + path.replace(/^\//, '');
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Enhanced AppConfig that uses ConfigLoader
|
|
199
|
+
const AppConfig = {
|
|
200
|
+
app: null,
|
|
201
|
+
languages: null,
|
|
202
|
+
categories: null,
|
|
203
|
+
mediaTypes: null,
|
|
204
|
+
loaded: false,
|
|
205
|
+
source: 'local',
|
|
206
|
+
paths: null,
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Load all configuration files
|
|
210
|
+
*/
|
|
211
|
+
async load() {
|
|
212
|
+
try {
|
|
213
|
+
const config = await ConfigLoader.init();
|
|
214
|
+
|
|
215
|
+
this.app = config.app;
|
|
216
|
+
this.languages = config.languages;
|
|
217
|
+
this.categories = config.categories;
|
|
218
|
+
this.mediaTypes = config.mediaTypes;
|
|
219
|
+
this.source = config.source;
|
|
220
|
+
this.paths = config.paths;
|
|
221
|
+
this.loaded = true;
|
|
222
|
+
|
|
223
|
+
console.log(`✅ Configuration loaded from ${config.source}`);
|
|
224
|
+
return true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('❌ Failed to load configuration:', error);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get API base URL
|
|
233
|
+
*/
|
|
234
|
+
getApiUrl() {
|
|
235
|
+
return this.app?.api?.baseUrl || 'http://127.0.0.1:5001';
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get supported language codes
|
|
240
|
+
*/
|
|
241
|
+
getLanguageCodes() {
|
|
242
|
+
return this.languages?.supportedLanguages.map(l => l.code) || ['en'];
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get default language
|
|
247
|
+
*/
|
|
248
|
+
getDefaultLanguage() {
|
|
249
|
+
return this.languages?.defaultLanguage || 'en';
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get all content types
|
|
254
|
+
*/
|
|
255
|
+
getAllContentTypes() {
|
|
256
|
+
return this.categories?.contentTypes || this.categories?.categories || [];
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get content type configuration by ID
|
|
261
|
+
*/
|
|
262
|
+
getContentType(contentTypeId) {
|
|
263
|
+
return this.getAllContentTypes().find(c => c.id === contentTypeId);
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get category configuration by ID (legacy, returns content type)
|
|
268
|
+
*/
|
|
269
|
+
getCategory(categoryId) {
|
|
270
|
+
return this.getContentType(categoryId);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get all categories (legacy, returns content types)
|
|
275
|
+
*/
|
|
276
|
+
getAllCategories() {
|
|
277
|
+
return this.getAllContentTypes();
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get all media types
|
|
282
|
+
*/
|
|
283
|
+
getAllMediaTypes() {
|
|
284
|
+
return this.mediaTypes?.mediaTypes || [];
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get media type configuration by ID
|
|
289
|
+
*/
|
|
290
|
+
getMediaType(mediaTypeId) {
|
|
291
|
+
return this.getAllMediaTypes().find(m => m.id === mediaTypeId);
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get content types by media type
|
|
296
|
+
*/
|
|
297
|
+
getContentTypesByMedia(mediaTypeId) {
|
|
298
|
+
return this.getAllContentTypes().filter(ct => ct.mediaType === mediaTypeId);
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get categories that support galleries (based on media type)
|
|
303
|
+
*/
|
|
304
|
+
getGalleryCategories() {
|
|
305
|
+
const galleryTypes = [];
|
|
306
|
+
for (const ct of this.getAllContentTypes()) {
|
|
307
|
+
const mediaType = this.getMediaType(ct.mediaType);
|
|
308
|
+
if (mediaType && mediaType.supportsGallery) {
|
|
309
|
+
galleryTypes.push(ct.id);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return galleryTypes;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get data file path for a category
|
|
317
|
+
*/
|
|
318
|
+
getCategoryDataFile(categoryId) {
|
|
319
|
+
const category = this.getCategory(categoryId);
|
|
320
|
+
if (!category) return `data/${categoryId}.json`;
|
|
321
|
+
|
|
322
|
+
// If remote, return full URL, otherwise relative path
|
|
323
|
+
const fileName = category.dataFile.split('/').pop();
|
|
324
|
+
return ConfigLoader.getDataPath(fileName);
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get optional fields for a category
|
|
329
|
+
*/
|
|
330
|
+
getCategoryFields(categoryId) {
|
|
331
|
+
const category = this.getCategory(categoryId);
|
|
332
|
+
return category?.fields?.optional || [];
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if a field exists for a category
|
|
337
|
+
*/
|
|
338
|
+
categoryHasField(categoryId, fieldName) {
|
|
339
|
+
const fields = this.getCategoryFields(categoryId);
|
|
340
|
+
return fields.some(f => f.name === fieldName);
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create multilingual object with all supported languages
|
|
345
|
+
*/
|
|
346
|
+
createMultilingualObject(value) {
|
|
347
|
+
const obj = {};
|
|
348
|
+
this.getLanguageCodes().forEach(code => {
|
|
349
|
+
obj[code] = value;
|
|
350
|
+
});
|
|
351
|
+
return obj;
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get app setting
|
|
356
|
+
*/
|
|
357
|
+
getSetting(path) {
|
|
358
|
+
const parts = path.split('.');
|
|
359
|
+
let value = this.app;
|
|
360
|
+
for (const part of parts) {
|
|
361
|
+
value = value?.[part];
|
|
362
|
+
if (value === undefined) return null;
|
|
363
|
+
}
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Export for use in other modules
|
|
369
|
+
window.AppConfig = AppConfig;
|
|
370
|
+
window.ConfigLoader = ConfigLoader;
|