@smartnet360/svelte-components 0.0.119 → 0.0.120
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/dist/apps/antenna-tools/band-config.d.ts +53 -0
- package/dist/apps/antenna-tools/band-config.js +112 -0
- package/dist/apps/antenna-tools/components/AntennaControls.svelte +558 -0
- package/dist/apps/antenna-tools/components/AntennaControls.svelte.d.ts +16 -0
- package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +304 -0
- package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte.d.ts +8 -0
- package/dist/apps/antenna-tools/components/AntennaTools.svelte +597 -0
- package/dist/apps/antenna-tools/components/AntennaTools.svelte.d.ts +42 -0
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +278 -0
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte.d.ts +3 -0
- package/dist/apps/antenna-tools/components/DbNotification.svelte +67 -0
- package/dist/apps/antenna-tools/components/DbNotification.svelte.d.ts +18 -0
- package/dist/apps/antenna-tools/components/JsonImporter.svelte +115 -0
- package/dist/apps/antenna-tools/components/JsonImporter.svelte.d.ts +6 -0
- package/dist/apps/antenna-tools/components/MSIConverter.svelte +282 -0
- package/dist/apps/antenna-tools/components/MSIConverter.svelte.d.ts +6 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarAreaChart.svelte +123 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarAreaChart.svelte.d.ts +16 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarLineChart.svelte +123 -0
- package/dist/apps/antenna-tools/components/chart-engines/PolarLineChart.svelte.d.ts +16 -0
- package/dist/apps/antenna-tools/components/chart-engines/index.d.ts +9 -0
- package/dist/apps/antenna-tools/components/chart-engines/index.js +9 -0
- package/dist/apps/antenna-tools/components/index.d.ts +8 -0
- package/dist/apps/antenna-tools/components/index.js +10 -0
- package/dist/apps/antenna-tools/db.d.ts +28 -0
- package/dist/apps/antenna-tools/db.js +45 -0
- package/dist/apps/antenna-tools/index.d.ts +26 -0
- package/dist/apps/antenna-tools/index.js +40 -0
- package/dist/apps/antenna-tools/stores/antennas.d.ts +13 -0
- package/dist/apps/antenna-tools/stores/antennas.js +25 -0
- package/dist/apps/antenna-tools/stores/db-status.d.ts +32 -0
- package/dist/apps/antenna-tools/stores/db-status.js +38 -0
- package/dist/apps/antenna-tools/stores/index.d.ts +5 -0
- package/dist/apps/antenna-tools/stores/index.js +5 -0
- package/dist/apps/antenna-tools/types.d.ts +137 -0
- package/dist/apps/antenna-tools/types.js +16 -0
- package/dist/apps/antenna-tools/utils/antenna-helpers.d.ts +83 -0
- package/dist/apps/antenna-tools/utils/antenna-helpers.js +198 -0
- package/dist/apps/antenna-tools/utils/chart-engines/index.d.ts +5 -0
- package/dist/apps/antenna-tools/utils/chart-engines/index.js +5 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-area-utils.d.ts +94 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-area-utils.js +151 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-line-utils.d.ts +93 -0
- package/dist/apps/antenna-tools/utils/chart-engines/polar-line-utils.js +139 -0
- package/dist/apps/antenna-tools/utils/db-utils.d.ts +50 -0
- package/dist/apps/antenna-tools/utils/db-utils.js +266 -0
- package/dist/apps/antenna-tools/utils/index.d.ts +7 -0
- package/dist/apps/antenna-tools/utils/index.js +7 -0
- package/dist/apps/antenna-tools/utils/msi-parser.d.ts +21 -0
- package/dist/apps/antenna-tools/utils/msi-parser.js +215 -0
- package/dist/apps/antenna-tools/utils/recent-antennas.d.ts +24 -0
- package/dist/apps/antenna-tools/utils/recent-antennas.js +64 -0
- package/package.json +1 -1
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
import { loadAntennas } from '../utils/db-utils';
|
|
6
|
+
import { STANDARD_BANDS } from '../band-config';
|
|
7
|
+
import type { Antenna } from '../types';
|
|
8
|
+
|
|
9
|
+
let antennas = $state<Antenna[]>([]);
|
|
10
|
+
let isLoading = $state(true);
|
|
11
|
+
let searchQuery = $state('');
|
|
12
|
+
let selectedBand = $state<number | null>(null);
|
|
13
|
+
|
|
14
|
+
// Computed stats
|
|
15
|
+
let stats = $derived({
|
|
16
|
+
total: antennas.length,
|
|
17
|
+
uniqueModels: new Set(antennas.map(a => a.name)).size,
|
|
18
|
+
bands: [...new Set(antennas.map(a => a.frequency))].sort((a, b) => a - b),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Filtered antennas based on search and band filter
|
|
22
|
+
let filteredAntennas = $derived(() => {
|
|
23
|
+
let result = antennas;
|
|
24
|
+
|
|
25
|
+
// Filter by band
|
|
26
|
+
if (selectedBand !== null) {
|
|
27
|
+
result = result.filter(a => a.frequency === selectedBand);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Filter by search query
|
|
31
|
+
if (searchQuery.trim()) {
|
|
32
|
+
const query = searchQuery.toLowerCase();
|
|
33
|
+
result = result.filter(a =>
|
|
34
|
+
a.name.toLowerCase().includes(query) ||
|
|
35
|
+
a.tilt?.toString().includes(query)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Group antennas by model name for display
|
|
43
|
+
let groupedAntennas = $derived(() => {
|
|
44
|
+
const groups = new Map<string, Antenna[]>();
|
|
45
|
+
|
|
46
|
+
for (const antenna of filteredAntennas()) {
|
|
47
|
+
const existing = groups.get(antenna.name) || [];
|
|
48
|
+
existing.push(antenna);
|
|
49
|
+
groups.set(antenna.name, existing);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Sort groups by name
|
|
53
|
+
return Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
async function loadData() {
|
|
57
|
+
try {
|
|
58
|
+
isLoading = true;
|
|
59
|
+
antennas = await loadAntennas();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Failed to load antennas:', error);
|
|
62
|
+
} finally {
|
|
63
|
+
isLoading = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearFilters() {
|
|
68
|
+
searchQuery = '';
|
|
69
|
+
selectedBand = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onMount(() => {
|
|
73
|
+
loadData();
|
|
74
|
+
});
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<div class="database-viewer">
|
|
78
|
+
{#if isLoading}
|
|
79
|
+
<div class="text-center py-5">
|
|
80
|
+
<div class="spinner-border text-primary" role="status">
|
|
81
|
+
<span class="visually-hidden">Loading...</span>
|
|
82
|
+
</div>
|
|
83
|
+
<p class="text-muted mt-2">Loading antenna data...</p>
|
|
84
|
+
</div>
|
|
85
|
+
{:else if antennas.length === 0}
|
|
86
|
+
<div class="text-center py-5">
|
|
87
|
+
<i class="bi bi-database text-muted" style="font-size: 3rem;"></i>
|
|
88
|
+
<h5 class="mt-3">No Antenna Data</h5>
|
|
89
|
+
<p class="text-muted">Import MSI files or JSON data to populate the database.</p>
|
|
90
|
+
</div>
|
|
91
|
+
{:else}
|
|
92
|
+
<!-- Summary Stats -->
|
|
93
|
+
<div class="row g-3 mb-4">
|
|
94
|
+
<div class="col-md-4">
|
|
95
|
+
<div class="card bg-primary text-white h-100">
|
|
96
|
+
<div class="card-body text-center">
|
|
97
|
+
<h2 class="mb-0">{stats.total}</h2>
|
|
98
|
+
<small>Total Antennas</small>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="col-md-4">
|
|
103
|
+
<div class="card bg-success text-white h-100">
|
|
104
|
+
<div class="card-body text-center">
|
|
105
|
+
<h2 class="mb-0">{stats.uniqueModels}</h2>
|
|
106
|
+
<small>Unique Models</small>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="col-md-4">
|
|
111
|
+
<div class="card bg-info text-white h-100">
|
|
112
|
+
<div class="card-body text-center">
|
|
113
|
+
<h2 class="mb-0">{stats.bands.length}</h2>
|
|
114
|
+
<small>Frequency Bands</small>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Search and Filter -->
|
|
121
|
+
<div class="card mb-4">
|
|
122
|
+
<div class="card-body">
|
|
123
|
+
<div class="row g-3 align-items-end">
|
|
124
|
+
<div class="col-md-5">
|
|
125
|
+
<label class="form-label" for="searchInput">
|
|
126
|
+
<i class="bi bi-search me-1"></i>Search
|
|
127
|
+
</label>
|
|
128
|
+
<input
|
|
129
|
+
id="searchInput"
|
|
130
|
+
type="text"
|
|
131
|
+
class="form-control"
|
|
132
|
+
placeholder="Search by antenna name..."
|
|
133
|
+
bind:value={searchQuery}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="col-md-5">
|
|
137
|
+
<label class="form-label">Filter by Band</label>
|
|
138
|
+
<div class="btn-group w-100 flex-wrap" role="group">
|
|
139
|
+
<button
|
|
140
|
+
type="button"
|
|
141
|
+
class="btn btn-sm {selectedBand === null ? 'btn-primary' : 'btn-outline-primary'}"
|
|
142
|
+
onclick={() => selectedBand = null}
|
|
143
|
+
>
|
|
144
|
+
All
|
|
145
|
+
</button>
|
|
146
|
+
{#each STANDARD_BANDS as band}
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
class="btn btn-sm {selectedBand === parseInt(band.name) ? 'btn-primary' : 'btn-outline-primary'}"
|
|
150
|
+
onclick={() => selectedBand = parseInt(band.name)}
|
|
151
|
+
disabled={!stats.bands.includes(parseInt(band.name))}
|
|
152
|
+
>
|
|
153
|
+
{band.name}
|
|
154
|
+
</button>
|
|
155
|
+
{/each}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="col-md-2">
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
class="btn btn-outline-secondary w-100"
|
|
162
|
+
onclick={clearFilters}
|
|
163
|
+
>
|
|
164
|
+
<i class="bi bi-x-circle me-1"></i>Clear
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Results Info -->
|
|
172
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
173
|
+
<span class="text-muted">
|
|
174
|
+
Showing {filteredAntennas().length} of {antennas.length} antennas
|
|
175
|
+
{#if groupedAntennas().length > 0}
|
|
176
|
+
({groupedAntennas().length} models)
|
|
177
|
+
{/if}
|
|
178
|
+
</span>
|
|
179
|
+
<button type="button" class="btn btn-sm btn-outline-primary" onclick={loadData}>
|
|
180
|
+
<i class="bi bi-arrow-clockwise me-1"></i>Refresh
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Antenna Table -->
|
|
185
|
+
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
|
186
|
+
<table class="table table-sm table-hover table-striped mb-0">
|
|
187
|
+
<thead class="table-light sticky-top">
|
|
188
|
+
<tr>
|
|
189
|
+
<th>Model Name</th>
|
|
190
|
+
<th class="text-center" style="width: 80px;">Band</th>
|
|
191
|
+
<th class="text-center" style="width: 100px;">Orig. Freq</th>
|
|
192
|
+
<th class="text-center" style="width: 70px;">Tilt</th>
|
|
193
|
+
<th class="text-center" style="width: 80px;">Gain</th>
|
|
194
|
+
<th class="text-center" style="width: 60px;">H-Pts</th>
|
|
195
|
+
<th class="text-center" style="width: 60px;">V-Pts</th>
|
|
196
|
+
</tr>
|
|
197
|
+
</thead>
|
|
198
|
+
<tbody>
|
|
199
|
+
{#each filteredAntennas() as antenna (antenna.id ?? `${antenna.name}-${antenna.frequency}-${antenna.tilt}`)}
|
|
200
|
+
<tr>
|
|
201
|
+
<td>
|
|
202
|
+
<span class="fw-medium">{antenna.name}</span>
|
|
203
|
+
{#if antenna.polarization}
|
|
204
|
+
<span class="badge bg-secondary ms-1">{antenna.polarization}</span>
|
|
205
|
+
{/if}
|
|
206
|
+
</td>
|
|
207
|
+
<td class="text-center">
|
|
208
|
+
<span class="badge bg-primary">{antenna.frequency}</span>
|
|
209
|
+
</td>
|
|
210
|
+
<td class="text-center text-muted">
|
|
211
|
+
{antenna.originalFrequency} MHz
|
|
212
|
+
</td>
|
|
213
|
+
<td class="text-center">
|
|
214
|
+
{antenna.tilt}°
|
|
215
|
+
</td>
|
|
216
|
+
<td class="text-center">
|
|
217
|
+
{antenna.gain_dBd?.toFixed(1) ?? '-'} dBd
|
|
218
|
+
</td>
|
|
219
|
+
<td class="text-center text-muted">
|
|
220
|
+
{antenna.pattern?.length ?? 0}
|
|
221
|
+
</td>
|
|
222
|
+
<td class="text-center text-muted">
|
|
223
|
+
{antenna.vertical_pattern?.length ?? 0}
|
|
224
|
+
</td>
|
|
225
|
+
</tr>
|
|
226
|
+
{:else}
|
|
227
|
+
<tr>
|
|
228
|
+
<td colspan="7" class="text-center text-muted py-4">
|
|
229
|
+
No antennas match your search criteria
|
|
230
|
+
</td>
|
|
231
|
+
</tr>
|
|
232
|
+
{/each}
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<!-- Band Summary -->
|
|
238
|
+
{#if stats.bands.length > 0}
|
|
239
|
+
<div class="mt-4">
|
|
240
|
+
<h6 class="text-muted mb-2">
|
|
241
|
+
<i class="bi bi-bar-chart me-1"></i>Antennas per Band
|
|
242
|
+
</h6>
|
|
243
|
+
<div class="d-flex flex-wrap gap-2">
|
|
244
|
+
{#each STANDARD_BANDS as band}
|
|
245
|
+
{@const count = antennas.filter(a => a.frequency === parseInt(band.name)).length}
|
|
246
|
+
{#if count > 0}
|
|
247
|
+
<div class="badge bg-light text-dark border">
|
|
248
|
+
<strong>{band.name}</strong>: {count}
|
|
249
|
+
</div>
|
|
250
|
+
{/if}
|
|
251
|
+
{/each}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
{/if}
|
|
255
|
+
{/if}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<style>
|
|
259
|
+
.database-viewer {
|
|
260
|
+
min-height: 300px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.table th {
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
font-size: 0.85rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.table td {
|
|
269
|
+
font-size: 0.875rem;
|
|
270
|
+
vertical-align: middle;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.sticky-top {
|
|
274
|
+
position: sticky;
|
|
275
|
+
top: 0;
|
|
276
|
+
z-index: 1;
|
|
277
|
+
}
|
|
278
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade, fly } from 'svelte/transition';
|
|
3
|
+
import { dataOperationStatus } from '../stores/db-status';
|
|
4
|
+
import { onDestroy } from 'svelte';
|
|
5
|
+
|
|
6
|
+
let visible = false;
|
|
7
|
+
let message = '';
|
|
8
|
+
let type: 'success' | 'error' | 'info' = 'info';
|
|
9
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
10
|
+
|
|
11
|
+
const unsubscribe = dataOperationStatus.subscribe(status => {
|
|
12
|
+
if (!status.operation) return;
|
|
13
|
+
|
|
14
|
+
// Clear any existing timer
|
|
15
|
+
if (timer) clearTimeout(timer);
|
|
16
|
+
|
|
17
|
+
if (status.inProgress) {
|
|
18
|
+
visible = true;
|
|
19
|
+
message = status.message || `${status.operation} in progress...`;
|
|
20
|
+
type = 'info';
|
|
21
|
+
} else if (status.success) {
|
|
22
|
+
visible = true;
|
|
23
|
+
message = status.message || `${status.operation} completed successfully`;
|
|
24
|
+
type = 'success';
|
|
25
|
+
|
|
26
|
+
// Auto-hide success messages after 4 seconds
|
|
27
|
+
timer = setTimeout(() => {
|
|
28
|
+
visible = false;
|
|
29
|
+
}, 4000);
|
|
30
|
+
} else if (status.error) {
|
|
31
|
+
visible = true;
|
|
32
|
+
message = status.message || `${status.operation} failed: ${status.error}`;
|
|
33
|
+
type = 'error';
|
|
34
|
+
|
|
35
|
+
// Auto-hide error messages after 6 seconds
|
|
36
|
+
timer = setTimeout(() => {
|
|
37
|
+
visible = false;
|
|
38
|
+
}, 6000);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
onDestroy(() => {
|
|
43
|
+
unsubscribe();
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function dismiss() {
|
|
48
|
+
visible = false;
|
|
49
|
+
if (timer) clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#if visible}
|
|
54
|
+
<div class="toast-container position-fixed bottom-0 end-0 p-3" transition:fly={{ y: 32, duration: 200 }}>
|
|
55
|
+
<div
|
|
56
|
+
class={`toast show align-items-center text-bg-${type === 'error' ? 'danger' : type === 'success' ? 'success' : 'info'} border-0`}
|
|
57
|
+
role="alert"
|
|
58
|
+
aria-live="assertive"
|
|
59
|
+
aria-atomic="true"
|
|
60
|
+
>
|
|
61
|
+
<div class="d-flex">
|
|
62
|
+
<div class="toast-body">{message}</div>
|
|
63
|
+
<button type="button" class="btn-close btn-close-white me-2 m-auto" aria-label="Close" on:click={dismiss}></button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
{/if}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const DbNotification: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type DbNotification = InstanceType<typeof DbNotification>;
|
|
18
|
+
export default DbNotification;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<svelte:options runes={true} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { importFromJson, hasImportedData } from '../utils/db-utils';
|
|
5
|
+
import type { Antenna } from '../types';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
onDataRefresh?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { onDataRefresh }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let fileInput: HTMLInputElement | null = $state(null);
|
|
14
|
+
|
|
15
|
+
let isLoading = $state(false);
|
|
16
|
+
let message = $state('');
|
|
17
|
+
let error = $state('');
|
|
18
|
+
let antennaCount = $state(0);
|
|
19
|
+
|
|
20
|
+
async function handleFileUpload(event: Event) {
|
|
21
|
+
const input = event.target as HTMLInputElement;
|
|
22
|
+
fileInput = input;
|
|
23
|
+
const files = input.files;
|
|
24
|
+
|
|
25
|
+
if (!files || files.length === 0) return;
|
|
26
|
+
|
|
27
|
+
const file = files[0];
|
|
28
|
+
if (file.type !== 'application/json') {
|
|
29
|
+
error = 'Please select a JSON file.';
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
isLoading = true;
|
|
35
|
+
error = '';
|
|
36
|
+
message = 'Importing data...';
|
|
37
|
+
|
|
38
|
+
const antennas = await importFromJson(file);
|
|
39
|
+
antennaCount = antennas.length;
|
|
40
|
+
|
|
41
|
+
message = `Successfully imported ${antennaCount} antennas into the database.`;
|
|
42
|
+
|
|
43
|
+
// Notify parent of data refresh
|
|
44
|
+
onDataRefresh?.();
|
|
45
|
+
|
|
46
|
+
// Reset the file input
|
|
47
|
+
input.value = '';
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (e instanceof Error) {
|
|
50
|
+
error = `Error importing data: ${e.message}`;
|
|
51
|
+
} else {
|
|
52
|
+
error = 'An unknown error occurred during import.';
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
isLoading = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<div class="card border-0 shadow-sm">
|
|
61
|
+
<div class="card-body ">
|
|
62
|
+
|
|
63
|
+
{#if hasImportedData()}
|
|
64
|
+
<div class="alert alert-warning border-0 bg-warning-subtle text-warning-emphasis d-flex align-items-start gap-2" role="alert">
|
|
65
|
+
<i class="bi bi-exclamation-triangle"></i>
|
|
66
|
+
<div>
|
|
67
|
+
<strong>Existing data detected.</strong> The new file will overwrite your current antenna records.
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
71
|
+
|
|
72
|
+
<div class="mb-4">
|
|
73
|
+
<label class="form-label fw-semibold" for="jsonFileInput">JSON file</label>
|
|
74
|
+
<input
|
|
75
|
+
id="jsonFileInput"
|
|
76
|
+
class="form-control form-control-lg"
|
|
77
|
+
type="file"
|
|
78
|
+
accept=".json"
|
|
79
|
+
onchange={handleFileUpload}
|
|
80
|
+
disabled={isLoading}
|
|
81
|
+
bind:this={fileInput}
|
|
82
|
+
/>
|
|
83
|
+
<div class="form-text">Choose a `.json` export created by this tool or generated from MSI conversions.</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="d-flex align-items-center gap-3 mb-4">
|
|
87
|
+
<button class="btn btn-primary" type="button" onclick={() => fileInput && fileInput.click()} disabled={isLoading}>
|
|
88
|
+
<i class="bi bi-folder2-open me-2"></i>
|
|
89
|
+
Browse files
|
|
90
|
+
</button>
|
|
91
|
+
{#if isLoading}
|
|
92
|
+
<div class="d-flex align-items-center gap-2 text-muted">
|
|
93
|
+
<div class="spinner-border spinner-border-sm" role="status">
|
|
94
|
+
<span class="visually-hidden">Loading...</span>
|
|
95
|
+
</div>
|
|
96
|
+
<span>Importing data…</span>
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{#if message && !error}
|
|
102
|
+
<div class="alert alert-info border-0 bg-info-subtle text-info-emphasis d-flex align-items-start gap-2" role="alert">
|
|
103
|
+
<i class="bi bi-info-circle"></i>
|
|
104
|
+
<div>{message}</div>
|
|
105
|
+
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
|
|
108
|
+
{#if error}
|
|
109
|
+
<div class="alert alert-danger border-0 bg-danger-subtle text-danger-emphasis d-flex align-items-start gap-2" role="alert">
|
|
110
|
+
<i class="bi bi-exclamation-octagon"></i>
|
|
111
|
+
<div>{error}</div>
|
|
112
|
+
</div>
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|