Initial commit

This commit is contained in:
scorpion21211-cmd
2026-10-03 23:29:41 -04:00
commit e24bbbda5d
10586 changed files with 1030090 additions and 0 deletions
+579
View File
@@ -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]
+1
View File
@@ -0,0 +1 @@
uid://b3jrkpt271t81
+158
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://dr3frhbhm7nn
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uid://cxjitcjxagfe
+195
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://pd7unipywc00
+118
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://x7gvw0ql4ctd