jebin2 commited on
Commit
8c01e05
·
0 Parent(s):

Initial commit

Browse files
Files changed (8) hide show
  1. .gitattributes +11 -0
  2. .github/workflows/sync-to-hf.yml +61 -0
  3. Dockerfile +12 -0
  4. README.md +10 -0
  5. image.png +3 -0
  6. index.html +734 -0
  7. main.py +171 -0
  8. requirements.txt +1 -0
.gitattributes ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.jpg filter=lfs diff=lfs merge=lfs -text
2
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
3
+ *.png filter=lfs diff=lfs merge=lfs -text
4
+ *.gif filter=lfs diff=lfs merge=lfs -text
5
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
6
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
7
+ *.wav filter=lfs diff=lfs merge=lfs -text
8
+ *.ttf filter=lfs diff=lfs merge=lfs -text
9
+ *.db filter=lfs diff=lfs merge=lfs -text
10
+ sliding_puzzle filter=lfs diff=lfs merge=lfs -text
11
+ stockfish/stockfish-ubuntu-x86-64-avx2 filter=lfs diff=lfs merge=lfs -text
.github/workflows/sync-to-hf.yml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+ with:
15
+ fetch-depth: 0
16
+ lfs: true
17
+
18
+ - name: Configure Git
19
+ run: |
20
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
21
+ git config --global user.name "github-actions[bot]"
22
+
23
+ - name: Commit any uncommitted changes
24
+ run: |
25
+ git add -A
26
+ git diff --staged --quiet || git commit -m "Save working directory before LFS migration"
27
+
28
+ - name: Configure Git LFS
29
+ run: |
30
+ git lfs install
31
+
32
+ - name: Fetch all LFS objects
33
+ run: |
34
+ git lfs fetch --all
35
+ git lfs checkout
36
+
37
+ - name: Migrate existing files to LFS
38
+ run: |
39
+ git lfs migrate import --include="*.jpg,*.jpeg,*.png,*.gif,*.mp3,*.mp4,*.wav,*.ttf,*.db,sliding_puzzle,stockfish/stockfish-ubuntu-x86-64-avx2" --everything
40
+
41
+ - name: Track binary files with LFS
42
+ run: |
43
+ git lfs track "*.jpg" "*.jpeg" "*.png" "*.gif" "*.mp3" "*.mp4" "*.wav" "*.ttf" "*.db"
44
+ git lfs track "sliding_puzzle"
45
+ git lfs track "stockfish/stockfish-ubuntu-x86-64-avx2"
46
+ git add .gitattributes
47
+ git diff --staged --quiet || git commit -m "Configure Git LFS tracking"
48
+
49
+ - name: Fetch LFS objects after migration
50
+ run: |
51
+ git lfs fetch --all
52
+ git lfs pull
53
+
54
+ - name: Push to Hugging Face Space
55
+ env:
56
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
57
+ run: |
58
+ git remote remove space 2>/dev/null || true
59
+ git remote add space https://jebin2:${HF_TOKEN}@huggingface.co/spaces/jebin2/Paper
60
+ git lfs push --all space main
61
+ git push --force space main
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "main.py"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Paper
3
+ emoji: 🏃
4
+ colorFrom: gray
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
image.png ADDED

Git LFS Details

  • SHA256: ed0a902b09d7c227af0efd64fd4017fe448d8d8495dc44f3fe06cdb348b265dd
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB
index.html ADDED
@@ -0,0 +1,734 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Paper</title>
8
+ <style>
9
+ :root {
10
+ /* --- Cool Slate & Ink Palette --- */
11
+ --bg-primary: #f9f9fb;
12
+ --bg-secondary: #f1f3f6;
13
+ --bg-tertiary: #e8ebf0;
14
+ --text-primary: #2a324b;
15
+ --text-secondary: #5c677d;
16
+ --text-muted: #8a94a6;
17
+ --accent: #4a69bd;
18
+ --accent-hover: #3b5496;
19
+ --success: #3e8e7e;
20
+ --warning: #f0a500;
21
+ --error: #d9534f;
22
+ --border: #d1d8e0;
23
+ --shadow: rgba(74, 105, 189, 0.1);
24
+ --paper-shadow: rgba(42, 50, 75, 0.07);
25
+ --radius: 8px;
26
+ --transition: all 0.3s ease;
27
+ --paper-texture: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23f0ead6' fill-opacity='0.3'%3E%3Cpath d='M30 30c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12zm12 0c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
28
+ }
29
+
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ }
35
+ html, body {
36
+ height: 100%;
37
+ }
38
+
39
+ body {
40
+ font-family: 'Georgia', 'Times New Roman', serif;
41
+ background: linear-gradient(135deg, #f9f6ef 0%, #f1ebe0 100%);
42
+ background-attachment: fixed;
43
+ color: var(--text-primary);
44
+ line-height: 1.6;
45
+ position: relative;
46
+ }
47
+
48
+ body::before {
49
+ content: '';
50
+ position: fixed;
51
+ top: 0;
52
+ left: 0;
53
+ right: 0;
54
+ bottom: 0;
55
+ background: var(--paper-texture);
56
+ opacity: 0.4;
57
+ pointer-events: none;
58
+ z-index: 1;
59
+ }
60
+
61
+ /* Login Screen */
62
+ .login-screen {
63
+ display: flex;
64
+ justify-content: center;
65
+ align-items: center;
66
+ height: 100%;
67
+ padding: 20px;
68
+ position: relative;
69
+ z-index: 2;
70
+ background: var(--bg-secondary);
71
+ }
72
+
73
+ .login-screen::before {
74
+ content: '';
75
+ position: absolute;
76
+ top: 0;
77
+ left: 0;
78
+ right: 0;
79
+ bottom: 0;
80
+
81
+ pointer-events: none;
82
+ }
83
+
84
+ .login-box {
85
+ text-align: center;
86
+ padding: 40px 35px;
87
+ border-radius: var(--radius);
88
+ background: var(--bg-primary);
89
+ border: 2px solid var(--border);
90
+ box-shadow:
91
+ 0 8px 32px var(--paper-shadow),
92
+ 0 2px 8px var(--paper-shadow),
93
+ inset 0 1px 0 rgba(255, 255, 255, 0.8);
94
+ position: relative;
95
+ z-index: 1;
96
+ width: 100%;
97
+ max-width: 380px;
98
+ }
99
+
100
+ .login-box::before {
101
+ content: '';
102
+ position: absolute;
103
+ top: 0;
104
+ left: 0;
105
+ right: 0;
106
+ bottom: 0;
107
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(212, 175, 55, 0.03) 100%);
108
+ border-radius: var(--radius);
109
+ pointer-events: none;
110
+ }
111
+
112
+ .login-box h1 {
113
+ margin-bottom: 8px;
114
+ color: var(--text-primary);
115
+ font-weight: 400;
116
+ font-size: 36px;
117
+ letter-spacing: -0.5px;
118
+ font-family: 'Georgia', serif;
119
+ position: relative;
120
+ }
121
+
122
+ .login-subtitle {
123
+ color: var(--text-secondary);
124
+ font-size: 15px;
125
+ margin-bottom: 32px;
126
+ font-weight: 400;
127
+ font-style: italic;
128
+ line-height: 1.5;
129
+ }
130
+
131
+ .input-group {
132
+ position: relative;
133
+ margin-bottom: 28px;
134
+ }
135
+
136
+ .password-input {
137
+ width: 100%;
138
+ padding: 18px 22px;
139
+ font-size: 16px;
140
+ border: 2px solid var(--border);
141
+ border-radius: var(--radius);
142
+ background: var(--bg-secondary);
143
+ color: var(--text-primary);
144
+ text-align: center;
145
+ letter-spacing: 1px;
146
+ outline: none;
147
+ transition: var(--transition);
148
+ font-family: 'Courier New', monospace;
149
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.03);
150
+ }
151
+
152
+ .password-input:focus {
153
+ border-color: var(--accent);
154
+ box-shadow:
155
+ inset 0 2px 4px rgba(0, 0, 0, 0.03),
156
+ 0 0 0 3px rgba(212, 175, 55, 0.15);
157
+ background: var(--bg-primary);
158
+ }
159
+
160
+ .password-input::placeholder {
161
+ color: var(--text-muted);
162
+ letter-spacing: normal;
163
+ font-family: 'Georgia', serif;
164
+ font-style: italic;
165
+ }
166
+
167
+ .enter-btn {
168
+ width: 100%;
169
+ padding: 18px 32px;
170
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
171
+ color: var(--bg-primary);
172
+ border: none;
173
+ border-radius: var(--radius);
174
+ font-size: 16px;
175
+ font-weight: 500;
176
+ cursor: pointer;
177
+ transition: var(--transition);
178
+ position: relative;
179
+ overflow: hidden;
180
+ text-transform: uppercase;
181
+ letter-spacing: 0.5px;
182
+ font-family: 'Georgia', serif;
183
+ box-shadow:
184
+ 0 4px 15px rgba(212, 175, 55, 0.3),
185
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
186
+ }
187
+
188
+ .enter-btn:hover:not(:disabled) {
189
+ transform: translateY(-2px);
190
+ box-shadow:
191
+ 0 6px 20px rgba(212, 175, 55, 0.4),
192
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
193
+ }
194
+
195
+ .enter-btn:active {
196
+ transform: translateY(0);
197
+ }
198
+
199
+ .enter-btn:disabled {
200
+ opacity: 0.7;
201
+ cursor: not-allowed;
202
+ transform: none;
203
+ }
204
+
205
+ .error {
206
+ color: var(--error);
207
+ margin-top: 16px;
208
+ font-size: 14px;
209
+ min-height: 20px;
210
+ font-weight: 400;
211
+ font-style: italic;
212
+ }
213
+
214
+ /* Editor Screen */
215
+ .editor-screen {
216
+ display: none;
217
+ height: 100%;
218
+ background: var(--bg-primary);
219
+ flex-direction: column;
220
+ position: relative;
221
+ z-index: 2;
222
+ }
223
+
224
+ .header {
225
+ padding: 15px;
226
+ background: var(--bg-secondary);
227
+ border-bottom: 3px solid var(--border);
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: center;
231
+ box-shadow: 0 2px 10px var(--paper-shadow);
232
+ position: relative;
233
+ flex-shrink: 0;
234
+ }
235
+
236
+ .header::before {
237
+ content: '';
238
+ position: absolute;
239
+ bottom: 0;
240
+ left: 0;
241
+ right: 0;
242
+ height: 1px;
243
+ background: linear-gradient(90deg, transparent 0%, var(--accent) 50%, transparent 100%);
244
+ opacity: 0.3;
245
+ }
246
+
247
+ .header-left {
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 16px;
251
+ }
252
+
253
+ .app-title {
254
+ font-size: 24px;
255
+ font-weight: 400;
256
+ color: var(--text-primary);
257
+ font-family: 'Georgia', serif;
258
+ letter-spacing: -0.5px;
259
+ }
260
+
261
+ .header-right {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 16px;
265
+ flex-wrap: wrap;
266
+ }
267
+
268
+ .save-status {
269
+ font-size: 13px;
270
+ font-weight: 500;
271
+ padding: 8px 14px;
272
+ border-radius: 20px;
273
+ background: var(--bg-tertiary);
274
+ color: var(--text-secondary);
275
+ border: 1px solid var(--border);
276
+ min-width: 80px;
277
+ text-align: center;
278
+ font-family: 'Georgia', serif;
279
+ }
280
+
281
+ .save-status.saving {
282
+ background: linear-gradient(135deg, var(--warning) 0%, #d4850f 100%);
283
+ color: white;
284
+ border-color: var(--warning);
285
+ }
286
+
287
+ .save-status.saved {
288
+ background: linear-gradient(135deg, var(--success) 0%, #5a8a69 100%);
289
+ color: white;
290
+ border-color: var(--success);
291
+ }
292
+
293
+ .word-count {
294
+ font-size: 13px;
295
+ color: var(--text-muted);
296
+ font-weight: 400;
297
+ font-family: 'Georgia', serif;
298
+ font-style: italic;
299
+ }
300
+
301
+ .editor-container {
302
+ flex: 1;
303
+ position: relative;
304
+ margin: 20px;
305
+ border-radius: var(--radius);
306
+ background: var(--bg-primary);
307
+ border: 2px solid var(--border);
308
+ box-shadow:
309
+ inset 0 2px 8px rgba(0, 0, 0, 0.03),
310
+ 0 4px 20px var(--paper-shadow);
311
+ }
312
+
313
+ .editor-container::before {
314
+ content: '';
315
+ position: absolute;
316
+ top: 0;
317
+ left: 0;
318
+ right: 0;
319
+ bottom: 0;
320
+ background:
321
+ repeating-linear-gradient(
322
+ transparent,
323
+ transparent 29px
324
+ );
325
+ pointer-events: none;
326
+ z-index: 1;
327
+ border-radius: var(--radius);
328
+ }
329
+
330
+ .editor {
331
+ width: 100%;
332
+ height:100%;
333
+ padding: 25px 30px;
334
+ border: none;
335
+ outline: none;
336
+ font-family: 'Georgia', 'Times New Roman', serif;
337
+ font-size: 16px;
338
+ line-height: 1.8;
339
+ resize: none;
340
+ background: transparent;
341
+ color: var(--text-primary);
342
+ position: relative;
343
+ z-index: 2;
344
+ border-radius: var(--radius);
345
+ }
346
+
347
+ .editor::placeholder {
348
+ color: var(--text-muted);
349
+ font-style: italic;
350
+ opacity: 0.7;
351
+ }
352
+
353
+ /* Scrollbar styling */
354
+ .editor::-webkit-scrollbar {
355
+ width: 12px;
356
+ }
357
+
358
+ .editor::-webkit-scrollbar-track {
359
+ background: var(--bg-tertiary);
360
+ border-radius: 6px;
361
+ }
362
+
363
+ .editor::-webkit-scrollbar-thumb {
364
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%);
365
+ border-radius: 6px;
366
+ border: 2px solid var(--bg-tertiary);
367
+ }
368
+
369
+ .editor::-webkit-scrollbar-thumb:hover {
370
+ background: linear-gradient(135deg, var(--accent-hover) 0%, #9e7c15 100%);
371
+ }
372
+
373
+ /* Animation for mode switching */
374
+ .fade-in {
375
+ animation: fadeIn 0.4s ease-out;
376
+ }
377
+
378
+ @keyframes fadeIn {
379
+ from {
380
+ opacity: 0;
381
+ transform: translateY(20px) scale(0.98);
382
+ }
383
+ to {
384
+ opacity: 1;
385
+ transform: translateY(0) scale(1);
386
+ }
387
+ }
388
+
389
+ /* Mobile optimizations without @media */
390
+ .login-box {
391
+ min-height: auto;
392
+ }
393
+
394
+ .header-right {
395
+ justify-content: flex-end;
396
+ }
397
+
398
+ .word-count {
399
+ white-space: nowrap;
400
+ }
401
+
402
+ /* Focus improvements */
403
+ .password-input:focus,
404
+ .editor:focus {
405
+ outline: none;
406
+ }
407
+
408
+ /* Subtle animations */
409
+ .login-box {
410
+ animation: slideUp 0.6s ease-out;
411
+ }
412
+
413
+ @keyframes slideUp {
414
+ from {
415
+ opacity: 0;
416
+ transform: translateY(30px);
417
+ }
418
+ to {
419
+ opacity: 1;
420
+ transform: translateY(0);
421
+ }
422
+ }
423
+
424
+ .editor-container {
425
+ animation: paperUnfold 0.5s ease-out;
426
+ }
427
+
428
+ @keyframes paperUnfold {
429
+ from {
430
+ opacity: 0;
431
+ transform: scale(0.95) rotateX(5deg);
432
+ }
433
+ to {
434
+ opacity: 1;
435
+ transform: scale(1) rotateX(0deg);
436
+ }
437
+ }
438
+
439
+ /* Texture overlay for paper feel */
440
+ .editor-container::after {
441
+ content: '';
442
+ position: absolute;
443
+ top: 0;
444
+ left: 0;
445
+ right: 0;
446
+ bottom: 0;
447
+ background: var(--paper-texture);
448
+ opacity: 0.1;
449
+ pointer-events: none;
450
+ z-index: 1;
451
+ border-radius: var(--radius);
452
+ }
453
+ </style>
454
+ </head>
455
+ <body>
456
+ <div id="loginScreen" class="login-screen">
457
+ <div class="login-box">
458
+ <h1>Paper</h1>
459
+ <p class="login-subtitle">Perfect for temporary notes and secure sharing. Deleted after two days.</p>
460
+
461
+ <div class="input-group">
462
+ <input type="password" id="passwordInput" class="password-input" placeholder="min-8-char password" minlength="8" maxlength="100">
463
+ </div>
464
+
465
+ <button onclick="login()" class="enter-btn">Enter</button>
466
+ <div id="loginError" class="error"></div>
467
+ </div>
468
+ </div>
469
+
470
+ <div id="editorScreen" class="editor-screen">
471
+ <div class="header">
472
+ <div class="header-left">
473
+ <div class="app-title">Paper</div>
474
+ </div>
475
+ <div class="header-right">
476
+ <div id="wordCount" class="word-count">0 words</div>
477
+ <div id="saveStatus" class="save-status">Ready</div>
478
+ </div>
479
+ </div>
480
+ <div class="editor-container">
481
+ <textarea id="editor" class="editor" placeholder="Start typing..."></textarea>
482
+ </div>
483
+ </div>
484
+
485
+ <script>
486
+ let currentPassword = '';
487
+ let currentSalt = null;
488
+ let fileHash = '';
489
+ let saveTimeout = null;
490
+ let isWorking = false;
491
+
492
+ const PBKDF2_ITERATIONS = 250000;
493
+
494
+ // --- WORD COUNT ---
495
+ function updateWordCount() {
496
+ const text = document.getElementById('editor').value;
497
+ const words = text.trim() ? text.trim().split(/\s+/).length : 0;
498
+ const chars = text.length;
499
+ document.getElementById('wordCount').textContent = `${words} words, ${chars} chars`;
500
+ }
501
+
502
+ // --- CRYPTOGRAPHY (unchanged) ---
503
+ async function generateFilenameHash(password) {
504
+ const encoder = new TextEncoder();
505
+ const data = encoder.encode(password);
506
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
507
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
508
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
509
+ }
510
+
511
+ async function deriveKey(password, salt) {
512
+ const encoder = new TextEncoder();
513
+ const keyMaterial = await crypto.subtle.importKey(
514
+ 'raw',
515
+ encoder.encode(password),
516
+ { name: 'PBKDF2' },
517
+ false,
518
+ ['deriveKey']
519
+ );
520
+ return crypto.subtle.deriveKey(
521
+ {
522
+ name: 'PBKDF2',
523
+ salt: salt,
524
+ iterations: PBKDF2_ITERATIONS,
525
+ hash: 'SHA-256'
526
+ },
527
+ keyMaterial,
528
+ { name: 'AES-GCM', length: 256 },
529
+ true,
530
+ ['encrypt', 'decrypt']
531
+ );
532
+ }
533
+
534
+ async function encrypt(text, key) {
535
+ const encoder = new TextEncoder();
536
+ const data = encoder.encode(text);
537
+ const iv = crypto.getRandomValues(new Uint8Array(12));
538
+
539
+ const encryptedContent = await crypto.subtle.encrypt(
540
+ { name: 'AES-GCM', iv: iv },
541
+ key,
542
+ data
543
+ );
544
+
545
+ const combined = new Uint8Array(iv.length + encryptedContent.byteLength);
546
+ combined.set(iv);
547
+ combined.set(new Uint8Array(encryptedContent), iv.length);
548
+
549
+ return btoa(String.fromCharCode.apply(null, combined));
550
+ }
551
+
552
+ async function decrypt(encryptedBase64, key) {
553
+ try {
554
+ const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
555
+ const iv = combined.slice(0, 12);
556
+ const encryptedContent = combined.slice(12);
557
+
558
+ const decrypted = await crypto.subtle.decrypt(
559
+ { name: 'AES-GCM', iv: iv },
560
+ key,
561
+ encryptedContent
562
+ );
563
+
564
+ return new TextDecoder().decode(decrypted);
565
+ } catch (error) {
566
+ console.error('Decryption failed:', error);
567
+ throw new Error('Decryption failed. Check password.');
568
+ }
569
+ }
570
+
571
+ function base64ToUint8Array(base64) {
572
+ const binaryString = atob(base64);
573
+ const len = binaryString.length;
574
+ const bytes = new Uint8Array(len);
575
+ for (let i = 0; i < len; i++) {
576
+ bytes[i] = binaryString.charCodeAt(i);
577
+ }
578
+ return bytes;
579
+ }
580
+
581
+ // --- APPLICATION LOGIC ---
582
+ document.getElementById('passwordInput').focus();
583
+ document.getElementById('passwordInput').addEventListener('keypress', e => {
584
+ if (e.key === 'Enter') login();
585
+ });
586
+
587
+ // iOS Safari viewport fix
588
+ function fixIOSViewport() {
589
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
590
+ if (isIOS) {
591
+ const vh = window.innerHeight * 0.01;
592
+ document.documentElement.style.setProperty('--vh', `${vh}px`);
593
+
594
+ window.addEventListener('resize', () => {
595
+ const vh = window.innerHeight * 0.01;
596
+ document.documentElement.style.setProperty('--vh', `${vh}px`);
597
+ });
598
+ }
599
+ }
600
+
601
+ // Call on page load
602
+ fixIOSViewport();
603
+
604
+ async function login() {
605
+ if (isWorking) return;
606
+ isWorking = true;
607
+
608
+ const password = document.getElementById('passwordInput').value;
609
+ const errorDiv = document.getElementById('loginError');
610
+ const enterBtn = document.querySelector('.enter-btn');
611
+
612
+ errorDiv.textContent = '';
613
+ if (password.length < 8) {
614
+ errorDiv.textContent = 'Password must be at least 8 characters';
615
+ isWorking = false;
616
+ return;
617
+ }
618
+
619
+ enterBtn.textContent = 'Loading...';
620
+ enterBtn.disabled = true;
621
+
622
+ currentPassword = password;
623
+ fileHash = await generateFilenameHash(password);
624
+
625
+ try {
626
+ const response = await fetch('/api/load', {
627
+ method: 'POST',
628
+ headers: { 'Content-Type': 'application/json' },
629
+ body: JSON.stringify({ hash: fileHash })
630
+ });
631
+
632
+ const data = await response.json();
633
+
634
+ if (!response.ok) {
635
+ throw new Error(data.error || 'Login failed');
636
+ }
637
+
638
+ currentSalt = base64ToUint8Array(data.salt);
639
+
640
+ let content = '';
641
+ if (data.content) {
642
+ const key = await deriveKey(currentPassword, currentSalt);
643
+ content = await decrypt(data.content, key);
644
+ }
645
+
646
+ document.getElementById('editor').value = content;
647
+ document.getElementById('loginScreen').style.display = 'none';
648
+ document.getElementById('editorScreen').style.display = 'flex';
649
+ //let editor = document.getElementById('editor')
650
+ //editor.focus();
651
+ //editor.setSelectionRange(0, 0);
652
+ setupAutoSave();
653
+ updateSaveStatus('Ready');
654
+ updateWordCount();
655
+
656
+ } catch (error) {
657
+ errorDiv.textContent = error.message.includes('Decryption') ? 'Invalid password' : 'Connection error';
658
+ } finally {
659
+ isWorking = false;
660
+ enterBtn.textContent = 'Enter';
661
+ enterBtn.disabled = false;
662
+ }
663
+ }
664
+
665
+ function setupAutoSave() {
666
+ const editor = document.getElementById('editor');
667
+
668
+ editor.addEventListener('input', () => {
669
+ clearTimeout(saveTimeout);
670
+ updateSaveStatus('Typing...');
671
+ updateWordCount();
672
+
673
+ saveTimeout = setTimeout(saveContent, 1500);
674
+ });
675
+ }
676
+
677
+ async function saveContent() {
678
+ if (isWorking) return;
679
+ isWorking = true;
680
+
681
+ updateSaveStatus('Saving...');
682
+
683
+ const content = document.getElementById('editor').value;
684
+
685
+ try {
686
+ const key = await deriveKey(currentPassword, currentSalt);
687
+ const encryptedContent = await encrypt(content, key);
688
+
689
+ const response = await fetch('/api/save', {
690
+ method: 'POST',
691
+ headers: { 'Content-Type': 'application/json' },
692
+ body: JSON.stringify({
693
+ hash: fileHash,
694
+ content: encryptedContent
695
+ })
696
+ });
697
+
698
+ if (response.ok) {
699
+ updateSaveStatus('Saved');
700
+ } else {
701
+ const data = await response.json();
702
+ updateSaveStatus(`Error: ${data.error || 'Save failed'}`);
703
+ }
704
+ } catch (error) {
705
+ console.error('Save failed:', error);
706
+ updateSaveStatus('Save failed');
707
+ } finally {
708
+ isWorking = false;
709
+ }
710
+ }
711
+
712
+ function updateSaveStatus(status) {
713
+ const statusDiv = document.getElementById('saveStatus');
714
+ statusDiv.textContent = status;
715
+
716
+ statusDiv.className = 'save-status';
717
+ if (status.includes('Saving') || status.includes('Typing')) {
718
+ statusDiv.className += ' saving';
719
+ } else if (status === 'Saved') {
720
+ statusDiv.className += ' saved';
721
+ }
722
+ }
723
+
724
+ // Prevent accidental page close
725
+ window.addEventListener('beforeunload', function(e) {
726
+ const statusDiv = document.getElementById('saveStatus');
727
+ if (statusDiv.className.includes(" saving")) {
728
+ e.preventDefault();
729
+ e.returnValue = '';
730
+ }
731
+ });
732
+ </script>
733
+ </body>
734
+ </html>
main.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory, abort
2
+ import os
3
+ import glob
4
+ import re
5
+ import base64
6
+ import time
7
+
8
+ app = Flask(__name__)
9
+
10
+ # --- CONFIGURATION ---
11
+ # Store data in a dedicated directory
12
+ DATA_DIR = os.path.join('/tmp')
13
+ MAX_TOTAL_SIZE_MB = 100 # Max total size of all notes in MB
14
+ PURGE_TO_SIZE_MB = 80 # When purging, reduce total size to this
15
+ AGE_LIMIT_DAYS = 2
16
+
17
+ # --- SECURITY HELPER ---
18
+ def sanitize_hash(hash_string):
19
+ """
20
+ Validates that the hash is a 16-character hexadecimal string.
21
+ This is CRITICAL to prevent path traversal attacks.
22
+ """
23
+ if not isinstance(hash_string, str):
24
+ return False
25
+ # Check for length and character set
26
+ return bool(re.match(r'^[0-9a-f]{16}$', hash_string))
27
+
28
+ # --- FILE MANAGEMENT ---
29
+ def cleanup_files():
30
+ """
31
+ Remove old files based on two conditions:
32
+ 1. Any file older than AGE_LIMIT_DAYS is removed.
33
+ 2. If total size still exceeds MAX_TOTAL_SIZE_MB, the oldest remaining files are removed
34
+ until the total size is below PURGE_TO_SIZE_MB.
35
+ """
36
+ try:
37
+ content_files = glob.glob(os.path.join(DATA_DIR, '*_content.txt'))
38
+ if not content_files:
39
+ return
40
+
41
+ now = time.time()
42
+ age_limit_seconds = AGE_LIMIT_DAYS * 24 * 60 * 60
43
+ age_threshold = now - age_limit_seconds
44
+
45
+ all_file_info = []
46
+ for f_path in content_files:
47
+ try:
48
+ mtime = os.path.getmtime(f_path)
49
+ size = os.path.getsize(f_path)
50
+ all_file_info.append({'path': f_path, 'size': size, 'mtime': mtime})
51
+ except OSError:
52
+ continue
53
+
54
+ # --- Stage 1: Identify files to delete by age ---
55
+ files_to_keep = []
56
+ files_to_delete = []
57
+
58
+ for f_info in all_file_info:
59
+ if f_info['mtime'] < age_threshold:
60
+ files_to_delete.append(f_info)
61
+ else:
62
+ files_to_keep.append(f_info)
63
+
64
+ # --- Stage 2: Identify files to delete by size from the remaining pool ---
65
+ current_size_of_kept_files = sum(f['size'] for f in files_to_keep)
66
+ max_size_bytes = MAX_TOTAL_SIZE_MB * 1024 * 1024
67
+
68
+ if current_size_of_kept_files > max_size_bytes:
69
+ # Sort the files we were planning to keep by age (oldest first)
70
+ files_to_keep.sort(key=lambda x: x['mtime'])
71
+
72
+ target_size_bytes = PURGE_TO_SIZE_MB * 1024 * 1024
73
+
74
+ # Move oldest files from 'keep' to 'delete' until size is acceptable
75
+ while current_size_of_kept_files > target_size_bytes and files_to_keep:
76
+ file_to_move = files_to_keep.pop(0) # Oldest is at the front
77
+ files_to_delete.append(file_to_move)
78
+ current_size_of_kept_files -= file_to_move['size']
79
+
80
+ # --- Stage 3: Perform the actual deletion ---
81
+ if not files_to_delete:
82
+ return
83
+
84
+ print(f"Cleanup: Deleting {len(files_to_delete)} old/oversized file(s).")
85
+ for f_info in files_to_delete:
86
+ try:
87
+ content_path = f_info['path']
88
+ salt_path = content_path.replace('_content.txt', '_salt.txt')
89
+
90
+ os.remove(content_path)
91
+ if os.path.exists(salt_path):
92
+ os.remove(salt_path)
93
+ except OSError as e:
94
+ print(f"Cleanup: Error removing file {f_info['path']}: {e}")
95
+
96
+ except Exception as e:
97
+ # Log this error in a real application
98
+ print(f"Error during file cleanup: {e}")
99
+
100
+ # --- FLASK ROUTES ---
101
+ @app.route('/')
102
+ def index():
103
+ # Run cleanup routine after a successful save
104
+ cleanup_files()
105
+ return send_from_directory('.', 'index.html')
106
+
107
+ @app.route('/api/load', methods=['POST'])
108
+ def load_content():
109
+ data = request.json
110
+ file_hash = data.get('hash', '')
111
+
112
+ # CRITICAL: Sanitize input to prevent path traversal
113
+ if not sanitize_hash(file_hash):
114
+ return jsonify({'error': 'Invalid hash format'}), 400
115
+
116
+ content_path = os.path.join(DATA_DIR, f'{file_hash}_content.txt')
117
+ salt_path = os.path.join(DATA_DIR, f'{file_hash}_salt.txt')
118
+
119
+ try:
120
+ # Handle the salt first. If it doesn't exist, this is a new note.
121
+ if os.path.exists(salt_path):
122
+ with open(salt_path, 'r', encoding='utf-8') as f:
123
+ salt_b64 = f.read()
124
+ else:
125
+ # New note: generate a new, cryptographically secure salt
126
+ salt_bytes = os.urandom(16)
127
+ salt_b64 = base64.b64encode(salt_bytes).decode('utf-8')
128
+ # Save the new salt immediately
129
+ with open(salt_path, 'w', encoding='utf-8') as f:
130
+ f.write(salt_b64)
131
+
132
+ # Now, handle the content. It might not exist yet for a new note.
133
+ if os.path.exists(content_path):
134
+ with open(content_path, 'r', encoding='utf-8') as f:
135
+ encrypted_content = f.read()
136
+ else:
137
+ encrypted_content = ''
138
+
139
+ return jsonify({'content': encrypted_content, 'salt': salt_b64})
140
+ except Exception as e:
141
+ print(f"Error during load: {e}") # For debugging
142
+ return jsonify({'error': 'Failed to load content from server'}), 500
143
+
144
+ @app.route('/api/save', methods=['POST'])
145
+ def save_content():
146
+ data = request.json
147
+ file_hash = data.get('hash', '')
148
+ encrypted_content = data.get('content', '')
149
+
150
+ # CRITICAL: Sanitize input to prevent path traversal
151
+ if not sanitize_hash(file_hash):
152
+ return jsonify({'error': 'Invalid hash format'}), 400
153
+
154
+ # The client must provide content to save
155
+ if not isinstance(encrypted_content, str):
156
+ return jsonify({'error': 'Invalid content format'}), 400
157
+
158
+ content_path = os.path.join(DATA_DIR, f'{file_hash}_content.txt')
159
+
160
+ try:
161
+ # Save encrypted content directly
162
+ with open(content_path, 'w', encoding='utf-8') as f:
163
+ f.write(encrypted_content)
164
+
165
+ return jsonify({'status': 'saved'})
166
+ except Exception as e:
167
+ print(f"Error during save: {e}") # For debugging
168
+ return jsonify({'error': 'Save failed on server'}), 500
169
+
170
+ if __name__ == '__main__':
171
+ app.run(host='0.0.0.0', port=7860, debug=False)
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ flask==2.3.3