Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a0dd04e61 |
@@ -40,6 +40,7 @@ type fileView struct {
|
|||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Size string
|
Size string
|
||||||
|
SizeBytes int64
|
||||||
ContentType string
|
ContentType string
|
||||||
PreviewKind string
|
PreviewKind string
|
||||||
URL string
|
URL string
|
||||||
@@ -404,6 +405,7 @@ func (a *App) fileViewWithReactions(box services.Box, file services.File, reacti
|
|||||||
ID: file.ID,
|
ID: file.ID,
|
||||||
Name: file.Name,
|
Name: file.Name,
|
||||||
Size: helpers.FormatBytes(file.Size),
|
Size: helpers.FormatBytes(file.Size),
|
||||||
|
SizeBytes: file.Size,
|
||||||
ContentType: file.ContentType,
|
ContentType: file.ContentType,
|
||||||
PreviewKind: file.PreviewKind,
|
PreviewKind: file.PreviewKind,
|
||||||
URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
|
URL: fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
|
||||||
|
|||||||
@@ -151,6 +151,34 @@ func TestSocialPreviewBotGetsRawFilePreview(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilePreviewPageIncludesPreviewMetadata(t *testing.T) {
|
||||||
|
app, cleanup := newTestApp(t)
|
||||||
|
defer cleanup()
|
||||||
|
payload := uploadThroughApp(t, app)
|
||||||
|
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/d/"+payload.BoxID+"/f/"+payload.Files[0].ID, nil)
|
||||||
|
request.SetPathValue("boxID", payload.BoxID)
|
||||||
|
request.SetPathValue("fileID", payload.Files[0].ID)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
app.DownloadFile(response, request)
|
||||||
|
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
|
||||||
|
}
|
||||||
|
body := response.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`data-size-bytes="5"`,
|
||||||
|
`data-source-url="/d/` + payload.BoxID,
|
||||||
|
`data-download-url="/d/` + payload.BoxID,
|
||||||
|
`data-icon-url="/static/file-icons/`,
|
||||||
|
`data-preview-tabs`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("preview page missing %q: %s", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResumableUploadFlowCreatesNormalBox(t *testing.T) {
|
func TestResumableUploadFlowCreatesNormalBox(t *testing.T) {
|
||||||
app, cleanup := newTestApp(t)
|
app, cleanup := newTestApp(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
|||||||
header.Set("X-Frame-Options", "DENY")
|
header.Set("X-Frame-Options", "DENY")
|
||||||
header.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
header.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
header.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
header.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
||||||
header.Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; style-src 'self'; script-src 'self'; base-uri 'self'; frame-ancestors 'none'")
|
header.Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; frame-src 'self' about:; base-uri 'self'; frame-ancestors 'none'")
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,374 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-view {
|
||||||
|
width: min(72rem, calc(100% - 2rem));
|
||||||
|
min-height: auto;
|
||||||
|
padding-block: clamp(2rem, 7vh, 4.5rem);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card .card-content {
|
||||||
|
padding: clamp(1rem, 2.4vw, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title-group {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header .file-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.35rem, 2.4vw, 2rem);
|
||||||
|
line-height: 1.12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header .download-subtitle {
|
||||||
|
margin: 0.45rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 78%, var(--primary));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, color-mix(in srgb, var(--card) 94%, transparent), color-mix(in srgb, var(--background) 92%, transparent));
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-titlebar {
|
||||||
|
min-height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.72rem 0.9rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: color-mix(in srgb, var(--muted) 62%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-titlebar > div:first-child {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-titlebar strong {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-titlebar span {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-tools {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-fullscreen-button {
|
||||||
|
appearance: none;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 82%, var(--primary));
|
||||||
|
border-radius: calc(var(--radius) - 0.35rem);
|
||||||
|
background: color-mix(in srgb, var(--muted) 74%, transparent);
|
||||||
|
color: var(--foreground);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-fullscreen-button:hover {
|
||||||
|
background: color-mix(in srgb, var(--primary) 18%, var(--muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-fullscreen-button[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window-actions span {
|
||||||
|
width: 0.72rem;
|
||||||
|
height: 0.72rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 75%, var(--foreground));
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: color-mix(in srgb, var(--card) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tabs[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tab {
|
||||||
|
appearance: none;
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: calc(var(--radius) - 0.35rem);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tab:hover,
|
||||||
|
.preview-tab.is-active {
|
||||||
|
border-color: color-mix(in srgb, var(--border) 82%, var(--primary));
|
||||||
|
background: color-mix(in srgb, var(--muted) 78%, transparent);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-stage {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: clamp(18rem, 64vh, 38rem);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background:
|
||||||
|
linear-gradient(45deg, color-mix(in srgb, var(--muted) 18%, transparent) 25%, transparent 25%),
|
||||||
|
linear-gradient(-45deg, color-mix(in srgb, var(--muted) 18%, transparent) 25%, transparent 25%),
|
||||||
|
color-mix(in srgb, var(--background) 88%, #000);
|
||||||
|
background-position: 0 0, 0.5rem 0.5rem;
|
||||||
|
background-size: 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-stage > * {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-stage > img,
|
||||||
|
.preview-stage > video {
|
||||||
|
max-height: clamp(18rem, 64vh, 38rem);
|
||||||
|
width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-stage > audio {
|
||||||
|
width: min(42rem, calc(100% - 2rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-preview,
|
||||||
|
.large-preview-gate {
|
||||||
|
width: min(26rem, calc(100% - 2rem));
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-preview img {
|
||||||
|
width: 5.5rem;
|
||||||
|
height: 5.5rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-preview div {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-preview strong {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 1rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-preview span {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-preview-gate {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border) 82%, var(--danger));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, var(--card) 92%, #000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-preview-gate strong {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-preview-gate p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-preview-gate div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: clamp(18rem, 64vh, 38rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-audio-preview {
|
||||||
|
align-self: center;
|
||||||
|
width: min(42rem, calc(100% - 2rem));
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder[hidden],
|
||||||
|
.default-preview[hidden],
|
||||||
|
.native-preview[hidden],
|
||||||
|
.large-preview-gate[hidden],
|
||||||
|
.code-preview[hidden],
|
||||||
|
.render-preview[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder img {
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
object-fit: contain;
|
||||||
|
opacity: 0.78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: clamp(18rem, 64vh, 38rem);
|
||||||
|
overflow: auto;
|
||||||
|
background: #1b1724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview pre[class*="language-"] {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
overflow: visible;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview pre {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: visible;
|
||||||
|
color: #f5f3ff;
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview pre[class*="language-"] > code {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview code[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-preview .token.punctuation {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.render-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: clamp(18rem, 64vh, 38rem);
|
||||||
|
border: 0;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window:fullscreen,
|
||||||
|
.preview-window.is-render-fullscreen {
|
||||||
|
width: 100dvw;
|
||||||
|
height: 100dvh;
|
||||||
|
max-width: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window.is-render-fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window:fullscreen .preview-stage,
|
||||||
|
.preview-window.is-render-fullscreen .preview-stage {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
place-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-window:fullscreen .render-preview,
|
||||||
|
.preview-window.is-render-fullscreen .render-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.file-emblem {
|
.file-emblem {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
@@ -801,23 +1169,36 @@ html.reaction-picker-open body {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-stage {
|
@media (max-width: 720px) {
|
||||||
overflow: hidden;
|
.preview-view {
|
||||||
margin-bottom: 1rem;
|
width: min(100%, calc(100% - 1rem));
|
||||||
border: 1px solid var(--border);
|
padding-block: 1rem;
|
||||||
border-radius: var(--radius);
|
}
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-stage img,
|
.preview-header {
|
||||||
.preview-stage video {
|
flex-direction: column;
|
||||||
width: 100%;
|
align-items: stretch;
|
||||||
max-height: 55vh;
|
}
|
||||||
display: block;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-stage audio {
|
.preview-header .button {
|
||||||
width: calc(100% - 2rem);
|
justify-content: center;
|
||||||
margin: 1rem;
|
}
|
||||||
|
|
||||||
|
.preview-window-titlebar > div:first-child {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-stage,
|
||||||
|
.code-preview,
|
||||||
|
.render-preview,
|
||||||
|
.native-preview {
|
||||||
|
min-height: 18rem;
|
||||||
|
height: min(60vh, 32rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-stage > img,
|
||||||
|
.preview-stage > video {
|
||||||
|
max-height: min(60vh, 32rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
299
backend/static/css/80-markdown-preview.css
Normal file
299
backend/static/css/80-markdown-preview.css
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--md-bg: #0b0b16;
|
||||||
|
--md-fg: #f5f3ff;
|
||||||
|
--md-muted: #aaa4d6;
|
||||||
|
--md-panel: #17142d;
|
||||||
|
--md-panel-2: #211b3e;
|
||||||
|
--md-border: rgba(168, 150, 255, 0.24);
|
||||||
|
--md-link: #67e8f9;
|
||||||
|
--md-accent: #a78bfa;
|
||||||
|
--md-code-bg: #1b1724;
|
||||||
|
--md-block-code-bg: #0f111a;
|
||||||
|
--md-block-code-fg: #f8fafc;
|
||||||
|
--md-block-code-border: rgba(248, 250, 252, 0.16);
|
||||||
|
--md-shadow: rgba(0, 0, 0, 0.28);
|
||||||
|
--md-font: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
--md-mono: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="classic"] {
|
||||||
|
--md-bg: #09090b;
|
||||||
|
--md-fg: #fafafa;
|
||||||
|
--md-muted: #a1a1aa;
|
||||||
|
--md-panel: #18181b;
|
||||||
|
--md-panel-2: #27272a;
|
||||||
|
--md-border: rgba(255, 255, 255, 0.13);
|
||||||
|
--md-link: #e4e4e7;
|
||||||
|
--md-accent: #d4d4d8;
|
||||||
|
--md-code-bg: #111113;
|
||||||
|
--md-block-code-bg: #09090b;
|
||||||
|
--md-block-code-fg: #fafafa;
|
||||||
|
--md-block-code-border: rgba(250, 250, 250, 0.15);
|
||||||
|
--md-shadow: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] {
|
||||||
|
color-scheme: light;
|
||||||
|
--md-bg: #c0c0c0;
|
||||||
|
--md-fg: #000000;
|
||||||
|
--md-muted: #404040;
|
||||||
|
--md-panel: #ffffff;
|
||||||
|
--md-panel-2: #dfdfdf;
|
||||||
|
--md-border: #000000;
|
||||||
|
--md-link: #000078;
|
||||||
|
--md-accent: #000078;
|
||||||
|
--md-code-bg: #ffffff;
|
||||||
|
--md-block-code-bg: #000000;
|
||||||
|
--md-block-code-fg: #f5f5f5;
|
||||||
|
--md-block-code-border: #808080;
|
||||||
|
--md-shadow: transparent;
|
||||||
|
--md-font: "PixeloidSans", "PixelOperator", "Microsoft Sans Serif", Tahoma, sans-serif;
|
||||||
|
--md-mono: "PixelOperatorMono", Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="gruvbox"] {
|
||||||
|
--md-bg: #1d2021;
|
||||||
|
--md-fg: #ebdbb2;
|
||||||
|
--md-muted: #bdae93;
|
||||||
|
--md-panel: #282828;
|
||||||
|
--md-panel-2: #32302f;
|
||||||
|
--md-border: rgba(235, 219, 178, 0.2);
|
||||||
|
--md-link: #fabd2f;
|
||||||
|
--md-accent: #d79921;
|
||||||
|
--md-code-bg: #1b1d1e;
|
||||||
|
--md-block-code-bg: #161819;
|
||||||
|
--md-block-code-fg: #fbf1c7;
|
||||||
|
--md-block-code-border: rgba(251, 241, 199, 0.18);
|
||||||
|
--md-shadow: rgba(0, 0, 0, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="cyberpunk"] {
|
||||||
|
--md-bg: #08070d;
|
||||||
|
--md-fg: #fff36f;
|
||||||
|
--md-muted: #9bfaff;
|
||||||
|
--md-panel: #16131f;
|
||||||
|
--md-panel-2: #251d34;
|
||||||
|
--md-border: rgba(255, 242, 0, 0.34);
|
||||||
|
--md-link: #00f0ff;
|
||||||
|
--md-accent: #ff2a6d;
|
||||||
|
--md-code-bg: #100d18;
|
||||||
|
--md-block-code-bg: #07060b;
|
||||||
|
--md-block-code-fg: #f8fafc;
|
||||||
|
--md-block-code-border: rgba(0, 240, 255, 0.26);
|
||||||
|
--md-shadow: rgba(255, 42, 109, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PixeloidSans";
|
||||||
|
src: url("/static/fonts/pixeloid_sans/PixeloidSans.ttf") format("truetype");
|
||||||
|
font-weight: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PixeloidSans";
|
||||||
|
src: url("/static/fonts/pixeloid_sans/PixeloidSans-Bold.ttf") format("truetype");
|
||||||
|
font-weight: bold;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "PixelOperatorMono";
|
||||||
|
src: url("/static/fonts/pixel_operator/PixelOperatorMono.ttf") format("truetype");
|
||||||
|
font-weight: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 18% -10%, color-mix(in srgb, var(--md-accent) 18%, transparent), transparent 24rem),
|
||||||
|
var(--md-bg);
|
||||||
|
color: var(--md-fg);
|
||||||
|
font-family: var(--md-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="retro"] {
|
||||||
|
background-color: #000000;
|
||||||
|
background-image: url("/static/backgrounds/stars1.gif");
|
||||||
|
background-repeat: repeat;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="cyberpunk"] {
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(255, 242, 0, 0.035) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px),
|
||||||
|
var(--md-bg);
|
||||||
|
background-size: 100% 3px, 3rem 100%, auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: clamp(1rem, 4vw, 2.25rem);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 54rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(1rem, 3vw, 2rem);
|
||||||
|
border: 1px solid var(--md-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in srgb, var(--md-panel) 90%, transparent);
|
||||||
|
box-shadow: 0 20px 60px var(--md-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="retro"] main {
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--md-panel);
|
||||||
|
box-shadow:
|
||||||
|
inset -1px -1px 0 #404040,
|
||||||
|
inset 1px 1px 0 #ffffff,
|
||||||
|
inset -2px -2px 0 #808080,
|
||||||
|
inset 2px 2px 0 #dfdfdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="cyberpunk"] main {
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 4px 4px 0 rgba(255, 42, 109, 0.5), 0 0 24px rgba(0, 240, 255, 0.12);
|
||||||
|
clip-path: polygon(0 0, calc(100% - 0.9rem) 0, 100% 0.9rem, 100% 100%, 0.9rem 100%, 0 calc(100% - 0.9rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 1.4em 0 0.55em;
|
||||||
|
color: var(--md-fg);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:first-child,
|
||||||
|
h2:first-child,
|
||||||
|
h3:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(1.75rem, 5vw, 2.45rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
padding-bottom: 0.35rem;
|
||||||
|
border-bottom: 1px solid var(--md-border);
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
ul,
|
||||||
|
ol,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
table {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--md-link);
|
||||||
|
text-underline-offset: 0.18em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--md-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="retro"] img,
|
||||||
|
html[data-theme="retro"] video {
|
||||||
|
border-radius: 0;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 1px;
|
||||||
|
border: 0;
|
||||||
|
background: var(--md-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-left: 4px solid var(--md-accent);
|
||||||
|
background: color-mix(in srgb, var(--md-panel-2) 58%, transparent);
|
||||||
|
color: var(--md-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--md-block-code-border) !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--md-block-code-bg) !important;
|
||||||
|
color: var(--md-block-code-fg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--md-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code,
|
||||||
|
pre > code,
|
||||||
|
pre code[class*="language-"] {
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code {
|
||||||
|
padding: 0.12rem 0.28rem;
|
||||||
|
border: 1px solid var(--md-border);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background: color-mix(in srgb, var(--md-code-bg) 82%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="retro"] pre,
|
||||||
|
html[data-theme="retro"] :not(pre) > code {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border: 1px solid var(--md-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: color-mix(in srgb, var(--md-panel-2) 70%, transparent);
|
||||||
|
color: var(--md-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) td {
|
||||||
|
background: color-mix(in srgb, var(--md-panel-2) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: var(--md-accent);
|
||||||
|
color: var(--md-bg);
|
||||||
|
}
|
||||||
717
backend/static/js/45-preview.js
Normal file
717
backend/static/js/45-preview.js
Normal file
@@ -0,0 +1,717 @@
|
|||||||
|
(function () {
|
||||||
|
var preview = document.querySelector("[data-source-url][data-file-name][data-content-type]");
|
||||||
|
if (!preview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var SMALL_TEXT_BYTES = 50 * 1024;
|
||||||
|
var LARGE_PREVIEW_BYTES = 500 * 1024;
|
||||||
|
|
||||||
|
var state = {
|
||||||
|
fileName: preview.dataset.fileName || "",
|
||||||
|
contentType: (preview.dataset.contentType || "").toLowerCase(),
|
||||||
|
previewKind: preview.dataset.previewKind || "",
|
||||||
|
sizeBytes: Number(preview.dataset.sizeBytes || 0),
|
||||||
|
sizeLabel: preview.dataset.fileSize || "",
|
||||||
|
sourceURL: preview.dataset.sourceUrl || "",
|
||||||
|
downloadURL: preview.dataset.downloadUrl || "",
|
||||||
|
iconURL: preview.dataset.iconUrl || "",
|
||||||
|
activeMode: "",
|
||||||
|
defaultMode: "default",
|
||||||
|
pendingMode: "",
|
||||||
|
textSource: "",
|
||||||
|
textLoaded: false,
|
||||||
|
rawLoaded: false,
|
||||||
|
prismLoaded: false,
|
||||||
|
renderLoaded: false,
|
||||||
|
renderFullscreenFallback: false,
|
||||||
|
confirmedLargeModes: {},
|
||||||
|
tabs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
var els = {
|
||||||
|
tabs: preview.querySelector("[data-preview-tabs]"),
|
||||||
|
modeLabel: preview.querySelector("[data-preview-mode-label]"),
|
||||||
|
defaultPane: preview.querySelector("[data-default-preview]"),
|
||||||
|
imagePane: preview.querySelector("[data-image-preview]"),
|
||||||
|
videoPane: preview.querySelector("[data-video-preview]"),
|
||||||
|
browserAudioPane: preview.querySelector("[data-browser-audio-preview]"),
|
||||||
|
rawPane: preview.querySelector("[data-raw-preview]"),
|
||||||
|
rawOutput: preview.querySelector("[data-raw-output]"),
|
||||||
|
codePane: preview.querySelector("[data-code-preview]"),
|
||||||
|
codeOutput: preview.querySelector("[data-code-output]"),
|
||||||
|
renderPane: preview.querySelector("[data-render-preview]"),
|
||||||
|
fullscreenButton: preview.querySelector("[data-render-fullscreen]"),
|
||||||
|
gatePane: preview.querySelector("[data-large-preview-gate]"),
|
||||||
|
gateConfirm: preview.querySelector("[data-large-preview-confirm]"),
|
||||||
|
gateCancel: preview.querySelector("[data-large-preview-cancel]"),
|
||||||
|
placeholder: preview.querySelector("[data-preview-placeholder]")
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileType = detectFileType();
|
||||||
|
state.tabs = buildTabs(fileType);
|
||||||
|
state.defaultMode = chooseDefaultMode(fileType, state.tabs);
|
||||||
|
|
||||||
|
bindLargeGate();
|
||||||
|
bindThemeChanges();
|
||||||
|
bindRenderFullscreen();
|
||||||
|
renderTabs();
|
||||||
|
selectMode(state.defaultMode);
|
||||||
|
|
||||||
|
function detectFileType() {
|
||||||
|
var extension = extensionFor(state.fileName);
|
||||||
|
var baseType = state.contentType.split(";")[0].trim();
|
||||||
|
var language = languageFor(extension, baseType);
|
||||||
|
var isImage = state.previewKind === "image" || baseType.indexOf("image/") === 0 && baseType !== "image/svg+xml";
|
||||||
|
var isVideo = state.previewKind === "video" || baseType.indexOf("video/") === 0;
|
||||||
|
var isAudio = state.previewKind === "audio" || baseType.indexOf("audio/") === 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
extension: extension,
|
||||||
|
baseType: baseType,
|
||||||
|
language: language,
|
||||||
|
isTextLike: Boolean(language),
|
||||||
|
isHTML: language === "html",
|
||||||
|
isMarkdown: language === "markdown",
|
||||||
|
isImage: isImage,
|
||||||
|
isVideo: isVideo,
|
||||||
|
isAudio: isAudio,
|
||||||
|
isMobile: window.matchMedia && window.matchMedia("(max-width: 720px), (pointer: coarse)").matches
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTabs(type) {
|
||||||
|
var tabs = [{ mode: "default", label: "Default" }];
|
||||||
|
|
||||||
|
if (type.isImage) {
|
||||||
|
tabs.push({ mode: "image", label: "Image Preview" });
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isVideo) {
|
||||||
|
tabs.push({ mode: "video", label: "Video Preview" });
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isAudio) {
|
||||||
|
tabs.push({ mode: "browser-audio", label: "Browser Preview" });
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isTextLike) {
|
||||||
|
if (type.isHTML || type.isMarkdown) {
|
||||||
|
tabs.push({ mode: "render", label: "Render Preview" });
|
||||||
|
}
|
||||||
|
tabs.push({ mode: "raw", label: "Raw Preview" });
|
||||||
|
tabs.push({ mode: "code", label: "Code Preview" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseDefaultMode(type, tabs) {
|
||||||
|
if (state.sizeBytes > LARGE_PREVIEW_BYTES) {
|
||||||
|
if (type.isAudio && hasMode(tabs, "browser-audio")) {
|
||||||
|
return "browser-audio";
|
||||||
|
}
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.isImage) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (type.isVideo) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
if (type.isAudio) {
|
||||||
|
return "browser-audio";
|
||||||
|
}
|
||||||
|
if (type.isTextLike && state.sizeBytes > SMALL_TEXT_BYTES) {
|
||||||
|
return "raw";
|
||||||
|
}
|
||||||
|
if (type.isMarkdown) {
|
||||||
|
return "render";
|
||||||
|
}
|
||||||
|
if (type.isTextLike) {
|
||||||
|
return "code";
|
||||||
|
}
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTabs() {
|
||||||
|
if (!els.tabs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
els.tabs.innerHTML = "";
|
||||||
|
state.tabs.forEach(function (tab) {
|
||||||
|
var button = document.createElement("button");
|
||||||
|
button.className = "preview-tab";
|
||||||
|
button.type = "button";
|
||||||
|
button.dataset.previewTab = tab.mode;
|
||||||
|
button.textContent = tab.label;
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
selectMode(tab.mode);
|
||||||
|
});
|
||||||
|
els.tabs.appendChild(button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMode(mode) {
|
||||||
|
if (!hasMode(state.tabs, mode)) {
|
||||||
|
mode = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode !== "render") {
|
||||||
|
exitRenderFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresLargeConfirmation(mode)) {
|
||||||
|
showLargeGate(mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.activeMode = mode;
|
||||||
|
updateTabs(mode);
|
||||||
|
updateRenderFullscreenButton();
|
||||||
|
hideAllPanes();
|
||||||
|
setModeLabel(labelForMode(mode));
|
||||||
|
|
||||||
|
if (mode === "default") {
|
||||||
|
show(els.defaultPane);
|
||||||
|
} else if (mode === "image") {
|
||||||
|
show(els.imagePane);
|
||||||
|
} else if (mode === "video") {
|
||||||
|
show(els.videoPane);
|
||||||
|
} else if (mode === "browser-audio") {
|
||||||
|
show(els.browserAudioPane);
|
||||||
|
} else if (mode === "raw") {
|
||||||
|
show(els.rawPane);
|
||||||
|
ensureRawPreview();
|
||||||
|
} else if (mode === "code") {
|
||||||
|
show(els.codePane);
|
||||||
|
ensurePrismPreview();
|
||||||
|
} else if (mode === "render") {
|
||||||
|
show(els.renderPane);
|
||||||
|
if (fileType.isMarkdown) {
|
||||||
|
ensureMarkdownRenderPreview();
|
||||||
|
} else {
|
||||||
|
ensureHTMLRenderPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiresLargeConfirmation(mode) {
|
||||||
|
if (state.sizeBytes <= LARGE_PREVIEW_BYTES || state.confirmedLargeModes[mode]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mode === "raw" || mode === "code" || mode === "render";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLargeGate(mode) {
|
||||||
|
state.pendingMode = mode;
|
||||||
|
updateTabs(state.activeMode || state.defaultMode);
|
||||||
|
updateRenderFullscreenButton(false);
|
||||||
|
hideAllPanes();
|
||||||
|
show(els.gatePane);
|
||||||
|
setModeLabel("Large preview");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindLargeGate() {
|
||||||
|
if (els.gateConfirm) {
|
||||||
|
els.gateConfirm.addEventListener("click", function () {
|
||||||
|
if (!state.pendingMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.confirmedLargeModes[state.pendingMode] = true;
|
||||||
|
selectMode(state.pendingMode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (els.gateCancel) {
|
||||||
|
els.gateCancel.addEventListener("click", function () {
|
||||||
|
state.pendingMode = "";
|
||||||
|
selectMode(state.activeMode || state.defaultMode || "default");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindThemeChanges() {
|
||||||
|
var themeSelect = document.querySelector("[data-theme-select]");
|
||||||
|
if (!themeSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
themeSelect.addEventListener("change", function () {
|
||||||
|
window.setTimeout(function () {
|
||||||
|
if (!fileType.isMarkdown || !state.renderLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.renderLoaded = false;
|
||||||
|
if (state.activeMode === "render") {
|
||||||
|
ensureMarkdownRenderPreview();
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindRenderFullscreen() {
|
||||||
|
if (!els.fullscreenButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
els.fullscreenButton.addEventListener("click", function () {
|
||||||
|
if (isRenderFullscreen()) {
|
||||||
|
exitRenderFullscreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enterRenderFullscreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("fullscreenchange", updateRenderFullscreenButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTextLoaded() {
|
||||||
|
if (state.textLoaded) {
|
||||||
|
return Promise.resolve(state.textSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading("Loading preview...");
|
||||||
|
return fetch(state.sourceURL, { credentials: "same-origin" })
|
||||||
|
.then(function (response) {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Preview failed");
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(function (source) {
|
||||||
|
state.textSource = source;
|
||||||
|
state.textLoaded = true;
|
||||||
|
hide(els.placeholder);
|
||||||
|
return source;
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
showError("Preview unavailable");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRawPreview() {
|
||||||
|
if (state.rawLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureTextLoaded().then(function (source) {
|
||||||
|
els.rawOutput.textContent = source;
|
||||||
|
state.rawLoaded = true;
|
||||||
|
if (state.activeMode === "raw") {
|
||||||
|
hide(els.placeholder);
|
||||||
|
show(els.rawPane);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePrismPreview() {
|
||||||
|
if (state.prismLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading("Loading syntax preview...");
|
||||||
|
Promise.all([ensureTextLoaded(), loadPrism()])
|
||||||
|
.then(function (results) {
|
||||||
|
var source = results[0];
|
||||||
|
var language = fileType.language;
|
||||||
|
if (language === "json") {
|
||||||
|
source = formatJSON(source);
|
||||||
|
}
|
||||||
|
els.codeOutput.className = "language-" + language;
|
||||||
|
els.codeOutput.textContent = source;
|
||||||
|
if (window.Prism) {
|
||||||
|
window.Prism.highlightElement(els.codeOutput);
|
||||||
|
}
|
||||||
|
state.prismLoaded = true;
|
||||||
|
if (state.activeMode === "code") {
|
||||||
|
hide(els.placeholder);
|
||||||
|
show(els.codePane);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
showError("Syntax preview unavailable");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureHTMLRenderPreview() {
|
||||||
|
if (state.renderLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading("Rendering preview...");
|
||||||
|
ensureTextLoaded()
|
||||||
|
.then(function (source) {
|
||||||
|
els.renderPane.srcdoc = withBaseElement(source);
|
||||||
|
state.renderLoaded = true;
|
||||||
|
if (state.activeMode === "render") {
|
||||||
|
hide(els.placeholder);
|
||||||
|
show(els.renderPane);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
showError("Render preview unavailable");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureMarkdownRenderPreview() {
|
||||||
|
if (state.renderLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading("Rendering Markdown...");
|
||||||
|
Promise.all([ensureTextLoaded(), loadMarkdownLibs()])
|
||||||
|
.then(function (results) {
|
||||||
|
var markdown = results[0];
|
||||||
|
var html = parseMarkdown(markdown);
|
||||||
|
var clean = window.DOMPurify.sanitize(html);
|
||||||
|
els.renderPane.srcdoc = markdownDocument(clean);
|
||||||
|
state.renderLoaded = true;
|
||||||
|
if (state.activeMode === "render") {
|
||||||
|
hide(els.placeholder);
|
||||||
|
show(els.renderPane);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
showError("Markdown preview unavailable");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading(message) {
|
||||||
|
if (!els.placeholder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var text = els.placeholder.querySelector("p");
|
||||||
|
if (text) {
|
||||||
|
text.textContent = message;
|
||||||
|
}
|
||||||
|
show(els.placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
hideAllPanes();
|
||||||
|
var text = els.placeholder && els.placeholder.querySelector("p");
|
||||||
|
if (text) {
|
||||||
|
text.textContent = message;
|
||||||
|
}
|
||||||
|
show(els.placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAllPanes() {
|
||||||
|
hide(els.defaultPane);
|
||||||
|
hide(els.imagePane);
|
||||||
|
hide(els.videoPane);
|
||||||
|
hide(els.browserAudioPane);
|
||||||
|
hide(els.rawPane);
|
||||||
|
hide(els.codePane);
|
||||||
|
hide(els.renderPane);
|
||||||
|
hide(els.gatePane);
|
||||||
|
hide(els.placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterRenderFullscreen() {
|
||||||
|
if (state.activeMode !== "render") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview.requestFullscreen) {
|
||||||
|
var request = preview.requestFullscreen();
|
||||||
|
if (request && typeof request.catch === "function") {
|
||||||
|
request.catch(function () {
|
||||||
|
state.renderFullscreenFallback = true;
|
||||||
|
preview.classList.add("is-render-fullscreen");
|
||||||
|
updateRenderFullscreenButton();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.renderFullscreenFallback = true;
|
||||||
|
preview.classList.add("is-render-fullscreen");
|
||||||
|
updateRenderFullscreenButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitRenderFullscreen() {
|
||||||
|
if (document.fullscreenElement === preview && document.exitFullscreen) {
|
||||||
|
var exit = document.exitFullscreen();
|
||||||
|
if (exit && typeof exit.catch === "function") {
|
||||||
|
exit.catch(function () {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.renderFullscreenFallback = false;
|
||||||
|
preview.classList.remove("is-render-fullscreen");
|
||||||
|
updateRenderFullscreenButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRenderFullscreen() {
|
||||||
|
return document.fullscreenElement === preview || state.renderFullscreenFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRenderFullscreenButton(forceVisible) {
|
||||||
|
if (!els.fullscreenButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var visible = typeof forceVisible === "boolean" ? forceVisible : state.activeMode === "render";
|
||||||
|
els.fullscreenButton.hidden = !visible;
|
||||||
|
els.fullscreenButton.textContent = isRenderFullscreen() ? "Exit Full Screen" : "Full Screen";
|
||||||
|
els.fullscreenButton.setAttribute("aria-pressed", isRenderFullscreen() ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTabs(mode) {
|
||||||
|
if (!els.tabs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Array.prototype.forEach.call(els.tabs.querySelectorAll("[data-preview-tab]"), function (button) {
|
||||||
|
button.classList.toggle("is-active", button.dataset.previewTab === mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(element) {
|
||||||
|
if (element) {
|
||||||
|
element.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(element) {
|
||||||
|
if (element) {
|
||||||
|
element.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setModeLabel(label) {
|
||||||
|
if (els.modeLabel) {
|
||||||
|
els.modeLabel.textContent = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMode(tabs, mode) {
|
||||||
|
return tabs.some(function (tab) {
|
||||||
|
return tab.mode === mode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelForMode(mode) {
|
||||||
|
var labels = {
|
||||||
|
"default": "Default",
|
||||||
|
"image": "Image preview",
|
||||||
|
"video": "Video preview",
|
||||||
|
"browser-audio": "Browser preview",
|
||||||
|
"raw": "Raw preview",
|
||||||
|
"code": "Code preview",
|
||||||
|
"render": "Render preview"
|
||||||
|
};
|
||||||
|
return labels[mode] || "Preview";
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPrism() {
|
||||||
|
if (window.Prism) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Prism = window.Prism || {};
|
||||||
|
window.Prism.manual = true;
|
||||||
|
return Promise.all([
|
||||||
|
loadStyle("/static/lib/prismjs/prism.css"),
|
||||||
|
loadScript("/static/lib/prismjs/prism.js")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMarkdownLibs() {
|
||||||
|
if (window.marked && window.DOMPurify) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
loadScript("/static/lib/markdown/marked.umd.js"),
|
||||||
|
loadScript("/static/lib/markdown/purify.min.js")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadScript(src) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var existing = document.querySelector('script[src="' + src + '"]');
|
||||||
|
if (existing) {
|
||||||
|
if (existing.dataset.loaded === "true") {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
existing.addEventListener("load", resolve, { once: true });
|
||||||
|
existing.addEventListener("error", reject, { once: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.async = true;
|
||||||
|
script.src = src;
|
||||||
|
script.addEventListener("load", function () {
|
||||||
|
script.dataset.loaded = "true";
|
||||||
|
resolve();
|
||||||
|
}, { once: true });
|
||||||
|
script.addEventListener("error", reject, { once: true });
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStyle(href) {
|
||||||
|
if (document.querySelector('link[href="' + href + '"]')) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = href;
|
||||||
|
link.addEventListener("load", resolve, { once: true });
|
||||||
|
link.addEventListener("error", reject, { once: true });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdown(source) {
|
||||||
|
if (window.marked && typeof window.marked.parse === "function") {
|
||||||
|
return window.marked.parse(source);
|
||||||
|
}
|
||||||
|
if (window.marked && window.marked.marked && typeof window.marked.marked.parse === "function") {
|
||||||
|
return window.marked.marked.parse(source);
|
||||||
|
}
|
||||||
|
throw new Error("Marked unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownDocument(body) {
|
||||||
|
var theme = currentTheme();
|
||||||
|
var base = '<base href="' + escapeAttribute(new URL(state.sourceURL, window.location.href).href) + '">';
|
||||||
|
return '<!doctype html><html data-theme="' + escapeAttribute(theme) + '"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">' +
|
||||||
|
base +
|
||||||
|
'<link rel="stylesheet" href="/static/css/80-markdown-preview.css">' +
|
||||||
|
'<style>' + markdownThemeStyle(theme) + '</style>' +
|
||||||
|
'</head><body><main>' + body + '</main></body></html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownThemeStyle(theme) {
|
||||||
|
var themes = {
|
||||||
|
revamp: ["dark", "#0b0b16", "#f5f3ff", "#aaa4d6", "#17142d", "#211b3e", "rgba(168,150,255,0.24)", "#67e8f9", "#a78bfa", "#1b1724", "#0f111a", "#f8fafc", "rgba(248,250,252,0.16)", "rgba(0,0,0,0.28)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"],
|
||||||
|
classic: ["dark", "#09090b", "#fafafa", "#a1a1aa", "#18181b", "#27272a", "rgba(255,255,255,0.13)", "#e4e4e7", "#d4d4d8", "#111113", "#09090b", "#fafafa", "rgba(250,250,250,0.15)", "rgba(0,0,0,0.3)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"],
|
||||||
|
retro: ["light", "#c0c0c0", "#000000", "#404040", "#ffffff", "#dfdfdf", "#000000", "#000078", "#000078", "#ffffff", "#000000", "#f5f5f5", "#808080", "transparent", "\"PixeloidSans\",\"PixelOperator\",\"Microsoft Sans Serif\",Tahoma,sans-serif", "\"PixelOperatorMono\",Consolas,monospace"],
|
||||||
|
gruvbox: ["dark", "#1d2021", "#ebdbb2", "#bdae93", "#282828", "#32302f", "rgba(235,219,178,0.2)", "#fabd2f", "#d79921", "#1b1d1e", "#161819", "#fbf1c7", "rgba(251,241,199,0.18)", "rgba(0,0,0,0.26)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"],
|
||||||
|
cyberpunk: ["dark", "#08070d", "#fff36f", "#9bfaff", "#16131f", "#251d34", "rgba(255,242,0,0.34)", "#00f0ff", "#ff2a6d", "#100d18", "#07060b", "#f8fafc", "rgba(0,240,255,0.26)", "rgba(255,42,109,0.14)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"]
|
||||||
|
};
|
||||||
|
var t = themes[theme] || themes.revamp;
|
||||||
|
var vars = "color-scheme:" + t[0] + ";--md-bg:" + t[1] + ";--md-fg:" + t[2] + ";--md-muted:" + t[3] + ";--md-panel:" + t[4] + ";--md-panel-2:" + t[5] + ";--md-border:" + t[6] + ";--md-link:" + t[7] + ";--md-accent:" + t[8] + ";--md-code-bg:" + t[9] + ";--md-block-code-bg:" + t[10] + ";--md-block-code-fg:" + t[11] + ";--md-block-code-border:" + t[12] + ";--md-shadow:" + t[13] + ";--md-font:" + t[14] + ";--md-mono:" + t[15] + ";";
|
||||||
|
return ":root{" + vars + "}*{box-sizing:border-box}html,body{min-height:100%;margin:0;background:var(--md-bg);color:var(--md-fg);font-family:var(--md-font)}body{padding:clamp(1rem,4vw,2.25rem);font-size:16px;line-height:1.65}main{max-width:54rem;margin:0 auto;padding:clamp(1rem,3vw,2rem);border:1px solid var(--md-border);border-radius:10px;background:var(--md-panel);box-shadow:0 20px 60px var(--md-shadow)}a{color:var(--md-link)}h1,h2,h3,h4,h5,h6{color:var(--md-fg);line-height:1.2}code,kbd,pre{font-family:var(--md-mono)}pre{overflow:auto;padding:1rem;border:1px solid var(--md-block-code-border)!important;background:var(--md-block-code-bg)!important;color:var(--md-block-code-fg)!important}code{background:var(--md-code-bg);border-radius:4px;padding:.12rem .3rem}pre code,pre>code,pre code[class*=\"language-\"]{padding:0!important;background:transparent!important;color:inherit!important;border:0!important}blockquote{margin:1rem 0;padding:.2rem 1rem;border-left:3px solid var(--md-accent);color:var(--md-muted);background:var(--md-panel-2)}table{width:100%;border-collapse:collapse;display:block;overflow:auto}th,td{border:1px solid var(--md-border);padding:.5rem .7rem}hr{border:0;border-top:1px solid var(--md-border)}img,video{max-width:100%;height:auto}";
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentTheme() {
|
||||||
|
var theme = document.documentElement.dataset.theme || "revamp";
|
||||||
|
return /^(revamp|classic|retro|gruvbox|cyberpunk)$/.test(theme) ? theme : "revamp";
|
||||||
|
}
|
||||||
|
|
||||||
|
function withBaseElement(source) {
|
||||||
|
var base = '<base href="' + escapeAttribute(new URL(state.sourceURL, window.location.href).href) + '">';
|
||||||
|
if (/<head[\s>]/i.test(source)) {
|
||||||
|
return source.replace(/<head([^>]*)>/i, "<head$1>" + base);
|
||||||
|
}
|
||||||
|
return base + source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttribute(value) {
|
||||||
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJSON(source) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(source), null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionFor(name) {
|
||||||
|
var parts = name.toLowerCase().split(".");
|
||||||
|
return parts.length > 1 ? parts.pop() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageFor(extension, baseType) {
|
||||||
|
var extensionMap = {
|
||||||
|
"c": "c",
|
||||||
|
"cc": "cpp",
|
||||||
|
"conf": "nginx",
|
||||||
|
"cpp": "cpp",
|
||||||
|
"cs": "csharp",
|
||||||
|
"css": "css",
|
||||||
|
"csv": "csv",
|
||||||
|
"diff": "diff",
|
||||||
|
"dockerfile": "docker",
|
||||||
|
"go": "go",
|
||||||
|
"h": "c",
|
||||||
|
"hpp": "cpp",
|
||||||
|
"htm": "html",
|
||||||
|
"html": "html",
|
||||||
|
"ini": "ini",
|
||||||
|
"java": "java",
|
||||||
|
"js": "javascript",
|
||||||
|
"json": "json",
|
||||||
|
"jsx": "jsx",
|
||||||
|
"kt": "kotlin",
|
||||||
|
"log": "log",
|
||||||
|
"lua": "lua",
|
||||||
|
"md": "markdown",
|
||||||
|
"mdown": "markdown",
|
||||||
|
"markdown": "markdown",
|
||||||
|
"php": "php",
|
||||||
|
"pl": "perl",
|
||||||
|
"properties": "properties",
|
||||||
|
"py": "python",
|
||||||
|
"rb": "ruby",
|
||||||
|
"rs": "rust",
|
||||||
|
"sh": "bash",
|
||||||
|
"sql": "sql",
|
||||||
|
"swift": "swift",
|
||||||
|
"toml": "toml",
|
||||||
|
"ts": "typescript",
|
||||||
|
"tsx": "tsx",
|
||||||
|
"txt": "text",
|
||||||
|
"xml": "xml",
|
||||||
|
"yaml": "yaml",
|
||||||
|
"yml": "yaml",
|
||||||
|
"zig": "zig"
|
||||||
|
};
|
||||||
|
var typeMap = {
|
||||||
|
"application/javascript": "javascript",
|
||||||
|
"application/json": "json",
|
||||||
|
"application/ld+json": "json",
|
||||||
|
"application/markdown": "markdown",
|
||||||
|
"application/xml": "xml",
|
||||||
|
"application/x-httpd-php": "php",
|
||||||
|
"application/x-sh": "bash",
|
||||||
|
"image/svg+xml": "xml",
|
||||||
|
"text/css": "css",
|
||||||
|
"text/csv": "csv",
|
||||||
|
"text/html": "html",
|
||||||
|
"text/javascript": "javascript",
|
||||||
|
"text/markdown": "markdown",
|
||||||
|
"text/plain": "text",
|
||||||
|
"text/x-go": "go",
|
||||||
|
"text/xml": "xml"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (extensionMap[extension]) {
|
||||||
|
return extensionMap[extension];
|
||||||
|
}
|
||||||
|
if (typeMap[baseType]) {
|
||||||
|
return typeMap[baseType];
|
||||||
|
}
|
||||||
|
if (baseType.indexOf("+json") !== -1) {
|
||||||
|
return "json";
|
||||||
|
}
|
||||||
|
if (baseType.indexOf("+xml") !== -1) {
|
||||||
|
return "xml";
|
||||||
|
}
|
||||||
|
if (baseType.indexOf("text/") === 0) {
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
})();
|
||||||
79
backend/static/lib/markdown/marked.umd.js
Normal file
79
backend/static/lib/markdown/marked.umd.js
Normal file
File diff suppressed because one or more lines are too long
3
backend/static/lib/markdown/purify.min.js
vendored
Normal file
3
backend/static/lib/markdown/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
backend/static/lib/prismjs/prism.css
Normal file
4
backend/static/lib/prismjs/prism.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* PrismJS 1.30.0
|
||||||
|
https://prismjs.com/download#themes=prism-dark&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+armasm+arturo+asciidoc+aspnet+asm6502+asmatmel+autohotkey+autoit+avisynth+avro-idl+awk+bash+basic+batch+bbcode+bbj+bicep+birb+bison+bnf+bqn+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cfscript+chaiscript+cil+cilkc+cilkcpp+clojure+cmake+cobol+coffeescript+concurnas+csp+cooklang+coq+crystal+css-extras+csv+cue+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+dot+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+false+firestore-security-rules+flow+fortran+ftl+gml+gap+gcode+gdscript+gedcom+gettext+gherkin+git+glsl+gn+linker-script+go+go-module+gradle+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+hoon+http+hpkp+hsts+ichigojam+icon+icu-message-format+idris+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jexl+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keepalived+keyman+kotlin+kumir+kusto+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+log+lolcode+lua+magma+makefile+markdown+markup-templating+mata+matlab+maxscript+mel+mermaid+metafont+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nevod+nginx+nim+nix+nsis+objectivec+ocaml+odin+opencl+openqasm+oz+parigp+parser+pascal+pascaligo+psl+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plant-uml+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+qsharp+q+qml+qore+r+racket+cshtml+jsx+tsx+reason+regex+rego+renpy+rescript+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+squirrel+stan+stata+iecst+stylus+supercollider+swift+systemd+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+tremor+turtle+twig+typescript+typoscript+unrealscript+uorazor+uri+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+web-idl+wgsl+wiki+wolfram+wren+xeora+xml-doc+xojo+xquery+yaml+yang+zig&plugins=line-numbers */
|
||||||
|
code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-shadow:0 -.1em .2em #000;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red}
|
||||||
|
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
|
||||||
301
backend/static/lib/prismjs/prism.js
Normal file
301
backend/static/lib/prismjs/prism.js
Normal file
File diff suppressed because one or more lines are too long
@@ -60,6 +60,7 @@
|
|||||||
<script defer src="/static/js/30-token-copy.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/30-token-copy.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/35-pagination.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/35-pagination.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/40-upload.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/40-upload.js?version={{.AppVersion}}"></script>
|
||||||
|
<script defer src="/static/js/45-preview.js?version={{.AppVersion}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="dark">
|
<body class="dark">
|
||||||
<a class="skip-link" href="#main">Skip to content</a>
|
<a class="skip-link" href="#main">Skip to content</a>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{{define "preview.html"}}{{template "base" .}}{{end}}
|
{{define "preview.html"}}{{template "base" .}}{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<section class="download-view" aria-labelledby="preview-title">
|
<section class="download-view preview-view" aria-labelledby="preview-title">
|
||||||
<div class="card download-card">
|
<div class="card download-card preview-card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{if .Data.Locked}}
|
{{if .Data.Locked}}
|
||||||
<div class="file-emblem" aria-hidden="true">
|
<div class="file-emblem" aria-hidden="true">
|
||||||
@@ -12,23 +12,65 @@
|
|||||||
<p class="download-subtitle">Unlock the box before viewing this file.</p>
|
<p class="download-subtitle">Unlock the box before viewing this file.</p>
|
||||||
<a class="button button-primary button-wide" href="/d/{{.Data.Box.ID}}">Unlock box</a>
|
<a class="button button-primary button-wide" href="/d/{{.Data.Box.ID}}">Unlock box</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="preview-stage">
|
<header class="preview-header">
|
||||||
{{if eq .Data.File.PreviewKind "image"}}
|
<div class="preview-title-group">
|
||||||
<img src="{{.Data.DownloadURL}}?inline=1" alt="{{.Data.File.Name}}">
|
<h1 id="preview-title" class="file-name" title="{{.Data.File.Name}}">{{.Data.File.Name}}</h1>
|
||||||
{{else if eq .Data.File.PreviewKind "video"}}
|
<p class="download-subtitle">{{.Data.File.Size}} · {{.Data.File.ContentType}}</p>
|
||||||
<video src="{{.Data.DownloadURL}}?inline=1" poster="{{.Data.File.ThumbnailURL}}" controls preload="metadata"></video>
|
</div>
|
||||||
{{else if eq .Data.File.PreviewKind "audio"}}
|
<a class="button button-primary" href="{{.Data.DownloadURL}}">
|
||||||
<audio src="{{.Data.DownloadURL}}?inline=1" controls preload="metadata"></audio>
|
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||||
{{else}}
|
Download
|
||||||
<img src="{{.Data.File.ThumbnailURL}}" alt="">
|
</a>
|
||||||
{{end}}
|
</header>
|
||||||
|
|
||||||
|
<div class="preview-window" data-preview-kind="{{.Data.File.PreviewKind}}" data-file-name="{{.Data.File.Name}}" data-content-type="{{.Data.File.ContentType}}" data-size-bytes="{{.Data.File.SizeBytes}}" data-source-url="{{.Data.DownloadURL}}?inline=1" data-download-url="{{.Data.DownloadURL}}" data-icon-url="{{.Data.File.IconURL}}" data-file-size="{{.Data.File.Size}}">
|
||||||
|
<div class="preview-window-titlebar">
|
||||||
|
<div>
|
||||||
|
<strong data-preview-mode-label>Preview</strong>
|
||||||
|
<span>{{.Data.File.ContentType}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-window-tools">
|
||||||
|
<button class="preview-fullscreen-button" type="button" data-render-fullscreen hidden>Full Screen</button>
|
||||||
|
<div class="preview-window-actions" aria-hidden="true"><span></span><span></span><span></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-tabs" data-preview-tabs></div>
|
||||||
|
<div class="preview-stage">
|
||||||
|
<div class="default-preview" data-default-preview hidden>
|
||||||
|
<img src="{{.Data.File.IconURL}}" alt="" loading="lazy">
|
||||||
|
<div>
|
||||||
|
<strong title="{{.Data.File.Name}}">{{.Data.File.Name}}</strong>
|
||||||
|
<span>{{.Data.File.Size}} · {{.Data.File.ContentType}}</span>
|
||||||
|
</div>
|
||||||
|
<a class="button button-primary" href="{{.Data.DownloadURL}}">
|
||||||
|
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<img class="native-preview native-image-preview" data-image-preview src="{{.Data.DownloadURL}}?inline=1" alt="{{.Data.File.Name}}" hidden>
|
||||||
|
<video class="native-preview native-video-preview" data-video-preview src="{{.Data.DownloadURL}}?inline=1" poster="{{.Data.File.ThumbnailURL}}" controls preload="metadata" hidden></video>
|
||||||
|
<audio class="native-preview native-audio-preview" data-browser-audio-preview src="{{.Data.DownloadURL}}?inline=1" controls preload="metadata" hidden></audio>
|
||||||
|
<div class="code-preview raw-code-preview" data-raw-preview hidden>
|
||||||
|
<pre><code data-raw-output></code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="code-preview prism-code-preview" data-code-preview hidden>
|
||||||
|
<pre class="line-numbers"><code data-code-output></code></pre>
|
||||||
|
</div>
|
||||||
|
<iframe class="render-preview" data-render-preview title="Rendered preview of {{.Data.File.Name}}" sandbox hidden></iframe>
|
||||||
|
<div class="large-preview-gate" data-large-preview-gate hidden>
|
||||||
|
<strong>Large preview</strong>
|
||||||
|
<p>This file is larger than 500 KB. Loading this preview may be slow on some devices.</p>
|
||||||
|
<div>
|
||||||
|
<button class="button button-primary" type="button" data-large-preview-confirm>Load anyway</button>
|
||||||
|
<button class="button button-outline" type="button" data-large-preview-cancel>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-placeholder" data-preview-placeholder hidden>
|
||||||
|
<img src="{{.Data.File.IconURL}}" alt="">
|
||||||
|
<p>Preparing preview...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 id="preview-title" class="file-name" title="{{.Data.File.Name}}">{{.Data.File.Name}}</h1>
|
|
||||||
<p class="download-subtitle">{{.Data.File.Size}} · {{.Data.File.ContentType}}</p>
|
|
||||||
<a class="button button-primary button-wide" href="{{.Data.DownloadURL}}">
|
|
||||||
<svg viewBox="0 0 24 24" role="img" focusable="false" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" /></svg>
|
|
||||||
Download file
|
|
||||||
</a>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user