diff --git a/data/supporters.json b/data/supporters.json new file mode 100644 index 00000000..d7a3cc40 --- /dev/null +++ b/data/supporters.json @@ -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 +} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index f14d73e3..ed31bb8f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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": { diff --git a/locales/en.json b/locales/en.json index 9d9e824c..40f3f9b9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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": { diff --git a/locales/es.json b/locales/es.json index a7e96ff5..06ddaebc 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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": { diff --git a/locales/fr.json b/locales/fr.json index 31ec9e15..560c73f7 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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": { diff --git a/locales/he.json b/locales/he.json index 75d39a28..f9ce3965 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1342,7 +1342,14 @@ "showWechatQR": "הצג קוד QR של WeChat", "hideWechatQR": "הסתר קוד QR של WeChat" }, - "footer": "תודה על השימוש במנהל LoRA! ❤️" + "footer": "תודה על השימוש במנהל LoRA! ❤️", + "supporters": { + "title": "תודה לכל התומכים", + "subtitle": "תודה ל־{count} תומכים שהפכו את הפרויקט הזה לאפשרי", + "specialThanks": "תודה מיוחדת", + "allSupporters": "כל התומכים", + "totalCount": "{count} תומכים בסך הכל" + } }, "toast": { "general": { diff --git a/locales/ja.json b/locales/ja.json index 0beb5231..db5705a8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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": { diff --git a/locales/ko.json b/locales/ko.json index 545d9dd2..92b6de8b 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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": { diff --git a/locales/ru.json b/locales/ru.json index b2eaecbf..4dcbdbdb 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 5fbed5f7..843d66de 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1342,7 +1342,14 @@ "showWechatQR": "显示微信二维码", "hideWechatQR": "隐藏微信二维码" }, - "footer": "感谢使用 LoRA 管理器!❤️" + "footer": "感谢使用 LoRA 管理器!❤️", + "supporters": { + "title": "感谢所有支持者", + "subtitle": "感谢 {count} 位支持者让这个项目成为可能", + "specialThanks": "特别感谢", + "allSupporters": "所有支持者", + "totalCount": "共 {count} 位支持者" + } }, "toast": { "general": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 9dcf22ee..849294ad 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1342,7 +1342,14 @@ "showWechatQR": "顯示微信二維碼", "hideWechatQR": "隱藏微信二維碼" }, - "footer": "感謝您使用 LoRA 管理器!❤️" + "footer": "感謝您使用 LoRA 管理器!❤️", + "supporters": { + "title": "感謝所有支持者", + "subtitle": "感謝 {count} 位支持者讓這個專案成為可能", + "specialThanks": "特別感謝", + "allSupporters": "所有支持者", + "totalCount": "共 {count} 位支持者" + } }, "toast": { "general": { diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index 23e5575a..2d73376f 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -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, } diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 2ccc0436..88ed56e0 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -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" diff --git a/py/routes/misc_route_registrar.py b/py/routes/misc_route_registrar.py index 1d6c4e7e..e2443644 100644 --- a/py/routes/misc_route_registrar.py +++ b/py/routes/misc_route_registrar.py @@ -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"), diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 1f5c2a6c..acdb4e8a 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -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, ) diff --git a/static/css/base.css b/static/css/base.css index ddcac9b9..bc2a3545 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -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; diff --git a/static/css/components/modal/support-modal.css b/static/css/components/modal/support-modal.css index f860a93c..a058f6fb 100644 --- a/static/css/components/modal/support-modal.css +++ b/static/css/components/modal/support-modal.css @@ -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 */ @@ -239,4 +264,181 @@ .folder-item:hover { 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; + } } \ No newline at end of file diff --git a/static/js/components/Header.js b/static/js/components/Header.js index 49be670f..05736803 100644 --- a/static/js/components/Header.js +++ b/static/js/components/Header.js @@ -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); + } } }); } diff --git a/static/js/services/supportersService.js b/static/js/services/supportersService.js new file mode 100644 index 00000000..868a52f8 --- /dev/null +++ b/static/js/services/supportersService.js @@ -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} 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 => ` +
+ ${supporter} +
+ `) + .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 + ? '·' + : ''; + return ` + ${supporter}${separator} + `; + }) + .join(''); + } +} diff --git a/templates/components/modals/support_modal.html b/templates/components/modals/support_modal.html index 71298544..fbbdccd7 100644 --- a/templates/components/modals/support_modal.html +++ b/templates/components/modals/support_modal.html @@ -2,90 +2,133 @@