Skip to content

Commit 8b85705

Browse files
committed
Implement view saving to disk
1 parent fc2e1ed commit 8b85705

File tree

6 files changed

+300
-21
lines changed

6 files changed

+300
-21
lines changed

package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
"tslib": "^2.6.2",
1818
"typescript": "^5.2.2",
1919
"vite": "^5.2.0",
20-
"vite-tsconfig-paths": "^4.3.2"
21-
},
22-
"dependencies": {
20+
"vite-tsconfig-paths": "^4.3.2",
21+
"@types/wicg-file-system-access": "^2023.10.5"
2322
}
2423
}

pnpm-lock.yaml

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/FileSelector.svelte

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<script lang="ts">
2+
import {
3+
viewFromJson,
4+
concertViews,
5+
currentViewName,
6+
} from "src/lib/stores";
7+
8+
export let mode: "select" | "error" | "hidden" = "select";
9+
10+
let fileSelectorInput: HTMLInputElement;
11+
12+
let fileName: string | null = null;
13+
let fileContents: string | null = null;
14+
let highlighted = false;
15+
let errorMessage: string | null = null;
16+
17+
function handleDrop(event: DragEvent) {
18+
event.preventDefault();
19+
highlighted = false;
20+
21+
if (event.dataTransfer) {
22+
const file = event.dataTransfer.files[0];
23+
fileName = file.name;
24+
25+
const reader = new FileReader();
26+
reader.onload = (e) => {
27+
fileContents = e.target!.result as string;
28+
};
29+
reader.readAsText(file);
30+
}
31+
}
32+
33+
function handleInput() {
34+
if (fileSelectorInput.files) {
35+
const file = fileSelectorInput.files[0];
36+
fileName = file.name;
37+
38+
const reader = new FileReader();
39+
reader.onload = (e) => {
40+
fileContents = e.target!.result as string;
41+
};
42+
reader.readAsText(file);
43+
}
44+
}
45+
46+
function close() {
47+
mode = "hidden";
48+
fileName = null;
49+
fileContents = null;
50+
}
51+
52+
function submit() {
53+
if (fileName === null) {
54+
return;
55+
}
56+
try {
57+
const newViews = viewFromJson(fileContents!);
58+
console.log(newViews);
59+
for (let [viewName, concerts] of newViews) {
60+
// Automatically rename views if they already exist
61+
while ($concertViews.has(viewName)) {
62+
viewName = viewName + "*";
63+
}
64+
$concertViews.set(viewName, concerts);
65+
$concertViews = new Map($concertViews); // Required to trigger store update
66+
$currentViewName = viewName;
67+
}
68+
close();
69+
} catch (e) {
70+
mode = "error";
71+
errorMessage = e.message;
72+
}
73+
}
74+
</script>
75+
76+
{#if mode === "select"}
77+
<button id="background-close" on:click={close}>
78+
<div id="background-close" />
79+
</button>
80+
81+
<div id="file-select">
82+
<button
83+
id="file-drop"
84+
class:highlighted
85+
on:drop={handleDrop}
86+
on:dragover={(e) => {
87+
e.preventDefault();
88+
highlighted = true;
89+
}}
90+
on:dragleave={() => {
91+
highlighted = false;
92+
}}
93+
on:click={() => fileSelectorInput.click()}
94+
>
95+
<p>Drag a file here, or click to select a file</p>
96+
{#if fileContents === null}
97+
<p class="greyed">No file selected...</p>
98+
{:else}
99+
<p class="green">✅ {fileName}</p>
100+
{/if}
101+
</button>
102+
<input
103+
type="file"
104+
id="file-selector"
105+
bind:this={fileSelectorInput}
106+
on:change={handleInput}
107+
/>
108+
109+
<button class="larger-text bold" on:click={submit}
110+
>Load view from file</button
111+
>
112+
<button class="larger-text" on:click={close}>Close</button>
113+
</div>
114+
{:else if mode === "error"}
115+
<button id="background-close" on:click={close}>
116+
<div id="background-close" />
117+
</button>
118+
119+
<div class="error">
120+
<p class="bold">Sorry! There was an error loading the file:</p>
121+
<p>{errorMessage}</p>
122+
<button
123+
class="larger-text"
124+
on:click={() => {
125+
mode = "select";
126+
}}>Try again</button
127+
>
128+
<button class="larger-text" on:click={close}>Close</button>
129+
</div>
130+
{/if}
131+
132+
<svelte:window
133+
on:keydown={(e) => {
134+
if (e.key === "Escape") {
135+
close();
136+
}
137+
}}
138+
/>
139+
140+
<style>
141+
button#background-close {
142+
display: contents;
143+
}
144+
145+
div#background-close {
146+
position: absolute;
147+
height: 100vh;
148+
width: 100vw;
149+
top: 0;
150+
left: 0;
151+
background-color: rgba(0, 0, 0, 0.5);
152+
z-index: 1;
153+
cursor: pointer;
154+
}
155+
156+
div#file-select,
157+
div.error {
158+
position: absolute;
159+
z-index: 2;
160+
padding: 20px;
161+
border-radius: 10px;
162+
height: max-content;
163+
width: max-content;
164+
top: 50vh;
165+
left: 50vw;
166+
transform: translate(-50%, -50%);
167+
background-color: #e9e9e9;
168+
display: flex;
169+
flex-direction: column;
170+
gap: 10px;
171+
}
172+
173+
div.error {
174+
color: #eb4281;
175+
}
176+
177+
input#file-selector {
178+
display: none;
179+
}
180+
181+
button#file-drop {
182+
border: 3px dashed #ccc;
183+
border-radius: 5px;
184+
padding: 20px;
185+
text-align: center;
186+
font-size: 110%;
187+
cursor: pointer;
188+
background-color: #f0f0f0;
189+
display: flex;
190+
flex-direction: column;
191+
gap: 10px;
192+
align-items: center;
193+
}
194+
195+
button#file-drop.highlighted {
196+
border-color: #32a852;
197+
background-color: #e9f7ed;
198+
}
199+
200+
p {
201+
margin: 0;
202+
}
203+
204+
p.greyed,
205+
p.green {
206+
max-width: 230px;
207+
overflow: hidden;
208+
white-space: nowrap;
209+
text-overflow: ellipsis;
210+
}
211+
212+
p.greyed {
213+
color: #888;
214+
}
215+
216+
p.green {
217+
color: #32a852;
218+
}
219+
220+
.bold {
221+
font-weight: bold;
222+
}
223+
224+
.larger-text {
225+
width: 100%;
226+
font-size: 110%;
227+
}
228+
</style>

src/components/ViewList.svelte

+39-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
selectedConcertIndices,
88
} from "src/lib/stores";
99
import { initialFilters } from "src/lib/filters";
10+
import FileSelector from "src/components/FileSelector.svelte";
1011
1112
export let allConcerts: Concert[];
1213
export let shownIndices: number[];
@@ -51,6 +52,11 @@
5152
$currentViewName = newViewName;
5253
}
5354
55+
let fileSelectorMode: "select" | "error" | "hidden" = "hidden";
56+
function addViewFromJSON() {
57+
fileSelectorMode = "select";
58+
}
59+
5460
function getNewViewName(): string | null {
5561
const newViewName = prompt("Enter a name for the new view");
5662
if (newViewName === null) {
@@ -77,7 +83,30 @@
7783
let concertIds = notUndefined($concertViews.get(viewName)).map(
7884
(c) => c.id,
7985
);
80-
console.log(JSON.stringify({ viewName, concerts: concertIds }));
86+
const obj: { [viewName: string]: string[] } = {};
87+
obj[viewName] = concertIds;
88+
const exportJson = JSON.stringify(obj);
89+
console.log(exportJson);
90+
console.log("Exporting view", viewName);
91+
92+
const filePickerOpts: SaveFilePickerOptions = {
93+
types: [
94+
{
95+
description: "concert list",
96+
accept: { "application/json": [".json"] },
97+
},
98+
],
99+
suggestedName: "concerts.json",
100+
};
101+
showSaveFilePicker(filePickerOpts)
102+
.then((fileHandle) => {
103+
console.log("Writing to file", fileHandle.name);
104+
return fileHandle.createWritable();
105+
})
106+
.then((writableStream) => {
107+
writableStream.write(exportJson);
108+
writableStream.close();
109+
});
81110
}
82111
83112
// Silence type errors
@@ -110,7 +139,7 @@
110139
{:else}
111140
<div class="dropdown-trigger">
112141
<button
113-
class="view-button add-new-view"
142+
class="view-button dropdown-button"
114143
class:active={$currentViewName === viewName}
115144
on:click={() => setViewName(viewName)}
116145
>
@@ -133,7 +162,7 @@
133162
{/if}
134163
{/each}
135164
<div class="dropdown-trigger">
136-
<button class="view-button add-new-view">
165+
<button class="view-button dropdown-button">
137166
Add new view <span class="smol">▼</span>
138167
<div class="dropdown-options">
139168
<button on:click={addEmptyView}>New empty view</button>
@@ -143,10 +172,14 @@
143172
<button on:click={addViewFromSelectedConcerts}
144173
>... from currently selected concerts</button
145174
>
175+
<button on:click={addViewFromJSON}
176+
>... from a file upload</button
177+
>
146178
</div>
147179
</button>
148180
</div>
149181
</div>
182+
<FileSelector bind:mode={fileSelectorMode} />
150183

151184
<style>
152185
button.view-button {
@@ -162,7 +195,7 @@
162195
button.active {
163196
background-color: #c1eaf5;
164197
border-color: #32aecf;
165-
box-shadow: 0 0 3px #32aecf;
198+
box-shadow: 0 0 1px #32aecf;
166199
}
167200
168201
div.view-list {
@@ -172,15 +205,15 @@
172205
align-items: baseline;
173206
}
174207
175-
button.add-new-view {
208+
button.dropdown-button {
176209
position: relative;
177210
}
178211
179212
div.dropdown-options {
180213
display: none;
181214
}
182215
183-
button.add-new-view:hover > div.dropdown-options {
216+
button.dropdown-button:hover > div.dropdown-options {
184217
display: flex;
185218
flex-direction: column;
186219
gap: 0px;

0 commit comments

Comments
 (0)