eye-care 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.
Files changed (63) hide show
  1. package/AGENTS.md +48 -0
  2. package/CODE_OF_CONDUCT.md +44 -0
  3. package/CONTRIBUTING.md +67 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/bin/eye-care.js +22 -0
  7. package/build/icon.ico +0 -0
  8. package/build/icon.png +0 -0
  9. package/build/tray.png +0 -0
  10. package/build/tray@2x.png +0 -0
  11. package/out/data/exercises.js +120 -0
  12. package/out/data/exercises.js.map +1 -0
  13. package/out/data/i18n.js +211 -0
  14. package/out/data/i18n.js.map +1 -0
  15. package/out/main/backgrounds.js +180 -0
  16. package/out/main/backgrounds.js.map +1 -0
  17. package/out/main/index.js +258 -0
  18. package/out/main/index.js.map +1 -0
  19. package/out/main/preferences.js +71 -0
  20. package/out/main/preferences.js.map +1 -0
  21. package/out/main/scheduler.js +106 -0
  22. package/out/main/scheduler.js.map +1 -0
  23. package/out/preload/break.js +9 -0
  24. package/out/preload/break.js.map +1 -0
  25. package/out/preload/settings.js +18 -0
  26. package/out/preload/settings.js.map +1 -0
  27. package/out/renderer/backgrounds/forest.svg +13 -0
  28. package/out/renderer/backgrounds/mountains.svg +13 -0
  29. package/out/renderer/backgrounds/sea.svg +13 -0
  30. package/out/renderer/backgrounds/sunny-sky.svg +16 -0
  31. package/out/renderer/backgrounds/sunset.svg +18 -0
  32. package/out/renderer/break.css +265 -0
  33. package/out/renderer/break.html +38 -0
  34. package/out/renderer/break.js +452 -0
  35. package/out/renderer/settings.css +198 -0
  36. package/out/renderer/settings.html +110 -0
  37. package/out/renderer/settings.js +407 -0
  38. package/out/shared/types.js +23 -0
  39. package/out/shared/types.js.map +1 -0
  40. package/package.json +100 -0
  41. package/scripts/copy-static.js +27 -0
  42. package/scripts/make-icon.js +120 -0
  43. package/src/data/exercises.ts +120 -0
  44. package/src/data/i18n.ts +297 -0
  45. package/src/main/backgrounds.ts +144 -0
  46. package/src/main/index.ts +233 -0
  47. package/src/main/preferences.ts +34 -0
  48. package/src/main/scheduler.ts +105 -0
  49. package/src/preload/break.ts +9 -0
  50. package/src/preload/settings.ts +18 -0
  51. package/src/renderer/backgrounds/forest.svg +13 -0
  52. package/src/renderer/backgrounds/mountains.svg +13 -0
  53. package/src/renderer/backgrounds/sea.svg +13 -0
  54. package/src/renderer/backgrounds/sunny-sky.svg +16 -0
  55. package/src/renderer/backgrounds/sunset.svg +18 -0
  56. package/src/renderer/break.css +265 -0
  57. package/src/renderer/break.html +38 -0
  58. package/src/renderer/break.js +452 -0
  59. package/src/renderer/settings.css +198 -0
  60. package/src/renderer/settings.html +110 -0
  61. package/src/renderer/settings.js +407 -0
  62. package/src/shared/types.ts +82 -0
  63. package/tsconfig.json +19 -0
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" />
6
+ <title>eye-care — Settings</title>
7
+ <link rel="stylesheet" href="settings.css" />
8
+ </head>
9
+ <body>
10
+ <div class="wrap">
11
+ <h1 id="settingsTitle">eye-care Settings</h1>
12
+ <p class="hint" id="settingsHint">Adjust break intervals and durations. Changes are saved automatically.</p>
13
+
14
+ <section>
15
+ <h2 id="generalSection">General</h2>
16
+ <label class="row">
17
+ <span id="languageLabel">Language</span>
18
+ <select id="language">
19
+ <option value="en">English</option>
20
+ <option value="ko">한국어</option>
21
+ </select>
22
+ </label>
23
+ <label class="row">
24
+ <span id="soundLabel">Sound notifications</span>
25
+ <input type="checkbox" id="soundEnabled" />
26
+ </label>
27
+ <label class="row">
28
+ <span id="strictLabel">Strict mode (no skip)</span>
29
+ <input type="checkbox" id="strictMode" />
30
+ </label>
31
+ </section>
32
+
33
+ <section>
34
+ <h2 id="miniSection">Mini break</h2>
35
+ <label class="row">
36
+ <span id="miniEnableLabel">Enable mini breaks</span>
37
+ <input type="checkbox" id="miniEnabled" />
38
+ </label>
39
+ <label class="row">
40
+ <span id="miniIntervalLabel">Interval (minutes)</span>
41
+ <input type="number" id="miniInterval" min="1" max="120" />
42
+ </label>
43
+ <label class="row">
44
+ <span id="miniDurationLabel">Duration (seconds)</span>
45
+ <input type="number" id="miniDuration" min="5" max="300" />
46
+ </label>
47
+ </section>
48
+
49
+ <section>
50
+ <h2 id="longSection">Long break</h2>
51
+ <label class="row">
52
+ <span id="longEnableLabel">Enable long breaks</span>
53
+ <input type="checkbox" id="longEnabled" />
54
+ </label>
55
+ <label class="row">
56
+ <span id="longIntervalLabel">Interval (minutes)</span>
57
+ <input type="number" id="longInterval" min="5" max="240" />
58
+ </label>
59
+ <label class="row">
60
+ <span id="longDurationLabel">Duration (seconds)</span>
61
+ <input type="number" id="longDuration" min="30" max="1800" />
62
+ </label>
63
+ </section>
64
+
65
+ <section>
66
+ <h2 id="bgSection">Background</h2>
67
+ <label class="row">
68
+ <span id="bgModeLabel">Background mode</span>
69
+ <select id="bgMode">
70
+ <option value="none">None</option>
71
+ <option value="builtin">Built-in</option>
72
+ <option value="user">User images</option>
73
+ <option value="random">Random</option>
74
+ </select>
75
+ </label>
76
+ <label class="row" id="bgPoolRow" style="display:none">
77
+ <span id="bgPoolLabel">Random pool</span>
78
+ <select id="bgPool">
79
+ <option value="builtin">Built-in</option>
80
+ <option value="user">User images</option>
81
+ <option value="all">All</option>
82
+ </select>
83
+ </label>
84
+ <label class="row" id="bgBlurRow" style="display:none">
85
+ <span id="bgBlurLabel">Blur</span>
86
+ <input type="range" id="bgBlur" min="0" max="20" step="1" value="3" />
87
+ <span id="bgBlurValue" class="range-value">3</span>
88
+ </label>
89
+ <div class="bg-gallery" id="bgGallery"></div>
90
+ <div class="bg-actions" id="bgActions">
91
+ <button type="button" id="bgAddBtn" class="btn-secondary">Add image...</button>
92
+ <button type="button" id="bgDeleteBtn" class="btn-secondary">Delete selected</button>
93
+ </div>
94
+ <p class="bg-hint" id="bgHint"></p>
95
+ </section>
96
+
97
+ <p class="status" id="status"></p>
98
+
99
+ <p class="medical" id="medicalDisclaimer">
100
+ eye-care is for eye-fatigue relief only. It is not a medical device and does not diagnose, treat, or cure any condition. If you experience persistent eye discomfort, consult a licensed ophthalmologist.
101
+ </p>
102
+
103
+ <footer class="app-footer">
104
+ <div class="app-version" id="appVersion"></div>
105
+ <div class="app-author" id="appAuthor"></div>
106
+ </footer>
107
+ </div>
108
+ <script src="settings.js"></script>
109
+ </body>
110
+ </html>
@@ -0,0 +1,407 @@
1
+ (function () {
2
+ const STRINGS = {
3
+ en: {
4
+ settingsTitle: "eye-care Settings",
5
+ settingsHint: "Adjust break intervals and durations. Changes are saved automatically.",
6
+ generalSection: "General",
7
+ languageLabel: "Language",
8
+ soundLabel: "Sound notifications",
9
+ strictLabel: "Strict mode (no skip)",
10
+ miniSection: "Mini break",
11
+ miniEnableLabel: "Enable mini breaks",
12
+ miniIntervalLabel: "Interval (minutes)",
13
+ miniDurationLabel: "Duration (seconds)",
14
+ longSection: "Long break",
15
+ longEnableLabel: "Enable long breaks",
16
+ longIntervalLabel: "Interval (minutes)",
17
+ longDurationLabel: "Duration (seconds)",
18
+ saved: "Saved",
19
+ bgSection: "Background",
20
+ bgModeLabel: "Background mode",
21
+ bgPoolLabel: "Random pool",
22
+ bgNone: "None",
23
+ bgBuiltin: "Built-in",
24
+ bgUser: "User images",
25
+ bgRandom: "Random",
26
+ bgAdd: "Add image...",
27
+ bgDelete: "Delete selected",
28
+ bgHintNone: "No background shown during breaks.",
29
+ bgHintBuiltin: "Choose a built-in nature scene for restful breaks.",
30
+ bgHintUser: "Add your own images. Soft, low-contrast nature scenes work best.",
31
+ bgHintRandom: "A random background is picked for each break.",
32
+ bgBlurLabel: "Blur strength",
33
+ bgNoImages: "No user images yet. Click \"Add image...\" to select one.",
34
+ bgAdded: "Added: ",
35
+ bgDeleted: "Deleted: ",
36
+ bgSelectPrompt: "Select an image first.",
37
+ bgNoFileSelected: "No file selected.",
38
+ bgError: "Error: ",
39
+ medicalDisclaimer:
40
+ "eye-care is for eye-fatigue relief only. It is not a medical device and does not diagnose, treat, or cure any condition. If you experience persistent eye discomfort, consult a licensed ophthalmologist.",
41
+ },
42
+ ko: {
43
+ settingsTitle: "eye-care 설정",
44
+ settingsHint: "휴식 간격과 시간을 조정하세요. 변경사항은 자동 저장됩니다.",
45
+ generalSection: "일반",
46
+ languageLabel: "언어",
47
+ soundLabel: "소리 알림",
48
+ strictLabel: "엄격 모드 (건너뛰기 금지)",
49
+ miniSection: "미니 휴식",
50
+ miniEnableLabel: "미니 휴식 사용",
51
+ miniIntervalLabel: "간격 (분)",
52
+ miniDurationLabel: "시간 (초)",
53
+ longSection: "긴 휴식",
54
+ longEnableLabel: "긴 휴식 사용",
55
+ longIntervalLabel: "간격 (분)",
56
+ longDurationLabel: "시간 (초)",
57
+ saved: "저장됨",
58
+ bgSection: "배경화면",
59
+ bgModeLabel: "배경 모드",
60
+ bgPoolLabel: "랜덤 풀",
61
+ bgNone: "없음",
62
+ bgBuiltin: "기본 제공",
63
+ bgUser: "사용자 이미지",
64
+ bgRandom: "랜덤",
65
+ bgAdd: "이미지 추가...",
66
+ bgDelete: "선택 삭제",
67
+ bgHintNone: "휴식 중 배경화면을 표시하지 않습니다.",
68
+ bgHintBuiltin: "편안한 휴식을 위한 기본 자연 풍경을 선택하세요.",
69
+ bgHintUser: "직접 이미지를 추가할 수 있어요. 부드럽고 저대비 자연 풍경이 가장 좋습니다.",
70
+ bgHintRandom: "매 휴식마다 랜덤으로 배경이 선택됩니다.",
71
+ bgBlurLabel: "흐림 정도",
72
+ bgNoImages: "사용자 이미지가 없습니다. \"이미지 추가...\"를 눌러 선택하세요.",
73
+ bgAdded: "추가됨: ",
74
+ bgDeleted: "삭제됨: ",
75
+ bgSelectPrompt: "이미지를 먼저 선택하세요.",
76
+ bgNoFileSelected: "선택된 파일이 없습니다.",
77
+ bgError: "오류: ",
78
+ medicalDisclaimer:
79
+ "eye-care는 눈 피로 완화 목적입니다. 의료기기가 아니며 질환을 진단·치료·완치하지 않습니다. 지속적인 눈 불편함이 있다면 안과 전문의와 상담하세요.",
80
+ },
81
+ };
82
+
83
+ const ids = [
84
+ "miniEnabled", "miniInterval", "miniDuration",
85
+ "longEnabled", "longInterval", "longDuration",
86
+ "soundEnabled", "strictMode", "language", "bgMode", "bgPool", "bgBlur",
87
+ ];
88
+ const els = {};
89
+ ids.forEach(function (id) { els[id] = document.getElementById(id); });
90
+ const statusEl = document.getElementById("status");
91
+ const bgGallery = document.getElementById("bgGallery");
92
+ const bgAddBtn = document.getElementById("bgAddBtn");
93
+ const bgDeleteBtn = document.getElementById("bgDeleteBtn");
94
+ const bgHintEl = document.getElementById("bgHint");
95
+ const bgActions = document.getElementById("bgActions");
96
+ const bgPoolRow = document.getElementById("bgPoolRow");
97
+ const bgBlurRow = document.getElementById("bgBlurRow");
98
+ const bgBlurValue = document.getElementById("bgBlurValue");
99
+
100
+ const textEls = {
101
+ settingsTitle: document.getElementById("settingsTitle"),
102
+ settingsHint: document.getElementById("settingsHint"),
103
+ generalSection: document.getElementById("generalSection"),
104
+ languageLabel: document.getElementById("languageLabel"),
105
+ soundLabel: document.getElementById("soundLabel"),
106
+ strictLabel: document.getElementById("strictLabel"),
107
+ miniSection: document.getElementById("miniSection"),
108
+ miniEnableLabel: document.getElementById("miniEnableLabel"),
109
+ miniIntervalLabel: document.getElementById("miniIntervalLabel"),
110
+ miniDurationLabel: document.getElementById("miniDurationLabel"),
111
+ longSection: document.getElementById("longSection"),
112
+ longEnableLabel: document.getElementById("longEnableLabel"),
113
+ longIntervalLabel: document.getElementById("longIntervalLabel"),
114
+ longDurationLabel: document.getElementById("longDurationLabel"),
115
+ bgSection: document.getElementById("bgSection"),
116
+ bgModeLabel: document.getElementById("bgModeLabel"),
117
+ bgPoolLabel: document.getElementById("bgPoolLabel"),
118
+ bgBlurLabel: document.getElementById("bgBlurLabel"),
119
+ medicalDisclaimer: document.getElementById("medicalDisclaimer"),
120
+ };
121
+
122
+ let currentLang = "en";
123
+ let saveTimer = null;
124
+ let bgConfig = { mode: "builtin", selected: "sunny-sky", userImages: [], randomPool: "all" };
125
+ let selectedBgName = null;
126
+
127
+ function tr(key) {
128
+ const s = STRINGS[currentLang] || STRINGS.en;
129
+ return s[key] || STRINGS.en[key] || key;
130
+ }
131
+
132
+ function applyLang(lang) {
133
+ currentLang = lang;
134
+ const s = STRINGS[lang] || STRINGS.en;
135
+ document.documentElement.lang = lang;
136
+ for (const k in textEls) {
137
+ if (textEls[k] && s[k]) textEls[k].textContent = s[k];
138
+ }
139
+ bgAddBtn.textContent = tr("bgAdd");
140
+ bgDeleteBtn.textContent = tr("bgDelete");
141
+ const mode = els.bgMode.value;
142
+ updateBgHint(mode);
143
+ updateBgOptionLabels();
144
+ renderBgGallery();
145
+ }
146
+
147
+ function updateBgHint(mode) {
148
+ if (mode === "none") bgHintEl.textContent = tr("bgHintNone");
149
+ else if (mode === "builtin") bgHintEl.textContent = tr("bgHintBuiltin");
150
+ else if (mode === "user") bgHintEl.textContent = tr("bgHintUser");
151
+ else if (mode === "random") bgHintEl.textContent = tr("bgHintRandom");
152
+ }
153
+
154
+ function updateBgOptionLabels() {
155
+ const modeOpts = els.bgMode.options;
156
+ modeOpts[0].textContent = tr("bgNone");
157
+ modeOpts[1].textContent = tr("bgBuiltin");
158
+ modeOpts[2].textContent = tr("bgUser");
159
+ modeOpts[3].textContent = tr("bgRandom");
160
+ const poolOpts = els.bgPool.options;
161
+ poolOpts[0].textContent = tr("bgBuiltin");
162
+ poolOpts[1].textContent = tr("bgUser");
163
+ poolOpts[2].textContent = tr("bgBuiltin") + " + " + tr("bgUser");
164
+ }
165
+
166
+ function showStatus(msg) {
167
+ statusEl.textContent = msg;
168
+ clearTimeout(saveTimer);
169
+ saveTimer = setTimeout(function () { statusEl.textContent = ""; }, 2000);
170
+ }
171
+
172
+ function gatherPrefs() {
173
+ return {
174
+ language: els.language.value,
175
+ background: bgConfig,
176
+ miniBreakEnabled: els.miniEnabled.checked,
177
+ miniBreakIntervalMinutes: parseInt(els.miniInterval.value, 10) || 20,
178
+ miniBreakDurationSeconds: parseInt(els.miniDuration.value, 10) || 20,
179
+ longBreakEnabled: els.longEnabled.checked,
180
+ longBreakIntervalMinutes: parseInt(els.longInterval.value, 10) || 50,
181
+ longBreakDurationSeconds: parseInt(els.longDuration.value, 10) || 300,
182
+ soundEnabled: els.soundEnabled.checked,
183
+ strictMode: els.strictMode.checked,
184
+ };
185
+ }
186
+
187
+ function applyPrefs(prefs) {
188
+ if (prefs.language) {
189
+ els.language.value = prefs.language;
190
+ applyLang(prefs.language);
191
+ }
192
+ if (prefs.background) {
193
+ bgConfig = prefs.background;
194
+ els.bgMode.value = bgConfig.mode || "builtin";
195
+ if (bgConfig.randomPool) els.bgPool.value = bgConfig.randomPool;
196
+ if (typeof bgConfig.blurStrength === "number") {
197
+ els.bgBlur.value = bgConfig.blurStrength;
198
+ bgBlurValue.textContent = bgConfig.blurStrength;
199
+ }
200
+ selectedBgName = bgConfig.selected || null;
201
+ }
202
+ els.miniEnabled.checked = !!prefs.miniBreakEnabled;
203
+ els.miniInterval.value = prefs.miniBreakIntervalMinutes;
204
+ els.miniDuration.value = prefs.miniBreakDurationSeconds;
205
+ els.longEnabled.checked = !!prefs.longBreakEnabled;
206
+ els.longInterval.value = prefs.longBreakIntervalMinutes;
207
+ els.longDuration.value = prefs.longBreakDurationSeconds;
208
+ els.soundEnabled.checked = !!prefs.soundEnabled;
209
+ els.strictMode.checked = !!prefs.strictMode;
210
+ renderBgGallery();
211
+ }
212
+
213
+ function renderBgGallery() {
214
+ while (bgGallery.firstChild) bgGallery.removeChild(bgGallery.firstChild);
215
+ const mode = els.bgMode.value;
216
+
217
+ if (mode === "random") {
218
+ bgActions.classList.add("hidden");
219
+ bgPoolRow.style.display = "";
220
+ bgBlurRow.style.display = "";
221
+ } else if (mode === "user") {
222
+ bgActions.classList.remove("hidden");
223
+ bgPoolRow.style.display = "none";
224
+ bgBlurRow.style.display = "";
225
+ } else if (mode === "builtin") {
226
+ bgActions.classList.add("hidden");
227
+ bgPoolRow.style.display = "none";
228
+ bgBlurRow.style.display = "";
229
+ } else {
230
+ bgActions.classList.add("hidden");
231
+ bgPoolRow.style.display = "none";
232
+ bgBlurRow.style.display = "none";
233
+ }
234
+
235
+ if (mode === "none" || mode === "random") {
236
+ bgGallery.classList.remove("list-mode");
237
+ if (mode === "none") return;
238
+ const note = document.createElement("p");
239
+ note.className = "bg-hint";
240
+ note.textContent = tr("bgHintRandom");
241
+ bgGallery.appendChild(note);
242
+ return;
243
+ }
244
+
245
+ bgAddBtn.disabled = false;
246
+ bgDeleteBtn.disabled = !selectedBgName;
247
+
248
+ const list = mode === "builtin"
249
+ ? window.eyeCare.backgrounds.listBuiltin()
250
+ : window.eyeCare.backgrounds.listUser();
251
+
252
+ Promise.resolve(list).then(function (names) {
253
+ if (!names || names.length === 0) {
254
+ const empty = document.createElement("p");
255
+ empty.className = "bg-hint";
256
+ empty.textContent = tr("bgNoImages");
257
+ bgGallery.appendChild(empty);
258
+ return;
259
+ }
260
+ if (names.length > 6) {
261
+ bgGallery.classList.add("list-mode");
262
+ } else {
263
+ bgGallery.classList.remove("list-mode");
264
+ }
265
+ if (!selectedBgName || names.indexOf(selectedBgName) === -1) {
266
+ selectedBgName = names[0];
267
+ bgConfig.selected = selectedBgName;
268
+ }
269
+ names.forEach(function (name) {
270
+ const thumb = document.createElement("div");
271
+ thumb.className = "bg-thumb";
272
+ if (name === selectedBgName) thumb.classList.add("selected");
273
+ thumb.dataset.name = name;
274
+ thumb.addEventListener("click", function () {
275
+ selectedBgName = name;
276
+ bgConfig.selected = name;
277
+ Array.prototype.forEach.call(bgGallery.children, function (c) {
278
+ c.classList.remove("selected");
279
+ });
280
+ thumb.classList.add("selected");
281
+ scheduleSave();
282
+ });
283
+ window.eyeCare.backgrounds.loadFile(name, mode).then(function (res) {
284
+ if (!res) return;
285
+ const img = document.createElement("img");
286
+ img.src = "data:" + res.mime + ";base64," + res.data;
287
+ img.alt = name;
288
+ thumb.appendChild(img);
289
+ });
290
+ const label = document.createElement("div");
291
+ label.className = "bg-name";
292
+ label.textContent = name;
293
+ thumb.appendChild(label);
294
+ bgGallery.appendChild(thumb);
295
+ });
296
+ });
297
+ }
298
+
299
+ function scheduleSave() {
300
+ clearTimeout(saveTimer);
301
+ saveTimer = setTimeout(function () {
302
+ bgConfig.mode = els.bgMode.value;
303
+ if (selectedBgName) bgConfig.selected = selectedBgName;
304
+ window.eyeCare.updatePreferences(gatherPrefs()).then(function () {
305
+ showStatus(STRINGS[currentLang].saved);
306
+ });
307
+ }, 400);
308
+ }
309
+
310
+ els.language.addEventListener("change", function () {
311
+ applyLang(els.language.value);
312
+ scheduleSave();
313
+ });
314
+
315
+ els.bgMode.addEventListener("change", function () {
316
+ const mode = els.bgMode.value;
317
+ bgConfig.mode = mode;
318
+ updateBgHint(mode);
319
+ renderBgGallery();
320
+ scheduleSave();
321
+ });
322
+
323
+ els.bgPool.addEventListener("change", function () {
324
+ bgConfig.randomPool = els.bgPool.value;
325
+ scheduleSave();
326
+ });
327
+
328
+ els.bgBlur.addEventListener("input", function () {
329
+ bgBlurValue.textContent = els.bgBlur.value;
330
+ bgConfig.blurStrength = parseInt(els.bgBlur.value, 10);
331
+ scheduleSave();
332
+ });
333
+
334
+ bgAddBtn.addEventListener("click", function () {
335
+ if (els.bgMode.value !== "user") {
336
+ els.bgMode.value = "user";
337
+ bgConfig.mode = "user";
338
+ bgHintEl.textContent = tr("bgHintUser");
339
+ }
340
+ window.eyeCare.backgrounds.addUser().then(function (res) {
341
+ if (res && res.added && res.added.length > 0) {
342
+ showStatus(tr("bgAdded") + res.added.join(", "));
343
+ const cfg = window.eyeCare.backgrounds.get();
344
+ Promise.resolve(cfg).then(function (c) {
345
+ bgConfig = c;
346
+ if (bgConfig.userImages.length > 0) {
347
+ bgConfig.selected = bgConfig.userImages[0];
348
+ selectedBgName = bgConfig.userImages[0];
349
+ }
350
+ renderBgGallery();
351
+ scheduleSave();
352
+ });
353
+ } else if (res && res.added && res.added.length === 0) {
354
+ showStatus(currentLang === "ko" ? "선택된 파일이 없습니다." : "No file selected.");
355
+ }
356
+ }).catch(function (err) {
357
+ console.error("addUser failed:", err);
358
+ showStatus(currentLang === "ko" ? "오류: " + (err && err.message ? err.message : "") : "Error: " + (err && err.message ? err.message : ""));
359
+ });
360
+ });
361
+
362
+ bgDeleteBtn.addEventListener("click", function () {
363
+ if (!selectedBgName) {
364
+ showStatus(tr("bgSelectPrompt"));
365
+ return;
366
+ }
367
+ window.eyeCare.backgrounds.deleteUser(selectedBgName).then(function () {
368
+ showStatus(tr("bgDeleted") + selectedBgName);
369
+ const cfg = window.eyeCare.backgrounds.get();
370
+ Promise.resolve(cfg).then(function (c) {
371
+ bgConfig = c;
372
+ selectedBgName = bgConfig.userImages[0] || null;
373
+ bgConfig.selected = selectedBgName || "";
374
+ renderBgGallery();
375
+ });
376
+ });
377
+ });
378
+
379
+ ids.forEach(function (id) {
380
+ const el = els[id];
381
+ if (id === "language" || id === "bgMode" || id === "bgPool" || id === "bgBlur") return;
382
+ el.addEventListener("change", scheduleSave);
383
+ el.addEventListener("input", scheduleSave);
384
+ });
385
+
386
+ window.eyeCare.getPreferences().then(function (prefs) {
387
+ applyPrefs(prefs);
388
+ });
389
+
390
+ if (window.eyeCare.getAppInfo) {
391
+ window.eyeCare.getAppInfo().then(function (info) {
392
+ const versionEl = document.getElementById("appVersion");
393
+ const authorEl = document.getElementById("appAuthor");
394
+ if (versionEl) versionEl.textContent = "eye-care v" + info.version;
395
+ if (authorEl) {
396
+ const link = document.createElement("a");
397
+ link.href = "#";
398
+ link.textContent = "by " + info.author;
399
+ link.addEventListener("click", function (e) {
400
+ e.preventDefault();
401
+ if (info.homepage) window.open(info.homepage, "_blank");
402
+ });
403
+ authorEl.appendChild(link);
404
+ }
405
+ });
406
+ }
407
+ })();
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PREFERENCES = void 0;
4
+ exports.DEFAULT_PREFERENCES = {
5
+ language: "en",
6
+ background: {
7
+ mode: "builtin",
8
+ selected: "sunny-sky",
9
+ userImages: [],
10
+ randomPool: "all",
11
+ blurStrength: 3,
12
+ },
13
+ miniBreakEnabled: true,
14
+ miniBreakIntervalMinutes: 20,
15
+ miniBreakDurationSeconds: 20,
16
+ longBreakEnabled: true,
17
+ longBreakIntervalMinutes: 50,
18
+ longBreakDurationSeconds: 300,
19
+ soundEnabled: true,
20
+ strictMode: false,
21
+ firstRun: true,
22
+ };
23
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":";;;AA0Da,QAAA,mBAAmB,GAAgB;IAC9C,QAAQ,EAAE,IAAI;IACd,UAAU,EAAE;QACV,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,WAAW;QACrB,UAAU,EAAE,EAAE;QACd,UAAU,EAAE,KAAK;QACjB,YAAY,EAAE,CAAC;KAChB;IACD,gBAAgB,EAAE,IAAI;IACtB,wBAAwB,EAAE,EAAE;IAC5B,wBAAwB,EAAE,EAAE;IAC5B,gBAAgB,EAAE,IAAI;IACtB,wBAAwB,EAAE,EAAE;IAC5B,wBAAwB,EAAE,GAAG;IAC7B,YAAY,EAAE,IAAI;IAClB,UAAU,EAAE,KAAK;IACjB,QAAQ,EAAE,IAAI;CACf,CAAC"}
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "eye-care",
3
+ "version": "1.0.0",
4
+ "description": "A PC eye-exercise reminder app based on ophthalmologist guidelines (20-20-20 rule, focus shifts, palming). Helps monitor-heavy users take guided eye breaks.",
5
+ "main": "out/main/index.js",
6
+ "bin": {
7
+ "eye-care": "bin/eye-care.js"
8
+ },
9
+ "author": {
10
+ "name": "uptodatelabs",
11
+ "email": "hello@uptodatelabs.com"
12
+ },
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/uptodatelabs/eye-care",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/uptodatelabs/eye-care.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/uptodatelabs/eye-care/issues"
21
+ },
22
+ "keywords": [
23
+ "eye-care",
24
+ "eye-exercise",
25
+ "break-reminder",
26
+ "20-20-20",
27
+ "health",
28
+ "electron",
29
+ "productivity"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "scripts": {
35
+ "dev": "tsc -p tsconfig.json && npm run copy-static && concurrently -k -n tsc,electron -c cyan,magenta \"tsc -p tsconfig.json --watch --preserveWatchOutput\" \"wait-on out/main/index.js && electron .\"",
36
+ "dev:fast": "npm run build && electron .",
37
+ "build": "tsc -p tsconfig.json && npm run copy-static && npm run make-icon",
38
+ "copy-static": "node scripts/copy-static.js",
39
+ "make-icon": "node scripts/make-icon.js",
40
+ "start": "npm run build && electron .",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
42
+ "dist": "npm run build && electron-builder --win",
43
+ "dist:mac": "npm run build && electron-builder --mac",
44
+ "dist:linux": "npm run build && electron-builder --linux",
45
+ "clean": "rimraf out dist"
46
+ },
47
+ "build": {
48
+ "appId": "com.uptodatelabs.eyecare",
49
+ "productName": "eye-care",
50
+ "copyright": "Copyright © 2026 uptodatelabs",
51
+ "directories": {
52
+ "output": "dist",
53
+ "buildResources": "build"
54
+ },
55
+ "files": [
56
+ "out/**/*",
57
+ "build/**/*",
58
+ "package.json"
59
+ ],
60
+ "win": {
61
+ "target": [
62
+ {
63
+ "target": "nsis",
64
+ "arch": ["x64"]
65
+ }
66
+ ],
67
+ "icon": "build/icon.ico"
68
+ },
69
+ "nsis": {
70
+ "oneClick": false,
71
+ "perMachine": false,
72
+ "allowToChangeInstallationDirectory": true,
73
+ "createDesktopShortcut": true,
74
+ "createStartMenuShortcut": true,
75
+ "shortcutName": "eye-care",
76
+ "runAfterFinish": true,
77
+ "deleteAppDataOnUninstall": false
78
+ },
79
+ "mac": {
80
+ "target": ["dmg"],
81
+ "icon": "build/icon.icns",
82
+ "category": "public.app-category.healthcare-fitness"
83
+ },
84
+ "linux": {
85
+ "target": ["AppImage", "deb"],
86
+ "icon": "build/icon.png",
87
+ "category": "Utility"
88
+ }
89
+ },
90
+ "dependencies": {
91
+ "electron": "^33.0.0"
92
+ },
93
+ "devDependencies": {
94
+ "concurrently": "^10.0.5",
95
+ "electron-builder": "^25.1.0",
96
+ "rimraf": "^6.0.1",
97
+ "typescript": "^5.6.0",
98
+ "wait-on": "^9.1.0"
99
+ }
100
+ }
@@ -0,0 +1,27 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const root = path.resolve(__dirname, "..");
5
+ const srcRenderer = path.join(root, "src", "renderer");
6
+ const outRenderer = path.join(root, "out", "renderer");
7
+
8
+ if (!fs.existsSync(outRenderer)) {
9
+ fs.mkdirSync(outRenderer, { recursive: true });
10
+ }
11
+
12
+ function copyDir(src, dest) {
13
+ const entries = fs.readdirSync(src, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const s = path.join(src, entry.name);
16
+ const d = path.join(dest, entry.name);
17
+ if (entry.isDirectory()) {
18
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
19
+ copyDir(s, d);
20
+ } else {
21
+ fs.copyFileSync(s, d);
22
+ }
23
+ }
24
+ }
25
+
26
+ copyDir(srcRenderer, outRenderer);
27
+ console.log("Copied renderer static files to out/renderer");