@thibautrey/chatons-extension-minecraft-servers 1.0.0 → 1.1.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "@thibautrey/chatons-extension-minecraft-servers",
3
3
  "name": "Minecraft Servers",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "description": "Manage and inspect a list of Minecraft servers from Chatons.",
6
6
  "icon": "icon.svg",
7
7
  "capabilities": [
package/index.html CHANGED
@@ -48,6 +48,14 @@
48
48
  background: color-mix(in srgb, var(--ce-muted, #e5e7eb) 60%, transparent);
49
49
  font-size: 12px;
50
50
  }
51
+ .section-header {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ gap: 12px;
56
+ margin-bottom: 12px;
57
+ flex-wrap: wrap;
58
+ }
51
59
  .field-grid {
52
60
  display: grid;
53
61
  grid-template-columns: 1fr 1fr;
@@ -56,11 +64,14 @@
56
64
  .field-grid .full {
57
65
  grid-column: 1 / -1;
58
66
  }
59
- .actions {
67
+ .actions, .form-actions {
60
68
  display: flex;
61
69
  flex-wrap: wrap;
62
70
  gap: 10px;
63
71
  }
72
+ .form-actions {
73
+ margin-top: 16px;
74
+ }
64
75
  input, textarea, select {
65
76
  width: 100%;
66
77
  box-sizing: border-box;
@@ -80,6 +91,10 @@
80
91
  background: color-mix(in srgb, var(--ce-card, #ffffff) 88%, transparent);
81
92
  display: grid;
82
93
  gap: 10px;
94
+ transition: all 0.2s ease;
95
+ }
96
+ .server-card:hover {
97
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
83
98
  }
84
99
  .server-card.is-selected {
85
100
  border-color: color-mix(in srgb, #22c55e 55%, var(--ce-border, #d1d5db));
@@ -116,12 +131,20 @@
116
131
  }
117
132
  .badge.ok {
118
133
  background: rgba(34, 197, 94, 0.18);
134
+ color: #15803d;
119
135
  }
120
136
  .badge.warn {
121
137
  background: rgba(245, 158, 11, 0.18);
138
+ color: #a16207;
122
139
  }
123
140
  .badge.error {
124
141
  background: rgba(239, 68, 68, 0.18);
142
+ color: #dc2626;
143
+ }
144
+ .editing-badge {
145
+ background: rgba(59, 130, 246, 0.18);
146
+ color: #2563eb;
147
+ font-weight: 500;
125
148
  }
126
149
  .row-actions {
127
150
  display: flex;
@@ -137,6 +160,7 @@
137
160
  border-radius: 12px;
138
161
  background: color-mix(in srgb, var(--ce-muted, #e5e7eb) 55%, transparent);
139
162
  font-size: 13px;
163
+ text-align: center;
140
164
  }
141
165
  .status-box {
142
166
  display: grid;
@@ -154,11 +178,171 @@
154
178
  white-space: pre-wrap;
155
179
  word-break: break-word;
156
180
  }
181
+ .status-message {
182
+ padding: 10px 14px;
183
+ border-radius: 8px;
184
+ font-size: 13px;
185
+ display: none;
186
+ }
187
+ .status-message.visible {
188
+ display: block;
189
+ }
190
+ .status-message[data-kind="success"] {
191
+ background: rgba(34, 197, 94, 0.15);
192
+ color: #15803d;
193
+ }
194
+ .status-message[data-kind="error"] {
195
+ background: rgba(239, 68, 68, 0.15);
196
+ color: #dc2626;
197
+ }
198
+ .status-message[data-kind="loading"] {
199
+ background: rgba(59, 130, 246, 0.15);
200
+ color: #2563eb;
201
+ }
202
+ .help-text {
203
+ margin-top: 12px;
204
+ font-size: 12px;
205
+ }
206
+ .required {
207
+ color: #dc2626;
208
+ }
209
+ .field-error {
210
+ border-color: #dc2626 !important;
211
+ }
212
+ .field-error-msg {
213
+ color: #dc2626;
214
+ font-size: 11px;
215
+ margin-top: 4px;
216
+ }
217
+ .server-controls {
218
+ display: flex;
219
+ gap: 8px;
220
+ align-items: center;
221
+ flex-wrap: wrap;
222
+ }
223
+ .search-box {
224
+ position: relative;
225
+ flex: 1;
226
+ min-width: 150px;
227
+ }
228
+ .search-box input {
229
+ padding-right: 28px;
230
+ }
231
+ .search-clear {
232
+ position: absolute;
233
+ right: 8px;
234
+ top: 50%;
235
+ transform: translateY(-50%);
236
+ background: none;
237
+ border: none;
238
+ cursor: pointer;
239
+ font-size: 16px;
240
+ color: var(--ce-muted-fg, #6b7280);
241
+ padding: 4px;
242
+ }
243
+ .sort-select {
244
+ width: auto;
245
+ min-width: 120px;
246
+ }
247
+ .sort-direction {
248
+ padding: 6px 10px;
249
+ background: color-mix(in srgb, var(--ce-muted, #e5e7eb) 75%, transparent);
250
+ border: 1px solid color-mix(in srgb, var(--ce-border, #d1d5db) 90%, transparent);
251
+ border-radius: 6px;
252
+ cursor: pointer;
253
+ font-size: 14px;
254
+ }
255
+ .sort-direction:hover {
256
+ background: color-mix(in srgb, var(--ce-muted, #e5e7eb) 90%, transparent);
257
+ }
258
+ .search-results-count {
259
+ margin-top: 8px;
260
+ font-size: 12px;
261
+ }
262
+ .btn-icon {
263
+ margin-right: 4px;
264
+ }
265
+ /* Modal styles */
266
+ .modal-overlay {
267
+ position: fixed;
268
+ top: 0;
269
+ left: 0;
270
+ right: 0;
271
+ bottom: 0;
272
+ background: rgba(0, 0, 0, 0.5);
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: center;
276
+ z-index: 1000;
277
+ padding: 20px;
278
+ }
279
+ .modal-content {
280
+ background: color-mix(in srgb, var(--ce-card, #ffffff) 98%, transparent);
281
+ border-radius: 16px;
282
+ max-width: 400px;
283
+ width: 100%;
284
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
285
+ overflow: hidden;
286
+ }
287
+ .modal-header {
288
+ display: flex;
289
+ justify-content: space-between;
290
+ align-items: center;
291
+ padding: 16px 20px;
292
+ border-bottom: 1px solid color-mix(in srgb, var(--ce-border, #d1d5db) 90%, transparent);
293
+ }
294
+ .modal-header h3 {
295
+ margin: 0;
296
+ font-size: 16px;
297
+ }
298
+ .modal-close {
299
+ background: none;
300
+ border: none;
301
+ font-size: 24px;
302
+ cursor: pointer;
303
+ color: var(--ce-muted-fg, #6b7280);
304
+ padding: 0;
305
+ line-height: 1;
306
+ }
307
+ .modal-close:hover {
308
+ color: inherit;
309
+ }
310
+ .modal-body {
311
+ padding: 20px;
312
+ }
313
+ .modal-body p {
314
+ margin: 0 0 8px 0;
315
+ }
316
+ .modal-footer {
317
+ display: flex;
318
+ justify-content: flex-end;
319
+ gap: 10px;
320
+ padding: 16px 20px;
321
+ border-top: 1px solid color-mix(in srgb, var(--ce-border, #d1d5db) 90%, transparent);
322
+ }
323
+ /* Danger button style */
324
+ .chaton-ui-button--danger {
325
+ background: rgba(239, 68, 68, 0.15);
326
+ color: #dc2626;
327
+ border: 1px solid rgba(239, 68, 68, 0.3);
328
+ }
329
+ .chaton-ui-button--danger:hover {
330
+ background: rgba(239, 68, 68, 0.25);
331
+ }
332
+ /* Responsive */
157
333
  @media (max-width: 960px) {
158
334
  .layout, .field-grid {
159
335
  grid-template-columns: 1fr;
160
336
  }
161
337
  }
338
+ @media (max-width: 600px) {
339
+ .server-controls {
340
+ width: 100%;
341
+ }
342
+ .search-box {
343
+ width: 100%;
344
+ }
345
+ }
162
346
  </style>
163
347
  </head>
164
348
  <body>
package/index.js CHANGED
@@ -11,6 +11,12 @@ const DEFAULT_FORM = {
11
11
  tags: '',
12
12
  }
13
13
 
14
+ const SORT_OPTIONS = {
15
+ name: 'Nom',
16
+ status: 'Statut',
17
+ created: 'Date creation',
18
+ }
19
+
14
20
  function clone(value) {
15
21
  return JSON.parse(JSON.stringify(value))
16
22
  }
@@ -42,6 +48,10 @@ const state = {
42
48
  message: '',
43
49
  messageKind: '',
44
50
  loading: false,
51
+ searchQuery: '',
52
+ sortBy: 'name',
53
+ sortAsc: true,
54
+ pendingDeleteId: null,
45
55
  }
46
56
 
47
57
  function normalizeTagsText(tags) {
@@ -103,6 +113,47 @@ async function loadServers() {
103
113
  }
104
114
  }
105
115
 
116
+ function getFilteredServers() {
117
+ let servers = [...state.servers]
118
+
119
+ // Filter by search query
120
+ if (state.searchQuery) {
121
+ const query = state.searchQuery.toLowerCase()
122
+ servers = servers.filter(server => {
123
+ const name = (server.name || '').toLowerCase()
124
+ const host = (server.host || '').toLowerCase()
125
+ const tags = (server.tags || []).join(' ').toLowerCase()
126
+ return name.includes(query) || host.includes(query) || tags.includes(query)
127
+ })
128
+ }
129
+
130
+ // Sort servers
131
+ servers.sort((a, b) => {
132
+ let aVal, bVal
133
+ switch (state.sortBy) {
134
+ case 'status':
135
+ const aStatus = state.liveById[a.id]
136
+ const bStatus = state.liveById[b.id]
137
+ aVal = aStatus && aStatus.ok ? 1 : 0
138
+ bVal = bStatus && bStatus.ok ? 1 : 0
139
+ break
140
+ case 'created':
141
+ aVal = a.createdAt || ''
142
+ bVal = b.createdAt || ''
143
+ break
144
+ case 'name':
145
+ default:
146
+ aVal = (a.name || '').toLowerCase()
147
+ bVal = (b.name || '').toLowerCase()
148
+ }
149
+ if (aVal < bVal) return state.sortAsc ? -1 : 1
150
+ if (aVal > bVal) return state.sortAsc ? 1 : -1
151
+ return 0
152
+ })
153
+
154
+ return servers
155
+ }
156
+
106
157
  function describeLiveStatus(status) {
107
158
  if (!status) return '<span class="badge">Aucun statut charge</span>'
108
159
  if (status.ok === false) {
@@ -125,8 +176,9 @@ function describeLiveStatus(status) {
125
176
  function serverCardHtml(server) {
126
177
  const status = state.liveById[server.id]
127
178
  const endpoint = server.host + ':' + server.port
179
+ const isEditing = state.selectedId === server.id
128
180
  return `
129
- <div class="server-card ${state.selectedId === server.id ? 'is-selected' : ''}" data-server-id="${escapeHtml(server.id)}">
181
+ <div class="server-card ${isEditing ? 'is-selected' : ''}" data-server-id="${escapeHtml(server.id)}">
130
182
  <div class="server-head">
131
183
  <div class="server-title">
132
184
  <h3>${escapeHtml(server.name)}</h3>
@@ -134,16 +186,19 @@ function serverCardHtml(server) {
134
186
  <small class="muted">${escapeHtml(server.notes || '')}</small>
135
187
  </div>
136
188
  <div class="server-badges">
189
+ ${isEditing ? '<span class="badge editing-badge">Edition</span>' : ''}
137
190
  <span class="badge">${escapeHtml(server.edition || 'auto')}</span>
138
191
  ${(server.tags || []).map((tag) => `<span class="badge">${escapeHtml(tag)}</span>`).join('')}
139
192
  </div>
140
193
  </div>
141
194
  <div class="server-badges">${describeLiveStatus(status)}</div>
142
195
  <div class="row-actions">
143
- <button class="chaton-ui-button chaton-ui-button--ghost" data-action="select" data-server-id="${escapeHtml(server.id)}">Editer</button>
196
+ <button class="chaton-ui-button ${isEditing ? 'chaton-ui-button--primary' : 'chaton-ui-button--ghost'}" data-action="select" data-server-id="${escapeHtml(server.id)}">
197
+ ${isEditing ? 'En cours d\'edition' : 'Editer'}
198
+ </button>
144
199
  <button class="chaton-ui-button" data-action="probe" data-server-id="${escapeHtml(server.id)}">Tester</button>
145
- <button class="chaton-ui-button chaton-ui-button--ghost" data-action="players" data-server-id="${escapeHtml(server.id)}">Joueurs</button>
146
- <button class="chaton-ui-button chaton-ui-button--ghost" data-action="delete" data-server-id="${escapeHtml(server.id)}">Supprimer</button>
200
+ <button class="chaton-ui-button chaton-ui-button--ghost" data-action="copy" data-server-id="${escapeHtml(server.id)}" title="Copier l'adresse">Copier</button>
201
+ <button class="chaton-ui-button chaton-ui-button--danger" data-action="confirm-delete" data-server-id="${escapeHtml(server.id)}">Supprimer</button>
147
202
  </div>
148
203
  </div>
149
204
  `
@@ -154,7 +209,7 @@ function statusDetailHtml(server, status) {
154
209
  return '<div class="empty">Selectionne un serveur pour voir les details.</div>'
155
210
  }
156
211
  if (!status) {
157
- return '<div class="empty">Aucun statut live charge pour ce serveur.</div>'
212
+ return '<div class="empty">Aucun statut live charge pour ce serveur. Clique sur "Tester" pour obtenir le statut.</div>'
158
213
  }
159
214
  return `
160
215
  <div class="status-box">
@@ -164,11 +219,37 @@ function statusDetailHtml(server, status) {
164
219
  `
165
220
  }
166
221
 
222
+ function modalHtml() {
223
+ if (!state.pendingDeleteId) return ''
224
+ const server = state.servers.find(s => s.id === state.pendingDeleteId)
225
+ if (!server) return ''
226
+ return `
227
+ <div class="modal-overlay" id="deleteModal">
228
+ <div class="modal-content">
229
+ <div class="modal-header">
230
+ <h3>Confirmer la suppression</h3>
231
+ <button class="modal-close" data-action="cancel-delete">&times;</button>
232
+ </div>
233
+ <div class="modal-body">
234
+ <p>Es-tu sur de vouloir supprimer le serveur <strong>${escapeHtml(server.name)}</strong>&nbsp;?</p>
235
+ <p class="muted">Cette action est irreversible.</p>
236
+ </div>
237
+ <div class="modal-footer">
238
+ <button class="chaton-ui-button chaton-ui-button--ghost" data-action="cancel-delete">Annuler</button>
239
+ <button class="chaton-ui-button chaton-ui-button--danger" data-action="confirm-delete-final" data-server-id="${escapeHtml(server.id)}">Supprimer definitivement</button>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ `
244
+ }
245
+
167
246
  function render() {
168
247
  const app = document.getElementById('app')
169
248
  const selected = selectedServer()
249
+ const filteredServers = getFilteredServers()
170
250
  const statsCount = state.servers.length
171
251
  const onlineCount = Object.values(state.liveById).filter((entry) => entry && entry.ok !== false).length
252
+ const isEditing = !!state.form.id
172
253
 
173
254
  app.innerHTML = `
174
255
  <div class="toolbar">
@@ -178,24 +259,31 @@ function render() {
178
259
  </div>
179
260
  <div class="actions">
180
261
  <button id="refreshAllBtn" class="chaton-ui-button">Actualiser tout</button>
181
- <button id="resetFormBtn" class="chaton-ui-button chaton-ui-button--ghost">Nouveau serveur</button>
262
+ <button id="resetFormBtn" class="chaton-ui-button ${isEditing ? 'chaton-ui-button--primary' : 'chaton-ui-button--ghost'}">
263
+ ${isEditing ? 'Nouveau serveur' : '+ Ajouter'}
264
+ </button>
182
265
  </div>
183
266
  </div>
184
267
 
185
- <div id="statusMessage" class="muted"></div>
268
+ <div id="statusMessage" class="status-message ${state.messageKind ? 'visible' : ''}" data-kind="${escapeHtml(state.messageKind)}">${escapeHtml(state.message)}</div>
186
269
 
187
270
  <div class="layout">
188
271
  <section class="ce-card">
189
272
  <div class="ce-card__body">
190
- <h2 class="ce-section-title">Configuration</h2>
273
+ <div class="section-header">
274
+ <h2 class="ce-section-title">${isEditing ? 'Modifier le serveur' : 'Ajouter un serveur'}</h2>
275
+ ${isEditing ? '<span class="badge editing-badge">Mode edition</span>' : ''}
276
+ </div>
191
277
  <div class="field-grid">
192
278
  <div class="full ce-field">
193
- <label class="ce-label" for="field-name">Nom</label>
194
- <input id="field-name" value="${escapeHtml(state.form.name)}" placeholder="Mon serveur survie">
279
+ <label class="ce-label" for="field-name">Nom <span class="required">*</span></label>
280
+ <input id="field-name" value="${escapeHtml(state.form.name)}" placeholder="Mon serveur survie" class="${!state.form.name && state.form.host ? 'field-error' : ''}" required>
281
+ ${!state.form.name && state.form.host ? '<small class="field-error-msg">Le nom est requis</small>' : ''}
195
282
  </div>
196
283
  <div class="ce-field">
197
- <label class="ce-label" for="field-host">Host</label>
198
- <input id="field-host" value="${escapeHtml(state.form.host)}" placeholder="play.example.net">
284
+ <label class="ce-label" for="field-host">Host <span class="required">*</span></label>
285
+ <input id="field-host" value="${escapeHtml(state.form.host)}" placeholder="play.example.net" class="${!state.form.host && state.form.name ? 'field-error' : ''}" required>
286
+ ${!state.form.host && state.form.name ? '<small class="field-error-msg">Le host est requis</small>' : ''}
199
287
  </div>
200
288
  <div class="ce-field">
201
289
  <label class="ce-label" for="field-port">Port</label>
@@ -219,21 +307,41 @@ function render() {
219
307
  </div>
220
308
  </div>
221
309
  <input id="field-id" type="hidden" value="${escapeHtml(state.form.id)}">
222
- <div class="actions">
223
- <button id="saveBtn" class="chaton-ui-button chaton-ui-button--primary">${state.form.id ? 'Mettre a jour' : 'Ajouter'}</button>
224
- <button id="probeBtn" class="chaton-ui-button">Tester ce serveur</button>
310
+ <div class="form-actions">
311
+ <button id="saveBtn" class="chaton-ui-button chaton-ui-button--primary">
312
+ <span class="btn-icon">${isEditing ? '&#x2713;' : '+'}</span>
313
+ ${isEditing ? 'Mettre a jour' : 'Ajouter le serveur'}
314
+ </button>
315
+ <button id="probeBtn" class="chaton-ui-button" ${!state.form.host ? 'disabled' : ''}>Tester</button>
225
316
  <button id="playersBtn" class="chaton-ui-button chaton-ui-button--ghost">Lister les joueurs</button>
226
317
  </div>
227
- <div class="muted">Les informations live utilisent uniquement le ping public du serveur. La liste des joueurs depend de ce que le serveur expose.</div>
318
+ <div class="muted help-text">Les informations live utilisent uniquement le ping public du serveur. La liste des joueurs depend de ce que le serveur expose.</div>
228
319
  </div>
229
320
  </section>
230
321
 
231
322
  <section class="ce-card">
232
323
  <div class="ce-card__body">
233
- <h2 class="ce-section-title">Serveurs enregistres</h2>
324
+ <div class="section-header">
325
+ <h2 class="ce-section-title">Serveurs enregistres</h2>
326
+ <div class="server-controls">
327
+ <div class="search-box">
328
+ <input type="text" id="searchInput" placeholder="Rechercher..." value="${escapeHtml(state.searchQuery)}">
329
+ ${state.searchQuery ? '<button class="search-clear" data-action="clear-search">&times;</button>' : ''}
330
+ </div>
331
+ <select id="sortSelect" class="sort-select">
332
+ <option value="name" ${state.sortBy === 'name' ? 'selected' : ''}>Trier par nom</option>
333
+ <option value="status" ${state.sortBy === 'status' ? 'selected' : ''}>Trier par statut</option>
334
+ <option value="created" ${state.sortBy === 'created' ? 'selected' : ''}>Trier par date</option>
335
+ </select>
336
+ <button class="sort-direction" data-action="toggle-sort" title="Inverser l'ordre">
337
+ ${state.sortAsc ? '&#x2191;' : '&#x2193;'}
338
+ </button>
339
+ </div>
340
+ </div>
234
341
  <div class="server-list">
235
- ${state.servers.length ? state.servers.map(serverCardHtml).join('') : '<div class="empty">Aucun serveur configure pour le moment.</div>'}
342
+ ${filteredServers.length ? filteredServers.map(serverCardHtml).join('') : `<div class="empty">${state.searchQuery ? 'Aucun serveur ne correspond a ta recherche.' : 'Aucun serveur configure pour le moment.'}</div>`}
236
343
  </div>
344
+ ${state.searchQuery && filteredServers.length > 0 ? `<div class="search-results-count muted">${filteredServers.length} serveur(s) trouve(s)</div>` : ''}
237
345
  </div>
238
346
  </section>
239
347
  </div>
@@ -244,6 +352,8 @@ function render() {
244
352
  ${statusDetailHtml(selected, selected ? state.liveById[selected.id] : null)}
245
353
  </div>
246
354
  </section>
355
+
356
+ ${modalHtml()}
247
357
  `
248
358
 
249
359
  bindEvents()
@@ -272,6 +382,7 @@ async function saveCurrentForm() {
272
382
 
273
383
  if (!payload.name || !payload.host) {
274
384
  setMessage('Le nom et le host sont obligatoires.', 'error')
385
+ render()
275
386
  return
276
387
  }
277
388
 
@@ -281,6 +392,7 @@ async function saveCurrentForm() {
281
392
 
282
393
  if (!result || !result.ok) {
283
394
  setMessage(result && result.error ? result.error.message : 'Echec de sauvegarde.', 'error')
395
+ render()
284
396
  return
285
397
  }
286
398
 
@@ -295,10 +407,11 @@ async function probeServer(server) {
295
407
  setMessage('Aucun serveur selectionne.', 'error')
296
408
  return
297
409
  }
410
+ setMessage('Test en cours...', 'loading')
298
411
  const result = await call('minecraft.servers.status', { id: server.id })
299
412
  if (!result || !result.ok) {
300
413
  state.liveById[server.id] = { ok: false, error: result && result.error ? result.error.message : 'Probe failed' }
301
- setMessage(result && result.error ? result.error.message : 'Probe impossible.', 'error')
414
+ setMessage(result && result.error ? result.error.message : 'Test impossible.', 'error')
302
415
  render()
303
416
  return
304
417
  }
@@ -312,9 +425,11 @@ async function listPlayers(server) {
312
425
  setMessage('Aucun serveur selectionne.', 'error')
313
426
  return
314
427
  }
428
+ setMessage('Chargement des joueurs...', 'loading')
315
429
  const result = await call('minecraft.servers.players', { id: server.id })
316
430
  if (!result || !result.ok) {
317
431
  setMessage(result && result.error ? result.error.message : 'Impossible de lister les joueurs.', 'error')
432
+ render()
318
433
  return
319
434
  }
320
435
  const players = result.data && Array.isArray(result.data.players) ? result.data.players : []
@@ -325,10 +440,33 @@ async function listPlayers(server) {
325
440
  render()
326
441
  }
327
442
 
443
+ async function copyAddress(server) {
444
+ const address = server.host + ':' + server.port
445
+ try {
446
+ await navigator.clipboard.writeText(address)
447
+ setMessage('Adresse copiee: ' + address, 'success')
448
+ } catch (err) {
449
+ setMessage('Impossible de copier l\'adresse.', 'error')
450
+ }
451
+ render()
452
+ }
453
+
454
+ function confirmDelete(serverId) {
455
+ state.pendingDeleteId = serverId
456
+ render()
457
+ }
458
+
459
+ function cancelDelete() {
460
+ state.pendingDeleteId = null
461
+ render()
462
+ }
463
+
328
464
  async function removeServer(serverId) {
465
+ state.pendingDeleteId = null
329
466
  const result = await call('minecraft.servers.remove', { id: serverId })
330
467
  if (!result || !result.ok) {
331
468
  setMessage(result && result.error ? result.error.message : 'Suppression impossible.', 'error')
469
+ render()
332
470
  return
333
471
  }
334
472
  delete state.liveById[serverId]
@@ -341,6 +479,7 @@ async function removeServer(serverId) {
341
479
  }
342
480
 
343
481
  async function refreshAll() {
482
+ setMessage('Actualisation en cours...', 'loading')
344
483
  await loadServers()
345
484
  for (const server of state.servers) {
346
485
  const result = await call('minecraft.servers.status', { id: server.id })
@@ -356,6 +495,8 @@ function bindEvents() {
356
495
  const playersBtn = document.getElementById('playersBtn')
357
496
  const resetFormBtn = document.getElementById('resetFormBtn')
358
497
  const refreshAllBtn = document.getElementById('refreshAllBtn')
498
+ const searchInput = document.getElementById('searchInput')
499
+ const sortSelect = document.getElementById('sortSelect')
359
500
 
360
501
  if (saveBtn) {
361
502
  saveBtn.onclick = function () {
@@ -384,29 +525,60 @@ function bindEvents() {
384
525
  void refreshAll()
385
526
  }
386
527
  }
528
+ if (searchInput) {
529
+ searchInput.oninput = function () {
530
+ state.searchQuery = searchInput.value.trim()
531
+ render()
532
+ }
533
+ }
534
+ if (sortSelect) {
535
+ sortSelect.onchange = function () {
536
+ state.sortBy = sortSelect.value
537
+ render()
538
+ }
539
+ }
387
540
 
388
- Array.from(document.querySelectorAll('[data-action]')).forEach((node) => {
541
+ // Handle all click events via delegation
542
+ document.querySelectorAll('[data-action]').forEach((node) => {
389
543
  node.onclick = function () {
390
544
  const action = node.getAttribute('data-action')
391
545
  const serverId = node.getAttribute('data-server-id')
392
- const server = state.servers.find((entry) => entry.id === serverId)
393
- if (!server) return
394
- if (action === 'select') {
395
- state.selectedId = server.id
396
- state.form = formFromServer(server)
397
- render()
398
- return
399
- }
400
- if (action === 'probe') {
401
- void probeServer(server)
402
- return
403
- }
404
- if (action === 'players') {
405
- void listPlayers(server)
406
- return
407
- }
408
- if (action === 'delete') {
409
- void removeServer(server.id)
546
+ const server = serverId ? state.servers.find((entry) => entry.id === serverId) : null
547
+
548
+ switch (action) {
549
+ case 'select':
550
+ if (server) {
551
+ state.selectedId = server.id
552
+ state.form = formFromServer(server)
553
+ render()
554
+ }
555
+ break
556
+ case 'probe':
557
+ if (server) void probeServer(server)
558
+ break
559
+ case 'players':
560
+ if (server) void listPlayers(server)
561
+ break
562
+ case 'copy':
563
+ if (server) void copyAddress(server)
564
+ break
565
+ case 'confirm-delete':
566
+ if (serverId) confirmDelete(serverId)
567
+ break
568
+ case 'cancel-delete':
569
+ cancelDelete()
570
+ break
571
+ case 'confirm-delete-final':
572
+ if (serverId) void removeServer(serverId)
573
+ break
574
+ case 'toggle-sort':
575
+ state.sortAsc = !state.sortAsc
576
+ render()
577
+ break
578
+ case 'clear-search':
579
+ state.searchQuery = ''
580
+ render()
581
+ break
410
582
  }
411
583
  }
412
584
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thibautrey/chatons-extension-minecraft-servers",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Minecraft servers extension for Chatons",
5
5
  "type": "module",
6
6
  "license": "MIT"