mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-28 05:31:16 -03:00
Compare commits
7 Commits
519bafebc8
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237a015cde | ||
|
|
1ae2778baa | ||
|
|
84fcdb5f20 | ||
|
|
8a0b368b44 | ||
|
|
3990535505 | ||
|
|
3e961a9860 | ||
|
|
d6669f1d04 |
@@ -6,22 +6,29 @@
|
|||||||
"Scott R"
|
"Scott R"
|
||||||
],
|
],
|
||||||
"allSupporters": [
|
"allSupporters": [
|
||||||
|
"Brennok",
|
||||||
"Insomnia Art Designs",
|
"Insomnia Art Designs",
|
||||||
"2018cfh",
|
"2018cfh",
|
||||||
"megakirbs",
|
"megakirbs",
|
||||||
"Brennok",
|
"Arlecchino Shion",
|
||||||
|
"Rob Williams",
|
||||||
"W+K+White",
|
"W+K+White",
|
||||||
"wackop",
|
"wackop",
|
||||||
"Phil",
|
"Phil",
|
||||||
"Carl G.",
|
"Carl G.",
|
||||||
"Arlecchino Shion",
|
|
||||||
"Charles Blakemore",
|
"Charles Blakemore",
|
||||||
"Rob Williams",
|
|
||||||
"stone9k",
|
"stone9k",
|
||||||
"itismyelement",
|
"itismyelement",
|
||||||
"$MetaSamsara",
|
"$MetaSamsara",
|
||||||
|
"Gingko Biloba",
|
||||||
|
"Kiba",
|
||||||
"onesecondinosaur",
|
"onesecondinosaur",
|
||||||
|
"Christian Byrne",
|
||||||
|
"DM",
|
||||||
|
"Sen314",
|
||||||
|
"Estragon",
|
||||||
"Rosenthal",
|
"Rosenthal",
|
||||||
|
"ClockDaemon",
|
||||||
"Francisco Tatis",
|
"Francisco Tatis",
|
||||||
"Tobi_Swagg",
|
"Tobi_Swagg",
|
||||||
"Andrew Wilson",
|
"Andrew Wilson",
|
||||||
@@ -30,32 +37,38 @@
|
|||||||
"JongWon Han",
|
"JongWon Han",
|
||||||
"VantAI",
|
"VantAI",
|
||||||
"runte3221",
|
"runte3221",
|
||||||
|
"Illrigger",
|
||||||
|
"Tom Corrigan",
|
||||||
"FreelancerZ",
|
"FreelancerZ",
|
||||||
|
"Echo",
|
||||||
|
"Robert Stacey",
|
||||||
"Edgar Tejeda",
|
"Edgar Tejeda",
|
||||||
"Fraser Cross",
|
"Fraser Cross",
|
||||||
"Liam MacDougal",
|
"Liam MacDougal",
|
||||||
"Polymorphic Indeterminate",
|
"Polymorphic Indeterminate",
|
||||||
|
"Sterilized",
|
||||||
|
"JORGE+LUIZ+HUSSNI+MESSIAS",
|
||||||
"Marc Whiffen",
|
"Marc Whiffen",
|
||||||
"Skalabananen",
|
"Skalabananen",
|
||||||
"Birdy",
|
"Birdy",
|
||||||
"Kiba",
|
|
||||||
"Mozzel",
|
"Mozzel",
|
||||||
"Gingko Biloba",
|
|
||||||
"Reno Lam",
|
"Reno Lam",
|
||||||
|
"JSST",
|
||||||
"sig",
|
"sig",
|
||||||
"Christian Byrne",
|
|
||||||
"DM",
|
|
||||||
"Sen314",
|
|
||||||
"Estragon",
|
|
||||||
"J\\B/ 8r0wns0n",
|
"J\\B/ 8r0wns0n",
|
||||||
"Snaggwort",
|
"Snaggwort",
|
||||||
"Takkan",
|
"Takkan",
|
||||||
"Matt+J",
|
"Matt+J",
|
||||||
"ClockDaemon",
|
"Baekdoosixt",
|
||||||
|
"Jonathan Ross",
|
||||||
"KD",
|
"KD",
|
||||||
"Omnidex",
|
"Omnidex",
|
||||||
|
"Nazono_hito",
|
||||||
|
"daniel dove",
|
||||||
"Tyler Trebuchon",
|
"Tyler Trebuchon",
|
||||||
"Release Cabrakan",
|
"Release Cabrakan",
|
||||||
|
"JW Sin",
|
||||||
|
"Alex",
|
||||||
"SG",
|
"SG",
|
||||||
"carozzz",
|
"carozzz",
|
||||||
"James Dooley",
|
"James Dooley",
|
||||||
@@ -70,77 +83,71 @@
|
|||||||
"RedrockVP",
|
"RedrockVP",
|
||||||
"Wolffen",
|
"Wolffen",
|
||||||
"James Todd",
|
"James Todd",
|
||||||
|
"Wicked Choices by ASLPro3D",
|
||||||
"Steven Pfeiffer",
|
"Steven Pfeiffer",
|
||||||
"Tim",
|
"レプサイ",
|
||||||
"Timmy",
|
"Timmy",
|
||||||
"Johnny",
|
"Johnny",
|
||||||
|
"Tak",
|
||||||
"Lisster",
|
"Lisster",
|
||||||
"Michael Wong",
|
"Michael Wong",
|
||||||
"Illrigger",
|
"Big Red",
|
||||||
"whudunit",
|
"whudunit",
|
||||||
"Tom Corrigan",
|
"dl0901dm",
|
||||||
"JackieWang",
|
"JackieWang",
|
||||||
"fnkylove",
|
"fnkylove",
|
||||||
"Yushio",
|
"Yushio",
|
||||||
"Vik71it",
|
"Vik71it",
|
||||||
"Echo",
|
"Bishoujoker",
|
||||||
"Lilleman",
|
"Lilleman",
|
||||||
"Robert Stacey",
|
|
||||||
"PM",
|
"PM",
|
||||||
"Todd Keck",
|
"Todd Keck",
|
||||||
"Briton Heilbrun",
|
"Briton Heilbrun",
|
||||||
"Jorge Hussni",
|
"wildnut",
|
||||||
"Sterilized",
|
"Aleksander Wujczyk",
|
||||||
|
"AM Kuro",
|
||||||
"BadassArabianMofo",
|
"BadassArabianMofo",
|
||||||
"Pascal Dahle",
|
"Pascal Dahle",
|
||||||
"quarz",
|
"quarz",
|
||||||
"Greg",
|
"Greg",
|
||||||
"JSST",
|
|
||||||
"lmsupporter",
|
"lmsupporter",
|
||||||
|
"andrew.tappan",
|
||||||
"zounic",
|
"zounic",
|
||||||
"wfpearl",
|
"wfpearl",
|
||||||
"Baekdoosixt",
|
|
||||||
"Jonathan Ross",
|
|
||||||
"Jack B Nimble",
|
"Jack B Nimble",
|
||||||
"Nazono_hito",
|
|
||||||
"Melville Parrish",
|
"Melville Parrish",
|
||||||
"daniel dove",
|
|
||||||
"Lustre",
|
"Lustre",
|
||||||
"JW Sin",
|
"JaxMax",
|
||||||
"contrite831",
|
"contrite831",
|
||||||
"Alex",
|
|
||||||
"bh",
|
"bh",
|
||||||
"Marlon Daniels",
|
"Marlon Daniels",
|
||||||
"Starkselle",
|
"Starkselle",
|
||||||
"Aaron Bleuer",
|
"Aaron Bleuer",
|
||||||
"LacesOut!",
|
"LacesOut!",
|
||||||
"greebles",
|
"greebles",
|
||||||
|
"Some Guy Named Barry",
|
||||||
"M Postkasse",
|
"M Postkasse",
|
||||||
"Gooohokrbe",
|
"Gooohokrbe",
|
||||||
"Wicked Choices by ASLPro3D",
|
|
||||||
"OldBones",
|
"OldBones",
|
||||||
"Jacob Hoehler",
|
"Jacob Hoehler",
|
||||||
"FinalyFree",
|
"FinalyFree",
|
||||||
|
"Matt Wenzel",
|
||||||
"Weasyl",
|
"Weasyl",
|
||||||
"Lex Song",
|
"Lex Song",
|
||||||
"Cory Paza",
|
"Cory Paza",
|
||||||
"Tak",
|
|
||||||
"Gonzalo Andre Allendes Lopez",
|
"Gonzalo Andre Allendes Lopez",
|
||||||
"Zach Gonser",
|
"Zach Gonser",
|
||||||
"Big Red",
|
|
||||||
"Jimmy Ledbetter",
|
"Jimmy Ledbetter",
|
||||||
"Luc Job",
|
"Luc Job",
|
||||||
"dl0901dm",
|
|
||||||
"Philip Hempel",
|
"Philip Hempel",
|
||||||
"corde",
|
"corde",
|
||||||
"Nick Walker",
|
"Nick Walker",
|
||||||
"Bishoujoker",
|
"dan",
|
||||||
"aai",
|
"aai",
|
||||||
"Tori",
|
"Tori",
|
||||||
"wildnut",
|
"otaku fra",
|
||||||
"jean jahren",
|
"jean jahren",
|
||||||
"Aleksander Wujczyk",
|
"MiraiKuriyamaSy",
|
||||||
"AM Kuro",
|
|
||||||
"Ran C",
|
"Ran C",
|
||||||
"ViperC",
|
"ViperC",
|
||||||
"Penfore",
|
"Penfore",
|
||||||
@@ -149,17 +156,22 @@
|
|||||||
"Karl P.",
|
"Karl P.",
|
||||||
"Akira_HentAI",
|
"Akira_HentAI",
|
||||||
"Gordon Cole",
|
"Gordon Cole",
|
||||||
|
"Adam Taylor",
|
||||||
"AbstractAss",
|
"AbstractAss",
|
||||||
"andrew.tappan",
|
"Weird_With_A_Beard",
|
||||||
"N/A",
|
"N/A",
|
||||||
"The Spawn",
|
"The Spawn",
|
||||||
"graysock",
|
"graysock",
|
||||||
|
"Pozadine1",
|
||||||
|
"Qarob",
|
||||||
|
"AIGooner",
|
||||||
|
"Luc",
|
||||||
"Greenmoustache",
|
"Greenmoustache",
|
||||||
|
"Jackthemind",
|
||||||
"fancypants",
|
"fancypants",
|
||||||
"Eldithor",
|
"Eldithor",
|
||||||
"Joboshy",
|
"Joboshy",
|
||||||
"Digital",
|
"Digital",
|
||||||
"JaxMax",
|
|
||||||
"takyamtom",
|
"takyamtom",
|
||||||
"Bohemian Corporal",
|
"Bohemian Corporal",
|
||||||
"Dan",
|
"Dan",
|
||||||
@@ -170,42 +182,37 @@
|
|||||||
"carey6409",
|
"carey6409",
|
||||||
"Olive",
|
"Olive",
|
||||||
"太郎 ゲーム",
|
"太郎 ゲーム",
|
||||||
"Some Guy Named Barry",
|
"Roslynd",
|
||||||
"jinxedx",
|
"jinxedx",
|
||||||
"Cosmosis",
|
"Cosmosis",
|
||||||
"AELOX",
|
"AELOX",
|
||||||
"Dankin",
|
"Dankin",
|
||||||
"Nicfit23",
|
"Nicfit23",
|
||||||
"FloPro4Sho",
|
"FloPro4Sho",
|
||||||
|
"Cristian Vazquez",
|
||||||
"wamekukyouzin",
|
"wamekukyouzin",
|
||||||
"drum matthieu",
|
"drum matthieu",
|
||||||
"Dogmaster",
|
"Dogmaster",
|
||||||
"Matt Wenzel",
|
|
||||||
"Frank Nitty",
|
"Frank Nitty",
|
||||||
|
"Magic Noob",
|
||||||
"Christopher Michel",
|
"Christopher Michel",
|
||||||
"Serge Bekenkamp",
|
"Serge Bekenkamp",
|
||||||
|
"DougPeterson",
|
||||||
"LeoZero",
|
"LeoZero",
|
||||||
"Antonio Pontes",
|
"Antonio Pontes",
|
||||||
"ApathyJones",
|
"ApathyJones",
|
||||||
"Julian V",
|
"Julian V",
|
||||||
"Steven Owens",
|
"Steven Owens",
|
||||||
"nahinahi9",
|
"nahinahi9",
|
||||||
|
"Kevin John Duck",
|
||||||
"Dustin Chen",
|
"Dustin Chen",
|
||||||
"dan",
|
|
||||||
"Blackfish95",
|
"Blackfish95",
|
||||||
"Mouthlessman",
|
"Mouthlessman",
|
||||||
"Paul Kroll",
|
"Paul Kroll",
|
||||||
"otaku fra",
|
|
||||||
"MiraiKuriyamaSy",
|
|
||||||
"Bas Imagineer",
|
"Bas Imagineer",
|
||||||
"yuxz69",
|
"yuxz69",
|
||||||
"Adam Taylor",
|
|
||||||
"Weird_With_A_Beard",
|
|
||||||
"esthe",
|
"esthe",
|
||||||
"Pozadine1",
|
"decoy",
|
||||||
"Qarob",
|
|
||||||
"AIGooner",
|
|
||||||
"Luc",
|
|
||||||
"ProtonPrince",
|
"ProtonPrince",
|
||||||
"DiffDuck",
|
"DiffDuck",
|
||||||
"elu3199",
|
"elu3199",
|
||||||
@@ -217,46 +224,50 @@
|
|||||||
"wundershark",
|
"wundershark",
|
||||||
"mr_dinosaur",
|
"mr_dinosaur",
|
||||||
"Tyrswood",
|
"Tyrswood",
|
||||||
|
"Ray Wing",
|
||||||
|
"Ranzitho",
|
||||||
|
"Gus",
|
||||||
|
"MJG",
|
||||||
"linnfrey",
|
"linnfrey",
|
||||||
"IamAyam",
|
"IamAyam",
|
||||||
"skaterb949",
|
"skaterb949",
|
||||||
"Josef Lanzl",
|
"Josef Lanzl",
|
||||||
|
"Nerezza",
|
||||||
"confiscated Zyra",
|
"confiscated Zyra",
|
||||||
"Error_Rule34_Not_found",
|
"Error_Rule34_Not_found",
|
||||||
|
"aezin",
|
||||||
|
"jcay015",
|
||||||
"Gerald Welly",
|
"Gerald Welly",
|
||||||
"Roslynd",
|
"Erik Lopez",
|
||||||
|
"Mateo Curić",
|
||||||
"Tee Gee",
|
"Tee Gee",
|
||||||
"Geolog",
|
"Geolog",
|
||||||
"tarek helmi",
|
"tarek helmi",
|
||||||
"Neco28",
|
"Neco28",
|
||||||
|
"Eris3D",
|
||||||
"Max Marklund",
|
"Max Marklund",
|
||||||
"David Ortega",
|
"David Ortega",
|
||||||
"Cristian Vazquez",
|
|
||||||
"Magic Noob",
|
|
||||||
"Pronredn",
|
"Pronredn",
|
||||||
"DougPeterson",
|
"a _",
|
||||||
"Jeff",
|
"Jeff",
|
||||||
"Bruce",
|
"Bruce",
|
||||||
"lh qwe",
|
"lh qwe",
|
||||||
"Kevin John Duck",
|
"James Coleman",
|
||||||
"conner",
|
"conner",
|
||||||
"Kevin Christopher",
|
"Kevin Christopher",
|
||||||
|
"Chad Idk",
|
||||||
"dd",
|
"dd",
|
||||||
"Princess Bright Eyes",
|
"Princess Bright Eyes",
|
||||||
"Dušan Ryban",
|
"Dušan Ryban",
|
||||||
"Felipe dos Santos",
|
"Felipe dos Santos",
|
||||||
|
"sjon kreutz",
|
||||||
"John Statham",
|
"John Statham",
|
||||||
"Douglas Gaspar",
|
"Douglas Gaspar",
|
||||||
"Metryman55",
|
"Metryman55",
|
||||||
"AlexDuKaNa",
|
"AlexDuKaNa",
|
||||||
"George",
|
"George",
|
||||||
"dw",
|
"dw",
|
||||||
"decoy",
|
|
||||||
"Ray Wing",
|
|
||||||
"Ranzitho",
|
|
||||||
"Gus",
|
|
||||||
"地獄の禄",
|
"地獄の禄",
|
||||||
"MJG",
|
|
||||||
"David LaVallee",
|
"David LaVallee",
|
||||||
"ae",
|
"ae",
|
||||||
"Tr4shP4nda",
|
"Tr4shP4nda",
|
||||||
@@ -273,19 +284,20 @@
|
|||||||
"몽타주",
|
"몽타주",
|
||||||
"Kland",
|
"Kland",
|
||||||
"Hailshem",
|
"Hailshem",
|
||||||
|
"kudari",
|
||||||
|
"Naomi Hale Danchi",
|
||||||
|
"epicgamer0020690",
|
||||||
|
"Richard",
|
||||||
"奚明 刘",
|
"奚明 刘",
|
||||||
|
"Andrew",
|
||||||
"Brian M",
|
"Brian M",
|
||||||
"Nerezza",
|
"Robert Wegemund",
|
||||||
"sanborondon",
|
"sanborondon",
|
||||||
"준희 김",
|
"준희 김",
|
||||||
"Taylor Funk",
|
"Taylor Funk",
|
||||||
"aezin",
|
|
||||||
"Thought2Form",
|
"Thought2Form",
|
||||||
"jcay015",
|
|
||||||
"Kevin Picco",
|
"Kevin Picco",
|
||||||
"Erik Lopez",
|
"Sadlip",
|
||||||
"Mateo Curić",
|
|
||||||
"Eris3D",
|
|
||||||
"Tomohiro Baba",
|
"Tomohiro Baba",
|
||||||
"m",
|
"m",
|
||||||
"Noora",
|
"Noora",
|
||||||
@@ -294,32 +306,30 @@
|
|||||||
"Mattssn",
|
"Mattssn",
|
||||||
"Mikko Hemilä",
|
"Mikko Hemilä",
|
||||||
"Jamie Ogletree",
|
"Jamie Ogletree",
|
||||||
"a _",
|
"Michael Taylor",
|
||||||
"James Coleman",
|
|
||||||
"Martial",
|
"Martial",
|
||||||
"Emil Andersson",
|
"Emil Andersson",
|
||||||
"Ouro Boros",
|
"Ouro Boros",
|
||||||
"Chad Idk",
|
"Atilla Berke Pekduyar",
|
||||||
"Steam Steam",
|
"Steam Steam",
|
||||||
"CryptoTraderJK",
|
"CryptoTraderJK",
|
||||||
|
"Decx _",
|
||||||
"Yuji Kaneko",
|
"Yuji Kaneko",
|
||||||
"Davaitamin",
|
"Davaitamin",
|
||||||
"Rops Alot",
|
"Rops Alot",
|
||||||
"tedcor",
|
"tedcor",
|
||||||
"Sam",
|
"Sam",
|
||||||
"Fotek Design",
|
"Fotek Design",
|
||||||
"sjon kreutz",
|
|
||||||
"Ace Ventura",
|
"Ace Ventura",
|
||||||
|
"LarsesFPC",
|
||||||
"MadSpin",
|
"MadSpin",
|
||||||
"inbijiburu",
|
"inbijiburu",
|
||||||
"Nick “Loadstone” D",
|
"Nick “Loadstone” D",
|
||||||
"momokai",
|
"momokai",
|
||||||
"starbugx",
|
"starbugx",
|
||||||
"kudari",
|
|
||||||
"Naomi Hale Danchi",
|
|
||||||
"dc7431",
|
"dc7431",
|
||||||
"ken",
|
"ken",
|
||||||
"epicgamer0020690",
|
"Crocket",
|
||||||
"Joshua Porrata",
|
"Joshua Porrata",
|
||||||
"keemun",
|
"keemun",
|
||||||
"SuBu",
|
"SuBu",
|
||||||
@@ -339,22 +349,24 @@
|
|||||||
"KitKatM",
|
"KitKatM",
|
||||||
"socrasteeze",
|
"socrasteeze",
|
||||||
"OrganicArtifact",
|
"OrganicArtifact",
|
||||||
|
"MudkipMedkitz",
|
||||||
|
"deanbrian",
|
||||||
|
"Alex Wortman",
|
||||||
|
"Cody",
|
||||||
|
"emadsultan",
|
||||||
"Vir",
|
"Vir",
|
||||||
"gzmzmvp",
|
"gzmzmvp",
|
||||||
"Richard",
|
|
||||||
"Andrew",
|
|
||||||
"Robert Wegemund",
|
|
||||||
"Littlehuggy",
|
"Littlehuggy",
|
||||||
"Gregory Kozhemiak",
|
"Gregory Kozhemiak",
|
||||||
"Draven T",
|
"Draven T",
|
||||||
"mrjuan",
|
"mrjuan",
|
||||||
"Brian Buie",
|
"Brian Buie",
|
||||||
"Sadlip",
|
|
||||||
"Eric Whitney",
|
"Eric Whitney",
|
||||||
"Joey Callahan",
|
"Joey Callahan",
|
||||||
"Aquatic Coffee",
|
"Aquatic Coffee",
|
||||||
"Ivan Tadic",
|
"Ivan Tadic",
|
||||||
"Mike Simone",
|
"Mike Simone",
|
||||||
|
"John J Linehan",
|
||||||
"ethanfel",
|
"ethanfel",
|
||||||
"Elliot E",
|
"Elliot E",
|
||||||
"Morgandel",
|
"Morgandel",
|
||||||
@@ -366,34 +378,30 @@
|
|||||||
"Sloan Steddy",
|
"Sloan Steddy",
|
||||||
"Temikus",
|
"Temikus",
|
||||||
"Artokun",
|
"Artokun",
|
||||||
"Michael Taylor",
|
"hexxish",
|
||||||
"Derek Baker",
|
"Derek Baker",
|
||||||
"Anthony Faxlandez",
|
"Anthony Faxlandez",
|
||||||
"battu",
|
"battu",
|
||||||
"Michael Anthony Scott",
|
"Michael Anthony Scott",
|
||||||
"Atilla Berke Pekduyar",
|
|
||||||
"Nathan",
|
"Nathan",
|
||||||
"Decx _",
|
"NICHOLAS BAXLEY",
|
||||||
"Pat Hen",
|
"Pat Hen",
|
||||||
|
"Xeeosat",
|
||||||
|
"Ed Wang",
|
||||||
"Jordan Shaw",
|
"Jordan Shaw",
|
||||||
|
"g unit",
|
||||||
"Srdb",
|
"Srdb",
|
||||||
"四糸凜音",
|
"四糸凜音",
|
||||||
"Nihongasuki",
|
"Nihongasuki",
|
||||||
"LarsesFPC",
|
|
||||||
"JC",
|
"JC",
|
||||||
"Prompt Pirate",
|
"Prompt Pirate",
|
||||||
"uwutismxd",
|
"uwutismxd",
|
||||||
"FrxzenSnxw",
|
"FrxzenSnxw",
|
||||||
"zenobeus",
|
"zenobeus",
|
||||||
"Crocket",
|
|
||||||
"Jackthemind",
|
|
||||||
"ryoma",
|
"ryoma",
|
||||||
"Stryker",
|
"Stryker",
|
||||||
"ResidentDeviant",
|
"ResidentDeviant",
|
||||||
"MudkipMedkitz",
|
"Ginnie",
|
||||||
"deanbrian",
|
|
||||||
"Alex Wortman",
|
|
||||||
"Cody",
|
|
||||||
"Raku",
|
"Raku",
|
||||||
"smart.edge5178",
|
"smart.edge5178",
|
||||||
"InformedViewz",
|
"InformedViewz",
|
||||||
@@ -415,6 +423,15 @@
|
|||||||
"SpringBootisTrash",
|
"SpringBootisTrash",
|
||||||
"carsten",
|
"carsten",
|
||||||
"ikok",
|
"ikok",
|
||||||
|
"DarkRoast",
|
||||||
|
"letzte",
|
||||||
|
"Nasty+Hobbit",
|
||||||
|
"Sora+Yori",
|
||||||
|
"lrdchs2",
|
||||||
|
"Duk3+Rand0m",
|
||||||
|
"Nathen+Choi",
|
||||||
|
"T",
|
||||||
|
"cocona",
|
||||||
"ElitaSSJ4",
|
"ElitaSSJ4",
|
||||||
"David Schenck",
|
"David Schenck",
|
||||||
"Wolfe7D1",
|
"Wolfe7D1",
|
||||||
@@ -426,7 +443,6 @@
|
|||||||
"Goldwaters",
|
"Goldwaters",
|
||||||
"Kauffy",
|
"Kauffy",
|
||||||
"Zude",
|
"Zude",
|
||||||
"John J Linehan",
|
|
||||||
"Kyler",
|
"Kyler",
|
||||||
"Edward Kennedy",
|
"Edward Kennedy",
|
||||||
"Justin Blaylock",
|
"Justin Blaylock",
|
||||||
@@ -435,17 +451,14 @@
|
|||||||
"Vane Holzer",
|
"Vane Holzer",
|
||||||
"psytrax",
|
"psytrax",
|
||||||
"Cyrus Fett",
|
"Cyrus Fett",
|
||||||
"hexxish",
|
"Xenon Xue",
|
||||||
"notedfakes",
|
"notedfakes",
|
||||||
"Billy Gladky",
|
"Billy Gladky",
|
||||||
"NICHOLAS BAXLEY",
|
|
||||||
"Michael Scott",
|
"Michael Scott",
|
||||||
"Probis",
|
"Probis",
|
||||||
"Ed Wang",
|
|
||||||
"Wes Sims",
|
"Wes Sims",
|
||||||
"ItsGeneralButtNaked",
|
"ItsGeneralButtNaked",
|
||||||
"Donor4115",
|
"Donor4115",
|
||||||
"g unit",
|
|
||||||
"Distortik",
|
"Distortik",
|
||||||
"Filippo Ferrari",
|
"Filippo Ferrari",
|
||||||
"Youguang",
|
"Youguang",
|
||||||
@@ -460,9 +473,14 @@
|
|||||||
"Mitchell Robson",
|
"Mitchell Robson",
|
||||||
"Whitepinetrader",
|
"Whitepinetrader",
|
||||||
"POPPIN",
|
"POPPIN",
|
||||||
"Ginnie",
|
|
||||||
"emadsultan",
|
|
||||||
"nanana",
|
"nanana",
|
||||||
|
"ChaChanoKo",
|
||||||
|
"ghoulars",
|
||||||
|
"null",
|
||||||
|
"Beau",
|
||||||
|
"redcarrot",
|
||||||
|
"powerbot99",
|
||||||
|
"Fthehappy",
|
||||||
"g",
|
"g",
|
||||||
"J",
|
"J",
|
||||||
"Alan+Cano",
|
"Alan+Cano",
|
||||||
@@ -474,15 +492,6 @@
|
|||||||
"quantenmecha",
|
"quantenmecha",
|
||||||
"Jason+Nash",
|
"Jason+Nash",
|
||||||
"BillyBoy84",
|
"BillyBoy84",
|
||||||
"DarkRoast",
|
|
||||||
"letzte",
|
|
||||||
"Nasty+Hobbit",
|
|
||||||
"Sora+Yori",
|
|
||||||
"lrdchs2",
|
|
||||||
"Duk3+Rand0m",
|
|
||||||
"Nathen+Choi",
|
|
||||||
"T",
|
|
||||||
"cocona",
|
|
||||||
"Buecyb99",
|
"Buecyb99",
|
||||||
"Welkor",
|
"Welkor",
|
||||||
"John Martin",
|
"John Martin",
|
||||||
@@ -491,6 +500,8 @@
|
|||||||
"moranqianlong",
|
"moranqianlong",
|
||||||
"Kalli Core",
|
"Kalli Core",
|
||||||
"Time Valentine",
|
"Time Valentine",
|
||||||
|
"Christian Schäfer",
|
||||||
|
"りん あめ",
|
||||||
"Михал Михалыч",
|
"Михал Михалыч",
|
||||||
"Matt",
|
"Matt",
|
||||||
"Frogmilk",
|
"Frogmilk",
|
||||||
@@ -501,21 +512,26 @@
|
|||||||
"Anonym dkjglfleeoeldldldlkf",
|
"Anonym dkjglfleeoeldldldlkf",
|
||||||
"Ezokewn",
|
"Ezokewn",
|
||||||
"SendingRavens",
|
"SendingRavens",
|
||||||
"Xenon Xue",
|
|
||||||
"JackJohnnyJim",
|
"JackJohnnyJim",
|
||||||
|
"TenaciousD",
|
||||||
|
"Dmitry Ryzhov",
|
||||||
|
"Khánh Đặng",
|
||||||
"Edward Ten Eyck",
|
"Edward Ten Eyck",
|
||||||
"Michael Docherty",
|
"Michael Docherty",
|
||||||
|
"Jimmy Borup",
|
||||||
"Paul Hartsuyker",
|
"Paul Hartsuyker",
|
||||||
"Henrique Faiolli",
|
|
||||||
"elitassj",
|
"elitassj",
|
||||||
"Solixer",
|
"Solixer",
|
||||||
|
"Pete Pain",
|
||||||
"Jacob Winter",
|
"Jacob Winter",
|
||||||
"Ryan Presley Ng",
|
"Ryan Presley Ng",
|
||||||
"jinksta187",
|
"jinksta187",
|
||||||
|
"RHopkirk",
|
||||||
"Andrew Wilkinson",
|
"Andrew Wilkinson",
|
||||||
"Manu Thetug",
|
"Manu Thetug",
|
||||||
"Karlanx",
|
"Karlanx",
|
||||||
"Lyavph",
|
"Lyavph",
|
||||||
|
"Maxim",
|
||||||
"David",
|
"David",
|
||||||
"Meilo",
|
"Meilo",
|
||||||
"operationancut",
|
"operationancut",
|
||||||
@@ -537,6 +553,17 @@
|
|||||||
"Scott",
|
"Scott",
|
||||||
"Muratoraccio",
|
"Muratoraccio",
|
||||||
"D",
|
"D",
|
||||||
|
"2turbo",
|
||||||
|
"Somebody",
|
||||||
|
"Balut+Omelette",
|
||||||
|
"Dmitry+Viznesenskiy",
|
||||||
|
"tanjin90",
|
||||||
|
"sternenkrieger",
|
||||||
|
"eriick",
|
||||||
|
"Patrick+Bryan",
|
||||||
|
"Pascalou",
|
||||||
|
"lighthawke",
|
||||||
|
"Lev+Lanevskiy",
|
||||||
"low9",
|
"low9",
|
||||||
"Winged",
|
"Winged",
|
||||||
"YassineKhaled",
|
"YassineKhaled",
|
||||||
@@ -552,13 +579,6 @@
|
|||||||
"Alex",
|
"Alex",
|
||||||
"Jacky+Ho",
|
"Jacky+Ho",
|
||||||
"Karru",
|
"Karru",
|
||||||
"ghoulars",
|
|
||||||
"ChaChanoKo",
|
|
||||||
"null",
|
|
||||||
"Beau",
|
|
||||||
"redcarrot",
|
|
||||||
"powerbot99",
|
|
||||||
"Fthehappy",
|
|
||||||
"generic404",
|
"generic404",
|
||||||
"abattoirblues",
|
"abattoirblues",
|
||||||
"zounik",
|
"zounik",
|
||||||
@@ -568,9 +588,10 @@
|
|||||||
"Bob Barker",
|
"Bob Barker",
|
||||||
"edk",
|
"edk",
|
||||||
"Tú Nguyễn Lý Hoàng",
|
"Tú Nguyễn Lý Hoàng",
|
||||||
|
"shira1011",
|
||||||
|
"Ben D",
|
||||||
|
"G",
|
||||||
"Ronan Delevacq",
|
"Ronan Delevacq",
|
||||||
"Christian Schäfer",
|
|
||||||
"りん あめ",
|
|
||||||
"ja s",
|
"ja s",
|
||||||
"Doug Mason",
|
"Doug Mason",
|
||||||
"Jeremy Townsend",
|
"Jeremy Townsend",
|
||||||
@@ -580,38 +601,41 @@
|
|||||||
"Sean voets",
|
"Sean voets",
|
||||||
"Owen Gwosdz",
|
"Owen Gwosdz",
|
||||||
"Jarrid Lee",
|
"Jarrid Lee",
|
||||||
|
"Poophead27 Blyat",
|
||||||
"Kor",
|
"Kor",
|
||||||
"Joseph Hanson",
|
"Joseph Hanson",
|
||||||
"John Rednoulf",
|
"John Rednoulf",
|
||||||
|
"Spire",
|
||||||
"Boba Smith",
|
"Boba Smith",
|
||||||
"Devil Lude",
|
"Devil Lude",
|
||||||
"David Murcko",
|
"David Murcko",
|
||||||
"MR.Bear",
|
"MR.Bear",
|
||||||
"Jack Dole",
|
"Jack Dole",
|
||||||
|
"somethingtosay8",
|
||||||
|
"ivistorm",
|
||||||
"max blo",
|
"max blo",
|
||||||
"Sauv",
|
"Sauv",
|
||||||
"Steven",
|
"Steven",
|
||||||
"CptNeo",
|
"CptNeo",
|
||||||
"TenaciousD",
|
|
||||||
"Dmitry Ryzhov",
|
|
||||||
"Khánh Đặng",
|
|
||||||
"Maso",
|
"Maso",
|
||||||
|
"Ted Cart",
|
||||||
|
"Sage Himeros",
|
||||||
"Eric Ketchum",
|
"Eric Ketchum",
|
||||||
"Kevin Wallace",
|
"Kevin Wallace",
|
||||||
"Jimmy Borup",
|
"David Spearing",
|
||||||
"ChicRic",
|
"ChicRic",
|
||||||
"Tigon",
|
"Tigon",
|
||||||
"BastardSama",
|
"BastardSama",
|
||||||
"mercur",
|
"mercur",
|
||||||
"Pete Pain",
|
"Tania Nayelli Fernandez",
|
||||||
"RHopkirk",
|
"Draconach",
|
||||||
"Yavizu3d",
|
"Yavizu3d",
|
||||||
"Maxim",
|
|
||||||
"Yves Poezevara",
|
"Yves Poezevara",
|
||||||
"Teriak47",
|
"Teriak47",
|
||||||
"Just me",
|
"Just me",
|
||||||
"Raf Stahelin",
|
"Raf Stahelin",
|
||||||
"Вячеслав Маринин",
|
"Вячеслав Маринин",
|
||||||
|
"Dkommander22",
|
||||||
"Cola Matthew",
|
"Cola Matthew",
|
||||||
"OniNoKen",
|
"OniNoKen",
|
||||||
"Iain Wisely",
|
"Iain Wisely",
|
||||||
@@ -655,6 +679,17 @@
|
|||||||
"SelfishMedic",
|
"SelfishMedic",
|
||||||
"adderleighn",
|
"adderleighn",
|
||||||
"EnragedAntelope",
|
"EnragedAntelope",
|
||||||
|
"Monix",
|
||||||
|
"Trolinka",
|
||||||
|
"IshouI;_;",
|
||||||
|
"PredragR",
|
||||||
|
"Clauzmak",
|
||||||
|
"Nerick",
|
||||||
|
"JoL",
|
||||||
|
"Gold_miner_ego",
|
||||||
|
"SundayRage",
|
||||||
|
"YoruHime",
|
||||||
|
"matter",
|
||||||
"SRCRCOSS",
|
"SRCRCOSS",
|
||||||
"imer",
|
"imer",
|
||||||
"Akkas+Haque",
|
"Akkas+Haque",
|
||||||
@@ -675,18 +710,8 @@
|
|||||||
"Sildoren",
|
"Sildoren",
|
||||||
"Darvidous",
|
"Darvidous",
|
||||||
"Seon+Song",
|
"Seon+Song",
|
||||||
"2turbo",
|
|
||||||
"balut+omelette",
|
|
||||||
"Nebuleux",
|
"Nebuleux",
|
||||||
"Dmitry+Viznesenskiy",
|
|
||||||
"Tanjin90",
|
|
||||||
"Somebody",
|
|
||||||
"sternenkrieger",
|
|
||||||
"eriick",
|
|
||||||
"Join+Chun",
|
"Join+Chun",
|
||||||
"Pascalou",
|
|
||||||
"lighthawke",
|
|
||||||
"Terraformer",
|
|
||||||
"GDS+DEV",
|
"GDS+DEV",
|
||||||
"4rt+r3d",
|
"4rt+r3d",
|
||||||
"you+halo9",
|
"you+halo9",
|
||||||
@@ -712,17 +737,16 @@
|
|||||||
"_ G3n",
|
"_ G3n",
|
||||||
"Donovan Jenkins",
|
"Donovan Jenkins",
|
||||||
"Hans Meier",
|
"Hans Meier",
|
||||||
"shira1011",
|
|
||||||
"sicarius",
|
"sicarius",
|
||||||
"Michael Eid",
|
"Michael Eid",
|
||||||
|
"Wolf and Fox Legends",
|
||||||
"beersandbacon",
|
"beersandbacon",
|
||||||
"Neko Desco",
|
"Neko Desco",
|
||||||
"Bob barker",
|
"Bob barker",
|
||||||
"Ben D",
|
|
||||||
"Ninja Tom",
|
"Ninja Tom",
|
||||||
"G",
|
|
||||||
"karim ben brik",
|
"karim ben brik",
|
||||||
"Vinarus",
|
"Vinarus",
|
||||||
|
"Josh Snyder",
|
||||||
"Michael Zhu",
|
"Michael Zhu",
|
||||||
"Nemisu",
|
"Nemisu",
|
||||||
"Seraphy",
|
"Seraphy",
|
||||||
@@ -732,41 +756,42 @@
|
|||||||
"jumpd",
|
"jumpd",
|
||||||
"John C",
|
"John C",
|
||||||
"Rim",
|
"Rim",
|
||||||
|
"Room Light",
|
||||||
"Jairus Knudsen",
|
"Jairus Knudsen",
|
||||||
"Poophead27 Blyat",
|
|
||||||
"Xan Dionysus",
|
"Xan Dionysus",
|
||||||
|
"Patryk Serious",
|
||||||
"Nathan lee",
|
"Nathan lee",
|
||||||
"Lyle Liston",
|
"lylepaul",
|
||||||
"Middo",
|
"Middo",
|
||||||
"Forbidden Atelier",
|
"Forbidden Atelier",
|
||||||
"Thomas Sankowski",
|
"Thomas Sankowski",
|
||||||
"Spire",
|
|
||||||
"DrB",
|
"DrB",
|
||||||
"AZ Party Oasis",
|
"AZ Party Oasis",
|
||||||
"Adictedtohumping",
|
"Adictedtohumping",
|
||||||
|
"Snorklebort",
|
||||||
"Towelie",
|
"Towelie",
|
||||||
"TheFusion",
|
"TheFusion",
|
||||||
"matt",
|
"matt",
|
||||||
"dsffsdfsdfsdfsdfsdf",
|
"dsffsdfsdfsdfsdfsdf",
|
||||||
"somethingtosay8",
|
|
||||||
"Jean-françois SEMA",
|
"Jean-françois SEMA",
|
||||||
"3zS4QNQ4",
|
"3zS4QNQ4",
|
||||||
"Terminuz",
|
"Terminuz",
|
||||||
"Kurt",
|
"Kurt",
|
||||||
"ivistorm",
|
|
||||||
"Matt M.",
|
"Matt M.",
|
||||||
"Ivan Imes",
|
"Ivan Imes",
|
||||||
|
"J M",
|
||||||
|
"Bouya shaka",
|
||||||
"Faburizu",
|
"Faburizu",
|
||||||
"Jack Lawfield",
|
"Jack Lawfield",
|
||||||
"jimyjomson",
|
"jimyjomson",
|
||||||
"Borte",
|
"Borte",
|
||||||
|
"JaeHyun Jang",
|
||||||
"Chase Kwon",
|
"Chase Kwon",
|
||||||
"Ted Cart",
|
"yyuvuvu",
|
||||||
"Sage Himeros",
|
|
||||||
"Inyoshu",
|
"Inyoshu",
|
||||||
"Chad Barnes",
|
"Chad Barnes",
|
||||||
"Person Y",
|
"Person Y",
|
||||||
"David Spearing",
|
"Nomki",
|
||||||
"James Ming",
|
"James Ming",
|
||||||
"vanditking",
|
"vanditking",
|
||||||
"kripitonga",
|
"kripitonga",
|
||||||
@@ -787,5 +812,5 @@
|
|||||||
"Somebody",
|
"Somebody",
|
||||||
"CK"
|
"CK"
|
||||||
],
|
],
|
||||||
"totalCount": 784
|
"totalCount": 809
|
||||||
}
|
}
|
||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "Alle Syntax kopieren",
|
"copyAll": "Alle Syntax kopieren",
|
||||||
"refreshAll": "Alle Metadaten aktualisieren",
|
"refreshAll": "Alle Metadaten aktualisieren",
|
||||||
"repairMetadata": "Metadaten der Auswahl reparieren",
|
"repairMetadata": "Metadaten der Auswahl reparieren",
|
||||||
"reimportMetadata": "Metadaten der Auswahl neu importieren",
|
"reimportMetadata": "Aus Quelle neu importieren",
|
||||||
"checkUpdates": "Auswahl auf Updates prüfen",
|
"checkUpdates": "Auswahl auf Updates prüfen",
|
||||||
"moveAll": "Alle in Ordner verschieben",
|
"moveAll": "Alle in Ordner verschieben",
|
||||||
"autoOrganize": "Automatisch organisieren",
|
"autoOrganize": "Automatisch organisieren",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Stammverzeichnis",
|
"modelRoot": "Stammverzeichnis",
|
||||||
|
"moreOptions": "Weitere Optionen",
|
||||||
"collapseAll": "Alle Ordner einklappen",
|
"collapseAll": "Alle Ordner einklappen",
|
||||||
"pinSidebar": "Sidebar anheften",
|
"pinSidebar": "Sidebar anheften",
|
||||||
"unpinSidebar": "Sidebar lösen",
|
"unpinSidebar": "Sidebar lösen",
|
||||||
|
"hideOnThisPage": "Seitenleiste auf dieser Seite ausblenden",
|
||||||
|
"showSidebar": "Seitenleiste anzeigen",
|
||||||
|
"sidebarHiddenNotification": "Seitenleiste auf der Seite {page} ausgeblendet",
|
||||||
"switchToListView": "Zur Listenansicht wechseln",
|
"switchToListView": "Zur Listenansicht wechseln",
|
||||||
"switchToTreeView": "Zur Baumansicht wechseln",
|
"switchToTreeView": "Zur Baumansicht wechseln",
|
||||||
"recursiveOn": "Unterordner einbeziehen",
|
"recursiveOn": "Unterordner einbeziehen",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "Copy Selected Syntax",
|
"copyAll": "Copy Selected Syntax",
|
||||||
"refreshAll": "Refresh Selected Metadata",
|
"refreshAll": "Refresh Selected Metadata",
|
||||||
"repairMetadata": "Repair Metadata for Selected",
|
"repairMetadata": "Repair Metadata for Selected",
|
||||||
"reimportMetadata": "Re-import Metadata for Selected",
|
"reimportMetadata": "Re-import from Source",
|
||||||
"checkUpdates": "Check Updates for Selected",
|
"checkUpdates": "Check Updates for Selected",
|
||||||
"moveAll": "Move Selected to Folder",
|
"moveAll": "Move Selected to Folder",
|
||||||
"autoOrganize": "Auto-Organize Selected",
|
"autoOrganize": "Auto-Organize Selected",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Root",
|
"modelRoot": "Root",
|
||||||
|
"moreOptions": "More options",
|
||||||
"collapseAll": "Collapse All Folders",
|
"collapseAll": "Collapse All Folders",
|
||||||
"pinSidebar": "Pin Sidebar",
|
"pinSidebar": "Pin Sidebar",
|
||||||
"unpinSidebar": "Unpin Sidebar",
|
"unpinSidebar": "Unpin Sidebar",
|
||||||
|
"hideOnThisPage": "Hide sidebar on this page",
|
||||||
|
"showSidebar": "Show sidebar",
|
||||||
|
"sidebarHiddenNotification": "Folder sidebar hidden on {page} page",
|
||||||
"switchToListView": "Switch to List View",
|
"switchToListView": "Switch to List View",
|
||||||
"switchToTreeView": "Switch to Tree View",
|
"switchToTreeView": "Switch to Tree View",
|
||||||
"recursiveOn": "Include subfolders",
|
"recursiveOn": "Include subfolders",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "Copiar toda la sintaxis",
|
"copyAll": "Copiar toda la sintaxis",
|
||||||
"refreshAll": "Actualizar todos los metadatos",
|
"refreshAll": "Actualizar todos los metadatos",
|
||||||
"repairMetadata": "Reparar metadatos de la selección",
|
"repairMetadata": "Reparar metadatos de la selección",
|
||||||
"reimportMetadata": "Reimportar metadatos de la selección",
|
"reimportMetadata": "Reimportar desde origen",
|
||||||
"checkUpdates": "Comprobar actualizaciones para la selección",
|
"checkUpdates": "Comprobar actualizaciones para la selección",
|
||||||
"moveAll": "Mover todos a carpeta",
|
"moveAll": "Mover todos a carpeta",
|
||||||
"autoOrganize": "Auto-organizar seleccionados",
|
"autoOrganize": "Auto-organizar seleccionados",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Raíz",
|
"modelRoot": "Raíz",
|
||||||
|
"moreOptions": "Más opciones",
|
||||||
"collapseAll": "Colapsar todas las carpetas",
|
"collapseAll": "Colapsar todas las carpetas",
|
||||||
"pinSidebar": "Fijar barra lateral",
|
"pinSidebar": "Fijar barra lateral",
|
||||||
"unpinSidebar": "Desfijar barra lateral",
|
"unpinSidebar": "Desfijar barra lateral",
|
||||||
|
"hideOnThisPage": "Ocultar barra lateral en esta página",
|
||||||
|
"showSidebar": "Mostrar barra lateral",
|
||||||
|
"sidebarHiddenNotification": "Barra lateral oculta en la página {page}",
|
||||||
"switchToListView": "Cambiar a vista de lista",
|
"switchToListView": "Cambiar a vista de lista",
|
||||||
"switchToTreeView": "Cambiar a vista de árbol",
|
"switchToTreeView": "Cambiar a vista de árbol",
|
||||||
"recursiveOn": "Incluir subcarpetas",
|
"recursiveOn": "Incluir subcarpetas",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "Copier toute la syntaxe",
|
"copyAll": "Copier toute la syntaxe",
|
||||||
"refreshAll": "Actualiser toutes les métadonnées",
|
"refreshAll": "Actualiser toutes les métadonnées",
|
||||||
"repairMetadata": "Réparer les métadonnées de la sélection",
|
"repairMetadata": "Réparer les métadonnées de la sélection",
|
||||||
"reimportMetadata": "Ré-importer les métadonnées de la sélection",
|
"reimportMetadata": "Ré-importer depuis la source",
|
||||||
"checkUpdates": "Vérifier les mises à jour pour la sélection",
|
"checkUpdates": "Vérifier les mises à jour pour la sélection",
|
||||||
"moveAll": "Déplacer tout vers un dossier",
|
"moveAll": "Déplacer tout vers un dossier",
|
||||||
"autoOrganize": "Auto-organiser la sélection",
|
"autoOrganize": "Auto-organiser la sélection",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Racine",
|
"modelRoot": "Racine",
|
||||||
|
"moreOptions": "Plus d'options",
|
||||||
"collapseAll": "Réduire tous les dossiers",
|
"collapseAll": "Réduire tous les dossiers",
|
||||||
"pinSidebar": "Épingler la barre latérale",
|
"pinSidebar": "Épingler la barre latérale",
|
||||||
"unpinSidebar": "Désépingler la barre latérale",
|
"unpinSidebar": "Désépingler la barre latérale",
|
||||||
|
"hideOnThisPage": "Masquer la barre latérale sur cette page",
|
||||||
|
"showSidebar": "Afficher la barre latérale",
|
||||||
|
"sidebarHiddenNotification": "Barre latérale masquée sur la page {page}",
|
||||||
"switchToListView": "Passer en vue liste",
|
"switchToListView": "Passer en vue liste",
|
||||||
"switchToTreeView": "Passer en vue arborescence",
|
"switchToTreeView": "Passer en vue arborescence",
|
||||||
"recursiveOn": "Inclure les sous-dossiers",
|
"recursiveOn": "Inclure les sous-dossiers",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "העתק את כל התחבירים",
|
"copyAll": "העתק את כל התחבירים",
|
||||||
"refreshAll": "רענן את כל המטא-דאטה",
|
"refreshAll": "רענן את כל המטא-דאטה",
|
||||||
"repairMetadata": "תקן מטא-דאטה עבור הנבחרים",
|
"repairMetadata": "תקן מטא-דאטה עבור הנבחרים",
|
||||||
"reimportMetadata": "ייבא מחדש מטא-דאטה עבור הנבחרים",
|
"reimportMetadata": "ייבא מחדש ממקור",
|
||||||
"checkUpdates": "בדוק עדכונים לבחירה",
|
"checkUpdates": "בדוק עדכונים לבחירה",
|
||||||
"moveAll": "העבר הכל לתיקייה",
|
"moveAll": "העבר הכל לתיקייה",
|
||||||
"autoOrganize": "ארגן אוטומטית נבחרים",
|
"autoOrganize": "ארגן אוטומטית נבחרים",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "שורש",
|
"modelRoot": "שורש",
|
||||||
|
"moreOptions": "אפשרויות נוספות",
|
||||||
"collapseAll": "כווץ את כל התיקיות",
|
"collapseAll": "כווץ את כל התיקיות",
|
||||||
"pinSidebar": "נעל סרגל צד",
|
"pinSidebar": "נעל סרגל צד",
|
||||||
"unpinSidebar": "שחרר סרגל צד",
|
"unpinSidebar": "שחרר סרגל צד",
|
||||||
|
"hideOnThisPage": "הסתר סרגל צד בדף זה",
|
||||||
|
"showSidebar": "הצג סרגל צד",
|
||||||
|
"sidebarHiddenNotification": "סרגל הצד מוסתר בדף {page}",
|
||||||
"switchToListView": "עבור לתצוגת רשימה",
|
"switchToListView": "עבור לתצוגת רשימה",
|
||||||
"switchToTreeView": "תצוגת עץ",
|
"switchToTreeView": "תצוגת עץ",
|
||||||
"recursiveOn": "כלול תיקיות משנה",
|
"recursiveOn": "כלול תיקיות משנה",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "すべての構文をコピー",
|
"copyAll": "すべての構文をコピー",
|
||||||
"refreshAll": "すべてのメタデータを更新",
|
"refreshAll": "すべてのメタデータを更新",
|
||||||
"repairMetadata": "選択したレシピのメタデータを修復",
|
"repairMetadata": "選択したレシピのメタデータを修復",
|
||||||
"reimportMetadata": "選択したレシピを再インポート",
|
"reimportMetadata": "ソースから再インポート",
|
||||||
"checkUpdates": "選択項目の更新を確認",
|
"checkUpdates": "選択項目の更新を確認",
|
||||||
"moveAll": "すべてをフォルダに移動",
|
"moveAll": "すべてをフォルダに移動",
|
||||||
"autoOrganize": "自動整理を実行",
|
"autoOrganize": "自動整理を実行",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "ルート",
|
"modelRoot": "ルート",
|
||||||
|
"moreOptions": "その他のオプション",
|
||||||
"collapseAll": "すべてのフォルダを折りたたむ",
|
"collapseAll": "すべてのフォルダを折りたたむ",
|
||||||
"pinSidebar": "サイドバーを固定",
|
"pinSidebar": "サイドバーを固定",
|
||||||
"unpinSidebar": "サイドバーの固定を解除",
|
"unpinSidebar": "サイドバーの固定を解除",
|
||||||
|
"hideOnThisPage": "このページでサイドバーを非表示",
|
||||||
|
"showSidebar": "サイドバーを表示",
|
||||||
|
"sidebarHiddenNotification": "{page}ページでサイドバーが非表示になっています",
|
||||||
"switchToListView": "リストビューに切り替え",
|
"switchToListView": "リストビューに切り替え",
|
||||||
"switchToTreeView": "ツリー表示に切り替え",
|
"switchToTreeView": "ツリー表示に切り替え",
|
||||||
"recursiveOn": "サブフォルダーを含める",
|
"recursiveOn": "サブフォルダーを含める",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "모든 문법 복사",
|
"copyAll": "모든 문법 복사",
|
||||||
"refreshAll": "모든 메타데이터 새로고침",
|
"refreshAll": "모든 메타데이터 새로고침",
|
||||||
"repairMetadata": "선택한 레시피 메타데이터 복구",
|
"repairMetadata": "선택한 레시피 메타데이터 복구",
|
||||||
"reimportMetadata": "선택한 레시피 다시 가져오기",
|
"reimportMetadata": "소스에서 다시 가져오기",
|
||||||
"checkUpdates": "선택 항목 업데이트 확인",
|
"checkUpdates": "선택 항목 업데이트 확인",
|
||||||
"moveAll": "모두 폴더로 이동",
|
"moveAll": "모두 폴더로 이동",
|
||||||
"autoOrganize": "자동 정리 선택",
|
"autoOrganize": "자동 정리 선택",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "루트",
|
"modelRoot": "루트",
|
||||||
|
"moreOptions": "더 많은 옵션",
|
||||||
"collapseAll": "모든 폴더 접기",
|
"collapseAll": "모든 폴더 접기",
|
||||||
"pinSidebar": "사이드바 고정",
|
"pinSidebar": "사이드바 고정",
|
||||||
"unpinSidebar": "사이드바 고정 해제",
|
"unpinSidebar": "사이드바 고정 해제",
|
||||||
|
"hideOnThisPage": "이 페이지에서 사이드바 숨기기",
|
||||||
|
"showSidebar": "사이드바 표시",
|
||||||
|
"sidebarHiddenNotification": "{page} 페이지에서 사이드바가 숨겨져 있습니다",
|
||||||
"switchToListView": "목록 보기로 전환",
|
"switchToListView": "목록 보기로 전환",
|
||||||
"switchToTreeView": "트리 보기로 전환",
|
"switchToTreeView": "트리 보기로 전환",
|
||||||
"recursiveOn": "하위 폴더 포함",
|
"recursiveOn": "하위 폴더 포함",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "Копировать весь синтаксис",
|
"copyAll": "Копировать весь синтаксис",
|
||||||
"refreshAll": "Обновить все метаданные",
|
"refreshAll": "Обновить все метаданные",
|
||||||
"repairMetadata": "Восстановить метаданные для выбранных",
|
"repairMetadata": "Восстановить метаданные для выбранных",
|
||||||
"reimportMetadata": "Переимпортировать метаданные для выбранных",
|
"reimportMetadata": "Переимпортировать из источника",
|
||||||
"checkUpdates": "Проверить обновления для выбранных",
|
"checkUpdates": "Проверить обновления для выбранных",
|
||||||
"moveAll": "Переместить все в папку",
|
"moveAll": "Переместить все в папку",
|
||||||
"autoOrganize": "Автоматически организовать выбранные",
|
"autoOrganize": "Автоматически организовать выбранные",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Корень",
|
"modelRoot": "Корень",
|
||||||
|
"moreOptions": "Дополнительные параметры",
|
||||||
"collapseAll": "Свернуть все папки",
|
"collapseAll": "Свернуть все папки",
|
||||||
"pinSidebar": "Закрепить боковую панель",
|
"pinSidebar": "Закрепить боковую панель",
|
||||||
"unpinSidebar": "Открепить боковую панель",
|
"unpinSidebar": "Открепить боковую панель",
|
||||||
|
"hideOnThisPage": "Скрыть боковую панель на этой странице",
|
||||||
|
"showSidebar": "Показать боковую панель",
|
||||||
|
"sidebarHiddenNotification": "Боковая панель скрыта на странице {page}",
|
||||||
"switchToListView": "Переключить на вид списка",
|
"switchToListView": "Переключить на вид списка",
|
||||||
"switchToTreeView": "Переключить на древовидный вид",
|
"switchToTreeView": "Переключить на древовидный вид",
|
||||||
"recursiveOn": "Включать вложенные папки",
|
"recursiveOn": "Включать вложенные папки",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "复制所选中语法",
|
"copyAll": "复制所选中语法",
|
||||||
"refreshAll": "刷新所选中元数据",
|
"refreshAll": "刷新所选中元数据",
|
||||||
"repairMetadata": "修复所选中元数据",
|
"repairMetadata": "修复所选中元数据",
|
||||||
"reimportMetadata": "重新导入所选配方元数据",
|
"reimportMetadata": "从源重新导入",
|
||||||
"checkUpdates": "检查所选更新",
|
"checkUpdates": "检查所选更新",
|
||||||
"moveAll": "移动所选中到文件夹",
|
"moveAll": "移动所选中到文件夹",
|
||||||
"autoOrganize": "自动整理所选模型",
|
"autoOrganize": "自动整理所选模型",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "根目录",
|
"modelRoot": "根目录",
|
||||||
|
"moreOptions": "更多选项",
|
||||||
"collapseAll": "折叠所有文件夹",
|
"collapseAll": "折叠所有文件夹",
|
||||||
"pinSidebar": "固定侧边栏",
|
"pinSidebar": "固定侧边栏",
|
||||||
"unpinSidebar": "取消固定侧边栏",
|
"unpinSidebar": "取消固定侧边栏",
|
||||||
|
"hideOnThisPage": "隐藏此页面侧边栏",
|
||||||
|
"showSidebar": "显示侧边栏",
|
||||||
|
"sidebarHiddenNotification": "{page}页面的文件夹侧边栏已隐藏",
|
||||||
"switchToListView": "切换到列表视图",
|
"switchToListView": "切换到列表视图",
|
||||||
"switchToTreeView": "切换到树状视图",
|
"switchToTreeView": "切换到树状视图",
|
||||||
"recursiveOn": "包含子文件夹",
|
"recursiveOn": "包含子文件夹",
|
||||||
|
|||||||
@@ -693,7 +693,7 @@
|
|||||||
"copyAll": "複製全部語法",
|
"copyAll": "複製全部語法",
|
||||||
"refreshAll": "刷新全部 metadata",
|
"refreshAll": "刷新全部 metadata",
|
||||||
"repairMetadata": "修復所選中元數據",
|
"repairMetadata": "修復所選中元數據",
|
||||||
"reimportMetadata": "重新匯入所選配方元數據",
|
"reimportMetadata": "從來源重新匯入",
|
||||||
"checkUpdates": "檢查所選更新",
|
"checkUpdates": "檢查所選更新",
|
||||||
"moveAll": "全部移動到資料夾",
|
"moveAll": "全部移動到資料夾",
|
||||||
"autoOrganize": "自動整理所選模型",
|
"autoOrganize": "自動整理所選模型",
|
||||||
@@ -953,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "根目錄",
|
"modelRoot": "根目錄",
|
||||||
|
"moreOptions": "更多選項",
|
||||||
"collapseAll": "全部摺疊資料夾",
|
"collapseAll": "全部摺疊資料夾",
|
||||||
"pinSidebar": "固定側邊欄",
|
"pinSidebar": "固定側邊欄",
|
||||||
"unpinSidebar": "取消固定側邊欄",
|
"unpinSidebar": "取消固定側邊欄",
|
||||||
|
"hideOnThisPage": "隱藏此頁面側邊欄",
|
||||||
|
"showSidebar": "顯示側邊欄",
|
||||||
|
"sidebarHiddenNotification": "{page}頁面的資料夾側邊欄已隱藏",
|
||||||
"switchToListView": "切換至列表檢視",
|
"switchToListView": "切換至列表檢視",
|
||||||
"switchToTreeView": "切換到樹狀檢視",
|
"switchToTreeView": "切換到樹狀檢視",
|
||||||
"recursiveOn": "包含子資料夾",
|
"recursiveOn": "包含子資料夾",
|
||||||
|
|||||||
@@ -189,6 +189,10 @@ class LoraManager:
|
|||||||
|
|
||||||
# Register DownloadManager with ServiceRegistry
|
# Register DownloadManager with ServiceRegistry
|
||||||
await ServiceRegistry.get_download_manager()
|
await ServiceRegistry.get_download_manager()
|
||||||
|
|
||||||
|
# Initialize DownloadQueueService for persistent queue/history
|
||||||
|
await ServiceRegistry.get_download_queue_service()
|
||||||
|
|
||||||
await ServiceRegistry.get_backup_service()
|
await ServiceRegistry.get_backup_service()
|
||||||
|
|
||||||
from .services.metadata_service import initialize_metadata_providers
|
from .services.metadata_service import initialize_metadata_providers
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from ...services.use_cases import (
|
|||||||
)
|
)
|
||||||
from ...services.websocket_manager import WebSocketManager
|
from ...services.websocket_manager import WebSocketManager
|
||||||
from ...services.websocket_progress_callback import WebSocketProgressCallback
|
from ...services.websocket_progress_callback import WebSocketProgressCallback
|
||||||
|
from ...services.download_queue_service import DownloadQueueService
|
||||||
from ...services.errors import RateLimitError, ResourceNotFoundError
|
from ...services.errors import RateLimitError, ResourceNotFoundError
|
||||||
from ...utils.civitai_utils import resolve_license_payload
|
from ...utils.civitai_utils import resolve_license_payload
|
||||||
from ...utils.file_utils import calculate_sha256
|
from ...utils.file_utils import calculate_sha256
|
||||||
@@ -1567,6 +1568,255 @@ class ModelDownloadHandler:
|
|||||||
)
|
)
|
||||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Download queue / history handlers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def get_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
queue = await service.get_queue()
|
||||||
|
stats = await service.get_stats()
|
||||||
|
return web.json_response({"success": True, "queue": queue, "stats": stats})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error getting download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def add_to_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
download_id = request.query.get("download_id") or str(uuid.uuid4())
|
||||||
|
model_id_str = request.query.get("model_id")
|
||||||
|
model_version_id_str = request.query.get("model_version_id")
|
||||||
|
model_name = request.query.get("model_name", "")
|
||||||
|
version_name = request.query.get("version_name", "")
|
||||||
|
thumbnail_url = request.query.get("thumbnail_url", "")
|
||||||
|
source = request.query.get("source")
|
||||||
|
file_params_json = request.query.get("file_params")
|
||||||
|
|
||||||
|
model_id = int(model_id_str) if model_id_str else None
|
||||||
|
model_version_id = int(model_version_id_str) if model_version_id_str else None
|
||||||
|
file_params = json.loads(file_params_json) if file_params_json else None
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
item = await service.add_to_queue(
|
||||||
|
download_id=download_id,
|
||||||
|
model_id=model_id,
|
||||||
|
model_version_id=model_version_id,
|
||||||
|
model_name=model_name,
|
||||||
|
version_name=version_name,
|
||||||
|
thumbnail_url=thumbnail_url,
|
||||||
|
source=source,
|
||||||
|
file_params=file_params,
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "item": item})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error adding to download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def remove_from_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
removed = await service.remove_from_queue(download_id)
|
||||||
|
return web.json_response({"success": removed})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error removing from download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def move_queue_item_to_top(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
moved = await service.move_to_top(download_id)
|
||||||
|
return web.json_response({"success": moved})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error moving queue item to top: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def move_queue_item_to_end(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
moved = await service.move_to_end(download_id)
|
||||||
|
return web.json_response({"success": moved})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error moving queue item to end: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def clear_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
status_filter = request.query.get("status") or None
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
cleared = await service.clear_queue(status_filter=status_filter)
|
||||||
|
return web.json_response({"success": True, "cleared": cleared})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error clearing download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def get_download_history(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
limit = min(int(request.query.get("limit", "50")), 500)
|
||||||
|
offset = int(request.query.get("offset", "0"))
|
||||||
|
status_filter = request.query.get("status") or None
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
result = await service.get_history(
|
||||||
|
limit=limit, offset=offset, status_filter=status_filter
|
||||||
|
)
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"items": result["items"],
|
||||||
|
"total": result["total"],
|
||||||
|
"limit": result["limit"],
|
||||||
|
"offset": result["offset"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error getting download history: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def clear_download_history(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
status_filter = request.query.get("status") or None
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
cleared = await service.clear_history(status_filter=status_filter)
|
||||||
|
return web.json_response({"success": True, "cleared": cleared})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error clearing download history: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def delete_download_history_item(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
item_id = int(request.query.get("id", "0"))
|
||||||
|
if not item_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
deleted = await service.delete_history_item(item_id)
|
||||||
|
return web.json_response({"success": deleted})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error deleting download history item: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def retry_download_from_history(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
item_id = int(request.query.get("id", "0"))
|
||||||
|
if not item_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
item = await service.retry_from_history(item_id)
|
||||||
|
if item is None:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "History item not found or not retryable"},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "item": item})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error retrying download from history: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def retry_all_failed_downloads(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
retry_count = await service.retry_all_failed()
|
||||||
|
return web.json_response({"success": True, "retry_count": retry_count})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error retrying all failed downloads: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def complete_download_in_queue(self, request: web.Request) -> web.Response:
|
||||||
|
"""Atomically move a download from queue to history with terminal status."""
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
status = request.query.get("status", "completed")
|
||||||
|
error = request.query.get("error")
|
||||||
|
file_path = request.query.get("file_path")
|
||||||
|
try:
|
||||||
|
bytes_downloaded = int(request.query.get("bytes_downloaded", "0"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
bytes_downloaded = 0
|
||||||
|
total_bytes_raw = request.query.get("total_bytes")
|
||||||
|
total_bytes = int(total_bytes_raw) if total_bytes_raw else None
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
item = await service.complete_download(
|
||||||
|
download_id=download_id,
|
||||||
|
status=status,
|
||||||
|
error=error,
|
||||||
|
file_path=file_path,
|
||||||
|
bytes_downloaded=bytes_downloaded,
|
||||||
|
total_bytes=total_bytes,
|
||||||
|
)
|
||||||
|
if item is None:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Download not found in queue"}, status=404
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "item": item})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error completing download: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def get_download_stats(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
stats = await service.get_stats()
|
||||||
|
return web.json_response({"success": True, "stats": stats})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error getting download stats: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class ModelCivitaiHandler:
|
class ModelCivitaiHandler:
|
||||||
"""CivitAI integration endpoints."""
|
"""CivitAI integration endpoints."""
|
||||||
@@ -2596,6 +2846,19 @@ class ModelHandlerSet:
|
|||||||
"pause_download_get": self.download.pause_download_get,
|
"pause_download_get": self.download.pause_download_get,
|
||||||
"resume_download_get": self.download.resume_download_get,
|
"resume_download_get": self.download.resume_download_get,
|
||||||
"get_download_progress": self.download.get_download_progress,
|
"get_download_progress": self.download.get_download_progress,
|
||||||
|
"get_download_queue": self.download.get_download_queue,
|
||||||
|
"add_to_download_queue": self.download.add_to_download_queue,
|
||||||
|
"remove_from_download_queue": self.download.remove_from_download_queue,
|
||||||
|
"move_queue_item_to_top": self.download.move_queue_item_to_top,
|
||||||
|
"move_queue_item_to_end": self.download.move_queue_item_to_end,
|
||||||
|
"clear_download_queue": self.download.clear_download_queue,
|
||||||
|
"get_download_history": self.download.get_download_history,
|
||||||
|
"clear_download_history": self.download.clear_download_history,
|
||||||
|
"delete_download_history_item": self.download.delete_download_history_item,
|
||||||
|
"retry_download_from_history": self.download.retry_download_from_history,
|
||||||
|
"retry_all_failed_downloads": self.download.retry_all_failed_downloads,
|
||||||
|
"complete_download_in_queue": self.download.complete_download_in_queue,
|
||||||
|
"get_download_stats": self.download.get_download_stats,
|
||||||
"get_civitai_versions": self.civitai.get_civitai_versions,
|
"get_civitai_versions": self.civitai.get_civitai_versions,
|
||||||
"get_civitai_model_by_version": self.civitai.get_civitai_model_by_version,
|
"get_civitai_model_by_version": self.civitai.get_civitai_model_by_version,
|
||||||
"get_civitai_model_by_hash": self.civitai.get_civitai_model_by_hash,
|
"get_civitai_model_by_hash": self.civitai.get_civitai_model_by_hash,
|
||||||
|
|||||||
@@ -107,6 +107,37 @@ COMMON_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|||||||
RouteDefinition(
|
RouteDefinition(
|
||||||
"GET", "/api/lm/download-progress/{download_id}", "get_download_progress"
|
"GET", "/api/lm/download-progress/{download_id}", "get_download_progress"
|
||||||
),
|
),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/queue", "get_download_queue"),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/queue/add", "add_to_download_queue"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/remove", "remove_from_download_queue"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/move-to-top", "move_queue_item_to_top"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/move-to-end", "move_queue_item_to_end"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/clear", "clear_download_queue"
|
||||||
|
),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/history", "get_download_history"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/clear", "clear_download_history"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/delete", "delete_download_history_item"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/retry", "retry_download_from_history"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/retry-all", "retry_all_failed_downloads"
|
||||||
|
),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/stats", "get_download_stats"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/complete", "complete_download_in_queue"
|
||||||
|
),
|
||||||
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
||||||
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
||||||
)
|
)
|
||||||
|
|||||||
730
py/services/download_queue_service.py
Normal file
730
py/services/download_queue_service.py
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from ..utils.cache_paths import get_cache_base_dir
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_database_path() -> str:
|
||||||
|
base_dir = get_cache_base_dir(create=True)
|
||||||
|
history_dir = os.path.join(base_dir, "download_history")
|
||||||
|
os.makedirs(history_dir, exist_ok=True)
|
||||||
|
return os.path.join(history_dir, "download_queue.sqlite")
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadQueueService:
|
||||||
|
"""Persistent download queue and history manager backed by SQLite.
|
||||||
|
|
||||||
|
Provides a singleton interface for managing a download queue and
|
||||||
|
corresponding history table, both stored in a single SQLite database
|
||||||
|
under the cache directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance: Optional[DownloadQueueService] = None
|
||||||
|
_class_lock: asyncio.Lock = asyncio.Lock()
|
||||||
|
|
||||||
|
_SCHEMA = """
|
||||||
|
CREATE TABLE IF NOT EXISTS download_queue (
|
||||||
|
download_id TEXT PRIMARY KEY,
|
||||||
|
model_id INTEGER,
|
||||||
|
model_version_id INTEGER,
|
||||||
|
model_name TEXT NOT NULL DEFAULT '',
|
||||||
|
version_name TEXT DEFAULT '',
|
||||||
|
thumbnail_url TEXT DEFAULT '',
|
||||||
|
source TEXT,
|
||||||
|
file_params TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'queued',
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
progress INTEGER DEFAULT 0,
|
||||||
|
bytes_downloaded INTEGER DEFAULT 0,
|
||||||
|
total_bytes INTEGER,
|
||||||
|
bytes_per_second REAL DEFAULT 0.0,
|
||||||
|
error TEXT,
|
||||||
|
file_path TEXT,
|
||||||
|
added_at REAL NOT NULL,
|
||||||
|
started_at REAL,
|
||||||
|
completed_at REAL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dq_status ON download_queue(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dq_added ON download_queue(added_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS download_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
download_id TEXT,
|
||||||
|
model_id INTEGER,
|
||||||
|
model_version_id INTEGER,
|
||||||
|
model_name TEXT NOT NULL DEFAULT '',
|
||||||
|
version_name TEXT DEFAULT '',
|
||||||
|
thumbnail_url TEXT DEFAULT '',
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
error TEXT,
|
||||||
|
file_path TEXT,
|
||||||
|
bytes_downloaded INTEGER DEFAULT 0,
|
||||||
|
total_bytes INTEGER,
|
||||||
|
completed_at REAL NOT NULL,
|
||||||
|
is_already_exists INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dh_completed ON download_history(completed_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dh_status ON download_history(status);
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_instance(cls) -> DownloadQueueService:
|
||||||
|
"""Return the singleton instance, creating it if necessary."""
|
||||||
|
async with cls._class_lock:
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = cls()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, db_path: Optional[str] = None) -> None:
|
||||||
|
self._db_path = db_path or _resolve_database_path()
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._conn: Optional[sqlite3.Connection] = None
|
||||||
|
self._schema_initialized = False
|
||||||
|
self._ensure_directory()
|
||||||
|
self._initialize_schema()
|
||||||
|
|
||||||
|
def _ensure_directory(self) -> None:
|
||||||
|
directory = os.path.dirname(self._db_path)
|
||||||
|
if directory:
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
def _connect(self) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _get_conn(self) -> sqlite3.Connection:
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||||
|
self._conn.row_factory = sqlite3.Row
|
||||||
|
return self._conn
|
||||||
|
|
||||||
|
def _initialize_schema(self) -> None:
|
||||||
|
if self._schema_initialized:
|
||||||
|
return
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.executescript(self._SCHEMA)
|
||||||
|
conn.commit()
|
||||||
|
self._schema_initialized = True
|
||||||
|
|
||||||
|
def get_database_path(self) -> str:
|
||||||
|
"""Return the resolved database file path."""
|
||||||
|
return self._db_path
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the persistent SQLite connection, if open.
|
||||||
|
|
||||||
|
This is called before plugin update operations to release the
|
||||||
|
database file lock on Windows, allowing ``shutil.rmtree()`` to
|
||||||
|
succeed when the cache resides inside the plugin directory.
|
||||||
|
"""
|
||||||
|
if self._conn is not None:
|
||||||
|
try:
|
||||||
|
self._conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Queue methods
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def add_to_queue(
|
||||||
|
self,
|
||||||
|
download_id: str,
|
||||||
|
model_id: Optional[int] = None,
|
||||||
|
model_version_id: Optional[int] = None,
|
||||||
|
model_name: str = "",
|
||||||
|
version_name: str = "",
|
||||||
|
thumbnail_url: str = "",
|
||||||
|
source: Optional[str] = None,
|
||||||
|
file_params: Optional[dict[str, Any]] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Insert a new download into the queue.
|
||||||
|
|
||||||
|
Returns the inserted row as a dict (or an empty dict if the
|
||||||
|
download_id already exists).
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
file_params_json = json.dumps(file_params) if file_params is not None else None
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT OR IGNORE INTO download_queue (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, source, file_params,
|
||||||
|
status, priority, added_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
download_id,
|
||||||
|
model_id,
|
||||||
|
model_version_id,
|
||||||
|
model_name,
|
||||||
|
version_name,
|
||||||
|
thumbnail_url,
|
||||||
|
source,
|
||||||
|
file_params_json,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
return dict(row) if row else {}
|
||||||
|
|
||||||
|
async def get_queue(self) -> list[dict[str, Any]]:
|
||||||
|
"""Return all items in the queue ordered by priority then added time."""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_queue ORDER BY priority DESC, added_at ASC"
|
||||||
|
).fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
async def get_queued_count(self) -> int:
|
||||||
|
"""Return the number of items with status ``'queued'``."""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_queue WHERE status = 'queued'"
|
||||||
|
).fetchone()
|
||||||
|
return row["cnt"] if row else 0
|
||||||
|
|
||||||
|
async def update_status(
|
||||||
|
self,
|
||||||
|
download_id: str,
|
||||||
|
status: str,
|
||||||
|
**extra: Any,
|
||||||
|
) -> bool:
|
||||||
|
"""Update the status and/or extra fields of a queue item.
|
||||||
|
|
||||||
|
Accepted extra keyword arguments:
|
||||||
|
``progress``, ``error``, ``file_path``, ``bytes_downloaded``,
|
||||||
|
``total_bytes``, ``bytes_per_second``.
|
||||||
|
|
||||||
|
Returns ``True`` if a row was updated.
|
||||||
|
"""
|
||||||
|
allowed_extra = {
|
||||||
|
"progress",
|
||||||
|
"error",
|
||||||
|
"file_path",
|
||||||
|
"bytes_downloaded",
|
||||||
|
"total_bytes",
|
||||||
|
"bytes_per_second",
|
||||||
|
}
|
||||||
|
|
||||||
|
set_clauses: list[str] = ["status = ?"]
|
||||||
|
params: list[Any] = [status]
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if status in ("downloading",):
|
||||||
|
set_clauses.append("started_at = COALESCE(started_at, ?)")
|
||||||
|
params.append(now)
|
||||||
|
if status in ("completed", "failed", "canceled"):
|
||||||
|
set_clauses.append("completed_at = ?")
|
||||||
|
params.append(now)
|
||||||
|
|
||||||
|
for key, value in extra.items():
|
||||||
|
if key in allowed_extra:
|
||||||
|
set_clauses.append(f"{key} = ?")
|
||||||
|
params.append(value)
|
||||||
|
|
||||||
|
params.append(download_id)
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
f"UPDATE download_queue SET {', '.join(set_clauses)} "
|
||||||
|
"WHERE download_id = ?",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
async def remove_from_queue(self, download_id: str) -> bool:
|
||||||
|
"""Remove a single item from the queue by download_id.
|
||||||
|
|
||||||
|
Returns ``True`` if a row was deleted.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
async def move_to_top(self, download_id: str) -> bool:
|
||||||
|
"""Move an item to the front of the queue (highest priority).
|
||||||
|
|
||||||
|
Returns ``True`` if the item was found and updated.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT priority FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_row = conn.execute(
|
||||||
|
"SELECT MAX(priority) AS mx FROM download_queue"
|
||||||
|
).fetchone()
|
||||||
|
max_priority: int = max_row["mx"] if max_row["mx"] is not None else 0
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE download_queue SET priority = ? WHERE download_id = ?",
|
||||||
|
(max_priority + 1, download_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def move_to_end(self, download_id: str) -> bool:
|
||||||
|
"""Move an item to the end of the queue (lowest priority).
|
||||||
|
|
||||||
|
Returns ``True`` if the item was found and updated.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT priority FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
min_row = conn.execute(
|
||||||
|
"SELECT MIN(priority) AS mn FROM download_queue"
|
||||||
|
).fetchone()
|
||||||
|
min_priority: int = min_row["mn"] if min_row["mn"] is not None else 0
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE download_queue SET priority = ? WHERE download_id = ?",
|
||||||
|
(min_priority - 1, download_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def clear_queue(self, status_filter: Optional[str] = None) -> int:
|
||||||
|
"""Remove items from the queue.
|
||||||
|
|
||||||
|
When *status_filter* is provided only items with that status are
|
||||||
|
deleted. Returns the number of deleted rows.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
if status_filter is not None:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM download_queue WHERE status = ?",
|
||||||
|
(status_filter,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cursor = conn.execute("DELETE FROM download_queue")
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
async def complete_download(
|
||||||
|
self,
|
||||||
|
download_id: str,
|
||||||
|
status: str = "completed",
|
||||||
|
error: Optional[str] = None,
|
||||||
|
file_path: Optional[str] = None,
|
||||||
|
bytes_downloaded: int = 0,
|
||||||
|
total_bytes: Optional[int] = None,
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
"""Atomically move a download from the queue into the history table.
|
||||||
|
|
||||||
|
Looks up the queue record by ``download_id``, deletes it from the
|
||||||
|
queue, and inserts a corresponding history entry with the given
|
||||||
|
terminal status (``completed``, ``failed``, or ``canceled``).
|
||||||
|
|
||||||
|
Returns the original queue record (before deletion) on success,
|
||||||
|
or ``None`` if the download was not found in the queue.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_history (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, status, error, file_path,
|
||||||
|
bytes_downloaded, total_bytes, completed_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
row["download_id"],
|
||||||
|
row["model_id"],
|
||||||
|
row["model_version_id"],
|
||||||
|
row["model_name"],
|
||||||
|
row["version_name"],
|
||||||
|
row["thumbnail_url"],
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
file_path,
|
||||||
|
bytes_downloaded,
|
||||||
|
total_bytes,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
async def pop_next_download(self) -> Optional[dict[str, Any]]:
|
||||||
|
"""Atomically fetch and mark the next queued item as ``downloading``.
|
||||||
|
|
||||||
|
The item with the highest priority (and earliest ``added_at``
|
||||||
|
among ties) whose status is ``'queued'`` is selected, set to
|
||||||
|
``'downloading'``, and returned as a dict. Returns ``None`` if
|
||||||
|
the queue is empty.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM download_queue
|
||||||
|
WHERE status = 'queued'
|
||||||
|
ORDER BY priority DESC, added_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
download_id = row["download_id"]
|
||||||
|
now = time.time()
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE download_queue SET status = 'downloading', "
|
||||||
|
"started_at = COALESCE(started_at, ?) "
|
||||||
|
"WHERE download_id = ?",
|
||||||
|
(now, download_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
updated = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
return dict(updated) if updated else None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# History methods
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def add_to_history(
|
||||||
|
self,
|
||||||
|
download_id: Optional[str] = None,
|
||||||
|
model_id: Optional[int] = None,
|
||||||
|
model_version_id: Optional[int] = None,
|
||||||
|
model_name: str = "",
|
||||||
|
version_name: str = "",
|
||||||
|
thumbnail_url: str = "",
|
||||||
|
status: str = "completed",
|
||||||
|
error: Optional[str] = None,
|
||||||
|
file_path: Optional[str] = None,
|
||||||
|
bytes_downloaded: int = 0,
|
||||||
|
total_bytes: Optional[int] = None,
|
||||||
|
is_already_exists: int = 0,
|
||||||
|
) -> int:
|
||||||
|
"""Insert a record into the download history.
|
||||||
|
|
||||||
|
Returns the ``id`` (AUTOINCREMENT primary key) of the newly
|
||||||
|
inserted row.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_history (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, status, error, file_path,
|
||||||
|
bytes_downloaded, total_bytes, completed_at, is_already_exists
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
download_id,
|
||||||
|
model_id,
|
||||||
|
model_version_id,
|
||||||
|
model_name,
|
||||||
|
version_name,
|
||||||
|
thumbnail_url,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
file_path,
|
||||||
|
bytes_downloaded,
|
||||||
|
total_bytes,
|
||||||
|
now,
|
||||||
|
is_already_exists,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.lastrowid or 0
|
||||||
|
|
||||||
|
async def get_history(
|
||||||
|
self,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return a page of download history entries.
|
||||||
|
|
||||||
|
Returns a dict with keys ``items``, ``total``, ``limit``, and
|
||||||
|
``offset``.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
if status_filter is not None:
|
||||||
|
count_row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history WHERE status = ?",
|
||||||
|
(status_filter,),
|
||||||
|
).fetchone()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_history WHERE status = ? "
|
||||||
|
"ORDER BY completed_at DESC LIMIT ? OFFSET ?",
|
||||||
|
(status_filter, limit, offset),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
count_row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history"
|
||||||
|
).fetchone()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_history "
|
||||||
|
"ORDER BY completed_at DESC LIMIT ? OFFSET ?",
|
||||||
|
(limit, offset),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": [dict(row) for row in rows],
|
||||||
|
"total": count_row["cnt"] if count_row else 0,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def delete_history_item(self, id: int) -> bool:
|
||||||
|
"""Delete a single history entry by its *id*.
|
||||||
|
|
||||||
|
Returns ``True`` if a row was deleted.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM download_history WHERE id = ?",
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
async def clear_history(
|
||||||
|
self,
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
before_timestamp: Optional[float] = None,
|
||||||
|
) -> int:
|
||||||
|
"""Remove history entries matching the optional filters.
|
||||||
|
|
||||||
|
Both ``status_filter`` and ``before_timestamp`` can be combined
|
||||||
|
(AND logic). Returns the number of deleted rows.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
clauses: list[str] = []
|
||||||
|
params: list[Any] = []
|
||||||
|
|
||||||
|
if status_filter is not None:
|
||||||
|
clauses.append("status = ?")
|
||||||
|
params.append(status_filter)
|
||||||
|
if before_timestamp is not None:
|
||||||
|
clauses.append("completed_at < ?")
|
||||||
|
params.append(before_timestamp)
|
||||||
|
|
||||||
|
where = ""
|
||||||
|
if clauses:
|
||||||
|
where = " WHERE " + " AND ".join(clauses)
|
||||||
|
|
||||||
|
cursor = conn.execute(
|
||||||
|
f"DELETE FROM download_history{where}",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
async def get_history_count(self, status_filter: Optional[str] = None) -> int:
|
||||||
|
"""Return the number of history entries, optionally filtered by status."""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
if status_filter is not None:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history WHERE status = ?",
|
||||||
|
(status_filter,),
|
||||||
|
).fetchone()
|
||||||
|
else:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history"
|
||||||
|
).fetchone()
|
||||||
|
return row["cnt"] if row else 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Retry
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def retry_from_history(self, item_id: int) -> Optional[dict[str, Any]]:
|
||||||
|
"""Re-queue a failed or canceled download from history.
|
||||||
|
|
||||||
|
Looks up the history record by its primary key. If the status is
|
||||||
|
``failed`` or ``canceled`` a new queue entry is created with the
|
||||||
|
same model metadata and a fresh download id.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM download_history WHERE id = ?",
|
||||||
|
(item_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
status = str(row["status"])
|
||||||
|
if status not in ("failed", "canceled"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
new_id = str(uuid.uuid4())
|
||||||
|
now = time.time()
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_queue (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, source, file_params,
|
||||||
|
status, priority, added_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'queued', 0, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_id,
|
||||||
|
row["model_id"],
|
||||||
|
row["model_version_id"],
|
||||||
|
row["model_name"],
|
||||||
|
row["version_name"],
|
||||||
|
row["thumbnail_url"],
|
||||||
|
"retry",
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
queued = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(new_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
return dict(queued) if queued else None
|
||||||
|
|
||||||
|
async def retry_all_failed(self) -> int:
|
||||||
|
"""Re-queue all failed and canceled downloads from history.
|
||||||
|
|
||||||
|
Returns the number of items that were re-queued.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_history WHERE status IN ('failed', 'canceled')"
|
||||||
|
).fetchall()
|
||||||
|
if not rows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
count = 0
|
||||||
|
for row in rows:
|
||||||
|
new_id = str(uuid.uuid4())
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_queue (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, source, file_params,
|
||||||
|
status, priority, added_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'queued', 0, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_id,
|
||||||
|
row["model_id"],
|
||||||
|
row["model_version_id"],
|
||||||
|
row["model_name"],
|
||||||
|
row["version_name"],
|
||||||
|
row["thumbnail_url"],
|
||||||
|
"retry",
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Stats
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def get_stats(self) -> dict[str, int]:
|
||||||
|
"""Return aggregate counts across both tables.
|
||||||
|
|
||||||
|
Returns a dict with keys ``queued``, ``downloading``, ``paused``
|
||||||
|
(all from the queue table) and ``completed``, ``failed``,
|
||||||
|
``canceled`` (all from the history table).
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
queue_rows = conn.execute(
|
||||||
|
"SELECT status, COUNT(*) AS cnt FROM download_queue GROUP BY status"
|
||||||
|
).fetchall()
|
||||||
|
queue_stats: dict[str, int] = {}
|
||||||
|
for row in queue_rows:
|
||||||
|
queue_stats[str(row["status"])] = row["cnt"]
|
||||||
|
|
||||||
|
history_rows = conn.execute(
|
||||||
|
"SELECT status, COUNT(*) AS cnt FROM download_history GROUP BY status"
|
||||||
|
).fetchall()
|
||||||
|
history_stats: dict[str, int] = {}
|
||||||
|
for row in history_rows:
|
||||||
|
history_stats[str(row["status"])] = row["cnt"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"queued": queue_stats.get("queued", 0),
|
||||||
|
"downloading": queue_stats.get("downloading", 0),
|
||||||
|
"paused": queue_stats.get("paused", 0),
|
||||||
|
"completed": history_stats.get("completed", 0),
|
||||||
|
"failed": history_stats.get("failed", 0),
|
||||||
|
"canceled": history_stats.get("canceled", 0),
|
||||||
|
}
|
||||||
@@ -129,6 +129,18 @@ class RecipePersistenceService:
|
|||||||
if nsfw_level is not None and isinstance(nsfw_level, int):
|
if nsfw_level is not None and isinstance(nsfw_level, int):
|
||||||
recipe_data["preview_nsfw_level"] = nsfw_level
|
recipe_data["preview_nsfw_level"] = nsfw_level
|
||||||
|
|
||||||
|
# Compute recipe folder relative to recipes root, mirroring
|
||||||
|
# RecipeScanner._calculate_folder() which is only called during scan/load.
|
||||||
|
if recipe_scanner.recipes_dir:
|
||||||
|
recipe_file_dir = os.path.dirname(normalized_image_path)
|
||||||
|
try:
|
||||||
|
relative_folder = os.path.relpath(recipe_file_dir, recipe_scanner.recipes_dir)
|
||||||
|
if relative_folder in (".", ""):
|
||||||
|
relative_folder = ""
|
||||||
|
recipe_data["folder"] = relative_folder.replace(os.path.sep, "/")
|
||||||
|
except Exception:
|
||||||
|
recipe_data["folder"] = ""
|
||||||
|
|
||||||
json_filename = f"{recipe_id}.recipe.json"
|
json_filename = f"{recipe_id}.recipe.json"
|
||||||
json_path = os.path.join(recipes_dir, json_filename)
|
json_path = os.path.join(recipes_dir, json_filename)
|
||||||
json_path = os.path.normpath(json_path)
|
json_path = os.path.normpath(json_path)
|
||||||
|
|||||||
@@ -188,6 +188,25 @@ class ServiceRegistry:
|
|||||||
logger.debug(f"Created and registered {service_name}")
|
logger.debug(f"Created and registered {service_name}")
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_download_queue_service(cls):
|
||||||
|
"""Get or create the download queue service."""
|
||||||
|
service_name = "download_queue_service"
|
||||||
|
|
||||||
|
if service_name in cls._services:
|
||||||
|
return cls._services[service_name]
|
||||||
|
|
||||||
|
async with cls._get_lock(service_name):
|
||||||
|
if service_name in cls._services:
|
||||||
|
return cls._services[service_name]
|
||||||
|
|
||||||
|
from .download_queue_service import DownloadQueueService
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
cls._services[service_name] = service
|
||||||
|
logger.debug(f"Created and registered {service_name}")
|
||||||
|
return service
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_backup_service(cls):
|
async def get_backup_service(cls):
|
||||||
"""Get or create the backup service."""
|
"""Get or create the backup service."""
|
||||||
|
|||||||
@@ -221,6 +221,9 @@ class UsageStats:
|
|||||||
if "loras" in loaded_stats and isinstance(loaded_stats["loras"], dict):
|
if "loras" in loaded_stats and isinstance(loaded_stats["loras"], dict):
|
||||||
self.stats["loras"] = loaded_stats["loras"]
|
self.stats["loras"] = loaded_stats["loras"]
|
||||||
|
|
||||||
|
if "embeddings" in loaded_stats and isinstance(loaded_stats["embeddings"], dict):
|
||||||
|
self.stats["embeddings"] = loaded_stats["embeddings"]
|
||||||
|
|
||||||
if "total_executions" in loaded_stats:
|
if "total_executions" in loaded_stats:
|
||||||
self.stats["total_executions"] = loaded_stats["total_executions"]
|
self.stats["total_executions"] = loaded_stats["total_executions"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "1.0.11"
|
version = "1.1.0"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: var(--transition-base);
|
transition: var(--transition-base);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header:hover {
|
.sidebar-header:hover {
|
||||||
@@ -150,6 +151,120 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar More Options Dropdown ===== */
|
||||||
|
.sidebar-more-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 8px;
|
||||||
|
min-width: 190px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: calc(var(--z-overlay) + 20);
|
||||||
|
display: none;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-more-dropdown.open {
|
||||||
|
display: block;
|
||||||
|
animation: dropdownFadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: var(--transition-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item:hover {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item:hover i {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar Hidden Indicator (left edge) ===== */
|
||||||
|
.sidebar-hidden-indicator {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: var(--z-overlay);
|
||||||
|
width: 14px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--border-color);
|
||||||
|
opacity: 0.3;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s ease, background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator i {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator:hover i {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--text-color);
|
||||||
|
color: var(--bg-color);
|
||||||
|
font-size: 0.8em;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator:hover .sidebar-hidden-indicator-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export class SidebarManager {
|
|||||||
this.currentDropTarget = null;
|
this.currentDropTarget = null;
|
||||||
this.lastPageControls = null;
|
this.lastPageControls = null;
|
||||||
this.isDisabledBySetting = false;
|
this.isDisabledBySetting = false;
|
||||||
|
this.isDisabledByPage = false;
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
this.initializationPromise = null;
|
this.initializationPromise = null;
|
||||||
this.isCreatingFolder = false;
|
this.isCreatingFolder = false;
|
||||||
this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态
|
this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态
|
||||||
@@ -68,6 +70,10 @@ export class SidebarManager {
|
|||||||
this.handleSidebarDrop = this.handleSidebarDrop.bind(this);
|
this.handleSidebarDrop = this.handleSidebarDrop.bind(this);
|
||||||
this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this);
|
this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this);
|
||||||
this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this);
|
this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this);
|
||||||
|
this.handleMoreToggle = this.handleMoreToggle.bind(this);
|
||||||
|
this.handleMoreDropdownItemClick = this.handleMoreDropdownItemClick.bind(this);
|
||||||
|
this.handleDocumentClickForMore = this.handleDocumentClickForMore.bind(this);
|
||||||
|
this.getPageDisplayName = this.getPageDisplayName.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHostPageControls(pageControls) {
|
setHostPageControls(pageControls) {
|
||||||
@@ -100,6 +106,8 @@ export class SidebarManager {
|
|||||||
this.initializeDragAndDrop();
|
this.initializeDragAndDrop();
|
||||||
this.updateSidebarTitle();
|
this.updateSidebarTitle();
|
||||||
this.restoreSidebarState();
|
this.restoreSidebarState();
|
||||||
|
// Re-apply DOM visibility now that per-page state is known
|
||||||
|
this.updateDomVisibility(!this.isDisabledBySetting);
|
||||||
await this.loadFolderTree();
|
await this.loadFolderTree();
|
||||||
if (this.isDisabledBySetting && !forceInitialize) {
|
if (this.isDisabledBySetting && !forceInitialize) {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
@@ -143,6 +151,13 @@ export class SidebarManager {
|
|||||||
this.sidebarDragHandlersInitialized = false;
|
this.sidebarDragHandlersInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moreDropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (moreDropdown) {
|
||||||
|
moreDropdown.classList.remove('open');
|
||||||
|
}
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
|
this.hideSidebarHiddenIndicator();
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
this.pageControls = null;
|
this.pageControls = null;
|
||||||
this.pageType = null;
|
this.pageType = null;
|
||||||
@@ -151,6 +166,7 @@ export class SidebarManager {
|
|||||||
this.expandedNodes = new Set();
|
this.expandedNodes = new Set();
|
||||||
this.openDropdown = null;
|
this.openDropdown = null;
|
||||||
this.isHovering = false;
|
this.isHovering = false;
|
||||||
|
this.isDisabledByPage = false;
|
||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.recursiveSearchEnabled = true;
|
this.recursiveSearchEnabled = true;
|
||||||
@@ -217,6 +233,18 @@ export class SidebarManager {
|
|||||||
if (recursiveToggleBtn) {
|
if (recursiveToggleBtn) {
|
||||||
recursiveToggleBtn.removeEventListener('click', this.handleRecursiveToggle);
|
recursiveToggleBtn.removeEventListener('click', this.handleRecursiveToggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moreToggle = document.getElementById('sidebarMoreToggle');
|
||||||
|
if (moreToggle) {
|
||||||
|
moreToggle.removeEventListener('click', this.handleMoreToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreDropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (moreDropdown) {
|
||||||
|
moreDropdown.removeEventListener('click', this.handleMoreDropdownItemClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('click', this.handleDocumentClickForMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDragAndDrop() {
|
initializeDragAndDrop() {
|
||||||
@@ -1045,6 +1073,19 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// More options dropdown
|
||||||
|
const moreToggle = document.getElementById('sidebarMoreToggle');
|
||||||
|
if (moreToggle) {
|
||||||
|
moreToggle.addEventListener('click', this.handleMoreToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreDropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (moreDropdown) {
|
||||||
|
moreDropdown.addEventListener('click', this.handleMoreDropdownItemClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', this.handleDocumentClickForMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDocumentClick(event) {
|
handleDocumentClick(event) {
|
||||||
@@ -1066,6 +1107,7 @@ export class SidebarManager {
|
|||||||
this.isPinned = !this.isPinned;
|
this.isPinned = !this.isPinned;
|
||||||
this.updateAutoHideState();
|
this.updateAutoHideState();
|
||||||
this.updatePinButton();
|
this.updatePinButton();
|
||||||
|
this.updateMoreDropdownLabels();
|
||||||
this.saveSidebarState();
|
this.saveSidebarState();
|
||||||
this.updateContainerMargin();
|
this.updateContainerMargin();
|
||||||
}
|
}
|
||||||
@@ -1129,7 +1171,7 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAutoHideState() {
|
updateAutoHideState() {
|
||||||
if (this.isDisabledBySetting) return;
|
if (this.isDisabledBySetting || this.isDisabledByPage) return;
|
||||||
|
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
@@ -1174,9 +1216,12 @@ export class SidebarManager {
|
|||||||
|
|
||||||
if (!container || !sidebar || this.isDisabledBySetting) return;
|
if (!container || !sidebar || this.isDisabledBySetting) return;
|
||||||
|
|
||||||
// Reset margin to default
|
// Always reset margin first — needed when transitioning from visible to hidden
|
||||||
container.style.marginLeft = '';
|
container.style.marginLeft = '';
|
||||||
|
|
||||||
|
// When per-page disabled, skip adjustment but margin is already reset
|
||||||
|
if (this.isDisabledByPage) return;
|
||||||
|
|
||||||
// Only adjust margin if sidebar is visible and pinned
|
// Only adjust margin if sidebar is visible and pinned
|
||||||
if ((this.isPinned || this.isHovering) && this.isVisible) {
|
if ((this.isPinned || this.isHovering) && this.isVisible) {
|
||||||
const sidebarWidth = sidebar.offsetWidth;
|
const sidebarWidth = sidebar.offsetWidth;
|
||||||
@@ -1193,20 +1238,29 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDomVisibility(enabled) {
|
updateDomVisibility(enabled) {
|
||||||
|
// Per-page disable adds on top of global setting
|
||||||
|
const isVisible = enabled && !this.isDisabledByPage;
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
|
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.toggle('hidden-by-setting', !enabled);
|
sidebar.classList.toggle('hidden-by-setting', !isVisible);
|
||||||
sidebar.setAttribute('aria-hidden', (!enabled).toString());
|
sidebar.setAttribute('aria-hidden', (!isVisible).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoverArea) {
|
if (hoverArea) {
|
||||||
hoverArea.classList.toggle('hidden-by-setting', !enabled);
|
hoverArea.classList.toggle('hidden-by-setting', !isVisible);
|
||||||
if (!enabled) {
|
if (!isVisible) {
|
||||||
hoverArea.classList.add('disabled');
|
hoverArea.classList.add('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show or hide the "sidebar hidden" notification
|
||||||
|
if (enabled && this.isDisabledByPage) {
|
||||||
|
this.showSidebarHiddenIndicator();
|
||||||
|
} else {
|
||||||
|
this.hideSidebarHiddenIndicator();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSidebarEnabled(enabled) {
|
async setSidebarEnabled(enabled) {
|
||||||
@@ -1266,6 +1320,133 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== More Options Dropdown =====
|
||||||
|
|
||||||
|
handleMoreToggle(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const dropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
this.isMoreDropdownOpen = !dropdown.classList.contains('open');
|
||||||
|
dropdown.classList.toggle('open', this.isMoreDropdownOpen);
|
||||||
|
this.updateMoreDropdownLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoreDropdownItemClick(event) {
|
||||||
|
const item = event.target.closest('.sidebar-dropdown-item');
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const action = item.dataset.action;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
const dropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.classList.remove('open');
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'toggle-pin':
|
||||||
|
this.handlePinToggle(event);
|
||||||
|
break;
|
||||||
|
case 'toggle-hide':
|
||||||
|
this.toggleHideOnThisPage();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDocumentClickForMore(event) {
|
||||||
|
const dropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
const toggle = document.getElementById('sidebarMoreToggle');
|
||||||
|
if (!dropdown || !toggle) return;
|
||||||
|
|
||||||
|
if (!dropdown.contains(event.target) && !toggle.contains(event.target)) {
|
||||||
|
dropdown.classList.remove('open');
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMoreDropdownLabels() {
|
||||||
|
const pinLabel = document.getElementById('sidebarMorePinLabel');
|
||||||
|
if (pinLabel) {
|
||||||
|
pinLabel.textContent = this.isPinned
|
||||||
|
? translate('sidebar.unpinSidebar')
|
||||||
|
: translate('sidebar.pinSidebar');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideItem = document.querySelector('.sidebar-dropdown-item[data-action="toggle-hide"]');
|
||||||
|
if (hideItem) {
|
||||||
|
const hideIcon = hideItem.querySelector('i');
|
||||||
|
const hideLabel = hideItem.querySelector('span');
|
||||||
|
if (this.isDisabledByPage) {
|
||||||
|
hideLabel.textContent = translate('sidebar.showSidebar');
|
||||||
|
if (hideIcon) {
|
||||||
|
hideIcon.className = 'fas fa-eye';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hideLabel.textContent = translate('sidebar.hideOnThisPage');
|
||||||
|
if (hideIcon) {
|
||||||
|
hideIcon.className = 'fas fa-eye-slash';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleHideOnThisPage() {
|
||||||
|
this.isDisabledByPage = !this.isDisabledByPage;
|
||||||
|
setStorageItem(`${this.pageType}_sidebarDisabled`, this.isDisabledByPage);
|
||||||
|
this.updateDomVisibility(!this.isDisabledBySetting);
|
||||||
|
this.updateAutoHideState();
|
||||||
|
this.updateContainerMargin();
|
||||||
|
this.updateMoreDropdownLabels();
|
||||||
|
|
||||||
|
if (!this.isDisabledByPage) {
|
||||||
|
this.hideSidebarHiddenIndicator();
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
'sidebar.sidebarHiddenNotification',
|
||||||
|
{ page: this.getPageDisplayName() },
|
||||||
|
'info',
|
||||||
|
`Sidebar hidden on ${this.getPageDisplayName()} page`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageDisplayName() {
|
||||||
|
const names = {
|
||||||
|
loras: 'LoRAs',
|
||||||
|
recipes: 'Recipes',
|
||||||
|
checkpoints: 'Checkpoints',
|
||||||
|
embeddings: 'Embeddings',
|
||||||
|
};
|
||||||
|
return names[this.pageType] || this.pageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSidebarHiddenIndicator() {
|
||||||
|
if (document.getElementById('sidebarHiddenIndicator')) return;
|
||||||
|
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.id = 'sidebarHiddenIndicator';
|
||||||
|
indicator.className = 'sidebar-hidden-indicator';
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
<span class="sidebar-hidden-indicator-tooltip">${translate('sidebar.showSidebar')}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
indicator.addEventListener('click', () => {
|
||||||
|
this.toggleHideOnThisPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSidebarHiddenIndicator() {
|
||||||
|
const indicator = document.getElementById('sidebarHiddenIndicator');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadFolderTree() {
|
async loadFolderTree() {
|
||||||
try {
|
try {
|
||||||
if (this.displayMode === 'tree') {
|
if (this.displayMode === 'tree') {
|
||||||
@@ -1911,6 +2092,7 @@ export class SidebarManager {
|
|||||||
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
||||||
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
|
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
|
||||||
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
|
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
|
||||||
|
this.isDisabledByPage = getStorageItem(`${this.pageType}_sidebarDisabled`, false);
|
||||||
|
|
||||||
this.isPinned = isPinned;
|
this.isPinned = isPinned;
|
||||||
this.expandedNodes = new Set(expandedPaths);
|
this.expandedNodes = new Set(expandedPaths);
|
||||||
|
|||||||
@@ -998,7 +998,9 @@ function showNodeSelector(nodes, options = {}) {
|
|||||||
nodeSelectorState.enableSendAll = options.enableSendAll !== false;
|
nodeSelectorState.enableSendAll = options.enableSendAll !== false;
|
||||||
|
|
||||||
// Generate node list HTML with icons and proper colors
|
// Generate node list HTML with icons and proper colors
|
||||||
const nodeItems = Object.entries(safeNodes).map(([nodeKey, node]) => {
|
const nodeItems = Object.entries(safeNodes)
|
||||||
|
.sort(([, a], [, b]) => a.type - b.type || a.id - b.id)
|
||||||
|
.map(([nodeKey, node]) => {
|
||||||
const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle';
|
const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle';
|
||||||
const bgColor = node.bgcolor || DEFAULT_NODE_COLOR;
|
const bgColor = node.bgcolor || DEFAULT_NODE_COLOR;
|
||||||
const graphLabel = node.graph_name ? ` (${node.graph_name})` : '';
|
const graphLabel = node.graph_name ? ` (${node.graph_name})` : '';
|
||||||
|
|||||||
@@ -18,6 +18,20 @@
|
|||||||
<button class="sidebar-action-btn" id="sidebarPinToggle" title="{{ t('sidebar.unpinSidebar') }}">
|
<button class="sidebar-action-btn" id="sidebarPinToggle" title="{{ t('sidebar.unpinSidebar') }}">
|
||||||
<i class="fas fa-thumbtack"></i>
|
<i class="fas fa-thumbtack"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="sidebar-action-btn" id="sidebarMoreToggle" title="{{ t('sidebar.moreOptions') }}">
|
||||||
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Dropdown menu for more options -->
|
||||||
|
<div class="sidebar-more-dropdown" id="sidebarMoreDropdown">
|
||||||
|
<div class="sidebar-dropdown-item" data-action="toggle-pin">
|
||||||
|
<i class="fas fa-thumbtack"></i>
|
||||||
|
<span id="sidebarMorePinLabel">{{ t('sidebar.pinSidebar') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-dropdown-item" data-action="toggle-hide">
|
||||||
|
<i class="fas fa-eye-slash"></i>
|
||||||
|
<span>{{ t('sidebar.hideOnThisPage') }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user