Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 60 additions & 13 deletions view_image.v
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ fn (mut iv ImageView) generate_layout(mut window Window) Layout {
mut downloads := state_map[string, i64](mut window, ns_active_downloads, cap_moderate)
if !downloads.contains(iv.src) {
downloads.set(iv.src, time.now().unix())
spawn download_image(iv.src, base_path, mut window)
// Resolve optional auth header at dispatch time (main thread)
auth_header := if f := window.view_state.image_auth_header_fn {
f(iv.src)
} else {
''
}
spawn download_image(iv.src, base_path, auth_header, mut window)
}
mut layout := Layout{
shape: &Shape{
Expand Down Expand Up @@ -125,14 +131,24 @@ struct ImageFetchResult {
// download_image downloads a remote image to a local cache path.
// base_path has no extension; extension is determined from
// Content-Type header. Validates size (<50MB) and type (image/*).
fn download_image(url string, base_path string, mut w Window) {
spawn fn [url, base_path, mut w] () {
// auth_header, when non-empty, is sent as the Authorization header.
fn download_image(url string, base_path string, auth_header string, mut w Window) {
spawn fn [url, base_path, auth_header, mut w] () {
ch := chan ImageFetchResult{}

spawn fn [url, base_path, ch] () {
spawn fn [url, base_path, auth_header, ch] () {
max_size := i64(50 * 1024 * 1024)

head := http.head(url) or {
// Use a request with optional auth so restricted images (e.g.
// Zulip /user_uploads/) are accessible.
mut head_req := http.Request{
method: .head
url: url
}
if auth_header.len > 0 {
head_req.add_header(.authorization, auth_header)
}
head := head_req.do() or {
ch <- ImageFetchResult{
err_msg: 'Failed to fetch image headers for ${url}: ${err}'
is_err: true
Expand All @@ -150,27 +166,58 @@ fn download_image(url string, base_path string, mut w Window) {
return
}

// Validate content type
ct := head.header.get(.content_type) or { '' }
if !ct.starts_with('image/') {
// Validate content type — some servers omit Content-Type on HEAD;
// allow an empty type and rely on the GET response below.
ct_head := head.header.get(.content_type) or { '' }
if ct_head.len > 0 && !ct_head.starts_with('image/') {
ch <- ImageFetchResult{
err_msg: 'Invalid content type for image (expected image/*): ${url}'
is_err: true
}
return
}

// Determine extension from Content-Type
path := base_path + content_type_to_ext(ct)

// Download file
http.download_file(url, path) or {
// Download file using a GET request with optional auth
mut get_req := http.Request{
method: .get
url: url
}
if auth_header.len > 0 {
get_req.add_header(.authorization, auth_header)
}
resp := get_req.do() or {
ch <- ImageFetchResult{
err_msg: 'Failed to download image ${url}: ${err}'
is_err: true
}
return
}
if resp.status_code != 200 {
ch <- ImageFetchResult{
err_msg: 'Image download returned HTTP ${resp.status_code}: ${url}'
is_err: true
}
return
}

// Determine extension from Content-Type (prefer GET response)
ct := resp.header.get(.content_type) or { ct_head }
if ct.len > 0 && !ct.starts_with('image/') {
ch <- ImageFetchResult{
err_msg: 'Invalid content type for image (expected image/*): ${url}'
is_err: true
}
return
}

path := base_path + content_type_to_ext(if ct.len > 0 { ct } else { 'image/png' })
os.write_file(path, resp.body) or {
ch <- ImageFetchResult{
err_msg: 'Failed to write cached image ${path}: ${err}'
is_err: true
}
return
}

ch <- ImageFetchResult{
is_err: false
Expand Down
8 changes: 7 additions & 1 deletion view_rtf.v
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,13 @@ fn rtf_on_click(layout &Layout, mut e Event, mut w Window) {
} else if found_run.link.starts_with('#') {
w.scroll_to_view(found_run.link[1..])
} else {
os.open_uri(found_run.link) or {}
// Give app-level handler first refusal
if handler := w.view_state.link_handler {
handler(found_run.link, mut e, mut w)
}
if !e.is_handled {
os.open_uri(found_run.link) or {}
}
}
e.is_handled = true
}
Expand Down
26 changes: 16 additions & 10 deletions view_state.v
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ import sokol.sapp
// dedicated fields for type safety.
struct ViewState {
mut:
cursor_on_sticky bool // keeps the cursor visible during cursor movement
id_focus u32 // current view that has focus
input_cursor_on bool = true // used by cursor blink animation
menu_key_nav bool // true, menu navigated by keyboard
mouse_cursor sapp.MouseCursor // arrow, finger, ibeam, etc.
mouse_lock MouseLockCfg // mouse down/move/up/scroll/sliders, etc. use this
rtf_tooltip_rect gg.Rect // RTF abbreviation tooltip anchor rect
rtf_tooltip_text string // RTF abbreviation tooltip text
tooltip TooltipState // State for the active tooltip
registry StateRegistry // generic per-widget state maps
cursor_on_sticky bool // keeps the cursor visible during cursor movement
id_focus u32 // current view that has focus
input_cursor_on bool = true // used by cursor blink animation
menu_key_nav bool // true, menu navigated by keyboard
mouse_cursor sapp.MouseCursor // arrow, finger, ibeam, etc.
mouse_lock MouseLockCfg // mouse down/move/up/scroll/sliders, etc. use this
rtf_tooltip_rect gg.Rect // RTF abbreviation tooltip anchor rect
rtf_tooltip_text string // RTF abbreviation tooltip text
tooltip TooltipState // State for the active tooltip
registry StateRegistry // generic per-widget state maps
// link_handler, when set, is called before opening a link in the OS browser.
// If the handler sets e.is_handled = true the default os.open_uri is skipped.
link_handler ?fn (url string, mut e Event, mut w Window)
// image_auth_header_fn, when set, is called for every remote image URL to
// obtain an "Authorization" header value. Return an empty string to skip.
image_auth_header_fn ?fn (url string) string
image_map BoundedImageMap = BoundedImageMap{
max_size: 100
}
Expand Down
16 changes: 16 additions & 0 deletions window_api.v
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,22 @@ pub fn (mut window Window) dismiss_link_context_menu() {
window.view_state.link_context_menu_visible = false
}

// set_link_handler registers a global link-click interceptor.
// The handler is called before the default os.open_uri behavior.
// Set e.is_handled = true inside the handler to suppress the default action.
// Pass `none` to remove any previously registered handler.
pub fn (mut window Window) set_link_handler(handler ?fn (url string, mut e Event, mut w Window)) {
window.view_state.link_handler = handler
}

// set_image_auth_header_fn registers a callback that returns the value of
// an "Authorization" HTTP header to be sent when downloading remote images.
// Return an empty string from the callback to skip auth for a given URL.
// Pass `none` to remove any previously registered callback.
pub fn (mut window Window) set_image_auth_header_fn(f ?fn (url string) string) {
window.view_state.image_auth_header_fn = f
}

// set_rtf_tooltip shows a tooltip with the given text at the specified rect.
// Used for abbreviation tooltips in RTF views.
pub fn (mut window Window) set_rtf_tooltip(text string, rect gg.Rect) {
Expand Down
Loading