mbkbucket 3.0.1 → 3.2.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/src/app.js CHANGED
@@ -9,6 +9,7 @@ import { notFoundHandler, globalErrorHandler } from "./middleware/error-handler.
9
9
  import bucketRoutes from "./routes/index.js";
10
10
  import { packageJson } from "./config/index.js";
11
11
  import { createLogger } from "./utils/logger.js";
12
+ import { commonHandlebarsHelpers } from "./utils/helpers.js";
12
13
 
13
14
  const debugServer = createLogger('server');
14
15
 
@@ -46,8 +47,7 @@ export function createApp() {
46
47
  // Handlebars configuration
47
48
  server.engine("handlebars", engine({
48
49
  extname: ".handlebars",
49
- defaultLayout: "main",
50
- layoutsDir: path.join(__dirname, "..", "views", "layouts"),
50
+ defaultLayout: false,
51
51
  partialsDir: [
52
52
  path.join(__dirname, "..", "views", "templates"),
53
53
  path.join(__dirname, "..", "views", "templates", "notice"),
@@ -58,9 +58,7 @@ export function createApp() {
58
58
  ],
59
59
  cache: process.env.NODE_ENV === "production",
60
60
  helpers: {
61
- eq: function (a, b) {
62
- return a === b;
63
- },
61
+ ...commonHandlebarsHelpers,
64
62
  mbkbucket_cachebuster: function () {
65
63
  return "?v=" + packageJson.version;
66
64
  }
@@ -34,7 +34,9 @@ function resolvePrefix(req, res, prefix) {
34
34
  * Build a full S3 key from a prefix and filename, stripping trailing slashes.
35
35
  */
36
36
  function buildKey(prefix, fileName) {
37
- return `${prefix.replace(/\/+$/, '')}/${fileName}`;
37
+ const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
38
+ const cleanName = String(fileName || '').replace(/^\/+/, '');
39
+ return cleanPrefix ? `${cleanPrefix}/${cleanName}` : cleanName;
38
40
  }
39
41
 
40
42
  async function cleanupFolderMarker(folderPath, bucketName) {
@@ -121,8 +123,8 @@ export async function listFiles(req, res) {
121
123
  let legacyFallback = false;
122
124
 
123
125
  if (useOptimizedListing) {
124
- let listPrefix = effectivePrefix;
125
- if (listPrefix === getAppName() && !listPrefix.endsWith('/')) {
126
+ let listPrefix = effectivePrefix ? String(effectivePrefix).replace(/^\/+/, '') : '';
127
+ if (listPrefix && !listPrefix.endsWith('/')) {
126
128
  listPrefix += '/';
127
129
  }
128
130
 
@@ -135,11 +137,12 @@ export async function listFiles(req, res) {
135
137
 
136
138
  files = (result.Contents || []).filter(f => !f.Key.endsWith('/'));
137
139
  folders = (result.CommonPrefixes || [])
138
- .filter(p => p.Prefix !== effectivePrefix)
140
+ .filter(p => p.Prefix !== listPrefix && p.Prefix !== effectivePrefix)
139
141
  .map(p => p.Prefix);
140
142
  nextContinuationToken = result.NextContinuationToken;
143
+ }
141
144
 
142
- } else {
145
+ else {
143
146
  let continuationToken = undefined;
144
147
 
145
148
  do {
@@ -350,7 +353,14 @@ export async function createFolder(req, res) {
350
353
  const effectivePrefix = resolvePrefix(req, res, prefix);
351
354
  if (effectivePrefix === null) return;
352
355
 
353
- const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
356
+ const cleanName = String(folderName).replace(/^\/+/, '').replace(/\/+$/, '');
357
+ let folderKey;
358
+ if (effectivePrefix && !cleanName.startsWith(effectivePrefix)) {
359
+ const cleanPrefix = effectivePrefix.replace(/\/+$/, '');
360
+ folderKey = `${cleanPrefix}/${cleanName}/`;
361
+ } else {
362
+ folderKey = `${cleanName}/`;
363
+ }
354
364
 
355
365
  const markerExists = await fileExists(folderKey, bucketName);
356
366
  if (markerExists) return res.status(409).json({ success: false, error: 'Folder already exists' });
@@ -415,9 +425,11 @@ export async function downloadFileHandler(req, res) {
415
425
 
416
426
  try {
417
427
  const bucketName = req.activeBucket;
428
+ const rawKey = req.params.key;
429
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
418
430
  let keyToUse;
419
431
  try {
420
- keyToUse = ensureKeyHasAppPrefix(req.params.key);
432
+ keyToUse = ensureKeyHasAppPrefix(key);
421
433
  } catch (e) {
422
434
  return res.status(400).json({ success: false, error: e.message });
423
435
  }
@@ -1,5 +1,7 @@
1
+ import path from "path";
1
2
  import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
2
3
  import { renderPage, mbkautheVar } from "mbkauthe";
4
+ import { commonHandlebarsHelpers } from "../utils/helpers.js";
3
5
 
4
6
  /**
5
7
  * Render the main bucket dashboard page.
@@ -7,13 +9,9 @@ import { renderPage, mbkautheVar } from "mbkauthe";
7
9
  export function renderBucketDashboard(req, res) {
8
10
  const allBuckets = getAvailableBucketNames();
9
11
  const configuredDefaultBucket = mbkautheVar?.bucket;
10
-
11
- let bucketfilePath;
12
- if (mbkautheVar?.APP_NAME === "portal") {
13
- bucketfilePath = "bucketportal.handlebars";
14
- } else {
15
- bucketfilePath = "bucket.handlebars";
16
- }
12
+ const isPortal = mbkautheVar?.APP_NAME === "portal";
13
+ const bucketfilePath = isPortal ? "bucketportal.handlebars" : "bucket.handlebars";
14
+ const portalLayout = isPortal ? path.resolve(process.cwd(), "views", "layouts", "main") : false;
17
15
 
18
16
  let selectedBucket;
19
17
 
@@ -26,9 +24,10 @@ export function renderBucketDashboard(req, res) {
26
24
  isSelected: name === fallbackSelected,
27
25
  isDefault: name === configuredDefaultBucket
28
26
  }));
29
- return renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
27
+ return renderPage(req, res, bucketfilePath, isPortal, {
30
28
  page: "Admin Bucket",
31
- layout: true,
29
+ helpers: commonHandlebarsHelpers,
30
+ ...(isPortal ? { layout: portalLayout } : {}),
32
31
  bucketvar: mbkautheVar?.bucket,
33
32
  selectedBucket: fallbackSelected,
34
33
  bucketOptions,
@@ -45,8 +44,10 @@ export function renderBucketDashboard(req, res) {
45
44
  isDefault: name === configuredDefaultBucket
46
45
  }));
47
46
 
48
- renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
47
+ renderPage(req, res, bucketfilePath, isPortal, {
49
48
  page: "Admin Bucket",
49
+ helpers: commonHandlebarsHelpers,
50
+ ...(isPortal ? { layout: portalLayout } : {}),
50
51
  bucketvar: selectedBucket,
51
52
  selectedBucket,
52
53
  bucketOptions,
@@ -192,9 +192,11 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
192
192
  export async function viewFile(req, res) {
193
193
  try {
194
194
  const bucketName = req.activeBucket;
195
+ const rawKey = req.params.key;
196
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
195
197
  let keyToUse;
196
198
  try {
197
- keyToUse = ensureKeyHasAppPrefix(req.params.key);
199
+ keyToUse = ensureKeyHasAppPrefix(key);
198
200
  } catch (e) {
199
201
  return res.status(400).json({ message: e.message });
200
202
  }
@@ -216,7 +218,8 @@ export async function viewFile(req, res) {
216
218
  export async function playerPage(req, res) {
217
219
  try {
218
220
  const bucketName = req.activeBucket;
219
- const key = req.params.key;
221
+ const rawKey = req.params.key;
222
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
220
223
  let keyToUse;
221
224
  try {
222
225
  keyToUse = ensureKeyHasAppPrefix(key);
@@ -240,7 +243,8 @@ export async function playerPage(req, res) {
240
243
  export async function publicView(req, res) {
241
244
  try {
242
245
  const bucketName = resolveBucketName();
243
- const key = req.params.key;
246
+ const rawKey = req.params.key;
247
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
244
248
 
245
249
  // No directory indexing
246
250
  if (!key || key.endsWith('/')) {
@@ -61,6 +61,6 @@ router.post('/create-folder', sessRole('SuperAdmin'), createFolder);
61
61
  router.post('/delete', sessRole('SuperAdmin'), deleteItems);
62
62
 
63
63
  // Download
64
- router.get('/download/:key(*)', sessRole('SuperAdmin'), downloadFileHandler);
64
+ router.get('/download/*key', sessRole('SuperAdmin'), downloadFileHandler);
65
65
 
66
66
  export default router;
@@ -12,14 +12,14 @@ const router = express.Router();
12
12
  router.use(requireBucketView);
13
13
 
14
14
  // Player page for video/audio
15
- router.get('/player/:key(*)', sessRole('SuperAdmin'), playerPage);
15
+ router.get('/player/*key', sessRole('SuperAdmin'), playerPage);
16
16
 
17
17
  // Inline file view
18
- router.get('/view/:key(*)', sessRole('SuperAdmin'), viewFile);
18
+ router.get('/view/*key', sessRole('SuperAdmin'), viewFile);
19
19
 
20
20
  // Public view (conditionally enabled)
21
21
  if (mbkbucketVar?.publiView_enabled) {
22
- router.get('/p_view/:key(*)', pviewRateLimit, pviewSecurity, publicView);
22
+ router.get('/p_view/*key', pviewRateLimit, pviewSecurity, publicView);
23
23
  }
24
24
 
25
25
  export default router;
@@ -74,6 +74,9 @@ function isRootModeApp(appName) {
74
74
  }
75
75
 
76
76
  export function ensureKeyHasAppPrefix(key = '') {
77
+ if (Array.isArray(key)) {
78
+ key = key.join('/');
79
+ }
77
80
  const app = getAppName();
78
81
  if (!key) throw new Error('Key is required');
79
82
 
@@ -103,6 +106,9 @@ export function ensureKeyHasAppPrefix(key = '') {
103
106
  }
104
107
 
105
108
  export function ensurePrefix(prefix = '') {
109
+ if (Array.isArray(prefix)) {
110
+ prefix = prefix.join('/');
111
+ }
106
112
  const app = getAppName();
107
113
 
108
114
  if (prefix && (prefix.includes('../') || prefix.includes('..\\') || /\.\.[\\/]/.test(prefix))) {
@@ -369,12 +375,13 @@ export async function downloadFile(key, options = {}) {
369
375
  key: keyToDownload,
370
376
  };
371
377
  } catch (error) {
372
- console.error(`Download failed for key ${key}:`, error);
373
-
374
- if (error && error.$metadata && error.$metadata.httpStatusCode === 304) {
378
+ if (error && (error.$metadata?.httpStatusCode === 304 || error.name === '304' || error.name === 'NotModified')) {
379
+ debugS3('File not modified (304) for key %s', keyToDownload);
375
380
  return { notModified: true, key: keyToDownload };
376
381
  }
377
382
 
383
+ console.error(`Download failed for key ${key}:`, error);
384
+
378
385
  if (error.name === 'NoSuchKey') {
379
386
  throw new Error(`File not found: ${key}`);
380
387
  } else if (error.name === 'AccessDenied') {
@@ -171,3 +171,27 @@ export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
171
171
  </body>
172
172
  </html>`;
173
173
  }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Common Handlebars helpers
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export const commonHandlebarsHelpers = {
180
+ eq: (a, b) => a === b,
181
+ neq: (a, b) => a !== b,
182
+ or: (...args) => args.slice(0, -1).some(Boolean),
183
+ and: (...args) => args.slice(0, -1).every(Boolean),
184
+ not: (val) => !val,
185
+ gt: (a, b) => a > b,
186
+ gte: (a, b) => a >= b,
187
+ lt: (a, b) => a < b,
188
+ lte: (a, b) => a <= b,
189
+ includes: (arr, val) => (Array.isArray(arr) ? arr.includes(val) : false),
190
+ json: (obj) => JSON.stringify(obj, null, 2),
191
+ jsonStringify: (obj) => JSON.stringify(obj),
192
+ getInitials: (username) => {
193
+ if (!username) return '?';
194
+ const parts = username.split(/[._]/);
195
+ return parts.map(p => p.charAt(0)).join('').toUpperCase().substring(0, 2);
196
+ },
197
+ };
@@ -7,7 +7,7 @@
7
7
  <title>{{#if page}}{{page}} - {{/if}}Web Portal</title>
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
11
  <script src="/mbkauthe/main.js" defer></script>
12
12
  <script src="https://portal.mbktech.org/Assets/Scripts/auto-ids.js" defer></script>
13
13
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
@@ -26,26 +26,34 @@
26
26
  --warning-color-ml: #d97706;
27
27
  --success-color-ml: #16a34a;
28
28
  --info-color-ml: #3b82f6;
29
- --border-radius-r-ml: 0.5rem;
30
- --border-radius-sm-ml: 0.25rem;
31
- --border-radius-lg-ml: 0.75rem;
32
- --shadow-s-ml: 0 2px 4px rgba(0, 0, 0, 0.1);
33
- --shadow-md-ml: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
34
- --shadow-lg-ml: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
35
- --transition-base-ml: all 0.2s ease;
36
- --transition-fast-ml: all 0.15s ease;
37
- --main-border-color: var(--gray-light-color-ml);
38
- --color-accent: var(--primary-color-ml);
39
- --color-text: var(--dark-color-ml);
40
- --color-bg: var(--light-color-ml);
29
+
30
+ --primary: var(--primary-color-ml);
31
+ --primary-lt: var(--primary-light-color-ml);
32
+ --primary-dk: var(--primary-dark-color-ml);
33
+ --primary-hover: #0d635c;
34
+
35
+ --body-bg: var(--light-color-ml);
36
+ --bg-surface: rgba(255, 255, 255, 0.88);
37
+ --bg-surface-secondary: rgba(244, 247, 243, 0.7);
38
+ --bg-surface-tertiary: #e9f2ee;
39
+
40
+ --border-color: rgba(15, 118, 110, 0.15);
41
+ --border-color-light: rgba(15, 118, 110, 0.08);
42
+
43
+ --text: var(--dark-color-ml);
44
+ --text-secondary: #4a5c5a;
45
+ --text-muted: var(--gray-color-ml);
46
+ --text-disabled: #a0b2af;
47
+
41
48
  --font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
42
- --font-family-headings: "Sora", "Segoe UI", sans-serif
49
+ --font-family-headings: "Sora", "Segoe UI", sans-serif;
50
+ --font-mono: 'JetBrains Mono', monospace;
43
51
  }
44
52
 
45
53
  * {
46
54
  margin: 0;
47
55
  padding: 0;
48
- box-sizing: border-box
56
+ box-sizing: border-box;
49
57
  }
50
58
 
51
59
  body {
@@ -61,7 +69,7 @@
61
69
  line-height: 1.6;
62
70
  font-size: 14px;
63
71
  position: relative;
64
- isolation: isolate
72
+ isolation: isolate;
65
73
  }
66
74
 
67
75
  body::before,
@@ -70,7 +78,7 @@
70
78
  position: fixed;
71
79
  border-radius: 999px;
72
80
  pointer-events: none;
73
- z-index: -1
81
+ z-index: -1;
74
82
  }
75
83
 
76
84
  body::before {
@@ -78,7 +86,7 @@
78
86
  height: 260px;
79
87
  left: -80px;
80
88
  top: 18vh;
81
- background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%)
89
+ background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%);
82
90
  }
83
91
 
84
92
  body::after {
@@ -86,23 +94,7 @@
86
94
  height: 300px;
87
95
  right: -120px;
88
96
  bottom: 10vh;
89
- background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%)
90
- }
91
-
92
- /* Removed unused sidebar/menu styles to reduce CSS size. */
93
-
94
- .logo {
95
- font-family: var(--font-family-headings);
96
- font-size: 1.65rem;
97
- font-weight: 700;
98
- color: #0b4f4a;
99
- display: flex;
100
- align-items: center;
101
- gap: .5rem
102
- }
103
-
104
- .logo img {
105
- width: 35px
97
+ background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%);
106
98
  }
107
99
 
108
100
  .main-content {
@@ -110,7 +102,7 @@
110
102
  display: flex;
111
103
  flex-direction: column;
112
104
  overflow: hidden;
113
- transition: var(--transition-base-ml)
105
+ min-height: 100vh;
114
106
  }
115
107
 
116
108
  .top-nav {
@@ -119,139 +111,77 @@
119
111
  padding: .75rem 1.5rem;
120
112
  display: flex;
121
113
  align-items: center;
122
- justify-content: flex-end;
123
- box-shadow: var(--shadow-md-ml);
114
+ justify-content: space-between;
115
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
124
116
  z-index: 90;
125
117
  border-bottom: 1px solid rgba(255, 255, 255, .75);
126
- gap: 1rem
118
+ gap: 1rem;
127
119
  }
128
120
 
129
- /* Removed unused notification and profile styles to reduce CSS size. */
121
+ .logo {
122
+ font-family: var(--font-family-headings);
123
+ font-size: 1.5rem;
124
+ font-weight: 700;
125
+ color: #0b4f4a;
126
+ display: flex;
127
+ align-items: center;
128
+ gap: .5rem;
129
+ cursor: pointer;
130
+ text-decoration: none;
131
+ }
132
+
133
+ .logo img {
134
+ width: 32px;
135
+ height: 32px;
136
+ }
130
137
 
131
138
  .content-area {
132
139
  flex: 1;
133
- padding: 1.6rem;
134
- overflow-y: auto
140
+ padding: 1.5rem;
141
+ overflow-y: auto;
135
142
  }
136
143
 
137
144
  .content-area::-webkit-scrollbar {
138
- width: 8px
145
+ width: 8px;
139
146
  }
140
147
 
141
148
  .content-area::-webkit-scrollbar-track {
142
- background: 0 0
149
+ background: transparent;
143
150
  }
144
151
 
145
152
  .content-area::-webkit-scrollbar-thumb {
146
153
  background: var(--gray-color-ml);
147
- border-radius: 4px
154
+ border-radius: 4px;
148
155
  }
149
156
 
150
157
  .content-area::-webkit-scrollbar-thumb:hover {
151
- background: var(--primary-color-ml)
158
+ background: var(--primary-color-ml);
152
159
  }
153
160
 
154
- .page-header {
155
- display: flex;
156
- align-items: center;
157
- justify-content: space-between;
158
- margin-bottom: 1.2rem;
159
- padding: 1rem 1.2rem;
160
- border: 1px solid rgba(255, 255, 255, .9);
161
- border-radius: 1rem;
162
- background: rgba(255, 255, 255, .74);
163
- box-shadow: 0 12px 34px rgba(15, 23, 42, .07)
164
- }
165
-
166
- .page-title {
167
- font-family: var(--font-family-headings);
168
- font-size: 1.5rem;
169
- font-weight: 700;
170
- color: var(--dark-color-ml);
171
- margin: 0
172
- }
173
-
174
- .breadcrumb {
175
- display: flex;
176
- align-items: center;
177
- color: var(--gray-color-ml);
178
- font-size: .875rem;
179
- background: linear-gradient(135deg, rgba(15, 118, 110, .08), rgba(251, 146, 60, .08));
180
- padding: .4rem .85rem;
181
- border-radius: 999px;
182
- border: 1px solid rgba(15, 118, 110, .15)
183
- }
184
-
185
- .breadcrumb a {
186
- color: var(--primary-color-ml);
187
- text-decoration: none;
188
- transition: var(--transition-base-ml);
189
- font-weight: 500
190
- }
191
-
192
- .breadcrumb a:hover {
193
- color: var(--primary-dark-color-ml)
194
- }
195
-
196
- .breadcrumb .separator {
197
- margin: 0 .5rem;
198
- color: var(--gray-color-ml)
199
- }
200
-
201
- @media (max-width:992px) {
202
- .main-content {
203
- margin-left: 0 !important
204
- }
205
-
206
- .content-area {
207
- padding: 1.5rem
208
- }
209
- }
210
-
211
- @media (max-width:768px) {
161
+ @media (max-width: 768px) {
212
162
  .top-nav {
213
- padding: .25rem 1rem
163
+ padding: .5rem 1rem;
214
164
  }
215
-
216
165
  .content-area {
217
- padding: 1rem
218
- }
219
-
220
- .page-header {
221
- flex-direction: row;
222
- align-items: flex-start;
223
- gap: 1rem
224
- }
225
-
226
- .page-title {
227
- font-size: 1.5rem
166
+ padding: 0.85rem;
228
167
  }
229
168
  }
230
-
231
-
232
169
  </style>
233
170
  {{> showmessage }}
234
171
  </head>
235
172
 
236
173
  <body>
237
-
238
174
  <div class="main-content">
239
175
  <nav class="top-nav">
240
- <div class="logo">
241
- <img onclick="window.location='/mbkbucket'" src="/icon.svg" alt="Logo">
242
- <span onclick="window.location='/mbkbucket'" style="cursor:pointer;">BKBucket</span>
176
+ <div class="logo" onclick="window.location='/mbkbucket'">
177
+ <img src="/icon.svg" alt="Logo">
178
+ <span>MBKBucket</span>
243
179
  </div>
244
180
 
245
181
  {{> profilemenu }}
246
182
  </nav>
247
183
 
248
184
  <div class="content-area">
249
- <div class="page-header">
250
- <h1 class="page-title">{{#if page}}{{page}}{{else}}Dashboard{{/if}}</h1>
251
- <div class="breadcrumb">
252
- <span>{{#if page}}{{page}}{{else}}Home{{/if}}</span>
253
- </div>
254
- </div>
255
185
  {{> bucketadmincontent }}
256
186
  </div>
257
187
  </div>
@@ -1,37 +1,33 @@
1
1
  <!-- Delete Confirmation Modal -->
2
2
  <div class="modal" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
3
- <div class="modal-dialog">
3
+ <div class="modal-dialog modal-dialog-centered">
4
4
  <div class="modal-content">
5
5
  <div class="modal-header">
6
6
  <h5 class="modal-title" id="deleteModalLabel">
7
- <i class="fas fa-exclamation-triangle"></i>
7
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="var(--red)" stroke-width="1.5" class="modal-title-icon"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>
8
8
  Confirm Delete
9
9
  </h5>
10
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
11
- onclick="closeModal()">&times;</button>
10
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeModal()">&times;</button>
12
11
  </div>
13
- <div class="modal-body">
14
- <i class="fas fa-trash-alt fa-3x"></i>
15
- <h6>Are you sure you want to delete this file?</h6>
16
- <p class="modal-body-text-style">
17
- <strong id="deleteFileName" style="color: var(--dark-color-ml);"></strong>
18
- </p>
19
- <p class="modal-body-text-top">
20
- <small>This action cannot be undone.</small>
12
+ <div class="modal-body text-center py-4">
13
+ <div class="delete-icon-wrap mb-3">
14
+ <svg width="44" height="44" viewBox="0 0 16 16" fill="none" stroke="var(--red)" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>
15
+ </div>
16
+ <h6 class="modal-confirm-heading">Are you sure you want to delete this?</h6>
17
+ <p class="modal-item-name-preview mt-2">
18
+ <code id="deleteFileName"></code>
21
19
  </p>
20
+ <p class="text-muted small mt-2">This action cannot be undone.</p>
22
21
  </div>
23
22
  <div class="modal-footer">
24
- <button type="button" class="btn btn-secondary" onclick="closeModal()">
25
- <i class="fas fa-times"></i>
26
- Cancel
27
- </button>
23
+ <button type="button" class="btn btn-outline" onclick="closeModal()">Cancel</button>
28
24
  <form id="deleteForm" method="post" action="/mbkbucket/delete" style="display: inline;">
29
25
  <input type="hidden" name="key" id="deleteKey">
30
26
  <input type="hidden" name="prefix" value="{{prefix}}">
31
27
  <input type="hidden" name="folder" id="deleteIsFolder" value="">
32
28
  <button type="submit" class="btn btn-danger" id="deleteConfirmButton">
33
- <i class="fas fa-trash"></i>
34
- <span id="deleteConfirmLabel">Delete File</span>
29
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>
30
+ <span id="deleteConfirmLabel">Delete</span>
35
31
  </button>
36
32
  </form>
37
33
  </div>
@@ -1,20 +1,32 @@
1
- <div class="table-responsive" aria-label="Loading files" aria-busy="true">
2
- <div class="skeleton-row">
3
- <div class="skeleton-box s-40"></div>
4
- <div class="skeleton-box s-200"></div>
5
- <div class="skeleton-box s-80"></div>
6
- <div class="skeleton-box s-200"></div>
1
+ <div class="fm-skeleton-grid" aria-label="Loading files" aria-busy="true">
2
+ <div class="fm-skeleton-card">
3
+ <div class="fm-skeleton-icon"></div>
4
+ <div class="fm-skeleton-line w-75"></div>
5
+ <div class="fm-skeleton-line w-50"></div>
7
6
  </div>
8
- <div class="skeleton-row">
9
- <div class="skeleton-box s-40"></div>
10
- <div class="skeleton-box s-200"></div>
11
- <div class="skeleton-box s-80"></div>
12
- <div class="skeleton-box s-200"></div>
7
+ <div class="fm-skeleton-card">
8
+ <div class="fm-skeleton-icon"></div>
9
+ <div class="fm-skeleton-line w-75"></div>
10
+ <div class="fm-skeleton-line w-50"></div>
13
11
  </div>
14
- <div class="skeleton-row">
15
- <div class="skeleton-box s-40"></div>
16
- <div class="skeleton-box s-200"></div>
17
- <div class="skeleton-box s-80"></div>
18
- <div class="skeleton-box s-200"></div>
12
+ <div class="fm-skeleton-card">
13
+ <div class="fm-skeleton-icon"></div>
14
+ <div class="fm-skeleton-line w-75"></div>
15
+ <div class="fm-skeleton-line w-50"></div>
16
+ </div>
17
+ <div class="fm-skeleton-card">
18
+ <div class="fm-skeleton-icon"></div>
19
+ <div class="fm-skeleton-line w-75"></div>
20
+ <div class="fm-skeleton-line w-50"></div>
21
+ </div>
22
+ <div class="fm-skeleton-card">
23
+ <div class="fm-skeleton-icon"></div>
24
+ <div class="fm-skeleton-line w-75"></div>
25
+ <div class="fm-skeleton-line w-50"></div>
26
+ </div>
27
+ <div class="fm-skeleton-card">
28
+ <div class="fm-skeleton-icon"></div>
29
+ <div class="fm-skeleton-line w-75"></div>
30
+ <div class="fm-skeleton-line w-50"></div>
19
31
  </div>
20
32
  </div>