Initial commit
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
@tool
|
||||
extends PanelContainer
|
||||
class_name CghAssetCard
|
||||
## One asset card — 1:1 with the After Effects addon card anatomy:
|
||||
## square thumbnail (object-fit: cover) + badges (resolution pill / NEW / Premium)
|
||||
## + favorite ♥ + cross-addon ✓ downloaded tick + title + "Category / Subcategory"
|
||||
## + a .cdl-row footer: [⬇ label | sep | ▾ dropdown]; Upgrade = orange gradient.
|
||||
## The card background is ALWAYS #1e1e24 (uniform grid); hover only lifts the border.
|
||||
|
||||
signal request_download(asset: Dictionary, entry: Dictionary) # entry={} -> best free
|
||||
signal request_upgrade(asset: Dictionary)
|
||||
signal favorite_toggled(asset: Dictionary, is_fav: bool)
|
||||
signal preview_requested(card, asset) # hover -> main_dock loads still frames
|
||||
signal flipbook_sheet_requested(card, asset) # hover on a flipbook -> main_dock fetches the real sprite sheet
|
||||
signal cancel_requested(card) # ✕ on the in-flight download
|
||||
signal delete_requested(asset: Dictionary) # 🗑 delete the downloaded file(s) -> re-download
|
||||
signal viewed(asset) # user opened the format/options dropdown
|
||||
signal login_required() # guest tried to favourite -> ask them to log in
|
||||
|
||||
const CARD_W := 168 # reference width
|
||||
const MIN_W := 92 # min width so the responsive column count always fits the dock
|
||||
const HOVER_DELAY := 0.12 # brief debounce before the NETWORK still-preview (flipbooks play instantly)
|
||||
const PREVIEW_FPS := 0.45 # seconds per still frame
|
||||
|
||||
# dropdown special item ids (above any file index)
|
||||
const ID_FAV := 9000
|
||||
const ID_COPY := 9001
|
||||
const ID_VIEW := 9002
|
||||
const ID_DEL := 9003
|
||||
|
||||
var asset: Dictionary
|
||||
var auth: CghAuth # set by main_dock; favourites need login
|
||||
var _locked := false
|
||||
var _thumb: TextureRect
|
||||
var _wrap: Control
|
||||
var _title: Label
|
||||
var _subtitle: Label
|
||||
var _action: Button
|
||||
var _menu_btn: MenuButton
|
||||
var _sep: ColorRect # divider between the main button and the ▾ dropdown
|
||||
var _badge_row: HBoxContainer
|
||||
var _fav_btn: Button
|
||||
var _progress: ProgressBar
|
||||
var _footer: PanelContainer
|
||||
var _prog_row: HBoxContainer
|
||||
var _thumb_base: Texture2D
|
||||
var _preview_frames: Array = []
|
||||
var _preview_idx := 0
|
||||
var _preview_loaded := false
|
||||
var _flipbook_sheet_asked := false # asked main_dock for this flipbook's real sprite sheet already
|
||||
var _menu_entries: Array = [] # deduped entries backing the dropdown items
|
||||
var _delay_timer: Timer
|
||||
var _preview_timer: Timer
|
||||
|
||||
func setup(a: Dictionary) -> void:
|
||||
asset = a
|
||||
custom_minimum_size = Vector2(MIN_W, 0) # min width; height is content-driven
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_theme_stylebox_override("panel", CghTheme.card_box(false))
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
mouse_entered.connect(_on_enter)
|
||||
mouse_exited.connect(_on_exit)
|
||||
|
||||
# hover-preview timers
|
||||
_delay_timer = Timer.new()
|
||||
_delay_timer.one_shot = true
|
||||
_delay_timer.wait_time = HOVER_DELAY
|
||||
_delay_timer.timeout.connect(_start_preview)
|
||||
add_child(_delay_timer)
|
||||
_preview_timer = Timer.new()
|
||||
_preview_timer.wait_time = PREVIEW_FPS
|
||||
_preview_timer.timeout.connect(_next_preview_frame)
|
||||
add_child(_preview_timer)
|
||||
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 4)
|
||||
# PASS (not the default STOP) so a hover anywhere over this container — thumbnail,
|
||||
# title, subtitle, footer gaps — propagates up to the card and fires mouse_entered.
|
||||
# Without this the card only "saw" hovers on the bare label rows, so the preview
|
||||
# played only when the pointer was on/below the title (the thumbnail ate the event).
|
||||
v.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
add_child(v)
|
||||
|
||||
# --- square thumbnail wrap (keeps a 1:1 aspect as the card width changes) ---
|
||||
_wrap = Control.new()
|
||||
_wrap.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_wrap.custom_minimum_size = Vector2(0, MIN_W)
|
||||
_wrap.clip_contents = true
|
||||
_wrap.mouse_filter = Control.MOUSE_FILTER_PASS # let hover over the thumbnail reach the card
|
||||
_wrap.resized.connect(_keep_square)
|
||||
v.add_child(_wrap)
|
||||
|
||||
# dark bg behind the (possibly still-loading) thumbnail
|
||||
var thumb_bg := ColorRect.new()
|
||||
thumb_bg.color = CghConfig.C_NAV
|
||||
thumb_bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
thumb_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_wrap.add_child(thumb_bg)
|
||||
|
||||
_thumb = TextureRect.new()
|
||||
_thumb.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_thumb.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||||
_thumb.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_thumb.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_wrap.add_child(_thumb)
|
||||
|
||||
# badges top-left
|
||||
_badge_row = HBoxContainer.new()
|
||||
_badge_row.position = Vector2(5, 5)
|
||||
_badge_row.add_theme_constant_override("separation", 3)
|
||||
_badge_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_wrap.add_child(_badge_row)
|
||||
|
||||
# top-right cluster: favorite ♥
|
||||
var topright := HBoxContainer.new()
|
||||
topright.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
topright.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
topright.offset_top = 4
|
||||
topright.offset_right = -4
|
||||
topright.add_theme_constant_override("separation", 2)
|
||||
_wrap.add_child(topright)
|
||||
|
||||
_fav_btn = Button.new()
|
||||
_fav_btn.flat = true
|
||||
_fav_btn.custom_minimum_size = Vector2(22, 22)
|
||||
_fav_btn.pressed.connect(_on_fav)
|
||||
topright.add_child(_fav_btn)
|
||||
_refresh_fav_icon()
|
||||
|
||||
# --- title ---
|
||||
_title = Label.new()
|
||||
_title.text = CghAsset.title(asset)
|
||||
_title.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_title.clip_text = true
|
||||
_title.add_theme_color_override("font_color", CghConfig.C_TEXT1)
|
||||
_title.add_theme_font_size_override("font_size", 12)
|
||||
_title.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
v.add_child(_title)
|
||||
|
||||
# --- subtitle: "Category / Subcategory" ---
|
||||
_subtitle = Label.new()
|
||||
_subtitle.text = _subtitle_text()
|
||||
_subtitle.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
_subtitle.clip_text = true
|
||||
_subtitle.add_theme_color_override("font_color", CghConfig.C_TEXT3)
|
||||
_subtitle.add_theme_font_size_override("font_size", 10)
|
||||
_subtitle.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
v.add_child(_subtitle)
|
||||
|
||||
# --- footer: .cdl-row pill [ main | sep | ▾ ] ---
|
||||
_footer = PanelContainer.new()
|
||||
_footer.add_theme_stylebox_override("panel", CghTheme.footer_pill_box())
|
||||
_footer.custom_minimum_size = Vector2(0, 24)
|
||||
_footer.mouse_filter = Control.MOUSE_FILTER_PASS # gaps around the buttons still hover the card
|
||||
v.add_child(_footer)
|
||||
var frow := HBoxContainer.new()
|
||||
frow.add_theme_constant_override("separation", 0)
|
||||
_footer.add_child(frow)
|
||||
|
||||
_action = Button.new()
|
||||
_action.flat = true
|
||||
_action.custom_minimum_size = Vector2(0, 22)
|
||||
_action.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_action.add_theme_font_size_override("font_size", 11)
|
||||
_action.pressed.connect(_on_action)
|
||||
frow.add_child(_action)
|
||||
|
||||
_sep = ColorRect.new()
|
||||
_sep.color = CghConfig.C_BORDER
|
||||
_sep.custom_minimum_size = Vector2(1, 14)
|
||||
_sep.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
frow.add_child(_sep)
|
||||
|
||||
_menu_btn = MenuButton.new()
|
||||
_menu_btn.flat = true
|
||||
_menu_btn.text = "▾"
|
||||
_menu_btn.custom_minimum_size = Vector2(22, 22)
|
||||
frow.add_child(_menu_btn)
|
||||
_build_format_menu()
|
||||
_menu_btn.get_popup().about_to_popup.connect(func(): viewed.emit(asset))
|
||||
|
||||
# --- progress row (hidden until downloading): thin accent bar + ✕ cancel ---
|
||||
_prog_row = HBoxContainer.new()
|
||||
_prog_row.add_theme_constant_override("separation", 4)
|
||||
_prog_row.visible = false
|
||||
v.add_child(_prog_row)
|
||||
_progress = ProgressBar.new()
|
||||
_progress.custom_minimum_size = Vector2(0, 6)
|
||||
_progress.show_percentage = false
|
||||
_progress.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_progress.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
var pfill := StyleBoxFlat.new()
|
||||
pfill.bg_color = CghConfig.C_ACCENT
|
||||
pfill.set_corner_radius_all(2)
|
||||
_progress.add_theme_stylebox_override("fill", pfill)
|
||||
var pbg := StyleBoxFlat.new()
|
||||
pbg.bg_color = Color(0.5, 0.5, 0.5, 0.15)
|
||||
pbg.set_corner_radius_all(2)
|
||||
_progress.add_theme_stylebox_override("background", pbg)
|
||||
_prog_row.add_child(_progress)
|
||||
var cancel := Button.new()
|
||||
cancel.flat = true
|
||||
cancel.text = "✕"
|
||||
cancel.tooltip_text = "Cancel download"
|
||||
cancel.custom_minimum_size = Vector2(18, 18)
|
||||
cancel.add_theme_font_size_override("font_size", 10)
|
||||
cancel.add_theme_color_override("font_color", CghConfig.C_TEXT3)
|
||||
cancel.add_theme_color_override("font_hover_color", CghConfig.C_ERR)
|
||||
cancel.pressed.connect(func(): cancel_requested.emit(self))
|
||||
_prog_row.add_child(cancel)
|
||||
|
||||
_apply_state()
|
||||
|
||||
func _keep_square() -> void:
|
||||
if not _wrap:
|
||||
return
|
||||
var w := _wrap.size.x
|
||||
if w > 1.0 and absf(_wrap.custom_minimum_size.y - w) > 0.5:
|
||||
_wrap.custom_minimum_size.y = w
|
||||
|
||||
func _subtitle_text() -> String:
|
||||
var cat := CghAsset.category_title(asset)
|
||||
return cat if cat != "" else "Asset"
|
||||
|
||||
func _apply_state() -> void:
|
||||
# Badges (NEW / Premium / resolution) intentionally removed per design.
|
||||
_locked = not CghAsset.has_free_file(asset)
|
||||
|
||||
# footer main button — Upgrade (orange) / Import (downloaded) / <res> / Download
|
||||
if _locked:
|
||||
_action.text = "Upgrade"
|
||||
_action.flat = false
|
||||
_action.add_theme_stylebox_override("normal", CghTheme.brand_button_box())
|
||||
_action.add_theme_stylebox_override("hover", CghTheme.brand_button_box(true))
|
||||
_action.add_theme_stylebox_override("pressed", CghTheme.brand_button_box(true))
|
||||
_action.add_theme_color_override("font_color", Color.WHITE)
|
||||
_action.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
# A locked (patreon) asset for a non-paying user has nothing to choose/download,
|
||||
# so hide the ▾ dropdown + divider — the orange Upgrade fills the whole footer.
|
||||
_menu_btn.visible = false
|
||||
_sep.visible = false
|
||||
else:
|
||||
var best := CghAsset.best_free_entry(asset)
|
||||
var rl: String = best.get("res", "") if not best.is_empty() else ""
|
||||
# Cross-addon: "downloaded" if any CGHEVEN addon grabbed this asset (id match).
|
||||
var dl: bool = CghDownloads.is_downloaded(asset)
|
||||
if dl:
|
||||
_action.text = ("Import %s" % rl).strip_edges() if rl != "" else "Import"
|
||||
else:
|
||||
_action.text = ("⤓ %s" % rl) if rl != "" else "⤓ Download"
|
||||
_action.flat = true
|
||||
_action.add_theme_color_override("font_color", CghConfig.C_TEXT2)
|
||||
_action.add_theme_color_override("font_hover_color", CghConfig.C_ACCENT)
|
||||
_menu_btn.visible = true
|
||||
_menu_btn.disabled = false
|
||||
_sep.visible = true
|
||||
|
||||
## Public: re-evaluate the download state (called after a delete) so the footer flips
|
||||
## between Download / Import and the dropdown's delete row appears/disappears.
|
||||
func refresh_state() -> void:
|
||||
_apply_state()
|
||||
_build_format_menu()
|
||||
|
||||
func _top_res_label() -> String:
|
||||
var best := -1
|
||||
var lbl := ""
|
||||
for e in CghAsset.file_entries(asset):
|
||||
if e["res"] != "" and e["res_rank"] >= best:
|
||||
best = e["res_rank"]; lbl = e["res"]
|
||||
return lbl
|
||||
|
||||
# AE-style dropdown: grouped by FORMAT, one row per resolution (deduped).
|
||||
func _build_format_menu() -> void:
|
||||
var pm := _menu_btn.get_popup()
|
||||
pm.clear()
|
||||
pm.add_theme_stylebox_override("panel", CghTheme.dropdown_box())
|
||||
_menu_entries = []
|
||||
var entries := CghAsset.importable_entries(asset)
|
||||
# Hide the low-poly preview .glb from the menu when real files exist (it's only an
|
||||
# internal download fallback) — keeps the list clean.
|
||||
var non_preview := entries.filter(func(e): return not e.get("preview", false))
|
||||
if not non_preview.is_empty():
|
||||
entries = non_preview
|
||||
var pref := ["GLTF", "GLB", "FBX", "OBJ", "ZIP", "EXR", "HDR", "PNG", "JPG", "JPEG", "WEBP"]
|
||||
var verb := "Download"
|
||||
if entries.is_empty():
|
||||
pm.add_item("No Godot-importable file", -1)
|
||||
pm.set_item_disabled(0, true)
|
||||
elif _collapse_by_resolution(entries):
|
||||
# Like Blender for 3D models / flipbooks: ONE resolution ladder, no format
|
||||
# names (gltf/glb/fbx are interchangeable for Godot; png/jpg/webp likewise).
|
||||
var by_res := {} # res -> best entry
|
||||
for e in entries:
|
||||
var rk: String = e["res"]
|
||||
var cur = by_res.get(rk, null)
|
||||
if cur == null:
|
||||
by_res[rk] = e
|
||||
else:
|
||||
var better: bool = bool(cur["locked"]) and not bool(e["locked"])
|
||||
if not better and bool(cur["locked"]) == bool(e["locked"]):
|
||||
var ie: int = pref.find(_base_fmt(e["format"]))
|
||||
var ic: int = pref.find(_base_fmt(cur["format"]))
|
||||
if ie == -1: ie = 99
|
||||
if ic == -1: ic = 99
|
||||
better = ie < ic
|
||||
if better:
|
||||
by_res[rk] = e
|
||||
var rows: Array = by_res.values()
|
||||
rows.sort_custom(func(a, b): return int(a["res_rank"]) < int(b["res_rank"]))
|
||||
for e in rows:
|
||||
var v := "Import" if (CghDownloads.has_file(e["filename"]) or CghDownloads.is_res_downloaded(CghAsset.id(asset), e["res"])) else verb
|
||||
var label := ("%s (%s)" % [e["res"], v]) if e["res"] != "" else v
|
||||
if e["locked"]:
|
||||
label += " 🔒"
|
||||
_menu_entries.append(e)
|
||||
pm.add_item(label, _menu_entries.size() - 1)
|
||||
if e["locked"]:
|
||||
pm.set_item_disabled(pm.get_item_count() - 1, true)
|
||||
else:
|
||||
# Mixed media (VFX/HDRI): keep format groups (MP4 vs EXR actually differ).
|
||||
var groups := {} # format -> { res -> entry }
|
||||
var order := [] # format first-seen order
|
||||
for e in entries:
|
||||
var fmt: String = e["format"]
|
||||
var rk: String = e["res"]
|
||||
if not groups.has(fmt):
|
||||
groups[fmt] = {}
|
||||
order.append(fmt)
|
||||
var g: Dictionary = groups[fmt]
|
||||
if not g.has(rk) or (bool(g[rk]["locked"]) and not bool(e["locked"])):
|
||||
g[rk] = e
|
||||
order.sort_custom(func(a, b):
|
||||
var ia: int = pref.find(a); var ib: int = pref.find(b)
|
||||
if ia == -1: ia = 99
|
||||
if ib == -1: ib = 99
|
||||
return ia < ib)
|
||||
for fmt in order:
|
||||
pm.add_separator(fmt)
|
||||
var rows: Array = groups[fmt].values()
|
||||
rows.sort_custom(func(a, b): return int(a["res_rank"]) < int(b["res_rank"]))
|
||||
for e in rows:
|
||||
var v := "Import" if (CghDownloads.has_file(e["filename"]) or CghDownloads.is_res_downloaded(CghAsset.id(asset), e["res"])) else verb
|
||||
var label := ("%s (%s)" % [e["res"], v]) if e["res"] != "" else v
|
||||
if e["locked"]:
|
||||
label += " 🔒"
|
||||
_menu_entries.append(e)
|
||||
pm.add_item(label, _menu_entries.size() - 1)
|
||||
if e["locked"]:
|
||||
pm.set_item_disabled(pm.get_item_count() - 1, true)
|
||||
# AE dropdown footer items
|
||||
pm.add_separator()
|
||||
pm.add_item(("♥ Remove from Favorites" if CghFavorites.is_fav(asset) else "♡ Add to Favorites"), ID_FAV)
|
||||
pm.add_item("⧉ Copy asset link", ID_COPY)
|
||||
pm.add_item("🌐 View online", ID_VIEW)
|
||||
# Only offer delete when a copy is actually on disk — lets the user wipe a missing/
|
||||
# corrupt download so the footer flips back to Download and they can re-fetch.
|
||||
if CghDownloads.is_downloaded(asset):
|
||||
pm.add_item("🗑 Delete download", ID_DEL)
|
||||
if not pm.id_pressed.is_connected(_on_menu_pick):
|
||||
pm.id_pressed.connect(_on_menu_pick)
|
||||
|
||||
# True when every importable entry is a 3D model (gltf/glb/fbx/obj/zip) OR every one
|
||||
# is an image (png/jpg/webp) — those formats are interchangeable for Godot, so we show
|
||||
# a single resolution list instead of one group per format (matches the Blender addon).
|
||||
func _collapse_by_resolution(entries: Array) -> bool:
|
||||
var model := ["glb", "gltf", "fbx", "obj", "zip"]
|
||||
var image := ["png", "jpg", "jpeg", "webp"]
|
||||
var all_model := true
|
||||
var all_image := true
|
||||
for e in entries:
|
||||
if not (e["ext"] in model):
|
||||
all_model = false
|
||||
if not (e["ext"] in image):
|
||||
all_image = false
|
||||
return all_model or all_image
|
||||
|
||||
func _base_fmt(fmt: String) -> String:
|
||||
return fmt.replace(" (GS)", "").to_upper()
|
||||
|
||||
func _on_menu_pick(idx: int) -> void:
|
||||
match idx:
|
||||
ID_FAV:
|
||||
_on_fav()
|
||||
ID_COPY:
|
||||
DisplayServer.clipboard_set(_asset_url())
|
||||
ID_VIEW:
|
||||
OS.shell_open(_asset_url())
|
||||
ID_DEL:
|
||||
delete_requested.emit(asset)
|
||||
_:
|
||||
if idx >= 0 and idx < _menu_entries.size():
|
||||
request_download.emit(asset, _menu_entries[idx])
|
||||
|
||||
func _asset_url() -> String:
|
||||
return CghAsset.web_url(asset) # https://cgheven.com/assets/<assets_slug>, like Blender
|
||||
|
||||
func _on_action() -> void:
|
||||
if _locked:
|
||||
request_upgrade.emit(asset)
|
||||
else:
|
||||
request_download.emit(asset, {}) # {} -> caller uses best free entry
|
||||
|
||||
func _on_fav() -> void:
|
||||
# Favourites require login — a guest gets a "please login" prompt instead.
|
||||
if auth == null or not auth.is_logged_in():
|
||||
login_required.emit()
|
||||
return
|
||||
var now := CghFavorites.toggle(asset)
|
||||
_refresh_fav_icon()
|
||||
_build_format_menu()
|
||||
favorite_toggled.emit(asset, now)
|
||||
|
||||
func _refresh_fav_icon() -> void:
|
||||
var fav := CghFavorites.is_fav(asset)
|
||||
_fav_btn.text = "♥" if fav else "♡"
|
||||
_fav_btn.add_theme_color_override("font_color", CghConfig.C_ERR if fav else CghConfig.C_TEXT1)
|
||||
|
||||
func _add_res_badge(text: String) -> void:
|
||||
var b := Label.new()
|
||||
b.text = text
|
||||
b.add_theme_color_override("font_color", Color.WHITE)
|
||||
b.add_theme_font_size_override("font_size", 9)
|
||||
var p := PanelContainer.new()
|
||||
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
p.add_theme_stylebox_override("panel", CghTheme.res_badge_box())
|
||||
p.add_child(b)
|
||||
_badge_row.add_child(p)
|
||||
|
||||
func _add_badge(text: String, col: Color) -> void:
|
||||
var b := Label.new()
|
||||
b.text = text
|
||||
b.add_theme_color_override("font_color", Color.WHITE)
|
||||
b.add_theme_font_size_override("font_size", 9)
|
||||
var p := PanelContainer.new()
|
||||
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
p.add_theme_stylebox_override("panel", CghTheme.badge_box(col))
|
||||
p.add_child(b)
|
||||
_badge_row.add_child(p)
|
||||
|
||||
# ---- called by main_dock during this card's download ----
|
||||
func set_progress(pct: float) -> void:
|
||||
_prog_row.visible = true
|
||||
_footer.visible = false
|
||||
if pct < 0:
|
||||
_progress.indeterminate = true
|
||||
else:
|
||||
_progress.indeterminate = false
|
||||
_progress.value = pct
|
||||
|
||||
func set_download_done(_ok: bool) -> void:
|
||||
_prog_row.visible = false
|
||||
_footer.visible = true
|
||||
|
||||
## Download cancelled — restore the footer to its pre-download state.
|
||||
func reset_after_cancel() -> void:
|
||||
_prog_row.visible = false
|
||||
_footer.visible = true
|
||||
_apply_state()
|
||||
|
||||
func set_thumbnail(tex: Texture2D) -> void:
|
||||
if tex and _thumb:
|
||||
_thumb.texture = tex
|
||||
_thumb_base = tex
|
||||
|
||||
func get_thumb_url() -> String:
|
||||
return CghAsset.thumbnail(asset)
|
||||
|
||||
# ---------------- hover preview (image slideshow; Godot can't play mp4) ----------------
|
||||
func _on_enter() -> void:
|
||||
add_theme_stylebox_override("panel", CghTheme.card_box(true))
|
||||
if _preview_loaded:
|
||||
if _preview_frames.size() > 1:
|
||||
_preview_timer.start()
|
||||
return
|
||||
if CghAsset.is_flipbook(asset):
|
||||
# Flipbooks: play the REAL sprite sheet, not the card thumbnail. Ask main_dock to fetch
|
||||
# the low-res sheet once; it calls set_flipbook_sheet() when ready. (Slicing the card
|
||||
# thumbnail was unreliable — some categories, e.g. muzzle-flashes, use a single-frame
|
||||
# thumbnail whose layout doesn't match the sheet's NxM, so slicing it showed garbage.)
|
||||
if not _flipbook_sheet_asked:
|
||||
_flipbook_sheet_asked = true
|
||||
flipbook_sheet_requested.emit(self, asset)
|
||||
return
|
||||
# Non-flipbooks: brief debounce, then load still frames over the network.
|
||||
_delay_timer.start()
|
||||
|
||||
func _on_exit() -> void:
|
||||
# Moving the pointer onto a child button (Download / ▾ / ♥) fires mouse_exited on the
|
||||
# card even though the pointer is still INSIDE it. Ignore those spurious exits so the
|
||||
# preview keeps playing across the whole card — only stop when truly leaving the card.
|
||||
# (If the pointer then leaves the card VIA that child button, the card gets NO second
|
||||
# mouse_exited — the watchdog in _next_preview_frame catches that case and stops.)
|
||||
if is_visible_in_tree() and get_global_rect().has_point(get_global_mouse_position()):
|
||||
return
|
||||
_stop_preview()
|
||||
|
||||
# Stop the hover preview and restore the card to its resting look.
|
||||
func _stop_preview() -> void:
|
||||
add_theme_stylebox_override("panel", CghTheme.card_box(false))
|
||||
_delay_timer.stop()
|
||||
_preview_timer.stop()
|
||||
# If the flipbook sheet never actually loaded (download failed / not a flipbook), allow a
|
||||
# fresh attempt on the next hover instead of being stuck forever.
|
||||
if not _preview_loaded:
|
||||
_flipbook_sheet_asked = false
|
||||
if _thumb_base:
|
||||
_thumb.texture = _thumb_base # revert to thumbnail
|
||||
|
||||
func _start_preview() -> void:
|
||||
if _preview_loaded:
|
||||
return
|
||||
# Non-flipbooks: ask main_dock for still frames (flipbooks are handled in _on_enter via the
|
||||
# real sprite-sheet download).
|
||||
preview_requested.emit(self, asset)
|
||||
|
||||
# main_dock delivers the REAL low-res sprite sheet (as a texture) + its grid; slice it into
|
||||
# per-cell AtlasTexture frames and play. Using the actual sheet (not the card thumbnail)
|
||||
# guarantees the grid matches the image — this fixes categories like muzzle-flashes whose card
|
||||
# thumbnail is a single frame, not a contact sheet, so slicing it NxM produced garbage.
|
||||
func set_flipbook_sheet(tex: Texture2D, grid: Vector2i) -> void:
|
||||
if tex == null:
|
||||
return
|
||||
var cols: int = maxi(1, grid.x)
|
||||
var rows: int = maxi(1, grid.y)
|
||||
if cols * rows <= 1:
|
||||
return
|
||||
var tw := tex.get_width()
|
||||
var th := tex.get_height()
|
||||
if tw < cols or th < rows:
|
||||
return
|
||||
# INTEGER cell size (float-divide then floor) so regions land on exact pixel boundaries —
|
||||
# fractional regions sample slivers of the neighbouring cell and make the playback shimmer.
|
||||
# The last column/row absorbs any remainder pixels so we never read past the sheet edge.
|
||||
var cw := int(float(tw) / float(cols))
|
||||
var ch := int(float(th) / float(rows))
|
||||
if cw < 1 or ch < 1:
|
||||
return
|
||||
var frames := []
|
||||
for r in rows:
|
||||
for c in cols:
|
||||
var w := cw if c < cols - 1 else tw - cw * (cols - 1)
|
||||
var h := ch if r < rows - 1 else th - ch * (rows - 1)
|
||||
var at := AtlasTexture.new()
|
||||
at.atlas = tex
|
||||
at.region = Rect2(c * cw, r * ch, w, h)
|
||||
frames.append(at)
|
||||
if frames.size() <= 1:
|
||||
return
|
||||
_preview_frames = frames
|
||||
_preview_loaded = true
|
||||
_preview_idx = 0
|
||||
_preview_timer.wait_time = 0.04 # ~25 fps flipbook playback, smooth (stills use PREVIEW_FPS)
|
||||
# Only start playing if the pointer is still over this card (the sheet may have finished
|
||||
# downloading after the user moved on).
|
||||
if is_visible_in_tree() and get_global_rect().has_point(get_global_mouse_position()):
|
||||
_thumb.texture = frames[0]
|
||||
_preview_timer.start()
|
||||
|
||||
func set_preview_frames(frames: Array) -> void:
|
||||
_preview_frames = frames
|
||||
_preview_loaded = true
|
||||
if _preview_frames.size() >= 1 and is_visible_in_tree():
|
||||
if get_global_rect().has_point(get_global_mouse_position()):
|
||||
_preview_idx = 0
|
||||
_thumb.texture = _preview_frames[0]
|
||||
if _preview_frames.size() > 1:
|
||||
_preview_timer.start()
|
||||
|
||||
func _next_preview_frame() -> void:
|
||||
# Watchdog: when the pointer leaves the card VIA a child button, the card never gets a
|
||||
# second mouse_exited, so the preview would otherwise play forever. This frame timer only
|
||||
# runs while previewing, so re-check the pointer each tick and stop once it's truly gone.
|
||||
if not is_visible_in_tree() or not get_global_rect().has_point(get_global_mouse_position()):
|
||||
_stop_preview()
|
||||
return
|
||||
if _preview_frames.is_empty():
|
||||
return
|
||||
_preview_idx = (_preview_idx + 1) % _preview_frames.size()
|
||||
_thumb.texture = _preview_frames[_preview_idx]
|
||||
@@ -0,0 +1 @@
|
||||
uid://b3jrkpt271t81
|
||||
@@ -0,0 +1,158 @@
|
||||
@tool
|
||||
extends AcceptDialog
|
||||
class_name CghLoginDialog
|
||||
## Sign-in / Account dialog. Renders TWO views from the same window:
|
||||
## • Guest → "Sign in to CGHEVEN" + [ Login ][ Sign up ] brand buttons.
|
||||
## • Logged in → account card (email · plan · version) + [ Logout ].
|
||||
## The top-nav button opens this for both states, so account details live HERE
|
||||
## (the Settings popup no longer carries them).
|
||||
|
||||
signal logout_requested()
|
||||
|
||||
var auth: CghAuth
|
||||
var _content: VBoxContainer
|
||||
var _msg: Label
|
||||
|
||||
func build() -> void:
|
||||
title = "CGHEVEN — Account"
|
||||
min_size = Vector2i(380, 0)
|
||||
get_ok_button().visible = false # custom buttons below
|
||||
|
||||
_content = VBoxContainer.new()
|
||||
_content.add_theme_constant_override("separation", 10)
|
||||
_content.custom_minimum_size = Vector2(360, 0)
|
||||
add_child(_content)
|
||||
|
||||
auth.login_failed.connect(func(m): _set_msg(m))
|
||||
auth.logged_in.connect(func(_p): _render(); hide())
|
||||
_render()
|
||||
|
||||
## Open the dialog showing the right view for the current auth state.
|
||||
func open() -> void:
|
||||
_render()
|
||||
reset_size()
|
||||
popup_centered()
|
||||
|
||||
## Re-render in place (called when auth state changes while open).
|
||||
func refresh() -> void:
|
||||
if _content:
|
||||
_render()
|
||||
|
||||
func _render() -> void:
|
||||
if _content == null:
|
||||
return
|
||||
for c in _content.get_children():
|
||||
_content.remove_child(c)
|
||||
c.queue_free()
|
||||
if auth.is_logged_in():
|
||||
_build_profile()
|
||||
else:
|
||||
_build_login()
|
||||
|
||||
# ---------------- logged-in: account card ----------------
|
||||
func _build_profile() -> void:
|
||||
title = "CGHEVEN — Account"
|
||||
var h := Label.new()
|
||||
h.text = "Your account"
|
||||
h.add_theme_color_override("font_color", CghConfig.C_TEXT1)
|
||||
h.add_theme_font_size_override("font_size", 16)
|
||||
_content.add_child(h)
|
||||
|
||||
_kv("Account", auth.email if auth.email != "" else "Logged in")
|
||||
_kv("Plan", auth.plan)
|
||||
if auth.expires_at != "":
|
||||
_kv("Expires", _fmt_date(auth.expires_at))
|
||||
_kv("Version", CghConfig.ADDON_VERSION)
|
||||
|
||||
# Premium users (Pro/Studio/Bundle) have nothing to upgrade to — hide the button.
|
||||
if not auth.is_premium():
|
||||
var up := _brand_btn("⚡ Upgrade Plan", func(): OS.shell_open(CghConfig.pricing_url()))
|
||||
_content.add_child(up)
|
||||
|
||||
var lo := Button.new()
|
||||
lo.text = "Logout"
|
||||
lo.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
lo.custom_minimum_size = Vector2(0, 30)
|
||||
lo.pressed.connect(func(): logout_requested.emit(); hide())
|
||||
_content.add_child(lo)
|
||||
|
||||
# ---------------- guest: login / sign up ----------------
|
||||
func _build_login() -> void:
|
||||
title = "CGHEVEN — Sign in"
|
||||
var h := Label.new()
|
||||
h.text = "Sign in to CGHEVEN"
|
||||
h.add_theme_color_override("font_color", CghConfig.C_TEXT1)
|
||||
h.add_theme_font_size_override("font_size", 16)
|
||||
_content.add_child(h)
|
||||
|
||||
var sub := Label.new()
|
||||
sub.text = "Log in to download assets for your plan."
|
||||
sub.add_theme_color_override("font_color", CghConfig.C_TEXT2)
|
||||
sub.add_theme_font_size_override("font_size", 11)
|
||||
_content.add_child(sub)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
_content.add_child(row)
|
||||
row.add_child(_brand_btn("Login", func(): _start(false)))
|
||||
row.add_child(_brand_btn("Sign up", func(): _start(true)))
|
||||
|
||||
_msg = Label.new()
|
||||
_msg.add_theme_color_override("font_color", CghConfig.C_TEXT2)
|
||||
_msg.add_theme_font_size_override("font_size", 11)
|
||||
_msg.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_content.add_child(_msg)
|
||||
|
||||
# Format an ISO date ("2027-06-20T12:06:39.945Z") as "20 Jun 2027" for display.
|
||||
func _fmt_date(iso: String) -> String:
|
||||
if iso == "":
|
||||
return ""
|
||||
var date := iso.split("T")[0]
|
||||
var parts := date.split("-")
|
||||
if parts.size() != 3:
|
||||
return date
|
||||
var months := ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
||||
var mi := int(parts[1]) - 1
|
||||
var mon: String = months[mi] if mi >= 0 and mi < 12 else parts[1]
|
||||
return "%s %s %s" % [parts[2], mon, parts[0]]
|
||||
|
||||
# ---------------- builders ----------------
|
||||
func _kv(k: String, val: String) -> void:
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 8)
|
||||
var kl := Label.new()
|
||||
kl.text = k
|
||||
kl.custom_minimum_size = Vector2(80, 0)
|
||||
kl.add_theme_color_override("font_color", CghConfig.C_TEXT2)
|
||||
kl.add_theme_font_size_override("font_size", 12)
|
||||
var vl := Label.new()
|
||||
vl.text = val
|
||||
vl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
vl.clip_text = true
|
||||
vl.add_theme_color_override("font_color", CghConfig.C_TEXT1)
|
||||
vl.add_theme_font_size_override("font_size", 12)
|
||||
h.add_child(kl)
|
||||
h.add_child(vl)
|
||||
_content.add_child(h)
|
||||
|
||||
func _brand_btn(label: String, cb: Callable) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = label
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
b.custom_minimum_size = Vector2(0, 34)
|
||||
b.add_theme_stylebox_override("normal", CghTheme.brand_button_box())
|
||||
b.add_theme_stylebox_override("hover", CghTheme.brand_button_box(true))
|
||||
b.add_theme_stylebox_override("pressed", CghTheme.brand_button_box(true))
|
||||
b.add_theme_color_override("font_color", Color.WHITE)
|
||||
b.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
b.pressed.connect(cb)
|
||||
return b
|
||||
|
||||
func _start(register: bool) -> void:
|
||||
_set_msg("A browser window has opened — log in there, then come back.")
|
||||
auth.start_login(register)
|
||||
|
||||
func _set_msg(m: String) -> void:
|
||||
if _msg:
|
||||
_msg.text = m
|
||||
@@ -0,0 +1 @@
|
||||
uid://dr3frhbhm7nn
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://cxjitcjxagfe
|
||||
@@ -0,0 +1,195 @@
|
||||
@tool
|
||||
extends AcceptDialog
|
||||
class_name CghSettingsModal
|
||||
## Compact settings window (AE settings-modal style): App · Downloads ·
|
||||
## Community. Kept small/dense — all details fit a tight popup. Account details
|
||||
## live in the panel's Account dialog, not here. Downloads section lets the user
|
||||
## pick their own save folder.
|
||||
|
||||
signal logout_requested()
|
||||
signal check_updates_requested()
|
||||
signal download_dir_changed()
|
||||
|
||||
var auth: CghAuth
|
||||
var _path_lbl: Label
|
||||
var _dir_dialog: FileDialog
|
||||
var _upgrade_btn: Button # hidden for premium users (nothing to upgrade to)
|
||||
|
||||
func build() -> void:
|
||||
title = "CGHEVEN — Settings"
|
||||
# Small floor; the dialog auto-sizes to its (now compact) content.
|
||||
min_size = Vector2i(320, 0)
|
||||
# Wrap the window to its content's minimum size every time it opens — without this
|
||||
# the FIRST popup opens huge (content min-size not computed yet) and only the 2nd is
|
||||
# correct. wrap_controls makes it size right on the very first open.
|
||||
wrap_controls = true
|
||||
get_ok_button().text = "Close"
|
||||
|
||||
# outer padding (tight)
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_left", 12)
|
||||
margin.add_theme_constant_override("margin_right", 12)
|
||||
margin.add_theme_constant_override("margin_top", 7)
|
||||
margin.add_theme_constant_override("margin_bottom", 7)
|
||||
add_child(margin)
|
||||
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 3)
|
||||
v.custom_minimum_size = Vector2(300, 0)
|
||||
margin.add_child(v)
|
||||
|
||||
# ---------- App / actions ----------
|
||||
# Account details (email/plan) live in the panel's Account dialog now, NOT here.
|
||||
_section(v, "App")
|
||||
_kv(v, "Version", CghConfig.ADDON_VERSION)
|
||||
_action_btn(v, "Check for Updates", func(): check_updates_requested.emit())
|
||||
_upgrade_btn = _brand_btn(v, "⚡ Upgrade Plan", func(): OS.shell_open(CghConfig.pricing_url()))
|
||||
_gap(v)
|
||||
|
||||
# ---------- Downloads (user can choose the folder) ----------
|
||||
_section(v, "Downloads")
|
||||
var pathbox := PanelContainer.new()
|
||||
pathbox.add_theme_stylebox_override("panel", CghTheme.footer_pill_box())
|
||||
_path_lbl = Label.new()
|
||||
_path_lbl.text = CghConfig.primary_download_dir()
|
||||
_path_lbl.add_theme_color_override("font_color", CghConfig.C_TEXT2)
|
||||
_path_lbl.add_theme_font_size_override("font_size", 10)
|
||||
_path_lbl.autowrap_mode = TextServer.AUTOWRAP_ARBITRARY
|
||||
var pm := MarginContainer.new()
|
||||
for s in ["margin_left", "margin_right", "margin_top", "margin_bottom"]:
|
||||
pm.add_theme_constant_override(s, 6)
|
||||
pm.add_child(_path_lbl)
|
||||
pathbox.add_child(pm)
|
||||
v.add_child(pathbox)
|
||||
|
||||
# Change / Reset / Open — one tight row
|
||||
var brow := HBoxContainer.new()
|
||||
brow.add_theme_constant_override("separation", 4)
|
||||
_small_btn(brow, "Change folder…", _pick_folder)
|
||||
_small_btn(brow, "Reset", func():
|
||||
CghConfig.set_custom_download_dir("")
|
||||
_refresh_path()
|
||||
download_dir_changed.emit())
|
||||
_small_btn(brow, "Open", func():
|
||||
var d := CghConfig.primary_download_dir()
|
||||
DirAccess.make_dir_recursive_absolute(d)
|
||||
OS.shell_open(d))
|
||||
v.add_child(brow)
|
||||
|
||||
var note := Label.new()
|
||||
note.text = "Shared with all CGHEVEN addons (so downloads show everywhere)."
|
||||
note.add_theme_color_override("font_color", CghConfig.C_TEXT3)
|
||||
note.add_theme_font_size_override("font_size", 9)
|
||||
note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
v.add_child(note)
|
||||
_gap(v)
|
||||
|
||||
# ---------- Community ----------
|
||||
_section(v, "Community")
|
||||
_brand_btn(v, "Join Discord", func(): OS.shell_open(CghConfig.DISCORD))
|
||||
|
||||
# folder picker (native OS dialog)
|
||||
_dir_dialog = FileDialog.new()
|
||||
_dir_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
_dir_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
_dir_dialog.use_native_dialog = true
|
||||
_dir_dialog.title = "Choose download folder"
|
||||
_dir_dialog.dir_selected.connect(_on_dir_picked)
|
||||
add_child(_dir_dialog)
|
||||
|
||||
func refresh() -> void:
|
||||
_refresh_path()
|
||||
# Premium users (Pro/Studio/Bundle) have nothing to upgrade to — hide the button.
|
||||
if _upgrade_btn:
|
||||
_upgrade_btn.visible = not (auth != null and auth.is_premium())
|
||||
|
||||
## Open centered at the CORRECT size on the first click too. AcceptDialog computes its
|
||||
## content min-size only after it is shown, so the naive reset_size()+popup_centered()
|
||||
## opened huge the first time. Show → wait one frame (layout runs) → wrap to content →
|
||||
## re-center. Subsequent opens are already correct.
|
||||
func open_centered() -> void:
|
||||
refresh()
|
||||
popup_centered()
|
||||
await get_tree().process_frame
|
||||
reset_size()
|
||||
move_to_center()
|
||||
|
||||
func _pick_folder() -> void:
|
||||
if _dir_dialog:
|
||||
_dir_dialog.current_dir = CghConfig.primary_download_dir()
|
||||
_dir_dialog.popup_centered(Vector2i(700, 480))
|
||||
|
||||
func _on_dir_picked(dir: String) -> void:
|
||||
CghConfig.set_custom_download_dir(dir)
|
||||
_refresh_path()
|
||||
download_dir_changed.emit()
|
||||
|
||||
func _refresh_path() -> void:
|
||||
if _path_lbl:
|
||||
_path_lbl.text = CghConfig.primary_download_dir()
|
||||
|
||||
# ---------------- builders ----------------
|
||||
func _section(parent: Node, t: String) -> void:
|
||||
var l := Label.new()
|
||||
l.text = t.to_upper()
|
||||
l.add_theme_color_override("font_color", CghConfig.C_ACCENT)
|
||||
l.add_theme_font_size_override("font_size", 10)
|
||||
parent.add_child(l)
|
||||
var sep := HSeparator.new()
|
||||
parent.add_child(sep)
|
||||
|
||||
func _gap(parent: Node) -> void:
|
||||
var c := Control.new()
|
||||
c.custom_minimum_size = Vector2(0, 1)
|
||||
parent.add_child(c)
|
||||
|
||||
func _kv(parent: Node, k: String, val: String) -> Label:
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 8)
|
||||
var kl := Label.new()
|
||||
kl.text = k
|
||||
kl.custom_minimum_size = Vector2(80, 0)
|
||||
kl.add_theme_color_override("font_color", CghConfig.C_TEXT2)
|
||||
kl.add_theme_font_size_override("font_size", 11)
|
||||
var vl := Label.new()
|
||||
vl.text = val
|
||||
vl.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vl.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
vl.clip_text = true
|
||||
vl.add_theme_color_override("font_color", CghConfig.C_TEXT1)
|
||||
vl.add_theme_font_size_override("font_size", 11)
|
||||
h.add_child(kl)
|
||||
h.add_child(vl)
|
||||
parent.add_child(h)
|
||||
return vl
|
||||
|
||||
func _action_btn(parent: Node, label: String, cb: Callable) -> void:
|
||||
var b := Button.new()
|
||||
b.text = label
|
||||
b.custom_minimum_size = Vector2(0, 24)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
b.pressed.connect(cb)
|
||||
parent.add_child(b)
|
||||
|
||||
func _small_btn(parent: Node, label: String, cb: Callable) -> void:
|
||||
var b := Button.new()
|
||||
b.text = label
|
||||
b.custom_minimum_size = Vector2(0, 22)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
b.add_theme_font_size_override("font_size", 10)
|
||||
b.pressed.connect(cb)
|
||||
parent.add_child(b)
|
||||
|
||||
func _brand_btn(parent: Node, label: String, cb: Callable) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = label
|
||||
b.custom_minimum_size = Vector2(0, 26)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
b.add_theme_stylebox_override("normal", CghTheme.brand_button_box())
|
||||
b.add_theme_stylebox_override("hover", CghTheme.brand_button_box(true))
|
||||
b.add_theme_stylebox_override("pressed", CghTheme.brand_button_box(true))
|
||||
b.add_theme_color_override("font_color", Color.WHITE)
|
||||
b.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||
b.pressed.connect(cb)
|
||||
parent.add_child(b)
|
||||
return b
|
||||
@@ -0,0 +1 @@
|
||||
uid://pd7unipywc00
|
||||
@@ -0,0 +1,118 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
class_name CghTheme
|
||||
## Builds a Godot Theme resource in code that matches the After Effects addon
|
||||
## dark look 1:1 (StyleBoxFlat — rounded cards, dark bg, accent borders).
|
||||
|
||||
static func _box(bg: Color, radius: int, border := 0, border_col := Color.TRANSPARENT) -> StyleBoxFlat:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = bg
|
||||
sb.set_corner_radius_all(radius)
|
||||
sb.corner_detail = 8
|
||||
if border > 0:
|
||||
sb.set_border_width_all(border)
|
||||
sb.border_color = border_col
|
||||
sb.content_margin_left = 10
|
||||
sb.content_margin_right = 10
|
||||
sb.content_margin_top = 6
|
||||
sb.content_margin_bottom = 6
|
||||
return sb
|
||||
|
||||
static func build() -> Theme:
|
||||
var t := Theme.new()
|
||||
|
||||
# --- Panel / background ---
|
||||
t.set_stylebox("panel", "PanelContainer", _box(CghConfig.C_BG, 0))
|
||||
|
||||
# --- Buttons (default = subtle, accent on hover) ---
|
||||
var btn_normal := _box(CghConfig.C_CARD, CghConfig.RADIUS_MD, 1, CghConfig.C_BORDER)
|
||||
var btn_hover := _box(CghConfig.C_CARD_HOVER, CghConfig.RADIUS_MD, 1, CghConfig.C_ACCENT)
|
||||
var btn_pressed := _box(CghConfig.C_ACCENT_DIM, CghConfig.RADIUS_MD, 1, CghConfig.C_ACCENT)
|
||||
t.set_stylebox("normal", "Button", btn_normal)
|
||||
t.set_stylebox("hover", "Button", btn_hover)
|
||||
t.set_stylebox("pressed", "Button", btn_pressed)
|
||||
t.set_stylebox("focus", "Button", _box(Color.TRANSPARENT, CghConfig.RADIUS_MD))
|
||||
t.set_color("font_color", "Button", CghConfig.C_TEXT1)
|
||||
t.set_color("font_hover_color", "Button", CghConfig.C_TEXT1)
|
||||
t.set_color("font_pressed_color", "Button", CghConfig.C_ACCENT_HOVER)
|
||||
|
||||
# --- LineEdit (search box, key input) ---
|
||||
var le := _box(CghConfig.C_INPUT, CghConfig.RADIUS_MD, 1, CghConfig.C_BORDER)
|
||||
var le_focus := _box(CghConfig.C_INPUT, CghConfig.RADIUS_MD, 1, CghConfig.C_ACCENT)
|
||||
t.set_stylebox("normal", "LineEdit", le)
|
||||
t.set_stylebox("focus", "LineEdit", le_focus)
|
||||
t.set_color("font_color", "LineEdit", CghConfig.C_TEXT1)
|
||||
t.set_color("font_placeholder_color", "LineEdit", CghConfig.C_TEXT3)
|
||||
t.set_color("caret_color", "LineEdit", CghConfig.C_ACCENT)
|
||||
|
||||
# --- Labels ---
|
||||
t.set_color("font_color", "Label", CghConfig.C_TEXT1)
|
||||
|
||||
# --- OptionButton (sort / format dropdowns) ---
|
||||
t.set_stylebox("normal", "OptionButton", btn_normal)
|
||||
t.set_stylebox("hover", "OptionButton", btn_hover)
|
||||
t.set_stylebox("pressed", "OptionButton", btn_pressed)
|
||||
t.set_color("font_color", "OptionButton", CghConfig.C_TEXT1)
|
||||
|
||||
# --- ScrollContainer / grid background transparent over bg ---
|
||||
t.set_stylebox("panel", "ScrollContainer", _box(Color.TRANSPARENT, 0))
|
||||
|
||||
# --- PopupMenu (dropdowns) ---
|
||||
t.set_stylebox("panel", "PopupMenu", _box(CghConfig.C_BG2, CghConfig.RADIUS_LG, 1, CghConfig.C_BORDER))
|
||||
t.set_color("font_color", "PopupMenu", CghConfig.C_TEXT1)
|
||||
t.set_color("font_hover_color", "PopupMenu", CghConfig.C_ACCENT_HOVER)
|
||||
|
||||
return t
|
||||
|
||||
## Reusable styleboxes pulled by widgets (cards, badges, banner).
|
||||
## AE card: background is ALWAYS #1e1e24 — hover only brightens the border
|
||||
## (never recolors the card). This is what makes the grid look uniform.
|
||||
static func card_box(hover := false) -> StyleBoxFlat:
|
||||
var border_col := CghConfig.C_BORDER_HOVER if hover else CghConfig.C_BORDER
|
||||
return _box(CghConfig.C_CARD, CghConfig.RADIUS_LG, 1, border_col)
|
||||
|
||||
static func accent_button_box(hover := false) -> StyleBoxFlat:
|
||||
var c := CghConfig.C_ACCENT_HOVER if hover else CghConfig.C_ACCENT
|
||||
return _box(c, CghConfig.RADIUS_MD)
|
||||
|
||||
## Orange brand button (Login / Sign up / Upgrade) — solid mid-gradient color.
|
||||
static func brand_button_box(hover := false) -> StyleBoxFlat:
|
||||
var c := CghConfig.C_BRAND_HOVER if hover else CghConfig.C_BRAND
|
||||
return _box(c, CghConfig.RADIUS_MD)
|
||||
|
||||
## The AE .cdl-row footer pill (subtle grey, thin border, 4px radius).
|
||||
static func footer_pill_box(hover := false) -> StyleBoxFlat:
|
||||
var border_col := Color(0.357, 0.486, 1.0, 0.35) if hover else CghConfig.C_BORDER
|
||||
var sb := _box(CghConfig.C_FOOTER_PILL, CghConfig.RADIUS_SM, 1, border_col)
|
||||
sb.content_margin_left = 0
|
||||
sb.content_margin_right = 0
|
||||
sb.content_margin_top = 0
|
||||
sb.content_margin_bottom = 0
|
||||
return sb
|
||||
|
||||
## Dropdown / popover background (--bgd #1c1c22).
|
||||
static func dropdown_box() -> StyleBoxFlat:
|
||||
return _box(CghConfig.C_DROPDOWN, CghConfig.RADIUS_XL, 1, CghConfig.C_BORDER)
|
||||
|
||||
## Resolution badge — dark translucent pill, small radius (AE .rbadge).
|
||||
static func res_badge_box() -> StyleBoxFlat:
|
||||
var sb := _box(CghConfig.C_RES_BADGE, 3)
|
||||
sb.content_margin_left = 5
|
||||
sb.content_margin_right = 5
|
||||
sb.content_margin_top = 2
|
||||
sb.content_margin_bottom = 2
|
||||
return sb
|
||||
|
||||
## Coloured badge (NEW = red, Premium = orange) — small 3px pill like AE.
|
||||
static func badge_box(col: Color) -> StyleBoxFlat:
|
||||
var sb := _box(col, 3)
|
||||
sb.content_margin_left = 5
|
||||
sb.content_margin_right = 5
|
||||
sb.content_margin_top = 2
|
||||
sb.content_margin_bottom = 2
|
||||
return sb
|
||||
|
||||
static func banner_box() -> StyleBoxFlat:
|
||||
# brand gradient is approximated with a flat brand color (StyleBoxFlat has no gradient);
|
||||
# a true gradient uses a TextureRect with a GradientTexture2D behind the banner.
|
||||
return _box(CghConfig.C_BRAND_A, CghConfig.RADIUS_LG)
|
||||
@@ -0,0 +1 @@
|
||||
uid://x7gvw0ql4ctd
|
||||
Reference in New Issue
Block a user