refactor: move supporters loading to separate API endpoint

- Add SupportersHandler in misc_handlers.py to serve /api/lm/supporters
- Register new endpoint in misc_route_registrar.py
- Remove supporters from page load template context in model_handlers.py
- Create supportersService.js for frontend data fetching
- Update Header.js to fetch supporters when support modal opens
- Modify support_modal.html to use client-side rendering

This change improves page load performance by loading supporters data
on-demand instead of during initial page render.
This commit is contained in:
Will Miao
2026-02-28 20:14:20 +08:00
parent 77a2215e62
commit 78b55d10ba
20 changed files with 1206 additions and 89 deletions

621
data/supporters.json Normal file
View File

@@ -0,0 +1,621 @@
{
"specialThanks": [
"dispenser",
"EbonEagle",
"DanielMagPizza",
"Scott R"
],
"allSupporters": [
"megakirbs",
"Brennok",
"Christian Byrne",
"wackop",
"Insomnia Art Designs",
"2018cfh",
"Takkan",
"$MetaSamsara",
"onesecondinosaur",
"stone9k",
"Francisco Tatis",
"FreelancerZ",
"Gooohokrbe",
"JongWon Han",
"OldBones",
"Rosenthal",
"runte3221",
"Birdy",
"Fraser Cross",
"Polymorphic Indeterminate",
"Marc Whiffen",
"Kiba",
"Jorge Hussni",
"Reno Lam",
"Skalabananen",
"esthe",
"sig",
"DM",
"Sen314",
"Estragon",
"J\\B/ 8r0wns0n",
"Andrew Wilson",
"Buzzard",
"carozzz",
"ClockDaemon",
"Cosmosis",
"Echo",
"Edgar Tejeda",
"FloPro4Sho",
"fnkylove",
"Greybush",
"iamresist",
"Illrigger",
"JackieWang",
"James Dooley",
"James Todd",
"jmack",
"Julian V",
"KD",
"lh qwe",
"Lilleman",
"Lisster",
"Mark Corneglio",
"Michael Wong",
"Omnidex",
"PM",
"Release Cabrakan",
"Ricky Carter",
"Robert Stacey",
"SarcasticHashtag",
"SG",
"Steven Owens",
"tarek helmi",
"Tee Gee",
"Tim",
"Tobi_Swagg",
"Todd Keck",
"Tom Corrigan",
"VantAI",
"Vik71it",
"Wolffen",
"Yushio",
"zenbound",
"itismyelement",
"Mozzel",
"Gingko Biloba",
"Penfore",
"BadassArabianMofo",
"Liam MacDougal",
"Sterilized",
"Markus",
"quarz",
"Greg",
"Douglas Gaspar",
"JSST",
"AlexDuKaNa",
"Snaggwort",
"George",
"lmsupporter@fastmail.de",
"Phil",
"Carl G.",
"Arlecchino Shion",
"Charles Blakemore",
"IamAyam",
"wfpearl",
"Rob Williams",
"Aaron Bleuer",
"Adam Shaw",
"Alex",
"Anthony Rizzo",
"ASLPro3D",
"Baekdoosixt",
"bh",
"Big Red",
"Bishoujoker",
"Briton Heilbrun",
"confiscated Zyra",
"conner",
"contrite831",
"corde",
"Cory Paza",
"CryptoTraderJK",
"daniel dove",
"David Ortega",
"dl0901dm",
"FinalyFree",
"Graham Colehour",
"Jack B Nimble",
"Jacob Hoehler",
"Johnny",
"Jonathan Ross",
"JW Sin",
"LacesOut!",
"Luc Job",
"Lustre",
"Marlon Daniels",
"Melville Parrish",
"Nazono_hito",
"Nick Walker",
"Philip Hempel",
"Princess Bright Eyes",
"RedrockVP",
"Starkselle",
"Steven Pfeiffer",
"Tak",
"Timmy",
"Tomohiro Baba",
"Tori",
"Tyler Trebuchon",
"Weasyl",
"whudunit",
"wildnut",
"Yaboi",
"Zach Gonser",
"Davaitamin",
"Felipe dos Santos",
"Aleksander Wujczyk",
"AM Kuro",
"jean jahren",
"tedcor",
"Pascal Dahle",
"S Sang",
"MagnaInsomnia",
"Akira_HentAI",
"Karl P.",
"Gordon Cole",
"yuxz69",
"MadSpin",
"[anonymous]",
"dw",
"N/A",
"The Spawn",
"graysock",
"Greenmoustache",
"Gamalonia",
"fancypants",
"Vir",
"aai",
"AELOX",
"Anthony Faxlandez",
"ApathyJones",
"Aquatic Coffee",
"batblue",
"carey6409",
"Christopher Michel",
"Damon Cunliffe",
"dan",
"Digital",
"Dogmaster",
"Draven T",
"drum matthieu",
"Dustin Chen",
"ethanfel",
"Focuschannel",
"Gonzalo Andre Allendes Lopez",
"JaxMax",
"Jimmy Ledbetter",
"John Saveas",
"Jwk0205",
"LeoZero",
"Lex Song",
"M Postkasse",
"Matt Wenzel",
"Mattssn",
"Max Marklund",
"Mouthlessman",
"nahinahi9",
"Nicfit23",
"Noora",
"Olive",
"Serge Bekenkamp",
"Seth Christensen",
"Some Guy Named Barry",
"Steam Steam",
"takyamtom",
"ViperC",
"wamekukyouzin",
"奚明 刘",
"otaku fra",
"AbstractAss",
"semicolon drainpipe",
"Ran C",
"Thesharingbrother",
"Fotek Design",
"ResidentDeviant",
"Adam Taylor",
"JC",
"Weird_With_A_Beard",
"Prompt Pirate",
"Pozadine1",
"uwutismxd",
"Qarob",
"AIGooner",
"inbijiburu",
"decoy",
"Luc",
"ProtonPrince",
"DiffDuck",
"elu3199",
"Nick “Loadstone” D",
"Hasturkun",
"Jon Sandman",
"Ubivis",
"zounic",
"CloudValley",
"linnfrey",
"zenobeus",
"Jackthemind",
"Stryker",
"Pkrsky",
"raf8osz5",
"Antonio Pontes",
"aRtFuL_DodGeR",
"Billy Gladky",
"Blackfish95",
"blikkies",
"Bohemian Corporal",
"Bro Xie",
"Bruce",
"CrimsonDX",
"Cristian Vazquez",
"Dan",
"Dankin",
"DarkSunset",
"dd",
"DougPeterson",
"Error_Rule34_Not_found",
"Frank Nitty",
"Goldwaters",
"Griffin Dahlberg",
"jinxedx",
"Joboshy",
"Kevin Christopher",
"Kevin John Duck",
"Kyler",
"Magic Noob",
"Neco28",
"Ouro Boros",
"Paul Kroll",
"Probis",
"Roslynd",
"Shock Shockor",
"Spitfire_502",
"X",
"yer fey",
"Zude",
"太郎 ゲーム",
"준희 김",
"shrshpp",
"ItsGeneralButtNaked",
"John Statham",
"Nimess",
"Bas Imagineer",
"Pat Hen",
"Youguang",
"andrewzpong",
"FrxzenSnxw",
"BossGame",
"thesoftwaredruid",
"wundershark",
"mr_dinosaur",
"Tyrswood",
"Ray Wing",
"Ranzitho",
"Gus",
"地獄の禄",
"MJG",
"David LaVallee",
"ae",
"Tr4shP4nda",
"WRL_SPR",
"capn",
"Joseph",
"anonymousoddity",
"Mirko Katzula",
"dan",
"Piccio08",
"kumakichi",
"a _",
"aezin",
"battu",
"Brian M",
"Chad Idk",
"Chris",
"Emil Andersson",
"Emil Bernhoff",
"Erik Lopez",
"Eris3D",
"Geolog",
"Gerald Welly",
"Haru Yotu",
"James Coleman",
"Jamie Ogletree",
"jcay015",
"Jeff",
"John Martin",
"Josef Lanzl",
"Kevin Picco",
"m",
"Martial",
"Mateo Curić",
"Matura Arbeit",
"Michael Docherty",
"moranqianlong",
"Nerezza",
"Pierce McBride",
"sanborondon",
"SendingRavens",
"Taylor Funk",
"TBitz33",
"Thought2Form",
"Yuji Kaneko",
"elitassj4",
"Dušan Ryban",
"Jacob Winter",
"Jordan Shaw",
"Sam",
"Rops Alot",
"SRDB",
"sjon kreutz",
"g unit",
"Ace Ventura",
"David",
"Meilo",
"Nihongasuki",
"Pen Bouryoung",
"shinonomeiro",
"Snille",
"Metryman55",
"MaartenAlbers",
"khanh duy",
"xybrightsummer",
"ColdBread",
"PhilW",
"momokai",
"cppbel",
"starbugx",
"Janik",
"Moon Knight",
"몽타주",
"Kland",
"Hailshem",
"kudari",
"Naomi Hale Danchi",
"dc7431",
"ken",
"Inversity",
"Crocket",
"AIVORY3D",
"epicgamer0020690",
"Joshua Porrata",
"Cruel",
"keemun",
"SuBu",
"RedPIXel",
"MRBlack",
"Kevinj",
"Wind",
"Nexus",
"Mitchell Robson",
"Ramneek“Guy”Ashok",
"squid_actually",
"Nat_20",
"Kiyoe",
"Edward Weeks",
"kyoumei",
"RadStorm04",
"JohnDoe42054",
"BillyHill",
"humptynutz",
"emyth",
"Kalnei",
"ryoma7612",
"ResidentDeviant",
"Scott",
"gzmzmvp",
"Aeternyx",
"ahoystan",
"Andrew",
"Artokun",
"Atilla Berke Pekduyar",
"Bob Barker",
"Brian Buie",
"Decx _",
"Derek Baker",
"Doug Mason",
"Eric Whitney",
"hayden",
"Ivan Tadic",
"ja s",
"Jack Dole",
"Jacob McDaniel",
"Jeremy Townsend",
"Joey Callahan",
"Joshua Gray",
"kevin stoddard",
"Kevin Wallace",
"Kyron Mahan",
"Leland Saunders",
"Littlehuggy",
"Maso",
"Matheus Couto",
"Michael Anthony Scott",
"Michael Taylor",
"Mike Simone",
"Mikko Hemilä",
"Morgandel",
"mrjuan",
"Noah",
"Owen Gwosdz",
"Richard",
"Robert Wegemund",
"Sadlip",
"Sean voets",
"Sloan Steddy",
"Temikus",
"Thomas Wanner",
"y2Rxy7FdXzWo",
"YOU SINWOO",
"Paul Hartsuyker",
"ChicRic",
"mercur2002",
"J C",
"Distortik",
"Yves Poezevara",
"Teriak47",
"Just me",
"Raf Stahelin",
"Вячеслав Маринин",
"Cola Matthew",
"OniNoKen",
"Iain Wisely",
"Zertens",
"NOHOW",
"Apo",
"nekotxt",
"choowkee",
"Clusters",
"ibrahim",
"Highlandrise",
"philcoraz",
"mztn11",
"ImagineerNL",
"MrAcrtosSursus",
"al300680",
"pixl",
"Robin",
"chahknoir@gmail.com",
"Marcus thronico",
"nd01",
"keno94d",
"James Melzer",
"Bartleby",
"Renvertere",
"Rahuy",
"Hermann003",
"D",
"Foolish",
"RevyHiep",
"Captain_Swag",
"obkircher",
"Tree Tagger",
"gwyar",
"Coeur de cochon",
"D",
"edgecase",
"Neoxena",
"mrmhalo",
"michael.isaza1",
"chriphost",
"KitKatM",
"socrasteeze",
"dg",
"Whitepinetrader",
"Maarten Harms",
"OrganicArtifact",
"四糸凜音",
"MudkipMedkitz",
"Israel",
"deanbrian",
"POPPIN",
"Muratoraccio",
"SelfishMedic",
"Ginnie",
"Alex Wortman",
"Cody",
"adderleighn",
"Raku",
"smart.edge5178",
"emadsultan",
"InformedViewz",
"CHKeeho80",
"Bubbafett",
"leaf",
"Menard",
"Skyfire83",
"Adam Rinehart",
"D",
"Pitpe11",
"TheD1rtyD03",
"EnragedAntelope",
"moonpetal",
"SomeDude",
"g9p0o",
"nanana",
"TheHolySheep",
"Monte Won",
"SpringBootisTrash",
"carsten",
"ikok",
"_ G3n",
"ACTUALLY_the_Real_Willem_Dafoe",
"Adictedtohumping",
"AllTimeNoobie",
"Banana Joe",
"beersandbacon",
"Chad Barnes",
"Chase Kwon",
"CptNeo",
"Devil Lude",
"Dismem",
"Donovan Jenkins",
"edk",
"Edward Kennedy",
"elleshar666",
"Elliot E",
"EpicElric",
"Eric Ketchum",
"Ezokewn",
"Forbidden Atelier",
"giani kidd",
"gonzalo",
"Goober719",
"Gregory Kozhemiak",
"han b",
"hexxish",
"Ink Temptation",
"Invis",
"james",
"Jean-françois SEMA",
"John C",
"John J Linehan",
"jumpd",
"Justin Blaylock",
"Justin Houston",
"Kalli Core",
"Kauffy",
"Kurt",
"Maximilian Pyko",
"Mewtora",
"Michael Eid",
"Michael Scott",
"Michael Zhu",
"Middo",
"Nathan",
"Nathan lee",
"NICHOLAS BAXLEY",
"Nico",
"notedfakes",
"OrochiNights",
"psytrax",
"Rim",
"Seraphy",
"Theerat Jiramate",
"Towelie",
"Vane Holzer",
"Wolfe7D1",
"Xan Dionysus",
"雨の心 落",
"James Ming",
"vanditking",
"kripitonga",
"Rizzi",
"nimin",
"OMAR LUCIANO",
"BrentBertram",
"eumelzocker",
"dxjaymz",
"L C",
"Dude"
],
"totalCount": 614
}

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "WeChat QR-Code anzeigen",
"hideWechatQR": "WeChat QR-Code ausblenden"
},
"footer": "Vielen Dank, dass Sie LoRA Manager verwenden! ❤️"
"footer": "Vielen Dank, dass Sie LoRA Manager verwenden! ❤️",
"supporters": {
"title": "Danke an alle Unterstützer",
"subtitle": "Danke an {count} Unterstützer, die dieses Projekt möglich gemacht haben",
"specialThanks": "Besonderer Dank",
"allSupporters": "Alle Unterstützer",
"totalCount": "{count} Unterstützer insgesamt"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "Show WeChat QR Code",
"hideWechatQR": "Hide WeChat QR Code"
},
"footer": "Thank you for using LoRA Manager! ❤️"
"footer": "Thank you for using LoRA Manager! ❤️",
"supporters": {
"title": "Thank You To Our Supporters",
"subtitle": "Thanks to {count} supporters who made this project possible",
"specialThanks": "Special Thanks",
"allSupporters": "All Supporters",
"totalCount": "{count} supporters in total"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "Mostrar código QR de WeChat",
"hideWechatQR": "Ocultar código QR de WeChat"
},
"footer": "¡Gracias por usar el gestor de LoRA! ❤️"
"footer": "¡Gracias por usar el gestor de LoRA! ❤️",
"supporters": {
"title": "Gracias a todos los seguidores",
"subtitle": "Gracias a {count} seguidores que hicieron este proyecto posible",
"specialThanks": "Agradecimientos especiales",
"allSupporters": "Todos los seguidores",
"totalCount": "{count} seguidores en total"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "Afficher le QR Code WeChat",
"hideWechatQR": "Masquer le QR Code WeChat"
},
"footer": "Merci d'utiliser le Gestionnaire LoRA ! ❤️"
"footer": "Merci d'utiliser le Gestionnaire LoRA ! ❤️",
"supporters": {
"title": "Merci à tous les supporters",
"subtitle": "Merci aux {count} supporters qui ont rendu ce projet possible",
"specialThanks": "Remerciements spéciaux",
"allSupporters": "Tous les supporters",
"totalCount": "{count} supporters au total"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "הצג קוד QR של WeChat",
"hideWechatQR": "הסתר קוד QR של WeChat"
},
"footer": "תודה על השימוש במנהל LoRA! ❤️"
"footer": "תודה על השימוש במנהל LoRA! ❤️",
"supporters": {
"title": "תודה לכל התומכים",
"subtitle": "תודה ל־{count} תומכים שהפכו את הפרויקט הזה לאפשרי",
"specialThanks": "תודה מיוחדת",
"allSupporters": "כל התומכים",
"totalCount": "{count} תומכים בסך הכל"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "WeChat QRコードを表示",
"hideWechatQR": "WeChat QRコードを非表示"
},
"footer": "LoRA Managerをご利用いただきありがとうございます ❤️"
"footer": "LoRA Managerをご利用いただきありがとうございます ❤️",
"supporters": {
"title": "サポーターの皆様に感謝",
"subtitle": "{count} 名のサポーターの皆様に、このプロジェクトを実現していただきありがとうございます",
"specialThanks": "特別感謝",
"allSupporters": "全サポーター",
"totalCount": "サポーター {count} 名"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "WeChat QR 코드 표시",
"hideWechatQR": "WeChat QR 코드 숨기기"
},
"footer": "LoRA Manager를 사용해주셔서 감사합니다! ❤️"
"footer": "LoRA Manager를 사용해주셔서 감사합니다! ❤️",
"supporters": {
"title": "후원자 분들께 감사드립니다",
"subtitle": "이 프로젝트를 가능하게 해준 {count}명의 후원자분들께 감사드립니다",
"specialThanks": "특별 감사",
"allSupporters": "모든 후원자",
"totalCount": "총 {count}명의 후원자"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "Показать QR-код WeChat",
"hideWechatQR": "Скрыть QR-код WeChat"
},
"footer": "Спасибо за использование LoRA Manager! ❤️"
"footer": "Спасибо за использование LoRA Manager! ❤️",
"supporters": {
"title": "Спасибо всем сторонникам",
"subtitle": "Спасибо {count} сторонникам, которые сделали этот проект возможным",
"specialThanks": "Особая благодарность",
"allSupporters": "Все сторонники",
"totalCount": "Всего {count} сторонников"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "显示微信二维码",
"hideWechatQR": "隐藏微信二维码"
},
"footer": "感谢使用 LoRA 管理器!❤️"
"footer": "感谢使用 LoRA 管理器!❤️",
"supporters": {
"title": "感谢所有支持者",
"subtitle": "感谢 {count} 位支持者让这个项目成为可能",
"specialThanks": "特别感谢",
"allSupporters": "所有支持者",
"totalCount": "共 {count} 位支持者"
}
},
"toast": {
"general": {

View File

@@ -1342,7 +1342,14 @@
"showWechatQR": "顯示微信二維碼",
"hideWechatQR": "隱藏微信二維碼"
},
"footer": "感謝您使用 LoRA 管理器!❤️"
"footer": "感謝您使用 LoRA 管理器!❤️",
"supporters": {
"title": "感謝所有支持者",
"subtitle": "感謝 {count} 位支持者讓這個專案成為可能",
"specialThanks": "特別感謝",
"allSupporters": "所有支持者",
"totalCount": "共 {count} 位支持者"
}
},
"toast": {
"general": {

View File

@@ -9,6 +9,7 @@ objects that can be composed by the route controller.
from __future__ import annotations
import asyncio
import json
import logging
import os
import subprocess
@@ -218,6 +219,45 @@ class HealthCheckHandler:
return web.json_response({"status": "ok"})
class SupportersHandler:
"""Handler for supporters data."""
def __init__(self, logger: logging.Logger | None = None) -> None:
self._logger = logger or logging.getLogger(__name__)
def _load_supporters(self) -> dict:
"""Load supporters data from JSON file."""
try:
current_file = os.path.abspath(__file__)
root_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(current_file)))
)
supporters_path = os.path.join(root_dir, "data", "supporters.json")
if os.path.exists(supporters_path):
with open(supporters_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._logger.debug(f"Failed to load supporters data: {e}")
return {
"specialThanks": [],
"allSupporters": [],
"totalCount": 0
}
async def get_supporters(self, request: web.Request) -> web.Response:
"""Return supporters data as JSON."""
try:
supporters = self._load_supporters()
return web.json_response({"success": True, "supporters": supporters})
except Exception as exc:
self._logger.error("Error loading supporters: %s", exc, exc_info=True)
return web.json_response(
{"success": False, "error": str(exc)}, status=500
)
class SettingsHandler:
"""Sync settings between backend and frontend."""
@@ -1482,6 +1522,7 @@ class MiscHandlerSet:
metadata_archive: MetadataArchiveHandler,
filesystem: FileSystemHandler,
custom_words: CustomWordsHandler,
supporters: SupportersHandler,
) -> None:
self.health = health
self.settings = settings
@@ -1494,6 +1535,7 @@ class MiscHandlerSet:
self.metadata_archive = metadata_archive
self.filesystem = filesystem
self.custom_words = custom_words
self.supporters = supporters
def to_route_mapping(
self,
@@ -1522,6 +1564,7 @@ class MiscHandlerSet:
"open_file_location": self.filesystem.open_file_location,
"open_settings_location": self.filesystem.open_settings_location,
"search_custom_words": self.custom_words.search_custom_words,
"get_supporters": self.supporters.get_supporters,
}

View File

@@ -66,6 +66,27 @@ class ModelPageView:
self._logger = logger
self._app_version = self._get_app_version()
def _load_supporters(self) -> dict:
"""Load supporters data from JSON file."""
try:
current_file = os.path.abspath(__file__)
root_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(current_file)))
)
supporters_path = os.path.join(root_dir, "data", "supporters.json")
if os.path.exists(supporters_path):
with open(supporters_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
self._logger.debug(f"Failed to load supporters data: {e}")
return {
"specialThanks": [],
"allSupporters": [],
"totalCount": 0
}
def _get_app_version(self) -> str:
version = "1.0.0"
short_hash = "stable"

View File

@@ -26,6 +26,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("GET", "/api/lm/settings/libraries", "get_settings_libraries"),
RouteDefinition("POST", "/api/lm/settings/libraries/activate", "activate_library"),
RouteDefinition("GET", "/api/lm/health-check", "health_check"),
RouteDefinition("GET", "/api/lm/supporters", "get_supporters"),
RouteDefinition("POST", "/api/lm/open-file-location", "open_file_location"),
RouteDefinition("POST", "/api/lm/update-usage-stats", "update_usage_stats"),
RouteDefinition("GET", "/api/lm/get-usage-stats", "get_usage_stats"),

View File

@@ -29,6 +29,7 @@ from .handlers.misc_handlers import (
NodeRegistry,
NodeRegistryHandler,
SettingsHandler,
SupportersHandler,
TrainedWordsHandler,
UsageStatsHandler,
build_service_registry_adapter,
@@ -119,6 +120,7 @@ class MiscRoutes:
metadata_provider_factory=self._metadata_provider_factory,
)
custom_words = CustomWordsHandler()
supporters = SupportersHandler()
return self._handler_set_factory(
health=health,
@@ -132,6 +134,7 @@ class MiscRoutes:
metadata_archive=metadata_archive,
filesystem=filesystem,
custom_words=custom_words,
supporters=supporters,
)

View File

@@ -68,6 +68,7 @@ body {
--space-1: calc(8px * 1);
--space-2: calc(8px * 2);
--space-3: calc(8px * 3);
--space-4: calc(8px * 4);
/* Z-index Scale */
--z-base: 10;
@@ -77,6 +78,7 @@ body {
/* Border Radius */
--border-radius-base: 12px;
--border-radius-md: 12px;
--border-radius-sm: 8px;
--border-radius-xs: 4px;

View File

@@ -1,6 +1,26 @@
/* Support Modal Styles */
.support-modal {
max-width: 570px;
max-width: 1000px;
width: 90vw;
}
/* Two-column layout */
.support-container {
display: flex;
gap: var(--space-3);
min-height: 500px;
}
.support-left {
flex: 0 0 42%;
min-width: 0;
}
.support-right {
flex: 1;
min-width: 0;
border-left: 1px solid var(--lora-border);
padding-left: var(--space-4);
}
.support-header {
@@ -214,6 +234,11 @@
.support-links {
flex-direction: column;
}
.support-modal {
width: 95vw;
max-width: 95vw;
}
}
/* Civitai link styles */
@@ -240,3 +265,180 @@
border-color: var(--lora-accent);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Supporters Section Styles */
.supporters-section {
height: 100%;
display: flex;
flex-direction: column;
}
.supporters-header {
margin-bottom: var(--space-4);
}
.supporters-title {
display: flex;
align-items: center;
gap: var(--space-2);
margin: 0 0 var(--space-1) 0;
font-size: 1.3em !important;
color: var(--lora-accent) !important;
}
.supporters-title i {
opacity: 0.9;
}
.supporters-subtitle {
margin: 0;
font-size: 0.95em;
color: var(--text-color);
opacity: 0.6;
}
.supporters-group {
margin-bottom: var(--space-3);
}
.supporters-group-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 var(--space-2) 0;
font-size: 1em;
color: var(--text-color);
opacity: 0.8;
font-weight: 500;
}
.supporters-group-title i {
color: var(--lora-accent);
opacity: 0.7;
}
/* Special Thanks - Clean Card Style */
.special-thanks-group {
margin-bottom: var(--space-4);
}
.special-thanks-group .supporters-group-title {
margin-bottom: var(--space-3);
}
.special-thanks-group .supporters-group-title i {
color: #fbbf24;
}
.all-supporters-group .supporters-group-title i {
color: var(--lora-error);
opacity: 0.9;
}
.supporters-special-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
}
.supporter-special-card {
display: flex;
align-items: center;
padding: var(--space-2) var(--space-3);
background: var(--card-bg);
border: 1px solid var(--border-color);
border-left: 3px solid var(--lora-accent);
border-radius: var(--border-radius-sm);
transition: all 0.2s ease;
cursor: default;
}
.supporter-special-card:hover {
border-color: var(--lora-accent);
border-left-color: var(--lora-accent);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transform: translateX(4px);
}
.supporter-special-card .supporter-special-name {
font-size: 1em;
font-weight: 500;
color: var(--text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.supporter-special-card:hover .supporter-special-name {
color: var(--lora-accent);
}
/* All Supporters - Elegant Text Flow */
.all-supporters-group {
flex: 1;
display: flex;
flex-direction: column;
}
.all-supporters-group .supporters-group-title {
margin-bottom: var(--space-2);
}
.supporters-all-list {
display: flex;
flex-wrap: wrap;
align-items: baseline;
line-height: 2.2;
max-height: 550px;
overflow-y: auto;
padding: var(--space-2) 0;
color: var(--text-color);
}
.supporter-name-item {
font-size: 0.95em;
color: var(--text-color);
opacity: 0.85;
transition: all 0.2s ease;
white-space: nowrap;
cursor: default;
}
.supporter-name-item:hover {
opacity: 1;
color: var(--lora-accent);
}
.supporter-separator {
margin: 0 10px;
color: var(--text-color);
opacity: 0.25;
font-weight: 300;
user-select: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.support-container {
flex-direction: column;
}
.support-left {
flex: 1;
}
.support-right {
border-left: none;
border-top: 1px solid var(--lora-border);
padding-left: 0;
padding-top: var(--space-3);
}
.supporters-all-list {
max-height: 200px;
}
.supporters-special-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -5,6 +5,7 @@ import { FilterManager } from '../managers/FilterManager.js';
import { initPageState } from '../state/index.js';
import { getStorageItem } from '../utils/storageHelpers.js';
import { updateElementAttribute } from '../utils/i18nHelpers.js';
import { renderSupporters } from '../services/supportersService.js';
/**
* Header.js - Manages the application header behavior across different pages
@@ -85,9 +86,15 @@ export class HeaderManager {
// Handle support toggle
const supportToggle = document.getElementById('supportToggleBtn');
if (supportToggle) {
supportToggle.addEventListener('click', () => {
supportToggle.addEventListener('click', async () => {
if (window.modalManager) {
window.modalManager.toggleModal('supportModal');
// Load supporters data when modal opens
try {
await renderSupporters();
} catch (error) {
console.error('Error loading supporters:', error);
}
}
});
}

View File

@@ -0,0 +1,104 @@
/**
* Supporters service - Fetches and manages supporters data
*/
let supportersData = null;
let isLoading = false;
let loadPromise = null;
/**
* Fetch supporters data from the API
* @returns {Promise<Object>} Supporters data
*/
export async function fetchSupporters() {
// Return cached data if available
if (supportersData) {
return supportersData;
}
// Return existing promise if already loading
if (isLoading && loadPromise) {
return loadPromise;
}
isLoading = true;
loadPromise = fetch('/api/lm/supporters')
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch supporters: ${response.statusText}`);
}
return response.json();
})
.then(data => {
if (data.success && data.supporters) {
supportersData = data.supporters;
return supportersData;
}
throw new Error(data.error || 'Failed to load supporters data');
})
.catch(error => {
console.error('Error loading supporters:', error);
// Return empty data on error
return {
specialThanks: [],
allSupporters: [],
totalCount: 0
};
})
.finally(() => {
isLoading = false;
loadPromise = null;
});
return loadPromise;
}
/**
* Clear cached supporters data
*/
export function clearSupportersCache() {
supportersData = null;
}
/**
* Render supporters in the support modal
*/
export async function renderSupporters() {
const supporters = await fetchSupporters();
// Update subtitle with total count
const subtitleEl = document.getElementById('supportersSubtitle');
if (subtitleEl) {
// Get the translation key and replace count
const originalText = subtitleEl.textContent;
// Replace the count in the text (simple approach)
subtitleEl.textContent = originalText.replace(/\d+/, supporters.totalCount);
}
// Render special thanks
const specialThanksGrid = document.getElementById('specialThanksGrid');
if (specialThanksGrid && supporters.specialThanks) {
specialThanksGrid.innerHTML = supporters.specialThanks
.map(supporter => `
<div class="supporter-special-card" title="${supporter}">
<span class="supporter-special-name">${supporter}</span>
</div>
`)
.join('');
}
// Render all supporters
const supportersGrid = document.getElementById('supportersGrid');
if (supportersGrid && supporters.allSupporters) {
supportersGrid.innerHTML = supporters.allSupporters
.map((supporter, index, array) => {
const separator = index < array.length - 1
? '<span class="supporter-separator">·</span>'
: '';
return `
<span class="supporter-name-item" title="${supporter}">${supporter}</span>${separator}
`;
})
.join('');
}
}

View File

@@ -2,90 +2,133 @@
<div id="supportModal" class="modal">
<div class="modal-content support-modal">
<button class="close" onclick="modalManager.closeModal('supportModal')">&times;</button>
<div class="support-header">
<i class="fas fa-heart support-icon"></i>
<h2>{{ t('support.title') }}</h2>
</div>
<div class="support-content">
<p>{{ t('support.message') }}</p>
<div class="support-section">
<h3><i class="fas fa-comment"></i> {{ t('support.feedback.title') }}</h3>
<p>{{ t('support.feedback.description') }}</p>
<div class="support-links">
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager/issues/new" class="social-link" target="_blank">
<i class="fab fa-github"></i>
<span>{{ t('support.links.submitGithubIssue') }}</span>
</a>
<a href="https://discord.gg/vcqNrWVFvM" class="social-link" target="_blank">
<i class="fab fa-discord"></i>
<span>{{ t('support.links.joinDiscord') }}</span>
</a>
<div class="support-container">
<!-- Left Side: Support Options -->
<div class="support-left">
<div class="support-header">
<i class="fas fa-heart support-icon"></i>
<h2>{{ t('support.title') }}</h2>
</div>
<div class="support-content">
<p>{{ t('support.message') }}</p>
<div class="support-section">
<h3><i class="fas fa-comment"></i> {{ t('support.feedback.title') }}</h3>
<p>{{ t('support.feedback.description') }}</p>
<div class="support-links">
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager/issues/new" class="social-link" target="_blank">
<i class="fab fa-github"></i>
<span>{{ t('support.links.submitGithubIssue') }}</span>
</a>
<a href="https://discord.gg/vcqNrWVFvM" class="social-link" target="_blank">
<i class="fab fa-discord"></i>
<span>{{ t('support.links.joinDiscord') }}</span>
</a>
</div>
</div>
<div class="support-section">
<h3><i class="fas fa-rss"></i> {{ t('support.sections.followUpdates') }}</h3>
<div class="support-links">
<a href="https://www.youtube.com/@pixelpaws-ai" class="social-link" target="_blank">
<i class="fab fa-youtube"></i>
<span>{{ t('support.links.youtubeChannel') }}</span>
</a>
<a href="https://civitai.com/user/PixelPawsAI" class="social-link civitai-link" target="_blank">
<svg class="civitai-icon" viewBox="0 0 225 225" width="20" height="20">
<g transform="translate(0,225) scale(0.1,-0.1)" fill="currentColor">
<path d="M950 1899 c-96 -55 -262 -150 -367 -210 -106 -61 -200 -117 -208
-125 -13 -13 -15 -76 -15 -443 0 -395 1 -429 18 -443 9 -9 116 -73 237 -143
121 -70 283 -163 359 -208 76 -45 146 -80 155 -80 9 1 183 98 386 215 l370
215 2 444 3 444 -376 215 c-206 118 -378 216 -382 217 -4 1 -86 -43 -182 -98z
m346 -481 l163 -93 1 -57 0 -58 -89 0 c-87 0 -91 1 -166 44 l-78 45 -51 -30
c-28 -17 -61 -35 -73 -41 -21 -10 -23 -18 -23 -99 l0 -87 71 -41 c39 -23 73
-41 76 -41 3 0 37 18 75 40 68 39 72 40 164 40 l94 0 0 -53 c0 -60 23 -41
-198 -168 l-133 -77 -92 52 c-51 29 -126 73 -167 97 l-75 45 0 193 0 192 164
95 c91 52 167 94 169 94 2 0 78 -42 168 -92z"/>
</g>
</svg>
<span>{{ t('support.links.civitaiProfile') }}</span>
</a>
</div>
</div>
<div class="support-section">
<h3><i class="fas fa-coffee"></i> {{ t('support.sections.buyMeCoffee') }}</h3>
<p>{{ t('support.sections.coffeeDescription') }}</p>
<a href="https://ko-fi.com/pixelpawsai" class="kofi-button" target="_blank">
<i class="fas fa-mug-hot"></i>
<span>{{ t('support.links.supportKofi') }}</span>
</a>
</div>
<!-- Patreon Support Section -->
<div class="support-section">
<h3><i class="fab fa-patreon"></i> {{ t('support.sections.becomePatron') }}</h3>
<p>{{ t('support.sections.patronDescription') }}</p>
<a href="https://patreon.com/PixelPawsAI" class="patreon-button" target="_blank">
<i class="fab fa-patreon"></i>
<span>{{ t('support.links.supportPatreon') }}</span>
</a>
</div>
<!-- New section for Chinese payment methods -->
<div class="support-section">
<h3><i class="fas fa-qrcode"></i> {{ t('support.sections.wechatSupport') }}</h3>
<p>{{ t('support.sections.wechatDescription') }}</p>
<button class="secondary-btn qrcode-toggle" id="toggleQRCode">
<i class="fas fa-qrcode"></i>
<span class="toggle-text">{{ t('support.sections.showWechatQR') }}</span>
<i class="fas fa-chevron-down toggle-icon"></i>
</button>
<div class="qrcode-container" id="qrCodeContainer">
<img src="/loras_static/images/wechat-qr.webp" alt="WeChat Pay QR Code" class="qrcode-image">
</div>
</div>
<div class="support-footer">
<p>{{ t('support.footer') }}</p>
</div>
</div>
</div>
<div class="support-section">
<h3><i class="fas fa-rss"></i> {{ t('support.sections.followUpdates') }}</h3>
<div class="support-links">
<a href="https://www.youtube.com/@pixelpaws-ai" class="social-link" target="_blank">
<i class="fab fa-youtube"></i>
<span>{{ t('support.links.youtubeChannel') }}</span>
</a>
<a href="https://civitai.com/user/PixelPawsAI" class="social-link civitai-link" target="_blank">
<svg class="civitai-icon" viewBox="0 0 225 225" width="20" height="20">
<g transform="translate(0,225) scale(0.1,-0.1)" fill="currentColor">
<path d="M950 1899 c-96 -55 -262 -150 -367 -210 -106 -61 -200 -117 -208
-125 -13 -13 -15 -76 -15 -443 0 -395 1 -429 18 -443 9 -9 116 -73 237 -143
121 -70 283 -163 359 -208 76 -45 146 -80 155 -80 9 1 183 98 386 215 l370
215 2 444 3 444 -376 215 c-206 118 -378 216 -382 217 -4 1 -86 -43 -182 -98z
m346 -481 l163 -93 1 -57 0 -58 -89 0 c-87 0 -91 1 -166 44 l-78 45 -51 -30
c-28 -17 -61 -35 -73 -41 -21 -10 -23 -18 -23 -99 l0 -87 71 -41 c39 -23 73
-41 76 -41 3 0 37 18 75 40 68 39 72 40 164 40 l94 0 0 -53 c0 -60 23 -41
-198 -168 l-133 -77 -92 52 c-51 29 -126 73 -167 97 l-75 45 0 193 0 192 164
95 c91 52 167 94 169 94 2 0 78 -42 168 -92z"/>
</g>
</svg>
<span>{{ t('support.links.civitaiProfile') }}</span>
</a>
<!-- Right Side: Supporters -->
<div class="support-right">
<div class="supporters-section">
<div class="supporters-header">
<h2 class="supporters-title">
<i class="fas fa-hands-helping"></i>
{{ t('support.supporters.title') }}
</h2>
<p class="supporters-subtitle" id="supportersSubtitle">
{{ t('support.supporters.subtitle', count=0) }}
</p>
</div>
<!-- Special Thanks Section -->
<div class="supporters-group special-thanks-group">
<h3 class="supporters-group-title">
<i class="fas fa-star"></i>
{{ t('support.supporters.specialThanks') }}
</h3>
<div class="supporters-special-grid" id="specialThanksGrid">
<!-- Supporters will be loaded dynamically -->
</div>
</div>
<!-- All Supporters Section -->
<div class="supporters-group all-supporters-group">
<h3 class="supporters-group-title">
<i class="fas fa-heart"></i>
{{ t('support.supporters.allSupporters') }}
</h3>
<div class="supporters-all-list" id="supportersGrid">
<!-- Supporters will be loaded dynamically -->
</div>
</div>
</div>
</div>
<div class="support-section">
<h3><i class="fas fa-coffee"></i> {{ t('support.sections.buyMeCoffee') }}</h3>
<p>{{ t('support.sections.coffeeDescription') }}</p>
<a href="https://ko-fi.com/pixelpawsai" class="kofi-button" target="_blank">
<i class="fas fa-mug-hot"></i>
<span>{{ t('support.links.supportKofi') }}</span>
</a>
</div>
<!-- Patreon Support Section -->
<div class="support-section">
<h3><i class="fab fa-patreon"></i> {{ t('support.sections.becomePatron') }}</h3>
<p>{{ t('support.sections.patronDescription') }}</p>
<a href="https://patreon.com/PixelPawsAI" class="patreon-button" target="_blank">
<i class="fab fa-patreon"></i>
<span>{{ t('support.links.supportPatreon') }}</span>
</a>
</div>
<!-- New section for Chinese payment methods -->
<div class="support-section">
<h3><i class="fas fa-qrcode"></i> {{ t('support.sections.wechatSupport') }}</h3>
<p>{{ t('support.sections.wechatDescription') }}</p>
<button class="secondary-btn qrcode-toggle" id="toggleQRCode">
<i class="fas fa-qrcode"></i>
<span class="toggle-text">{{ t('support.sections.showWechatQR') }}</span>
<i class="fas fa-chevron-down toggle-icon"></i>
</button>
<div class="qrcode-container" id="qrCodeContainer">
<img src="/loras_static/images/wechat-qr.webp" alt="WeChat Pay QR Code" class="qrcode-image">
</div>
</div>
<div class="support-footer">
<p>{{ t('support.footer') }}</p>
</div>
</div>
</div>
</div>