cista 0.7.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- cista/_version.py +1 -1
- cista/preview.py +146 -38
- cista/protocol.py +1 -2
- cista/serve.py +9 -0
- cista/util/apphelpers.py +2 -0
- cista/watching.py +99 -36
- cista/wwwroot/assets/add-file-6a41e251.js +1 -0
- cista/wwwroot/assets/add-folder-62d934ef.js +1 -0
- cista/wwwroot/assets/arrow-a3a583f8.js +1 -0
- cista/wwwroot/assets/arrows-h-d9e36082.js +1 -0
- cista/wwwroot/assets/arrows-v-5223491a.js +1 -0
- cista/wwwroot/assets/check-f7566407.js +1 -0
- cista/wwwroot/assets/code-8e0123d4.js +1 -0
- cista/wwwroot/assets/copy-a70a3d1c.js +1 -0
- cista/wwwroot/assets/create-file-d995875e.js +1 -0
- cista/wwwroot/assets/create-folder-c448deda.js +1 -0
- cista/wwwroot/assets/cross-e74ed6f4.js +1 -0
- cista/wwwroot/assets/disk-f1376321.js +1 -0
- cista/wwwroot/assets/download-bad67930.js +1 -0
- cista/wwwroot/assets/exclamation-afec06bb.js +1 -0
- cista/wwwroot/assets/eye-5fde34a1.js +1 -0
- cista/wwwroot/assets/find-11711648.js +1 -0
- cista/wwwroot/assets/fullscreen-7f61e522.js +1 -0
- cista/wwwroot/assets/github-c9475017.js +1 -0
- cista/wwwroot/assets/index-0ccc4ea2.js +28 -0
- cista/wwwroot/assets/index-89d850f4.css +1 -0
- cista/wwwroot/assets/info-b3d76e57.js +1 -0
- cista/wwwroot/assets/link-5cd893e4.js +1 -0
- cista/wwwroot/assets/logo-6090454d.js +1 -0
- cista/wwwroot/assets/loop-531f6994.js +1 -0
- cista/wwwroot/assets/menu-ed0d8c47.js +1 -0
- cista/wwwroot/assets/next-ee82241a.js +1 -0
- cista/wwwroot/assets/open-8364df82.js +1 -0
- cista/wwwroot/assets/paste-0f86e193.js +1 -0
- cista/wwwroot/assets/pause-a2dd4670.js +1 -0
- cista/wwwroot/assets/pencil-bfd02151.js +1 -0
- cista/wwwroot/assets/play-83e40a03.js +1 -0
- cista/wwwroot/assets/plus-f463cfbc.js +1 -0
- cista/wwwroot/assets/previous-cbb778b5.js +1 -0
- cista/wwwroot/assets/reload-907eb866.js +1 -0
- cista/wwwroot/assets/rename-51dadde6.js +1 -0
- cista/wwwroot/assets/scissors-d32350cc.js +1 -0
- cista/wwwroot/assets/shuffle-00a003ad.js +1 -0
- cista/wwwroot/assets/signin-d7bd57fd.js +1 -0
- cista/wwwroot/assets/signout-4c4ff7fb.js +1 -0
- cista/wwwroot/assets/skip-0069d8d7.js +1 -0
- cista/wwwroot/assets/spinner-164c9b34.js +1 -0
- cista/wwwroot/assets/stop-3717c27d.js +1 -0
- cista/wwwroot/assets/trash-449f81ef.js +1 -0
- cista/wwwroot/assets/triangle-3ecaf29c.js +1 -0
- cista/wwwroot/assets/unfullscreen-175d46cd.js +1 -0
- cista/wwwroot/assets/up-arrow-164a366e.js +1 -0
- cista/wwwroot/assets/upload-cloud-df377f1a.js +1 -0
- cista/wwwroot/assets/user-3b5a32bc.js +1 -0
- cista/wwwroot/assets/user-cog-27e2c201.js +1 -0
- cista/wwwroot/assets/volume-high-f4e07edd.js +1 -0
- cista/wwwroot/assets/volume-low-f754322d.js +1 -0
- cista/wwwroot/assets/volume-medium-a5806088.js +1 -0
- cista/wwwroot/assets/volume-mute-405763b1.js +1 -0
- cista/wwwroot/assets/window-cross-86cb8236.js +1 -0
- cista/wwwroot/assets/window-d99968ac.js +1 -0
- cista/wwwroot/assets/wordwrap-cdcd3f93.js +1 -0
- cista/wwwroot/assets/zoomin-7d13bb9b.js +1 -0
- cista/wwwroot/assets/zoomout-daf52018.js +1 -0
- cista/wwwroot/index.html +2 -2
- {cista-0.7.2.dist-info → cista-1.0.0.dist-info}/METADATA +85 -61
- cista-1.0.0.dist-info/RECORD +86 -0
- {cista-0.7.2.dist-info → cista-1.0.0.dist-info}/WHEEL +1 -1
- cista/wwwroot/assets/add-file-0ff53f2c.js +0 -1
- cista/wwwroot/assets/add-folder-0ac641fd.js +0 -1
- cista/wwwroot/assets/arrow-6d57743c.js +0 -1
- cista/wwwroot/assets/arrows-h-9715a1dc.js +0 -1
- cista/wwwroot/assets/arrows-v-17478ebc.js +0 -1
- cista/wwwroot/assets/check-b37d535a.js +0 -1
- cista/wwwroot/assets/code-35dab0d8.js +0 -1
- cista/wwwroot/assets/copy-08cc120b.js +0 -1
- cista/wwwroot/assets/create-file-48e89b04.js +0 -1
- cista/wwwroot/assets/create-folder-b9d2fd99.js +0 -1
- cista/wwwroot/assets/cross-9008d2a4.js +0 -1
- cista/wwwroot/assets/disk-5e08f410.js +0 -1
- cista/wwwroot/assets/download-72de2989.js +0 -1
- cista/wwwroot/assets/exclamation-a7d9f408.js +0 -1
- cista/wwwroot/assets/eye-73e4e6be.js +0 -1
- cista/wwwroot/assets/find-ec0a7765.js +0 -1
- cista/wwwroot/assets/fullscreen-7cf77f36.js +0 -1
- cista/wwwroot/assets/github-768c474d.js +0 -1
- cista/wwwroot/assets/index-1332f321.js +0 -13
- cista/wwwroot/assets/index-456bb401.css +0 -1
- cista/wwwroot/assets/info-c28d09ff.js +0 -1
- cista/wwwroot/assets/link-8cf3bdf0.js +0 -1
- cista/wwwroot/assets/logo-d5f794ec.js +0 -1
- cista/wwwroot/assets/loop-fedc4169.js +0 -1
- cista/wwwroot/assets/menu-0c34e3e2.js +0 -1
- cista/wwwroot/assets/next-0a58f144.js +0 -1
- cista/wwwroot/assets/open-35afd848.js +0 -1
- cista/wwwroot/assets/paste-e0ef001a.js +0 -1
- cista/wwwroot/assets/pause-a35acf21.js +0 -1
- cista/wwwroot/assets/pencil-cd90f93a.js +0 -1
- cista/wwwroot/assets/play-f535c62d.js +0 -1
- cista/wwwroot/assets/plus-b2df8058.js +0 -1
- cista/wwwroot/assets/previous-85fd5e4d.js +0 -1
- cista/wwwroot/assets/reload-e27d8eda.js +0 -1
- cista/wwwroot/assets/rename-a6cb8477.js +0 -1
- cista/wwwroot/assets/scissors-c98a2f01.js +0 -1
- cista/wwwroot/assets/shuffle-07c5c925.js +0 -1
- cista/wwwroot/assets/signin-c2a74424.js +0 -1
- cista/wwwroot/assets/signout-ce13f56f.js +0 -1
- cista/wwwroot/assets/skip-81664c53.js +0 -1
- cista/wwwroot/assets/spinner-95826db5.js +0 -1
- cista/wwwroot/assets/stop-acb41526.js +0 -1
- cista/wwwroot/assets/trash-c8e2fdaa.js +0 -1
- cista/wwwroot/assets/triangle-c25a1114.js +0 -1
- cista/wwwroot/assets/unfullscreen-e7cc25a1.js +0 -1
- cista/wwwroot/assets/up-arrow-a110006a.js +0 -1
- cista/wwwroot/assets/upload-cloud-6cdf142b.js +0 -1
- cista/wwwroot/assets/user-2d648d32.js +0 -1
- cista/wwwroot/assets/user-cog-a90f18ca.js +0 -1
- cista/wwwroot/assets/volume-high-792bbac7.js +0 -1
- cista/wwwroot/assets/volume-low-d40f717a.js +0 -1
- cista/wwwroot/assets/volume-medium-073ff083.js +0 -1
- cista/wwwroot/assets/volume-mute-ed5cdbbb.js +0 -1
- cista/wwwroot/assets/window-4bf01d12.js +0 -1
- cista/wwwroot/assets/window-cross-d62113fd.js +0 -1
- cista/wwwroot/assets/wordwrap-1032ac82.js +0 -1
- cista/wwwroot/assets/zoomin-98208790.js +0 -1
- cista/wwwroot/assets/zoomout-ea6810cf.js +0 -1
- cista-0.7.2.dist-info/RECORD +0 -86
- {cista-0.7.2.dist-info → cista-1.0.0.dist-info}/entry_points.txt +0 -0
cista/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This file is automatically generated by hatch build.
|
|
2
|
-
__version__ = '0.
|
|
2
|
+
__version__ = '1.0.0'
|
cista/preview.py
CHANGED
|
@@ -4,12 +4,14 @@ import io
|
|
|
4
4
|
import mimetypes
|
|
5
5
|
import urllib.parse
|
|
6
6
|
from pathlib import PurePosixPath
|
|
7
|
+
from time import perf_counter
|
|
7
8
|
from urllib.parse import unquote
|
|
8
9
|
from wsgiref.handlers import format_date_time
|
|
9
10
|
|
|
10
11
|
import av
|
|
11
|
-
import av.datasets
|
|
12
12
|
import fitz # PyMuPDF
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pillow_heif
|
|
13
15
|
from PIL import Image
|
|
14
16
|
from sanic import Blueprint, empty, raw
|
|
15
17
|
from sanic.exceptions import NotFound
|
|
@@ -18,7 +20,7 @@ from sanic.log import logger
|
|
|
18
20
|
from cista import config
|
|
19
21
|
from cista.util.filename import sanitize
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
pillow_heif.register_heif_opener()
|
|
22
24
|
|
|
23
25
|
bp = Blueprint("preview", url_prefix="/preview")
|
|
24
26
|
|
|
@@ -28,20 +30,20 @@ async def preview(req, path):
|
|
|
28
30
|
"""Preview a file"""
|
|
29
31
|
maxsize = int(req.args.get("px", 1024))
|
|
30
32
|
maxzoom = float(req.args.get("zoom", 2.0))
|
|
31
|
-
quality = int(req.args.get("q",
|
|
33
|
+
quality = int(req.args.get("q", 60))
|
|
32
34
|
rel = PurePosixPath(sanitize(unquote(path)))
|
|
33
35
|
path = config.config.path / rel
|
|
34
36
|
stat = path.lstat()
|
|
35
37
|
etag = config.derived_secret(
|
|
36
38
|
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
|
37
39
|
).hex()
|
|
38
|
-
savename = PurePosixPath(path.name).with_suffix(".
|
|
40
|
+
savename = PurePosixPath(path.name).with_suffix(".avif")
|
|
39
41
|
headers = {
|
|
40
42
|
"etag": etag,
|
|
41
43
|
"last-modified": format_date_time(stat.st_mtime),
|
|
42
44
|
"cache-control": "max-age=604800, immutable"
|
|
43
45
|
+ ("" if config.config.public else ", private"),
|
|
44
|
-
"content-type": "image/
|
|
46
|
+
"content-type": "image/avif",
|
|
45
47
|
"content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
|
|
46
48
|
}
|
|
47
49
|
if req.headers.if_none_match == etag:
|
|
@@ -60,58 +62,164 @@ async def preview(req, path):
|
|
|
60
62
|
def dispatch(path, quality, maxsize, maxzoom):
|
|
61
63
|
if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"):
|
|
62
64
|
return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
|
|
63
|
-
|
|
65
|
+
type, _ = mimetypes.guess_type(path.name)
|
|
66
|
+
if type and type.startswith("video/"):
|
|
64
67
|
return process_video(path, quality=quality, maxsize=maxsize)
|
|
65
68
|
return process_image(path, quality=quality, maxsize=maxsize)
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
def process_image(path, *, maxsize, quality):
|
|
72
|
+
t_load_start = perf_counter()
|
|
69
73
|
img = Image.open(path)
|
|
70
|
-
|
|
71
|
-
img.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
except AttributeError:
|
|
80
|
-
...
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.error(f"Error rotating preview image: {e}")
|
|
83
|
-
# Save as webp
|
|
74
|
+
# Force decode to include I/O in load timing
|
|
75
|
+
img.load()
|
|
76
|
+
t_load_end = perf_counter()
|
|
77
|
+
# Resize
|
|
78
|
+
orig_w, orig_h = img.size
|
|
79
|
+
t_proc_start = perf_counter()
|
|
80
|
+
img.thumbnail((min(orig_w, maxsize), min(orig_h, maxsize)))
|
|
81
|
+
t_proc_end = perf_counter()
|
|
82
|
+
# Save as AVIF
|
|
84
83
|
imgdata = io.BytesIO()
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
t_save_start = perf_counter()
|
|
85
|
+
img.save(imgdata, format="avif", quality=quality, speed=10, max_threads=1)
|
|
86
|
+
t_save_end = perf_counter()
|
|
87
|
+
|
|
88
|
+
ret = imgdata.getvalue()
|
|
89
|
+
|
|
90
|
+
load_ms = (t_load_end - t_load_start) * 1000
|
|
91
|
+
proc_ms = (t_proc_end - t_proc_start) * 1000
|
|
92
|
+
save_ms = (t_save_end - t_save_start) * 1000
|
|
93
|
+
logger.debug(
|
|
94
|
+
"Preview image %s: load=%.1fms process=%.1fms save=%.1fms out=%.1fKB",
|
|
95
|
+
path.name,
|
|
96
|
+
load_ms,
|
|
97
|
+
proc_ms,
|
|
98
|
+
save_ms,
|
|
99
|
+
len(ret) / 1024,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return ret
|
|
87
103
|
|
|
88
104
|
|
|
89
105
|
def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
|
106
|
+
t_load_start = perf_counter()
|
|
90
107
|
pdf = fitz.open(path)
|
|
91
108
|
page = pdf.load_page(page_number)
|
|
92
109
|
w, h = page.rect[2:4]
|
|
93
110
|
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
|
94
111
|
mat = fitz.Matrix(zoom, zoom)
|
|
95
|
-
pix = page.get_pixmap(matrix=mat)
|
|
96
|
-
|
|
112
|
+
pix = page.get_pixmap(matrix=mat) # type: ignore[attr-defined]
|
|
113
|
+
t_load_end = perf_counter()
|
|
114
|
+
|
|
115
|
+
t_save_start = perf_counter()
|
|
116
|
+
ret = pix.pil_tobytes(format="avif", quality=quality, speed=10, max_threads=1)
|
|
117
|
+
t_save_end = perf_counter()
|
|
118
|
+
|
|
119
|
+
logger.debug(
|
|
120
|
+
"Preview pdf %s: load+render=%.1fms save=%.1fms",
|
|
121
|
+
path.name,
|
|
122
|
+
(t_load_end - t_load_start) * 1000,
|
|
123
|
+
(t_save_end - t_save_start) * 1000,
|
|
124
|
+
)
|
|
125
|
+
return ret
|
|
97
126
|
|
|
98
127
|
|
|
99
128
|
def process_video(path, *, maxsize, quality):
|
|
100
|
-
|
|
101
|
-
stream = container.streams.video[0]
|
|
102
|
-
stream.codec_context.skip_frame = "NONKEY"
|
|
103
|
-
rot = stream.side_data and stream.side_data.get(DISPLAYMATRIX) or 0
|
|
104
|
-
container.seek(container.duration // 8)
|
|
105
|
-
img = next(container.decode(stream)).to_image()
|
|
106
|
-
del stream
|
|
107
|
-
|
|
108
|
-
img.thumbnail((maxsize, maxsize))
|
|
129
|
+
frame = None
|
|
109
130
|
imgdata = io.BytesIO()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
istream = ostream = icc = occ = frame = None
|
|
132
|
+
t_load_start = perf_counter()
|
|
133
|
+
# Initialize to avoid "possibly unbound" in static analysis when exceptions occur
|
|
134
|
+
t_load_end = t_load_start
|
|
135
|
+
t_save_start = t_load_start
|
|
136
|
+
t_save_end = t_load_start
|
|
137
|
+
with (
|
|
138
|
+
av.open(str(path)) as icontainer,
|
|
139
|
+
av.open(imgdata, "w", format="avif") as ocontainer,
|
|
140
|
+
):
|
|
141
|
+
istream = icontainer.streams.video[0]
|
|
142
|
+
istream.codec_context.skip_frame = "NONKEY"
|
|
143
|
+
icontainer.seek((icontainer.duration or 0) // 8)
|
|
144
|
+
for frame in icontainer.decode(istream):
|
|
145
|
+
if frame.dts is not None:
|
|
146
|
+
break
|
|
147
|
+
else:
|
|
148
|
+
raise RuntimeError("No frames found in video")
|
|
149
|
+
|
|
150
|
+
# Resize frame to thumbnail size
|
|
151
|
+
if frame.width > maxsize or frame.height > maxsize:
|
|
152
|
+
scale_factor = min(maxsize / frame.width, maxsize / frame.height)
|
|
153
|
+
new_width = int(frame.width * scale_factor)
|
|
154
|
+
new_height = int(frame.height * scale_factor)
|
|
155
|
+
frame = frame.reformat(width=new_width, height=new_height)
|
|
156
|
+
|
|
157
|
+
# Simple rotation detection and logging
|
|
158
|
+
if frame.rotation:
|
|
159
|
+
try:
|
|
160
|
+
fplanes = frame.to_ndarray()
|
|
161
|
+
# Split into Y, U, V planes of proper dimensions
|
|
162
|
+
planes = [
|
|
163
|
+
fplanes[: frame.height],
|
|
164
|
+
fplanes[frame.height : frame.height + frame.height // 4].reshape(
|
|
165
|
+
frame.height // 2, frame.width // 2
|
|
166
|
+
),
|
|
167
|
+
fplanes[frame.height + frame.height // 4 :].reshape(
|
|
168
|
+
frame.height // 2, frame.width // 2
|
|
169
|
+
),
|
|
170
|
+
]
|
|
171
|
+
# Rotate
|
|
172
|
+
planes = [np.rot90(p, frame.rotation // 90) for p in planes]
|
|
173
|
+
# Restore PyAV format
|
|
174
|
+
planes = np.hstack([p.flat for p in planes]).reshape(
|
|
175
|
+
-1, planes[0].shape[1]
|
|
176
|
+
)
|
|
177
|
+
frame = av.VideoFrame.from_ndarray(planes, format=frame.format.name)
|
|
178
|
+
del planes, fplanes
|
|
179
|
+
except Exception as e:
|
|
180
|
+
if "not yet supported" in str(e):
|
|
181
|
+
logger.warning(
|
|
182
|
+
f"Not rotating {path.name} preview image by {frame.rotation}°:\n PyAV: {e}"
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
logger.exception(f"Error rotating video frame: {e}")
|
|
186
|
+
t_load_end = perf_counter()
|
|
187
|
+
|
|
188
|
+
t_save_start = perf_counter()
|
|
189
|
+
crf = str(int(63 * (1 - quality / 100) ** 2)) # Closely matching PIL quality-%
|
|
190
|
+
ostream = ocontainer.add_stream(
|
|
191
|
+
"av1",
|
|
192
|
+
options={
|
|
193
|
+
"crf": crf,
|
|
194
|
+
"usage": "realtime",
|
|
195
|
+
"cpu-used": "8",
|
|
196
|
+
"threads": "1",
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
assert isinstance(ostream, av.VideoStream)
|
|
200
|
+
ostream.width = frame.width
|
|
201
|
+
ostream.height = frame.height
|
|
202
|
+
icc = istream.codec_context
|
|
203
|
+
occ = ostream.codec_context
|
|
204
|
+
|
|
205
|
+
# Copy HDR metadata from input video stream
|
|
206
|
+
occ.color_primaries = icc.color_primaries
|
|
207
|
+
occ.color_trc = icc.color_trc
|
|
208
|
+
occ.colorspace = icc.colorspace
|
|
209
|
+
occ.color_range = icc.color_range
|
|
210
|
+
|
|
211
|
+
ocontainer.mux(ostream.encode(frame))
|
|
212
|
+
ocontainer.mux(ostream.encode(None)) # Flush the stream
|
|
213
|
+
t_save_end = perf_counter()
|
|
214
|
+
|
|
215
|
+
# Capture frame dimensions before cleanup
|
|
114
216
|
ret = imgdata.getvalue()
|
|
115
|
-
|
|
217
|
+
logger.debug(
|
|
218
|
+
"Preview video %s: load+decode=%.1fms save=%.1fms",
|
|
219
|
+
path.name,
|
|
220
|
+
(t_load_end - t_load_start) * 1000,
|
|
221
|
+
(t_save_end - t_save_start) * 1000,
|
|
222
|
+
)
|
|
223
|
+
del imgdata, istream, ostream, icc, occ, frame
|
|
116
224
|
gc.collect()
|
|
117
225
|
return ret
|
cista/protocol.py
CHANGED
|
@@ -127,8 +127,7 @@ class FileEntry(msgspec.Struct, array_like=True, frozen=True):
|
|
|
127
127
|
return f"{self.name} ({self.size}, {self.mtime})"
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
class Update(msgspec.Struct, array_like=True):
|
|
131
|
-
...
|
|
130
|
+
class Update(msgspec.Struct, array_like=True): ...
|
|
132
131
|
|
|
133
132
|
|
|
134
133
|
class UpdKeep(Update, tag="k"):
|
cista/serve.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
import signal
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
from sanic import Sanic
|
|
@@ -11,6 +12,14 @@ def run(*, dev=False):
|
|
|
11
12
|
"""Run Sanic main process that spawns worker processes to serve HTTP requests."""
|
|
12
13
|
from .app import app
|
|
13
14
|
|
|
15
|
+
# Set up immediate exit on Ctrl+C for faster termination
|
|
16
|
+
def signal_handler(signum, frame):
|
|
17
|
+
print("\nReceived interrupt signal, exiting immediately...")
|
|
18
|
+
os._exit(0)
|
|
19
|
+
|
|
20
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
21
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
22
|
+
|
|
14
23
|
url, opts = parse_listen(config.config.listen)
|
|
15
24
|
# Silence Sanic's warning about running in production rather than debug
|
|
16
25
|
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1"
|
cista/util/apphelpers.py
CHANGED
|
@@ -29,6 +29,8 @@ async def handle_sanic_exception(request, e):
|
|
|
29
29
|
if not message or not request.app.debug and code == 500:
|
|
30
30
|
message = "Internal Server Error"
|
|
31
31
|
message = f"⚠️ {message}" if code < 500 else f"🛑 {message}"
|
|
32
|
+
if code == 500:
|
|
33
|
+
logger.exception(e)
|
|
32
34
|
# Non-browsers get JSON errors
|
|
33
35
|
if "text/html" not in request.headers.accept:
|
|
34
36
|
return jres(
|
cista/watching.py
CHANGED
|
@@ -48,6 +48,7 @@ def treeiter(rootmod):
|
|
|
48
48
|
def treeget(rootmod: list[FileEntry], path: PurePosixPath):
|
|
49
49
|
begin = None
|
|
50
50
|
ret = []
|
|
51
|
+
|
|
51
52
|
for i, relpath, entry in treeiter(rootmod):
|
|
52
53
|
if begin is None:
|
|
53
54
|
if relpath == path:
|
|
@@ -57,6 +58,7 @@ def treeget(rootmod: list[FileEntry], path: PurePosixPath):
|
|
|
57
58
|
if entry.level <= len(path.parts):
|
|
58
59
|
break
|
|
59
60
|
ret.append(entry)
|
|
61
|
+
|
|
60
62
|
return begin, ret
|
|
61
63
|
|
|
62
64
|
|
|
@@ -77,28 +79,36 @@ def treeinspos(rootmod: list[FileEntry], relpath: PurePosixPath, relfile: int):
|
|
|
77
79
|
# root
|
|
78
80
|
level += 1
|
|
79
81
|
continue
|
|
82
|
+
|
|
80
83
|
ename = rel.parts[level - 1]
|
|
81
84
|
name = relpath.parts[level - 1]
|
|
85
|
+
|
|
82
86
|
esort = sortkey(ename)
|
|
83
87
|
nsort = sortkey(name)
|
|
84
88
|
# Non-leaf are always folders, only use relfile at leaf
|
|
85
89
|
isfile = relfile if len(relpath.parts) == level else 0
|
|
90
|
+
|
|
86
91
|
# First compare by isfile, then by sorting order and if that too matches then case sensitive
|
|
87
92
|
cmp = (
|
|
88
93
|
entry.isfile - isfile
|
|
89
94
|
or (esort > nsort) - (esort < nsort)
|
|
90
95
|
or (ename > name) - (ename < name)
|
|
91
96
|
)
|
|
97
|
+
|
|
92
98
|
if cmp > 0:
|
|
93
99
|
return i
|
|
94
100
|
if cmp < 0:
|
|
95
101
|
continue
|
|
102
|
+
|
|
96
103
|
level += 1
|
|
97
104
|
if level > len(relpath.parts):
|
|
98
|
-
|
|
105
|
+
logger.error(
|
|
106
|
+
f"insertpos level overflow: relpath={relpath}, i={i}, entry.name={entry.name}, entry.level={entry.level}, level={level}"
|
|
107
|
+
)
|
|
99
108
|
break
|
|
100
109
|
else:
|
|
101
110
|
i += 1
|
|
111
|
+
|
|
102
112
|
return i
|
|
103
113
|
|
|
104
114
|
|
|
@@ -179,21 +189,16 @@ def update_path(rootmod: list[FileEntry], relpath: PurePosixPath, loop):
|
|
|
179
189
|
"""Called on FS updates, check the filesystem and broadcast any changes."""
|
|
180
190
|
new = walk(relpath)
|
|
181
191
|
obegin, old = treeget(rootmod, relpath)
|
|
192
|
+
|
|
182
193
|
if old == new:
|
|
183
|
-
logger.debug(
|
|
184
|
-
f"Watch: Event without changes needed {relpath}"
|
|
185
|
-
if old
|
|
186
|
-
else f"Watch: Event with old and new missing: {relpath}"
|
|
187
|
-
)
|
|
188
194
|
return
|
|
195
|
+
|
|
189
196
|
if obegin is not None:
|
|
190
197
|
del rootmod[obegin : obegin + len(old)]
|
|
198
|
+
|
|
191
199
|
if new:
|
|
192
|
-
logger.debug(f"Watch: Update {relpath}" if old else f"Watch: Created {relpath}")
|
|
193
200
|
i = treeinspos(rootmod, relpath, new[0].isfile)
|
|
194
201
|
rootmod[i:i] = new
|
|
195
|
-
else:
|
|
196
|
-
logger.debug(f"Watch: Removed {relpath}")
|
|
197
202
|
|
|
198
203
|
|
|
199
204
|
def update_space(loop):
|
|
@@ -218,17 +223,35 @@ def format_update(old, new):
|
|
|
218
223
|
oremain, nremain = set(old), set(new)
|
|
219
224
|
update = []
|
|
220
225
|
keep_count = 0
|
|
226
|
+
iteration_count = 0
|
|
227
|
+
# Precompute index maps to allow deterministic tie-breaking when both
|
|
228
|
+
# candidates exist in both sequences but are not equal (rename/move cases)
|
|
229
|
+
old_pos = {e: i for i, e in enumerate(old)}
|
|
230
|
+
new_pos = {e: i for i, e in enumerate(new)}
|
|
231
|
+
|
|
221
232
|
while oidx < len(old) and nidx < len(new):
|
|
233
|
+
iteration_count += 1
|
|
234
|
+
|
|
235
|
+
# Emergency brake for potential infinite loops
|
|
236
|
+
if iteration_count > 50000:
|
|
237
|
+
logger.error(
|
|
238
|
+
f"format_update potential infinite loop! iteration={iteration_count}, oidx={oidx}, nidx={nidx}"
|
|
239
|
+
)
|
|
240
|
+
raise Exception(
|
|
241
|
+
f"format_update infinite loop detected at iteration {iteration_count}"
|
|
242
|
+
)
|
|
243
|
+
|
|
222
244
|
modified = False
|
|
223
245
|
# Matching entries are kept
|
|
224
246
|
if old[oidx] == new[nidx]:
|
|
225
247
|
entry = old[oidx]
|
|
226
|
-
oremain.
|
|
227
|
-
nremain.
|
|
248
|
+
oremain.discard(entry)
|
|
249
|
+
nremain.discard(entry)
|
|
228
250
|
keep_count += 1
|
|
229
251
|
oidx += 1
|
|
230
252
|
nidx += 1
|
|
231
253
|
continue
|
|
254
|
+
|
|
232
255
|
if keep_count > 0:
|
|
233
256
|
modified = True
|
|
234
257
|
update.append(UpdKeep(keep_count))
|
|
@@ -248,7 +271,7 @@ def format_update(old, new):
|
|
|
248
271
|
insert_items = []
|
|
249
272
|
while nidx < len(new) and new[nidx] not in oremain:
|
|
250
273
|
entry = new[nidx]
|
|
251
|
-
nremain.
|
|
274
|
+
nremain.discard(entry)
|
|
252
275
|
insert_items.append(entry)
|
|
253
276
|
nidx += 1
|
|
254
277
|
if insert_items:
|
|
@@ -256,9 +279,32 @@ def format_update(old, new):
|
|
|
256
279
|
update.append(UpdIns(insert_items))
|
|
257
280
|
|
|
258
281
|
if not modified:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
# Tie-break: both items exist in both lists but don't match here.
|
|
283
|
+
# Decide whether to delete old[oidx] first or insert new[nidx] first
|
|
284
|
+
# based on which alignment is closer.
|
|
285
|
+
if oidx >= len(old) or nidx >= len(new):
|
|
286
|
+
break
|
|
287
|
+
cur_old = old[oidx]
|
|
288
|
+
cur_new = new[nidx]
|
|
289
|
+
|
|
290
|
+
pos_old_in_new = new_pos.get(cur_old)
|
|
291
|
+
pos_new_in_old = old_pos.get(cur_new)
|
|
292
|
+
|
|
293
|
+
# Default distances if not present (shouldn't happen if in remain sets)
|
|
294
|
+
dist_del = (pos_old_in_new - nidx) if pos_old_in_new is not None else 1
|
|
295
|
+
dist_ins = (pos_new_in_old - oidx) if pos_new_in_old is not None else 1
|
|
296
|
+
|
|
297
|
+
# Prefer the operation with smaller forward distance; tie => delete
|
|
298
|
+
if dist_del <= dist_ins:
|
|
299
|
+
# Delete current old item
|
|
300
|
+
oremain.discard(cur_old)
|
|
301
|
+
update.append(UpdDel(1))
|
|
302
|
+
oidx += 1
|
|
303
|
+
else:
|
|
304
|
+
# Insert current new item
|
|
305
|
+
nremain.discard(cur_new)
|
|
306
|
+
update.append(UpdIns([cur_new]))
|
|
307
|
+
nidx += 1
|
|
262
308
|
|
|
263
309
|
# Diff any remaining
|
|
264
310
|
if keep_count > 0:
|
|
@@ -311,10 +357,7 @@ def watcher_inotify(loop):
|
|
|
311
357
|
while not quit.is_set():
|
|
312
358
|
i = inotify.adapters.InotifyTree(rootpath.as_posix())
|
|
313
359
|
# Initialize the tree from filesystem
|
|
314
|
-
t0 = time.perf_counter()
|
|
315
360
|
update_root(loop)
|
|
316
|
-
t1 = time.perf_counter()
|
|
317
|
-
logger.debug(f"Root update took {t1 - t0:.1f}s")
|
|
318
361
|
trefresh = time.monotonic() + 300.0
|
|
319
362
|
tspace = time.monotonic() + 5.0
|
|
320
363
|
# Watch for changes (frequent wakeups needed for quiting)
|
|
@@ -335,32 +378,52 @@ def watcher_inotify(loop):
|
|
|
335
378
|
if quit.is_set():
|
|
336
379
|
return
|
|
337
380
|
interesting = any(f in modified_flags for f in event[1])
|
|
338
|
-
if event[2] == rootpath.as_posix() and event[3] == "zzz":
|
|
339
|
-
logger.debug(f"Watch: {interesting=} {event=}")
|
|
340
381
|
if interesting:
|
|
341
382
|
# Update modified path
|
|
342
|
-
t0 = time.perf_counter()
|
|
343
383
|
path = PurePosixPath(event[2]) / event[3]
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
384
|
+
try:
|
|
385
|
+
rel_path = path.relative_to(rootpath)
|
|
386
|
+
update_path(rootmod, rel_path, loop)
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error(
|
|
389
|
+
f"Error processing inotify event for path {path}: {e}"
|
|
390
|
+
)
|
|
391
|
+
raise
|
|
347
392
|
if not dirty:
|
|
348
393
|
t = time.monotonic()
|
|
349
394
|
dirty = True
|
|
350
|
-
# Wait a maximum of 0.
|
|
351
|
-
if dirty and time.monotonic() >= t + 0.
|
|
395
|
+
# Wait a maximum of 0.2s to push the updates
|
|
396
|
+
if dirty and time.monotonic() >= t + 0.2:
|
|
352
397
|
break
|
|
353
398
|
if dirty and state.root != rootmod:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
399
|
+
try:
|
|
400
|
+
update = format_update(state.root, rootmod)
|
|
401
|
+
with state.lock:
|
|
402
|
+
broadcast(update, loop)
|
|
403
|
+
state.root = rootmod
|
|
404
|
+
except Exception:
|
|
405
|
+
logger.exception(
|
|
406
|
+
"format_update failed; falling back to full rescan"
|
|
407
|
+
)
|
|
408
|
+
# Fallback: full rescan and try diff again; last resort send full root
|
|
409
|
+
try:
|
|
410
|
+
fresh = walk(PurePosixPath())
|
|
411
|
+
try:
|
|
412
|
+
update = format_update(state.root, fresh)
|
|
413
|
+
with state.lock:
|
|
414
|
+
broadcast(update, loop)
|
|
415
|
+
state.root = fresh
|
|
416
|
+
except Exception:
|
|
417
|
+
logger.exception(
|
|
418
|
+
"Fallback diff failed; sending full root snapshot"
|
|
419
|
+
)
|
|
420
|
+
with state.lock:
|
|
421
|
+
broadcast(format_root(fresh), loop)
|
|
422
|
+
state.root = fresh
|
|
423
|
+
except Exception:
|
|
424
|
+
logger.exception(
|
|
425
|
+
"Full rescan failed; dropping this batch of updates"
|
|
426
|
+
)
|
|
364
427
|
|
|
365
428
|
del i # Free the inotify object
|
|
366
429
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as r}from"./index-0ccc4ea2.js";const n={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 28 28"};function s(a,e){return t(),o("svg",n,e[0]||(e[0]=[r("path",{d:"M19.2 2.6H6.1V29h19.8V9.3zM18.5 16v7.1h-5.3V16H8.7l7.1-7.1L23 16z"},null,-1)]))}const d={render:s};export{d as default,s as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o,c as t,a as r}from"./index-0ccc4ea2.js";const l={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"};function n(s,e){return o(),t("svg",l,e[0]||(e[0]=[r("path",{d:"M29 6H16l-1-2H4L2 8h28zM0 10l2 20h28l2-20zm18.3 9.5V27h-5.6v-7.5H8l7.5-7.5 7.5 7.5z"},null,-1)]))}const d={render:n};export{d as default,n as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as r}from"./index-0ccc4ea2.js";const s={xmlns:"http://www.w3.org/2000/svg",width:"640",height:"640",viewBox:"0 -32 640 640"};function a(n,e){return t(),o("svg",s,e[0]||(e[0]=[r("path",{d:"M495.46 365.98c-13.03-13.37-150.24-144.06-150.24-144.06A35.16 35.16 0 0 0 320 211.2a35.06 35.06 0 0 0-25.22 10.72s-137.2 130.7-150.27 144.06c-13 13.38-13.9 37.44 0 51.72 14 14.24 33.4 15.4 50.48 0L320 297.8l125.02 119.9c17.1 15.4 36.55 14.24 50.44 0 13.95-14.3 13.08-38.37 0-51.72"},null,-1)]))}const l={render:a};export{l as default,a as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o,c as t,a as r}from"./index-0ccc4ea2.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"-6 -2 44 36"};function n(a,e){return o(),t("svg",s,e[0]||(e[0]=[r("path",{d:"M12 18H6v4l-6-6 6-6v4h6zm8-4h6v-4l6 6-6 6v-4h-6z"},null,-1)]))}const c={render:n};export{c as default,n as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o,c as t,a as r}from"./index-0ccc4ea2.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -6 16 44"};function n(a,e){return o(),t("svg",s,e[0]||(e[0]=[r("path",{d:"M8 20v6h4l-6 6-6-6h4v-6zm-4-8V6H0l6-6 6 6H8v6z"},null,-1)]))}const c={render:n};export{c as default,n as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as r}from"./index-0ccc4ea2.js";const n={xmlns:"http://www.w3.org/2000/svg",width:"512",height:"512",viewBox:"-48 0 512 512"};function s(a,e){return t(),o("svg",n,e[0]||(e[0]=[r("path",{d:"M320 96 128 288l-64-64-64 64 128 128 256-256z"},null,-1)]))}const l={render:s};export{l as default,s as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as r}from"./index-0ccc4ea2.js";const n={xmlns:"http://www.w3.org/2000/svg",width:"512",height:"512",viewBox:"-24 8 512 512"};function s(a,e){return t(),o("svg",n,e[0]||(e[0]=[r("path",{d:"m304 96-48 48 112 112-112 112 48 48 144-160zm-160 0L0 256l144 160 48-48L80 256l112-112z"},null,-1)]))}const c={render:s};export{c as default,s as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o,c as t,a as r}from"./index-0ccc4ea2.js";const n={xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 36 36"};function s(h,e){return o(),t("svg",n,e[0]||(e[0]=[r("path",{d:"M26 8h-6V6l-6-6H0v24h12v8h20V14zm0 2.83L29.17 14H26zm-12-8L17.17 6H14zM2 2h10v6h6v14H2zm28 28H14v-6h6V10h4v6h6z"},null,-1)]))}const v={render:s};export{v as default,s as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as c,c as t,a as o}from"./index-0ccc4ea2.js";const r={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"};function n(s,e){return c(),t("svg",r,e[0]||(e[0]=[o("path",{d:"M19.2 2.6H6.1V29h19.8V9.3zm3 15c0 .2-.2.4-.4.4h-4.4v4.4c0 .2-.2.4-.4.4h-2.4c-.2 0-.4-.2-.4-.4V18H9.9c-.2 0-.4-.2-.4-.4v-2.4c0-.2.2-.4.4-.4h4.4v-4.4c0-.2.2-.4.4-.4H17c.2 0 .4.2.4.4v4.4h4.4c.2 0 .4.2.4.4z"},null,-1)]))}const l={render:n};export{l as default,n as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as c,c as t,a as o}from"./index-0ccc4ea2.js";const r={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"};function l(n,e){return c(),t("svg",r,e[0]||(e[0]=[o("path",{d:"M29 6H16l-1-2H4L2 8h28zM0 10l2 20h28l2-20zm22.8 11.2c0 .3-.2.5-.5.5h-5.2v5.2c0 .3-.2.5-.5.5h-2.8c-.3 0-.5-.2-.5-.5v-5.2H8.1c-.3 0-.5-.2-.5-.5v-2.8c0-.3.2-.5.5-.5h5.2v-5.2c0-.3.2-.5.5-.5h2.8c.3 0 .5.2.5.5v5.2h5.2c.3 0 .5.2.5.5z"},null,-1)]))}const a={render:l};export{a as default,l as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o,c as t,a as r}from"./index-0ccc4ea2.js";const s={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"};function n(l,e){return o(),t("svg",s,e[0]||(e[0]=[r("path",{d:"M25.3 8.56 17.88 16l7.44 7.44-1.86 1.87L16 17.9l-7.44 7.4-1.86-1.85L14.12 16 6.68 8.56 8.55 6.7 16 14.12l7.44-7.44z"},null,-1)]))}const c={render:n};export{c as default,n as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a}from"./index-0ccc4ea2.js";const r={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"};function s(n,e){return t(),o("svg",r,e[0]||(e[0]=[a("path",{d:"M24.27 3.2H6.4a3.2 3.2 0 0 0-3.2 3.2v19.2a3.2 3.2 0 0 0 3.2 3.2h19.2a3.2 3.2 0 0 0 3.2-3.2V8.2zm-1.87 9.6c0 .88-.72 1.6-1.6 1.6h-9.6a1.6 1.6 0 0 1-1.6-1.6v-8h12.8zm-1.6-6.4h-3.2v6.4h3.2z"},null,-1)]))}const d={render:s};export{d as default,s as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as s,c as e,a as o}from"./index-0ccc4ea2.js";const t={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 30 30"};function l(n,c){return s(),e("svg",t,c[0]||(c[0]=[o("path",{d:"M23 25.9c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3-.6.1-.8.3-.3.5-.3.8.1.6.3.8.5.3.8.3.6-.1.8-.3c.2-.3.3-.5.3-.8m4.6 0c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3-.6.1-.8.3-.3.5-.3.8.1.6.3.8.5.3.8.3.6-.1.8-.3c.2-.3.3-.5.3-.8m2.3-4v5.7c0 .5-.2.9-.5 1.2s-.7.5-1.2.5H1.9c-.5 0-.9-.2-1.2-.5s-.5-.7-.5-1.2v-5.7c0-.5.2-.9.5-1.2s.7-.5 1.2-.5h8.3l2.4 2.4c.7.7 1.5 1 2.4 1s1.7-.3 2.4-1l2.4-2.4h8.3c.5 0 .9.2 1.2.5q.6.45.6 1.2m-5.8-10.2c.2.5.1.9-.3 1.3l-8 8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3l-8-8c-.4-.3-.5-.8-.3-1.3S6.5 11 7 11h4.6V3c0-.3.1-.6.3-.8s.5-.3.8-.3h4.6c.3 0 .6.1.8.3s.3.5.3.8v8H23c.5 0 .8.2 1.1.7"},null,-1)]))}const a={render:l};export{a as default,l as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as n}from"./index-0ccc4ea2.js";const r={xmlns:"http://www.w3.org/2000/svg",width:"448",height:"448",viewBox:"-136 0 448 448"};function a(s,e){return t(),o("svg",r,e[0]||(e[0]=[n("path",{d:"M128 312v56q0 6.5-4.75 11.25T112 384H48q-6.5 0-11.25-4.75T32 368v-56q0-6.5 4.75-11.25T48 296h64q6.5 0 11.25 4.75T128 312m7.5-264-7 192q-.25 6.5-5.13 11.25T112 256H48q-6.5 0-11.38-4.75T31.5 240l-7-192q-.25-6.5 4.38-11.25T40 32h80q6.5 0 11.13 4.75T135.5 48"},null,-1)]))}const c={render:a};export{c as default,a as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as s,c,a as t}from"./index-0ccc4ea2.js";const o={xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 36 36"};function r(a,e){return s(),c("svg",o,e[0]||(e[0]=[t("path",{d:"M29.715 16c-1.696-2.625-4.018-4.875-6.804-6.304A7.94 7.94 0 0 1 24 13.714c0 4.411-3.589 8-8 8s-8-3.589-8-8c0-1.411.375-2.804 1.089-4.018C6.303 11.125 3.982 13.375 2.285 16c3.054 4.714 7.982 8 13.714 8s10.661-3.286 13.714-8zM16.858 9.143a.87.87 0 0 0-.857-.857c-2.982 0-5.429 2.446-5.429 5.429 0 .464.393.857.857.857s.857-.393.857-.857c0-2.036 1.679-3.714 3.714-3.714a.87.87 0 0 0 .857-.857zM32 16c0 .446-.143.857-.357 1.232-3.286 5.411-9.304 9.054-15.643 9.054S3.643 22.625.357 17.232C.143 16.857 0 16.446 0 16s.143-.857.357-1.232C3.643 9.375 9.661 5.714 16 5.714s12.357 3.661 15.643 9.054c.214.375.357.786.357 1.232"},null,-1)]))}const l={render:r};export{l as default,r as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as n}from"./index-0ccc4ea2.js";const r={xmlns:"http://www.w3.org/2000/svg",viewBox:"-12 -12 512 512"};function s(a,e){return t(),o("svg",r,e[0]||(e[0]=[n("path",{d:"M480 416 355.44 291.44C373.22 262.4 384 228.58 384 192 384 85.98 298 0 192 0 85.98 0 0 85.98 0 192c0 106 85.98 192 192 192 36.58 0 70.4-10.78 99.44-28.5L416 480c8.75 8.75 23.25 8.7 32 0l32-32a22.8 22.8 0 0 0 0-32m-288-96c-70.7 0-128-57.3-128-128S121.3 64 192 64s128 57.3 128 128-57.3 128-128 128"},null,-1)]))}const l={render:s};export{l as default,s as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as t,c as o,a as r}from"./index-0ccc4ea2.js";const v={xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 32 32"};function n(s,e){return t(),o("svg",v,e[0]||(e[0]=[r("path",{d:"M18.7 6.7h6.6v6.6h-2.6v-4h-4zm4 16v-4h2.6v6.6h-6.6v-2.6zm-16-9.4V6.7h6.6v2.6h-4v4zm2.6 5.4v4h4v2.6H6.7v-6.6z"},null,-1)]))}const a={render:n};export{a as default,n as render};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{o as e,c as o,a as r}from"./index-0ccc4ea2.js";const c={xmlns:"http://www.w3.org/2000/svg",width:"512",height:"512"};function n(s,t){return e(),o("svg",c,t[0]||(t[0]=[r("path",{d:"M256 6.3C114.6 6.3 0 121 0 262.3c0 113 73.4 209 175 243 13 2.3 17.6-5.6 17.6-12.4l-.4-48C121 460.5 106 415 106 415c-11.7-29.5-28.5-37.4-28.5-37.4-23.2-16 1.8-15.6 1.8-15.6 25.7 1.8 39.2 26.4 39.2 26.4 23 39.2 60 27.8 74.5 21.3 2.3-16.5 9-27.8 16.3-34.2C152.3 369 92.6 347 92.6 249c0-28 10-50.8 26.4-68.8-2.6-6.4-11.4-32.5 2.5-67.7 0 0 21.5-7 70.4 26.2 20-5.6 42-8.5 64-8.6 21.3.7 43.2 3 64 9 49-33 70-26 70-26 14 35.3 5 61.4 2.4 67.8 16.3 18 26.2 40.8 26.2 68.7 0 98.4-60 120-117 126.4 9.2 8 17.4 23.4 17.4 47.3l-.2 70.2c0 6.6 4.7 14.6 17.7 12 101.7-34 175-129.7 175-243C512 121 397.5 6 256 6z"},null,-1)]))}const l={render:n};export{l as default,n as render};
|