diff --git a/internal/app/action/action.go b/internal/app/action/action.go index e09f1a5..23c721a 100644 --- a/internal/app/action/action.go +++ b/internal/app/action/action.go @@ -450,6 +450,10 @@ func (a *Action) renderResults(w http.ResponseWriter, report string, valuesMap [ return a.renderResultsText(w, valuesStr) case apptype.JSON: return a.renderResultsJson(w, valuesMap) + case apptype.DOWNLOAD: + return a.renderResultsDownload(w, valuesMap) + case apptype.IMAGE: + return a.renderResultsImage(w, valuesMap) default: // Custom template being used for the results // Wrap the template output in a div with hx-swap-oob @@ -515,6 +519,18 @@ func (a *Action) renderResultsText(w http.ResponseWriter, valuesStr []string) er return err } +func (a *Action) renderResultsDownload(w http.ResponseWriter, valuesMap []map[string]any) error { + // Render the result values, using HTMX OOB + err := a.actionTemplate.ExecuteTemplate(w, "result-download", valuesMap) + return err +} + +func (a *Action) renderResultsImage(w http.ResponseWriter, valuesMap []map[string]any) error { + // Render the result values, using HTMX OOB + err := a.actionTemplate.ExecuteTemplate(w, "result-image", valuesMap) + return err +} + func (a *Action) renderResultsTable(w http.ResponseWriter, valuesMap []map[string]any) error { if len(valuesMap) == 0 { return a.actionTemplate.ExecuteTemplate(w, "result-empty", nil) diff --git a/internal/app/action/astatic/style.css b/internal/app/action/astatic/style.css index 0b35274..53daf7b 100644 --- a/internal/app/action/astatic/style.css +++ b/internal/app/action/astatic/style.css @@ -1035,6 +1035,15 @@ html { --glass-border-opacity: 15%; } + .btn-outline:hover { + --tw-border-opacity: 1; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity))); + } + .btn-outline.btn-primary:hover { --tw-text-opacity: 1; color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); @@ -1047,6 +1056,78 @@ html { } } + .btn-outline.btn-secondary:hover { + --tw-text-opacity: 1; + color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-secondary:hover { + background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + } + } + + .btn-outline.btn-accent:hover { + --tw-text-opacity: 1; + color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-accent:hover { + background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + } + } + + .btn-outline.btn-success:hover { + --tw-text-opacity: 1; + color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-success:hover { + background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + } + } + + .btn-outline.btn-info:hover { + --tw-text-opacity: 1; + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-info:hover { + background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + } + } + + .btn-outline.btn-warning:hover { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-warning:hover { + background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + } + } + + .btn-outline.btn-error:hover { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-error:hover { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } + } + .btn-disabled:hover, .btn[disabled]:hover, .btn:disabled:hover { @@ -1087,6 +1168,60 @@ html { display: none; } +.file-input { + height: 3rem; + flex-shrink: 1; + padding-inline-end: 1rem; + font-size: 1rem; + line-height: 2; + line-height: 1.5rem; + overflow: hidden; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-border-opacity: 0; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.file-input::file-selector-button { + margin-inline-end: 1rem; + display: inline-flex; + height: 100%; + flex-shrink: 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-size: 0.875rem; + line-height: 1.25rem; + line-height: 1em; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 200ms; + border-style: solid; + --tw-border-opacity: 1; + border-color: var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + font-weight: 600; + text-transform: uppercase; + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); + text-decoration-line: none; + border-width: var(--border-btn, 1px); + animation: button-pop var(--animation-btn, 0.25s) ease-out; +} + .footer { display: grid; width: 100%; @@ -1397,6 +1532,36 @@ html { background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); } + + .btn-outline.btn-secondary.btn-active { + background-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-s,oklch(var(--s)/1)) 90%, black); + } + + .btn-outline.btn-accent.btn-active { + background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); + } + + .btn-outline.btn-success.btn-active { + background-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-su,oklch(var(--su)/1)) 90%, black); + } + + .btn-outline.btn-info.btn-active { + background-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-in,oklch(var(--in)/1)) 90%, black); + } + + .btn-outline.btn-warning.btn-active { + background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + } + + .btn-outline.btn-error.btn-active { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } } .btn:focus-visible { @@ -1429,6 +1594,25 @@ html { --glass-border-opacity: 15%; } +.btn-outline { + border-color: currentColor; + background-color: transparent; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.btn-outline.btn-active { + --tw-border-opacity: 1; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity))); +} + .btn-outline.btn-primary { --tw-text-opacity: 1; color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); @@ -1439,6 +1623,66 @@ html { color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); } +.btn-outline.btn-secondary { + --tw-text-opacity: 1; + color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity))); +} + +.btn-outline.btn-secondary.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity))); +} + +.btn-outline.btn-accent { + --tw-text-opacity: 1; + color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity))); +} + +.btn-outline.btn-accent.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); +} + +.btn-outline.btn-success { + --tw-text-opacity: 1; + color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity))); +} + +.btn-outline.btn-success.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity))); +} + +.btn-outline.btn-info { + --tw-text-opacity: 1; + color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity))); +} + +.btn-outline.btn-info.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); +} + +.btn-outline.btn-warning { + --tw-text-opacity: 1; + color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity))); +} + +.btn-outline.btn-warning.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); +} + .btn.btn-disabled, .btn[disabled], .btn:disabled { @@ -1611,6 +1855,60 @@ html { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.file-input:focus { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.file-input-primary { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); +} + +.file-input-primary:focus { + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +.file-input-primary::file-selector-button { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.file-input-disabled, + .file-input[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + --tw-text-opacity: 0.2; +} + +.file-input-disabled::-moz-placeholder, .file-input[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.file-input-disabled::placeholder, + .file-input[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.file-input-disabled::file-selector-button, .file-input[disabled]::file-selector-button { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + .label-text { font-size: 0.875rem; line-height: 1.25rem; @@ -2066,6 +2364,17 @@ html { outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); } +.textarea-primary { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); +} + +.textarea-primary:focus { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + .textarea-success { --tw-border-opacity: 1; border-color: var(--fallback-su,oklch(var(--su)/var(--tw-border-opacity))); @@ -2261,10 +2570,6 @@ html { background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); } -.bg-transparent { - background-color: transparent; -} - .fill-current { fill: currentColor; } diff --git a/internal/app/action/result.go.html b/internal/app/action/result.go.html index 642d959..e7662ea 100644 --- a/internal/app/action/result.go.html +++ b/internal/app/action/result.go.html @@ -78,3 +78,27 @@ {{ end }} + +{{ block "result-download" . }} +
+
Output File
+ + {{ range . }} +

+ Download {{ .name }} +

+ {{ end }} +
+{{ end }} + +{{ block "result-image" . }} +
+
Output File
+ + {{ range . }} +

{{ .name }}

+ {{ .name }} +
+ {{ end }} +
+{{ end }} diff --git a/internal/app/app_plugins.go b/internal/app/app_plugins.go index b8a906d..7afd9cb 100644 --- a/internal/app/app_plugins.go +++ b/internal/app/app_plugins.go @@ -73,6 +73,7 @@ func (p *AppPlugins) GetPlugin(pluginInfo *plugin.PluginInfo, accountName string StoreInfo: p.app.storeInfo, Config: pluginConfig, AppConfig: p.app.appConfig, + AppPath: p.app.AppEntry.Path, } appPlugin, err := pluginInfo.Builder(pluginContext) if err != nil { diff --git a/internal/app/apptype/builtins.go b/internal/app/apptype/builtins.go index 9939455..9b28986 100644 --- a/internal/app/apptype/builtins.go +++ b/internal/app/apptype/builtins.go @@ -52,8 +52,10 @@ const ( READ = "READ" WRITE = "WRITE" - AUTO = "AUTO" - TABLE = "TABLE" + AUTO = "AUTO" + TABLE = "TABLE" + DOWNLOAD = "DOWNLOAD" + IMAGE = "IMAGE" ) var ( @@ -425,6 +427,8 @@ func CreateBuiltin() starlark.StringDict { WRITE: starlark.String(WRITE), AUTO: starlark.String(AUTO), TABLE: starlark.String(TABLE), + DOWNLOAD: starlark.String(DOWNLOAD), + IMAGE: starlark.String(IMAGE), "CONTAINER_URL": starlark.String(CONTAINER_URL), }, }, diff --git a/internal/app/fs_store.go b/internal/app/fs_store.go index 6e224d0..a100673 100644 --- a/internal/app/fs_store.go +++ b/internal/app/fs_store.go @@ -79,17 +79,17 @@ func backgroundCleanup(ctx context.Context, cleanupTicker *time.Ticker) { } func (f *fsPlugin) LoadFile(thread *starlark.Thread, builtin *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - var path starlark.String + var pathVal starlark.String visibility := starlark.String(UserAccess) mimeType := starlark.String("application/octet-stream") expiryMinutes := starlark.MakeInt(60) singleAccess := starlark.Bool(true) - if err := starlark.UnpackArgs("abs", args, kwargs, "path", &path, "visibility?", &visibility, "mime_type?", &mimeType, "expiry_minutes?", &expiryMinutes, "single_access", &singleAccess); err != nil { + if err := starlark.UnpackArgs("abs", args, kwargs, "path", &pathVal, "visibility?", &visibility, "mime_type?", &mimeType, "expiry_minutes?", &expiryMinutes, "single_access", &singleAccess); err != nil { return nil, err } - pathStr, err := filepath.Abs(string(path)) + pathStr, err := filepath.Abs(string(pathVal)) if err != nil { return nil, err } @@ -119,6 +119,9 @@ func (f *fsPlugin) LoadFile(thread *starlark.Thread, builtin *starlark.Builtin, createTime := time.Now() expireAt := createTime.Add(time.Duration(expiryMinutesInt) * time.Minute) + if expiryMinutesInt <= 0 { + expireAt = time.Unix(0, int64(^uint64(0)>>1)) + } id, err := ksuid.NewRandom() if err != nil { @@ -143,7 +146,19 @@ func (f *fsPlugin) LoadFile(thread *starlark.Thread, builtin *starlark.Builtin, if err != nil { return nil, err } - return starlark.String(userFile.Id), nil + + appPath := f.pluginContext.AppPath + if appPath == "/" { + appPath = "" + } + downloadUrl := fmt.Sprintf("%s%s/file/%s", appPath, types.APP_INTERNAL_URL_PREFIX, userFile.Id) + + ret := map[string]string{ + "id": userFile.Id, + "url": downloadUrl, + "name": userFile.FileName, + } + return NewResponse(ret), nil } func AddUserFile(ctx context.Context, file *types.UserFile) error { diff --git a/internal/app/setup.go b/internal/app/setup.go index 6de3e74..8152316 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -1010,6 +1010,13 @@ func (a *App) userFileHandler(w http.ResponseWriter, r *http.Request) { } if fileEntry.SingleAccess { + if strings.HasPrefix(fileEntry.FilePath, "file://") { + err := os.Remove(strings.TrimPrefix(fileEntry.FilePath, "file://")) + if err != nil { + fmt.Fprintf(os.Stderr, "error deleting file %s: %s", fileEntry.FilePath, err) + } + } + err = DeleteUserFile(r.Context(), fileID) if err != nil { a.Error().Err(err).Msgf("Error deleting file %s %s", fileID, fileEntry.FilePath) diff --git a/internal/types/types.go b/internal/types/types.go index 34cef70..a329b00 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -127,6 +127,7 @@ type PluginContext struct { StoreInfo *starlark_type.StoreInfo Config PluginSettings AppConfig AppConfig + AppPath string } // HttpConfig is the configuration for the HTTP server