code:
:: slave creation widgets [widget nobr]

/%
 Call as <<StartingGirlsWorkaround>>
%/
<<widget "StartingGirlsWorkaround">>
	<<display "Generate New Slave">>
	<<set $startingGirlCopied to 0>>
	<<set $activeSlave.health = 0>>
	<<set $activeSlave.devotion = 0>>
	<<set $activeSlave.trust = 0>>
	<<set $activeSlave.sexualQuirk to "none">>
	<<set $activeSlave.behavioralQuirk to "none">>
	<<set $activeSlave.fetishKnown to 1>>
	<<if $activeSlave.vagina < 0>>
	<<set $activeSlave.ovaries = 0>>
	<<elseif $activeSlave.vagina < 1>>
	<<set $activeSlave.vaginalSkill = 0>>
	<</if>>
	<<if $activeSlave.anus < 1>>
	<<set $activeSlave.analSkill = 0>>
	<</if>>

	<<set $activeSlave.actualAge to Number($activeSlave.actualAge)>>
	<<set $activeSlave.devotion = Number($activeSlave.devotion)>>
	<<set $activeSlave.trust = Number($activeSlave.trust)>>
	<<if $activeSlave.indenture >= 0>>
		<<set $activeSlave.indenture = Math.clamp($activeSlave.indenture, 26, 208)>>
	<</if>>
	<<set $activeSlave.height = Math.clamp($activeSlave.height, 85, 274)>>
	<<set $activeSlave.boobs = Math.clamp($activeSlave.boobs, 0, 10000)>>
	<<set $activeSlave.boobs = Number($activeSlave.boobs)>>
	<<set $activeSlave.boobs = Math.trunc($activeSlave.boobs/50)*50>>
<</widget>>

/%
 Call as <<StartingGirlsRefresh>>
%/
<<widget "StartingGirlsRefresh">>

	<<replace "#devotion">>
		@@color:yellow;Ambivalent.@@
	<</replace>>

	<<replace "#trust">>
		@@color:yellow;Fearful.@@
	<</replace>>

	<<replace "#legal">>
		Slave.
	<</replace>>

	<<replace "#voice">>
		<<if $activeSlave.voice == 0>>Mute.
		<<elseif $activeSlave.voice == 1>>Deep.
		<<elseif $activeSlave.voice is 2>>Normal.
		<<elseif $activeSlave.voice is 3>>High.
		<</if>>
	<</replace>>

	<<replace "#language">>
		<<if $activeSlave.accent == 0>>Unaccented.
		<<elseif $activeSlave.accent == 1>>Pretty $activeSlave.nationality accent.
		<<elseif $activeSlave.accent is 2>>Thick $activeSlave.nationality accent.
		<<else>>Not fluent.
		<</if>>
	<</replace>>

	<<replace "#age">>
		<<textbox "$activeSlave.actualAge" $activeSlave.actualAge "Starting Girls">>
	<</replace>>

	<<replace "#health">>
		@@color:yellow;Healthy.@@
	<</replace>>

	<<replace "#muscles">>
		Normal.
	<</replace>>

	<<replace "#waist">>
		Normal.
	<</replace>>

	<<if $seeExtreme == 1>>
		<<replace "#amp">>
			<<if $activeSlave.amp == 1>>Amputee.
			<<else>>Normal.
			<</if>>
		<</replace>>
	<</if>>

	<<replace "#height">>
		<<if $activeSlave.height < 150>>Petite.
		<<elseif $activeSlave.height < 160>>Short.
		<<elseif $activeSlave.height < 170>>Average.
		<<elseif $activeSlave.height < 185>>Tall.
		<<else>>Very tall.
		<</if>>
		<<textbox "$activeSlave.height" $activeSlave.height "Starting Girls">> cm
	<</replace>>

	<<replace "#weight">>
		<<if $activeSlave.weight < -95>>@@color:red;Emaciated.@@
		<<elseif $activeSlave.weight < -30>>@@color:red;Skinny.@@
		<<elseif $activeSlave.weight < -10>>Thin.
		<<elseif $activeSlave.weight <= 10>>Average.
		<<elseif $activeSlave.weight <= 30>>Plush.
		<<elseif $activeSlave.weight <= 95>>@@color:red;Chubby.@@
		<<else>>@@color:red;Fat.@@
		<</if>>
	<</replace>>

	<<if $seeRace == 1>>
		<<replace "#ethnicity">>
		<<textbox "$activeSlave.race" $activeSlave.race "Starting Girls">>
		<</replace>>
	<</if>>

	<<replace "#skin">>
		<<textbox "$activeSlave.skin" $activeSlave.skin "Starting Girls">>
	<</replace>>

	<<replace "#faceShape">>
		$activeSlave.faceShape
	<</replace>>

	<<replace "#face">>
		<<if $activeSlave.face < -2>>Very ugly.
		<<elseif $activeSlave.face is -2>>Ugly.
		<<elseif $activeSlave.face is -1>>Unattractive.
		<<elseif $activeSlave.face == 0>>Average.
		<<elseif $activeSlave.face == 1>>Attractive.
		<<elseif $activeSlave.face is 2>>Beautiful.
		<<else>>Very beautiful.
		<</if>>
	<</replace>>

	<<replace "#lips">>
		<<if $activeSlave.lips <= 10>>Thin.
		<<elseif $activeSlave.lips <= 20>>Normal.
		<<elseif $activeSlave.lips <= 40>>Plush.
		<<elseif $activeSlave.lips <= 70>>Big.
		<<elseif $activeSlave.lips <= 95>>Huge.
		<<else>>Facepussy.
		<</if>>
	<</replace>>

	<<replace "#teeth">>
		<<if $activeSlave.teeth is "crooked">>Crooked.
		<<elseif $activeSlave.teeth is "straightening braces">>Braces.
		<<else>>Straight.
		<</if>>
	<</replace>>

	<<replace "#vision">>
		<<if $seeExtreme == 1>>
			<<if $activeSlave.eyes is -2>>Blind.
			<<elseif $activeSlave.eyes is -1>>Nearsighted.
			<<else>>Normal.
			<</if>>
		<<else>>
			<<if $activeSlave.eyes is -1>>Nearsighted.
			<<else>>Normal.
			<</if>>
		<</if>>
	<</replace>>

	<<replace "#boobs">>
		<<if $activeSlave.boobs <= 200>>Flat.
		<<elseif $activeSlave.boobs <= 500>>Healthy.
		<<elseif $activeSlave.boobs <= 800>>Large.
		<<elseif $activeSlave.boobs <= 1200>>Very Large.
		<<elseif $activeSlave.boobs <= 2000>>Huge.
		<<elseif $activeSlave.boobs <= 4000>>Massive.
		<<elseif $activeSlave.boobs <= 6000>>Monstrous.
		<<else>>Science experiment.
		<</if>>
		<<textbox "$activeSlave.boobs" $activeSlave.boobs "Starting Girls">> CCs
	<</replace>>

	<<replace "#lactation">>
		<<if $activeSlave.lactation == 2>>Artificial.
		<<elseif $activeSlave.lactation == 1>>Natural.
		<<else>>None.
		<</if>>
	<</replace>>

	<<replace "#nipples">>
		$activeSlave.nipples
	<</replace>>

	<<replace "#areolae">>
		<<if $activeSlave.areolae == 1>>
		Large.
		<<elseif $activeSlave.areolae == 2>>
			Wide.
		<<elseif $activeSlave.areolae == 3>>
			Huge.
		<<else>>
			Normal.
		<</if>>
	<</replace>>

	<<replace "#shoulders">>
		<<if $activeSlave.shoulders < -1>>Very narrow.
		<<elseif $activeSlave.shoulders < 0>>Narrow.
		<<elseif $activeSlave.shoulders > 1>>Very broad.
		<<elseif $activeSlave.shoulders > 0>>Broad.
		<<else>>Feminine.
		<</if>>
	<</replace>>

	<<replace "#hips">>
		<<if $activeSlave.hips < -1>>Very narrow.
		<<elseif $activeSlave.hips < 0>>Narrow.
		<<elseif $activeSlave.hips > 1>>Very broad.
		<<elseif $activeSlave.hips > 0>>Broad.
		<<else>>Normal.
		<</if>>
	<</replace>>

	<<replace "#butt">>
		<<if $activeSlave.butt == 0>>Flat.
		<<elseif $activeSlave.butt < 2>>Small.
		<<elseif $activeSlave.butt < 3>>Plump.
		<<elseif $activeSlave.butt < 4>>Big.
		<<elseif $activeSlave.butt < 5>>Huge.
		<<elseif $activeSlave.butt < 6>>Enormous.
		<<elseif $activeSlave.butt < 7>>Gigantic.
		<<else>>Massive.
		<</if>>
	<</replace>>

	<<replace "#anus">>
		<<if $activeSlave.anus == 0>>@@color:lime;Virgin.@@
		<<elseif $activeSlave.anus == 1>>Normal.
		<<elseif $activeSlave.anus is 2>>Veteran.
		<<else>>Gaping.
		<</if>>
	<</replace>>

	<<replace "#vagina">>
		<<if $activeSlave.vagina is -1>>//No vagina.//
		<<elseif $activeSlave.vagina == 0>>@@color:lime;Virgin.@@
		<<elseif $activeSlave.vagina == 1>>Normal.
		<<elseif $activeSlave.vagina is 2>>Veteran.
		<<else>>Gaping.
		<</if>>
	<</replace>>

	<<replace "#vaginablock">>
		<<if $activeSlave.vagina > -1>>

		''Clit:''

			<<if $activeSlave.clit == 0>>Normal.
			<<elseif $activeSlave.clit == 1>>Large.
			<<else>>Huge.
			<</if>>

		<<click "Normal">>
			<<set $activeSlave.clit to 0>>
			<<replace #clit>>
				Normal.
			<</replace>>
		<</click>>
		|
		<<click "Large">>
			<<set $activeSlave.clit to 1>>
			<<replace #clit>>
				Large.
			<</replace>>
		<</click>>
		|
		<<click "Huge">>
			<<set $activeSlave.clit to 2>>
			<<replace #clit>>
				Huge.
			<</replace>>
		<</click>>
		<</if>>

		''Labia:''

			<<if $activeSlave.labia is 2>>Huge.
			<<elseif $activeSlave.labia == 1>>Large.
			<<else>>Normal.
			<</if>>

		<<click "Normal">>
			<<set $activeSlave.labia to 0>>
			<<replace #labia>>
				Normal.
			<</replace>>
		<</click>>
		|
		<<click "Large">>
			<<set $activeSlave.labia to 1>>
			<<replace #labia>>
				Large.
			<</replace>>
		<</click>>
		|
		<<click "Huge">>
			<<set $activeSlave.labia to 2>>
			<<replace #labia>>
				Huge.
			<</replace>>
		<</click>>

		''Vaginal wetness:''

		<<if $activeSlave.vaginaLube == 0>>Dry.
		<<elseif $activeSlave.vaginaLube == 1>>Normal.
		<<else>>Excessive.
		<</if>>

		<<click "Dry">><<set $activeSlave.vaginaLube = 0>><<replace "#wetness">>Dry.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Normal">><<set $activeSlave.vaginaLube = 1>><<replace "#wetness">>Normal.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Excessive">><<set $activeSlave.vaginaLube = 2>><<replace "#wetness">>Excessive.<</replace>><<StartingGirlsCost>><</click>>

		''Pregnancy:''

		<<if $activeSlave.preg > 20>>Ready to drop.
		<<elseif $activeSlave.preg > 20>>Advanced.
		<<elseif $activeSlave.preg > 5>>Early.
		<<elseif $activeSlave.preg > -2>>None.
		<<else>>Barren.
		<</if>>

		<<click "Ready to Drop">><<set $activeSlave.preg = 35,$activeSlave.pregType = 1,$activeSlave.pubertyXX = 1>><<replace "#preg">>Ready to drop.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Advanced">><<set $activeSlave.preg = 25,$activeSlave.pregType = 1,$activeSlave.pubertyXX = 1>><<replace "#preg">>Advanced.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Early">><<set $activeSlave.preg = 10,$activeSlave.pregType = 1,$activeSlave.pubertyXX = 1>><<replace "#preg">>Early.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "None">><<set $activeSlave.preg = 0,$activeSlave.pregType = 0,$activeSlave.pregSource = 0>><<replace "#preg">>None.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Barren">><<set $activeSlave.preg = -2,$activeSlave.pregType = 0,$activeSlave.pregSource = 0>><<replace "#preg">>Barren.<</replace>><<StartingGirlsCost>><</click>>

''Puberty:''

		<<if $activeSlave.pubertyXX == 1>>Postpubescent.
		<<else>>Prepubescent.
		<</if>>

		<<textbox "$activeSlave.pubertyAgeXX" $activeSlave.pubertyAgeXX "Starting Girls">>
		<<click "Postpubescent">><<set $activeSlave.pubertyXX = 1>><<replace "#pub">>Postpubescent.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Prepubescent">><<set $activeSlave.pubertyXX = 0,$activeSlave.pubertyAgeXX = $fertilityAge,$activeSlave.preg = 0,$activeSlave.pregType = 0,$activeSlave.pregSource = 0>><<replace "#pub">>Prepubescent.<</replace>><<StartingGirlsCost>><</click>>
		<<if $PC.dick == 1>>

 ''Father:''

		<<if $activeSlave.pregSource == -1>>My child.
		<<else>>
		<</if>>

		<<click "My child">><<set $activeSlave.pregSource = -1>><<replace "#father">>My child.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Clear">><<set $activeSlave.pregSource = 0>><<replace "#father">><</replace>><<StartingGirlsCost>><</click>>
		<</if>>
	<</replace>>

	<<replace "#dickblock">>
		<<if $seeDicks != 0>>

		''Penis:''

		<<if $activeSlave.dick == 0>>None.
		<<elseif $activeSlave.dick == 1>>Tiny.
		<<elseif $activeSlave.dick is 2>>Small.
		<<elseif $activeSlave.dick is 3>>Normal.
		<<elseif $activeSlave.dick is 4>>Large.
		<<else>>Massive.
		<</if>>

		<<click "No penis">><<set $activeSlave.dick = 0,$activeSlave.balls = 0,$activeSlave.clit = 0,$activeSlave.pubertyXY = 0,$activeSlave.pubertyAgeXY = $potencyAge>><<replace "#dick">>None.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Tiny">><<set $activeSlave.dick = 1,$activeSlave.clit = 0>><<replace "#dick">>Tiny.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Small">><<set $activeSlave.dick = 2,$activeSlave.clit = 0>><<replace "#dick">>Small.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Normal">><<set $activeSlave.dick = 3,$activeSlave.clit = 0>><<replace "#dick">>Normal.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Large">><<set $activeSlave.dick = 4,$activeSlave.clit = 0>><<replace "#dick">>Large.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Massive">><<set $activeSlave.dick = 5,$activeSlave.clit = 0>><<replace "#dick">>Massive.<</replace>><<StartingGirlsCost>><</click>>
		<<if $activeSlave.dick > 0>>

		''Foreskin:''

		<<if $activeSlave.foreskin == 0>>None.
		<<elseif $activeSlave.foreskin == 1>>Tiny.
		<<elseif $activeSlave.foreskin is 2>>Small.
		<<elseif $activeSlave.foreskin is 3>>Normal.
		<<elseif $activeSlave.foreskin is 4>>Large.
		<<else>>Massive.<</if>>

		<<if $seeCircumcision == 1>><<click "Circumcised">><<set $activeSlave.foreskin = 0>><<replace "#foreskin">>None.<</replace>><<StartingGirlsCost>><</click>> |<</if>>
		<<click "Tiny">><<set $activeSlave.foreskin = 1>><<replace "#foreskin">>Tiny.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Small">><<set $activeSlave.foreskin = 2>><<replace "#foreskin">>Small.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Normal">><<set $activeSlave.foreskin = 3>><<replace "#foreskin">>Normal.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Large">><<set $activeSlave.foreskin = 4>><<replace "#foreskin">>Large.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Massive">><<set $activeSlave.foreskin = 5>><<replace "#foreskin">>Massive.<</replace>><<StartingGirlsCost>><</click>>

		''Testicles:''

		<<if $activeSlave.balls == 0>>None.
		<<elseif $activeSlave.balls == 1>>Vestigial.
		<<elseif $activeSlave.balls is 2>>Small.
		<<elseif $activeSlave.balls is 3>>Normal.
		<<elseif $activeSlave.balls is 4>>Large.
		<<else>>Massive.<</if>>

		<<click "No testicles">><<set $activeSlave.balls = 0,$activeSlave.pubertyXY = 0,$activeSlave.pubertyAgeXY = $potencyAge>><<replace "#balls">>None.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Vestigial">><<set $activeSlave.balls = 1>><<replace "#balls">>Vestigial.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Small">><<set $activeSlave.balls = 2>><<replace "#balls">>Small.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Normal">><<set $activeSlave.balls = 3>><<replace "#balls">>Normal.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Large">><<set $activeSlave.balls = 4>><<replace "#balls">>Large.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Massive">><<set $activeSlave.balls = 5>><<replace "#balls">>Massive.<</replace>><<StartingGirlsCost>><</click>>

		''Puberty:''<<textbox "$activeSlave.pubertyAgeXY" $activeSlave.pubertyAgeXY "Starting Girls">>
		<<if $activeSlave.balls > 0>>

		''Ballsack:''

		<<if $activeSlave.scrotum == 0>>None.
		<<elseif $activeSlave.scrotum == 1>>Tiny.
		<<elseif $activeSlave.scrotum is 2>>Small.
		<<elseif $activeSlave.scrotum is 3>>Normal.
		<<elseif $activeSlave.scrotum is 4>>Large.
		<<else>>Massive.<</if>>

		<<click "None">><<set $activeSlave.scrotum = 0>><<replace "#scrotum">>None.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Tiny">><<set $activeSlave.scrotum = 1>><<replace "#scrotum">>Tiny.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Small">><<set $activeSlave.scrotum = 2>><<replace "#scrotum">>Small.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Normal">><<set $activeSlave.scrotum = 3>><<replace "#scrotum">>Normal.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Large">><<set $activeSlave.scrotum = 4>><<replace "#scrotum">>Large.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Massive">><<set $activeSlave.scrotum = 5>><<replace "#scrotum">>Massive.<</replace>><<StartingGirlsCost>><</click>>

''Puberty:''

		<<if $activeSlave.pubertyXY == 1>>Postpubescent.
		<<else>>Prepubescent.
		<</if>>

		<<click "Postpubescent">><<set $activeSlave.pubertyXY = 1>><<replace "#Mpub">>Postpubescent.<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Prepubescent">><<set $activeSlave.pubertyXY = 0,$activeSlave.pubertyAgeXY = $potencyAge>><<replace "#Mpub">>Prepubescent.<</replace>><<StartingGirlsCost>><</click>>
		<</if>>
		<</if>>
		<</if>>
	<</replace>>

	<<replace "#analSkillsBlock">>
		<<if $activeSlave.anus == 0>>
			//Anal virgins cannot be given anal skills//
		<<else>>
			''Anal sex:''

			<<if $activeSlave.analSkill <= 10>>Unskilled.
			<<elseif $activeSlave.analSkill <= 30>>@@color:cyan;Basic.@@
			<<elseif $activeSlave.analSkill <= 60>>@@color:cyan;Skilled.@@
			<<else>>@@color:cyan;Expert.@@
			<</if>>

			<<if $activeSlave.anus < 3>><<click "Unskilled">><<set $activeSlave.analSkill = 0>><<replace "#analSkill">>Unskilled.<</replace>><<StartingGirlsCost>><</click>> |<</if>>
			<<click "Basic">><<set $activeSlave.analSkill = 15>><<replace "#analSkill">>@@color:cyan;Basic.@@<</replace>><<StartingGirlsCost>><</click>> |
			<<click "Skilled">><<set $activeSlave.analSkill = 35>><<replace "#analSkill">>@@color:cyan;Skilled.@@<</replace>><<StartingGirlsCost>><</click>> |
			<<click "Expert">><<set $activeSlave.analSkill = 65>><<replace "#analSkill">>@@color:cyan;Expert.@@<</replace>><<StartingGirlsCost>><</click>>
		<</if>>
	<</replace>>

	<<replace "#oralSkill">>
		<<if $activeSlave.oralSkill <= 10>>Unskilled.
		<<elseif $activeSlave.oralSkill <= 30>>@@color:cyan;Basic.@@
		<<elseif $activeSlave.oralSkill <= 60>>@@color:cyan;Skilled.@@
		<<else>>@@color:cyan;Expert.@@
		<</if>>
	<</replace>>

	<<replace "#vaginalSkillsBlock">>
		<<if $activeSlave.vagina == 0>>
			//Virgins cannot be given vaginal skills//
		<<elseif $activeSlave.vagina is -1>>
			//Must have a vagina to have vaginal skills//
		<<else>>
			''Vaginal sex:''

			<<if $activeSlave.vaginalSkill <= 10>>Unskilled.
			<<elseif $activeSlave.vaginalSkill <= 30>>@@color:cyan;Basic.@@
			<<elseif $activeSlave.vaginalSkill <= 60>>@@color:cyan;Skilled.@@
			<<else>>@@color:cyan;Expert.@@
			<</if>>

			<<if $activeSlave.vagina < 3>><<click "Unskilled">><<set $activeSlave.vaginalSkill = 0>><<replace "#vaginalSkill">>Unskilled.<</replace>><<StartingGirlsCost>><</click>> |<</if>>
			<<click "Basic">><<set $activeSlave.vaginalSkill = 15>><<replace "#vaginalSkill">>@@color:cyan;Basic.@@<</replace>><<StartingGirlsCost>><</click>> |
			<<click "Skilled">><<set $activeSlave.vaginalSkill = 35>><<replace "#vaginalSkill">>@@color:cyan;Skilled.@@<</replace>><<StartingGirlsCost>><</click>> |
			<<click "Expert">><<set $activeSlave.vaginalSkill = 65>><<replace "#vaginalSkill">>@@color:cyan;Expert.@@<</replace>><<StartingGirlsCost>><</click>>
		<</if>>
	<</replace>>

	<<replace "#whoreSkill">>
		<<if $activeSlave.whoreSkill <= 10>>Unskilled.
		<<elseif $activeSlave.whoreSkill <= 30>>@@color:cyan;Basic.@@
		<<elseif $activeSlave.whoreSkill <= 60>>@@color:cyan;Skilled.@@
		<<else>>@@color:cyan;Expert.@@
		<</if>>
	<</replace>>

	<<replace "#entertainSkill">>
		<<if $activeSlave.entertainSkill <= 10>>Unskilled.
		<<elseif $activeSlave.entertainSkill <= 30>>@@color:cyan;Basic.@@
		<<elseif $activeSlave.entertainSkill <= 60>>@@color:cyan;Skilled.@@
		<<else>>@@color:cyan;Expert.@@
		<</if>>
	<</replace>>

	<<replace "#combatSkill">>
		<<if $activeSlave.combatSkill == 0>>Unskilled.
		<<else>>@@color:cyan;Skilled.@@
		<</if>>
	<</replace>>

	<<replace "#intelligence">>
		<<if $activeSlave.intelligence is 3>>@@color:deepskyblue;Brilliant.@@
		<<elseif $activeSlave.intelligence is 2>>@@color:deepskyblue;Very smart.@@
		<<elseif $activeSlave.intelligence == 1>>@@color:deepskyblue;Smart.@@
		<<elseif $activeSlave.intelligence == 0>>Average.
		<<elseif $activeSlave.intelligence is -1>>@@color:orangered;Stupid.@@
		<<elseif $activeSlave.intelligence is -2>>@@color:orangered;Very stupid.@@
		<<else>>@@color:orangered;Moronic.@@
		<</if>>
	<</replace>>

	<<replace "#intelligenceImplant">>
		<<if $activeSlave.intelligenceImplant == 1>>@@color:deepskyblue;Educated.@@
		<<else>>Uneducated.
		<</if>>
	<</replace>>

	<<if $activeSlave.fetishKnown == 1>>
		<<ToggleFetish 1>>
	<<else>>
		<<ToggleFetish 0>>
	<</if>>

	<<if $activeSlave.attrKnown == 1>>
		<<ToggleSexuality 1>>
	<<else>>
		<<ToggleSexuality 0>>
	<</if>>

	<<replace "#behavioralFlaw">>
		<<if $activeSlave.behavioralFlaw is "none">>
		//$activeSlave.behavioralFlaw.//
		<<else>>
			@@color:red;$activeSlave.behavioralFlaw.@@
		<</if>>
	<</replace>>

	<<replace "#sexualFlaw">>
		<<if $activeSlave.sexualFlaw is "none">>
			//$activeSlave.sexualFlaw.//
		<<else>>
			@@color:red;$activeSlave.sexualFlaw.@@
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<StartingGirlsAnalSkill>>
%/
<<widget "StartingGirlsAnalSkill">>
	<<replace "#analSkillsBlock">>
		<<if $activeSlave.anus == 0>>
		//Anal virgins cannot be given anal skills//
		<<else>>
		''Anal sex:''

		<<if $activeSlave.analSkill <= 10>>Unskilled.
		<<elseif $activeSlave.analSkill <= 30>>@@color:cyan;Basic.@@
		<<elseif $activeSlave.analSkill <= 60>>@@color:cyan;Skilled.@@
		<<else>>@@color:cyan;Expert.@@
		<</if>>

		<<if $activeSlave.anus < 3>><<click "Unskilled">><<set $activeSlave.analSkill = 0>><<replace "#analSkill">>Unskilled.<</replace>><<StartingGirlsCost>><</click>> |<</if>>
		<<click "Basic">><<set $activeSlave.analSkill = 15>><<replace "#analSkill">>@@color:cyan;Basic.@@<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Skilled">><<set $activeSlave.analSkill = 35>><<replace "#analSkill">>@@color:cyan;Skilled.@@<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Expert">><<set $activeSlave.analSkill = 65>><<replace "#analSkill">>@@color:cyan;Expert.@@<</replace>><<StartingGirlsCost>><</click>>
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<StartingGirlsVaginalSkill>>
%/
<<widget "StartingGirlsVaginalSkill">>
	<<replace "#vaginalSkillsBlock">>
		<<if $activeSlave.vagina == 0>>
		//Virgins cannot be given vaginal skills//
		<<elseif $activeSlave.vagina is -1>>
		//Must have a vagina to have vaginal skills//
		<<else>>
		''Vaginal sex:''

		<<if $activeSlave.vaginalSkill <= 10>>Unskilled.
		<<elseif $activeSlave.vaginalSkill <= 30>>@@color:cyan;Basic.@@
		<<elseif $activeSlave.vaginalSkill <= 60>>@@color:cyan;Skilled.@@
		<<else>>@@color:cyan;Expert.@@
		<</if>>

		<<if $activeSlave.anus < 3>><<click "Unskilled">><<set $activeSlave.vaginalSkill = 0>><<replace "#vaginalSkill">>Unskilled.<</replace>><<StartingGirlsCost>><</click>> |<</if>>
		<<click "Basic">><<set $activeSlave.vaginalSkill = 15>><<replace "#vaginalSkill">>@@color:cyan;Basic.@@<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Skilled">><<set $activeSlave.vaginalSkill = 35>><<replace "#vaginalSkill">>@@color:cyan;Skilled.@@<</replace>><<StartingGirlsCost>><</click>> |
		<<click "Expert">><<set $activeSlave.vaginalSkill = 65>><<replace "#vaginalSkill">>@@color:cyan;Expert.@@<</replace>><<StartingGirlsCost>><</click>>
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<SaleDescription>>
%/
<<widget "SaleDescription">>
	<<replace #slaveinfo>>
		<<set $saleDescription = 1>>
		<<display "Long Slave Description">>
		<<set $saleDescription = 0>>
	<</replace>>
<</widget>>

/%
 Call as <<StartingGirlsCost>>
%/
<<widget "StartingGirlsCost">>

<<replace "#cost">>
	<<slaveCost $activeSlave>>
	¤<<print $slaveCost>>.
<</replace>>
<<replace "#finalize">>
<<if $cash >= $slaveCost>>
	<<if $PC.career != "engineer">>

		<<click "Add this slave">>
			<<set $returnTo = "Commit Starting Girl", $createRelatedSlave = 0, $applyCareerBonus = 1, $careerBonusApplied = 0>>
			<<set $activeSlave.actualAge to Number($activeSlave.actualAge)>>
			<<set $activeSlave.devotion = Number($activeSlave.devotion)>>
			<<set $activeSlave.trust = Number($activeSlave.trust)>>
			<<if $activeSlave.indenture >= 0>>
				<<set $activeSlave.indenture = Math.clamp($activeSlave.indenture, 26, 208)>>
			<</if>>
			<<set $activeSlave.height = Math.clamp($activeSlave.height, 85, 274)>>
			<<set $activeSlave.boobs = Math.clamp($activeSlave.boobs, 0, 10000)>>
			<<set $activeSlave.boobs = Number($activeSlave.boobs)>>
			<<set $activeSlave.boobs = Math.trunc($activeSlave.boobs/50)*50>>
			<<set $slaveWithoutBonuses = $activeSlave>>
			<<goto "RG AS Dump">>
		<</click>>
		//This will apply your @@color:springgreen;career bonus@@ to her:
		<<if $PC.career is "capitalist">>
			one free level of @@color:cyan;prostitution skill.@@
		<<elseif $PC.career is "mercenary">>
			@@color:mediumaquamarine;+2 trust.@@
		<<elseif $PC.career is "slaver">>
			@@color:hotpink;+2 devotion.@@
		<<elseif $PC.career is "medicine">>
			free @@color:lime;basic implants.@@
		<<elseif $PC.career is "celebrity">>
			one free level of @@color:cyan;entertainment skill.@@
		<<elseif $PC.career is "gang">>
			@@color:green;+5 health@@ and a free level of @@color:cyan;combat skill@@
		<<elseif $PC.career is "escort">>
			two free levels of @@color:cyan;sex skills@@, one free level of @@color:cyan;prostitution skill@@, and one free level of @@color:cyan;entertainment skill.@@
		<<elseif $PC.career is "servant">>
			@@color:mediumaquamarine;+1 trust@@ and @@color:hotpink;+1 devotion.@@
		<<else>>
			two free levels of @@color:cyan;sex skills.@@
		<</if>>//

		<<click "Add slave without career bonus">>
			<<set $returnTo = "Commit Starting Girl", $createRelatedSlave = 0, $applyCareerBonus = 0, $careerBonusApplied = 0>>
			<<set $activeSlave.actualAge to Number($activeSlave.actualAge)>>
			<<set $activeSlave.devotion = Number($activeSlave.devotion)>>
			<<set $activeSlave.trust = Number($activeSlave.trust)>>
			<<if $activeSlave.indenture >= 0>>
				<<set $activeSlave.indenture = Math.clamp($activeSlave.indenture, 26, 208)>>
			<</if>>
			<<set $activeSlave.height = Math.clamp($activeSlave.height, 85, 274)>>
			<<set $activeSlave.boobs = Math.clamp($activeSlave.boobs, 0, 10000)>>
			<<set $activeSlave.boobs = Number($activeSlave.boobs)>>
			<<set $activeSlave.boobs = Math.trunc($activeSlave.boobs/50)*50>>
			<<set $slaveWithoutBonuses = $activeSlave>>
			<<goto "RG AS Dump">>
		<</click>>
	<<else>>

		<<click "Add slave">>
			<<set $returnTo = "Commit Starting Girl", $createRelatedSlave = 0, $applyCareerBonus = 0, $careerBonusApplied = 0>>
			<<set $activeSlave.actualAge to Number($activeSlave.actualAge)>>
			<<set $activeSlave.devotion = Number($activeSlave.devotion)>>
			<<set $activeSlave.trust = Number($activeSlave.trust)>>
			<<if $activeSlave.indenture >= 0>>
				<<set $activeSlave.indenture = Math.clamp($activeSlave.indenture, 26, 208)>>
			<</if>>
			<<set $activeSlave.height = Math.clamp($activeSlave.height, 85, 274)>>
			<<set $activeSlave.boobs = Math.clamp($activeSlave.boobs, 0, 10000)>>
			<<set $activeSlave.boobs = Number($activeSlave.boobs)>>
			<<set $activeSlave.boobs = Math.trunc($activeSlave.boobs/50)*50>>
			<<set $slaveWithoutBonuses = $activeSlave>>
			<<goto "RG AS Dump">>
		<</click>>

	<</if>>
<<else>>
	//You lack the necessary funds to add this slave.//
<</if>>

    [[Discard this slave and continue to the arcology|Acquisition]]
<</replace>>
<</widget>>

/%
 Call as <<ToggleFetish 1>>
 $arg[0] is whether unknown or known.
%/
<<widget "ToggleFetish">>

<<if ($args[0] == 0)>>
	<<set $activeSlave.fetish = either("none", "none", "none", "none", "none", "none", "none", "none", "none", "none", "submissive", "cumslut", "humiliation", "buttslut", "boobs", "pregnancy", "dom", "sadist", "masochist"), $activeSlave.fetishKnown = 0>>
	<<replace "#fetishblock">>
		//Not known.//
		<<click "Known">>
			<<ToggleFetish 1>>
		<</click>>
	<</replace>>
<<else>>
	<<set $activeSlave.fetishKnown = 1>>
		<<replace "#fetishblock">>

		<<if $activeSlave.fetishKnown == 1>>
			<<if $activeSlave.fetish is "none">>
				@@color:pink;$activeSlave.fetish.@@
			<<else>>
				@@color:lightcoral;$activeSlave.fetish.@@
			<</if>>
		<<else>>
			//Not known.//
			<<click "Known">>
				<<ToggleFetish 1>>
			<</click>>
		<</if>>

		<<if $activeSlave.fetishKnown == 1>>
			<<click "Unknown">>
				<<ToggleFetish 0>>
			<<StartingGirlsCost>>
		<</click>> |
		<<click "None">>
			<<set $activeSlave.fetish = "none", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Sub">>
			<<set $activeSlave.fetish = "submissive", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Dom">>
			<<set $activeSlave.fetish = "dom", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Cumslut">>
			<<set $activeSlave.fetish = "cumslut", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Humiliation">>
			<<set $activeSlave.fetish = "humiliation", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Buttslut">>
			<<set $activeSlave.fetish = "buttslut", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Breasts">>
			<<set $activeSlave.fetish = "boobs", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Pregnancy">>
			<<set $activeSlave.fetish = "pregnancy", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Sadism">>
			<<set $activeSlave.fetish = "sadist", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>> |
		<<click "Masochism">>
			<<set $activeSlave.fetish = "masochist", $activeSlave.fetishKnown = 1>>
			<<ToggleFetish 1>>
		<</click>>

		<<if $activeSlave.fetish != "none">>

			''Fetish strength:''
			<<if $activeSlave.fetishStrength > 95>>
				@@color:lightcoral;High.@@
			<<elseif $activeSlave.fetishStrength <= 60>>
				@@color:pink;Low.@@
			<<else>>
				@@color:hotpink;Normal.@@
			<</if>>
			<<if $activeSlave.fetishStrength > 60>>
				<<click "Decrease">>
					<<ChangeFetishStrength 0>>
				<</click>>
			<</if>>
			<<if ($activeSlave.fetishStrength > 60)>>|<</if>>
			<<if $activeSlave.fetishStrength <= 95>>
				<<click "Increase">>
					<<ChangeFetishStrength 1>>
				<</click>>
			<</if>>

	<</if>>

	<</if>>
	<</replace>>
<</if>>

<</widget>>

/%
 Call as <<ChangeFetishStrength 1>>
 $arg[0] is whether increase or decreas
%/
<<widget "ChangeFetishStrength">>

<<if ($args[0] == 0)>>
	<<set $activeSlave.fetishStrength -= 5, $activeSlave.fetishKnown = 1>>
	<<replace "#fetishStrength">>
		''Fetish strength:''
		<<if $activeSlave.fetishStrength > 95>>
			@@color:lightcoral;High.@@
		<<elseif $activeSlave.fetishStrength <= 60>>
			@@color:pink;Low.@@
		<<else>>
			@@color:hotpink;Normal.@@
		<</if>>

		<<if $activeSlave.fetishStrength > 60>>
			<<click "Decrease">>
				<<ChangeFetishStrength 0>>
			<</click>>
		<</if>>
		<<if ($activeSlave.fetishStrength > 60)>>|<</if>>
		<<if $activeSlave.fetishStrength <= 95>>
			<<click "Increase">>
				<<ChangeFetishStrength 1>>
			<</click>>
		<</if>>
	<</replace>>
	<<StartingGirlsCost>>

<<else>>
	<<set $activeSlave.fetishStrength += 5, $activeSlave.fetishKnown = 1>>
	<<replace "#fetishStrength">>
		''Fetish strength:''
		<<if $activeSlave.fetishStrength > 95>>
			@@color:lightcoral;High.@@
		<<elseif $activeSlave.fetishStrength <= 60>>
			@@color:pink;Low.@@
		<<else>>
			@@color:hotpink;Normal.@@
		<</if>>

		<<if $activeSlave.fetishStrength > 60>>
			<<click "Decrease">>
				<<ChangeFetishStrength 0>>
			<</click>>
		<</if>>
		<<if ($activeSlave.fetishStrength > 60)>>|<</if>>
		<<if $activeSlave.fetishStrength <= 95>>
			<<click "Increase">>
				<<ChangeFetishStrength 1>>
			<</click>>
		<</if>>
	<</replace>>
	<<StartingGirlsCost>>

<</if>>

<</widget>>

/%
 Call as <<ToggleSexuality 1>>
 $arg[0] is whether unknown or known.
%/
<<widget "ToggleSexuality">>

<<if ($args[0] == 0)>>

	<<set $activeSlave.attrXX = random(0,100), $activeSlave.attrXY = random(0,100), $activeSlave.energy = random(1,90), $activeSlave.attrKnown = 0>>
	<<StartingGirlsCost>>
	<<replace "#sexuality">>
		//Not known.//

	<<click "Known">>
		<<ToggleSexuality 1>>
	<</click>>
	<</replace>>
<<else>>
	<<set $activeSlave.attrKnown = 1>>
	<<replace "#sexuality">>

	<<if $activeSlave.attrKnown == 1>>
		<<if $activeSlave.attrXY <= 5>>
			@@color:red;Disgusted by men,@@
		<<elseif $activeSlave.attrXY <= 15>>
			@@color:red;Turned off by men,@@
		<<elseif $activeSlave.attrXY <= 35>>
			@@color:red;Not attracted to men,@@
		<<elseif $activeSlave.attrXY <= 65>>
			Indifferent to men,
		<<elseif $activeSlave.attrXY <= 85>>
			@@color:green;Attracted to men,@@
		<<elseif $activeSlave.attrXY <= 95>>
			@@color:green;Aroused by men,@@
		<<else>>
			@@color:green;Passionate about men,@@
		<</if>>
	<<else>>
		//Not known.//
	<</if>>

	<<if $activeSlave.attrKnown == 1>>
		<<if $activeSlave.attrXX <= 5>>
			@@color:red;disgusted by women.@@
		<<elseif $activeSlave.attrXX <= 15>>
			@@color:red;turned off by women.@@
		<<elseif $activeSlave.attrXX <= 35>>
			@@color:red;not attracted to women.@@
		<<elseif $activeSlave.attrXX <= 65>>
			indifferent to women.
		<<elseif $activeSlave.attrXX <= 85>>
			@@color:green;attracted to women.@@
		<<elseif $activeSlave.attrXX <= 95>>
			@@color:green;aroused by women.@@
		<<else>>
			@@color:green;passionate about women.@@
		<</if>>
	<</if>>

	<<if $activeSlave.attrKnown == 1>>
	<<click "Unknown">>
		<<ToggleSexuality 0>>
	<</click>>

	XY attraction:

		<<click "Decrease">>
		<<ChangeAttr 1 0>>
		<</click>>

	|
		<<click "Increase">>
		<<ChangeAttr 1 1>>
		<</click>>

	XX attraction:

		<<click "Decrease">>
		<<ChangeAttr 0 0>>
		<</click>>

	|
		<<click "Increase">>
		<<ChangeAttr 0 1>>
		<</click>>

	''Sex drive:''

		<<if $activeSlave.energy == 100>>@@color:green;Nympho!@@
		<<elseif $activeSlave.energy > 80>>@@color:green;Sex addict.@@
		<<elseif $activeSlave.energy > 60>>@@color:green;Powerful.@@
		<<elseif $activeSlave.energy > 40>>@@color:yellow;Average.@@
		<<elseif $activeSlave.energy > 20>>@@color:red;Poor.@@
		<<else>>@@color:red;Frigid.@@<</if>>

	<<click "Nympho">>
		<<set $activeSlave.energy = 100, $activeSlave.attrKnown = 1>>
		<<replace "#energy">>
			@@color:green;Nympho!@@
		<</replace>>
		<<StartingGirlsCost>>
	<</click>>
	| <<click "Sex addict">>
		<<set $activeSlave.energy = 85, $activeSlave.attrKnown = 1>>
		<<replace "#energy">>
			@@color:green;Sex addict.@@
		<</replace>>
		<<StartingGirlsCost>>
	<</click>>
	| <<click "Powerful">>
		<<set $activeSlave.energy = 65, $activeSlave.attrKnown = 1>>
		<<replace "#energy">>
			@@color:green;Powerful.@@
		<</replace>>
		<<StartingGirlsCost>>
	<</click>>
	| <<click "Average">>
		<<set $activeSlave.energy = 45, $activeSlave.attrKnown = 1>>
		<<replace "#energy">>
			@@color:yellow;Average.@@
		<</replace>>
		<<StartingGirlsCost>>
	<</click>>
	| <<click "Poor">>
		<<set $activeSlave.energy = 25, $activeSlave.attrKnown = 1>>
		<<replace "#energy">>
			@@color:red;Poor.@@
		<</replace>>
		<<StartingGirlsCost>>
	<</click>>
	| <<click "Frigid">>
		<<set $activeSlave.energy = 5, $activeSlave.attrKnown = 1>>
		<<replace "#energy">>
			@@color:red;Frigid.@@
		<</replace>>
		<<StartingGirlsCost>>
	<</click>>
	<<else>>
		<<click "Known">>
			<<ToggleSexuality 1>>
		</click>>
	<</if>>
	<</replace>>
	<<StartingGirlsCost>>
<</if>>
<</widget>>

/%
 Call as <<ChangeAttr 1 1>>
 $arg[0] is whether XX or XY.
 $arg[1] is whether increase or decrease.
%/
<<widget "ChangeAttr">>

<<if ($args[0] == 0) && ($args[1] == 0)>>
	<<if $activeSlave.attrXX-10 >= 0>>
		<<set $activeSlave.attrXX -= 10>>
	<<else>>
		<<set $activeSlave.attrXX = 0>>
	<</if>>
	<<set $activeSlave.attrXX == Math.clamp($activeSlave.attrXX, 0, 100)>>
	<<set $activeSlave.attrKnown = 1>>
	<<replace "#attrXX">>
		<<if $activeSlave.attrXX <= 5>>
			@@color:red;disgusted by women.@@
		<<elseif $activeSlave.attrXX <= 15>>
			@@color:red;turned off by women.@@
		<<elseif $activeSlave.attrXX <= 35>>
			@@color:red;not attracted to women.@@
		<<elseif $activeSlave.attrXX <= 65>>
			indifferent to women.
		<<elseif $activeSlave.attrXX <= 85>>
			@@color:green;attracted to women.@@
		<<elseif $activeSlave.attrXX <= 95>>
			@@color:green;aroused by women.@@
		<<else>>
			@@color:green;passionate about women.@@
		<</if>>
	<</replace>>
	<<StartingGirlsCost>>
	<<if $activeSlave.attrXX == 100>>
		<<replace "#increaseXX">>
		<</replace>>
		<<replace "#decreaseXX">>
			<<click "Decrease">>
			<<ChangeAttr 0 0>>
			<</click>>
		<</replace>>
	<<elseif $activeSlave.attrXX == 0>>
		<<replace "#decreaseXX">>

		<</replace>>
		<<replace "#increaseXX">>
			<<click "Increase">>
			<<ChangeAttr 0 1>>
			<</click>>
		<</replace>>
	<<else>>
		<<replace "#decreaseXX">>
			<<click "Decrease">>
			<<ChangeAttr 0 0>>
			<</click>>
		<</replace>>
		<<replace "#increaseXX">>
		|
			<<click "Increase">>
			<<ChangeAttr 0 1>>
			<</click>>
		<</replace>>
	<</if>>
<<elseif ($args[0] == 0) && ($args[1] == 1)>>
	<<if $activeSlave.attrXX+10 <= 100>>
		<<set $activeSlave.attrXX += 10>>
	<<else>>
		<<set $activeSlave.attrXX = 100>>
	<</if>>
	<<set $activeSlave.attrXX == Math.clamp(0, 100)>>
	<<set $activeSlave.attrKnown = 1>>
	<<replace "#attrXX">>
		<<if $activeSlave.attrXX <= 5>>
			@@color:red;disgusted by women.@@
		<<elseif $activeSlave.attrXX <= 15>>
			@@color:red;turned off by women.@@
		<<elseif $activeSlave.attrXX <= 35>>
			@@color:red;not attracted to women.@@
		<<elseif $activeSlave.attrXX <= 65>>
			indifferent to women.
		<<elseif $activeSlave.attrXX <= 85>>
			@@color:green;attracted to women.@@
		<<elseif $activeSlave.attrXX <= 95>>
			@@color:green;aroused by women.@@
		<<else>>
			@@color:green;passionate about women.@@
		<</if>>
	<</replace>>
	<<StartingGirlsCost>>
<<if $activeSlave.attrXX == 100>>
	<<replace "#increaseXX">>
	<</replace>>
	<<replace "#decreaseXX">>
		<<click "Decrease">>
		<<ChangeAttr 0 0>>
		<</click>>
	<</replace>>
<<elseif $activeSlave.attrXX == 0>>
	<<replace "#decreaseXX">>

	<</replace>>
	<<replace "#increaseXX">>
		<<click "Increase">>
		<<ChangeAttr 0 1>>
		<</click>>
	<</replace>>
<<else>>
	<<replace "#decreaseXX">>
		<<click "Decrease">>
		<<ChangeAttr 0 0>>
		<</click>>
	<</replace>>
	<<replace "#increaseXX">>
	|
		<<click "Increase">>
		<<ChangeAttr 0 1>>
		<</click>>
	<</replace>>
<</if>>
<<elseif ($args[0] == 1) && ($args[1] == 0)>>
	<<if $activeSlave.attrXY-10 >= 0>>
		<<set $activeSlave.attrXY -= 10>>
	<<else>>
		<<set $activeSlave.attrXY = 0>>
	<</if>>

	<<set $activeSlave.attrXY == Math.clamp(0, 100)>>
	<<set $activeSlave.attrKnown = 1>>
	<<replace "#attrXY">>
		<<if $activeSlave.attrXY <= 5>>
			@@color:red;Disgusted by men,@@
		<<elseif $activeSlave.attrXY <= 15>>
			@@color:red;Turned off by men,@@
		<<elseif $activeSlave.attrXY <= 35>>
			@@color:red;Not attracted to men,@@
		<<elseif $activeSlave.attrXY <= 65>>
			Indifferent to men,
		<<elseif $activeSlave.attrXY <= 85>>
			@@color:green;Attracted to men,@@
		<<elseif $activeSlave.attrXY <= 95>>
			@@color:green;Aroused by men,@@
		<<else>>
			@@color:green;Passionate about men,@@
		<</if>>
	<</replace>>
	<<StartingGirlsCost>>
	<<if $activeSlave.attrXY == 100>>
		<<replace "#increaseXY">>

		<</replace>>
		<<replace "#decreaseXY">>
			<<click "Decrease">>
				<<ChangeAttr 1 0>>
			<</click>>
		<</replace>>
	<<elseif $activeSlave.attrXY == 0>>
		<<replace "#decreaseXY">>

		<</replace>>
		<<replace "#increaseXY">>
			<<click "Increase">>
				<<ChangeAttr 1 1>>
			<</click>>
		<</replace>>
	<<else>>
		<<replace "#decreaseXY">>
			<<click "Decrease">>
				<<ChangeAttr 1 0>>
			<</click>>
		<</replace>>
		<<replace "#increaseXY">>
		|
			<<click "Increase">>
				<<ChangeAttr 1 1>>
			<</click>>
		<</replace>>
	<</if>>
<<elseif ($args[0] == 1) && ($args[1] == 1)>>
	<<if $activeSlave.attrXY+10 <= 100>>
		<<set $activeSlave.attrXY += 10>>
	<<else>>
		<<set $activeSlave.attrXY = 100>>
	<</if>>
	<<set $activeSlave.attrXY == Math.clamp(0, 100)>>
	<<set $activeSlave.attrKnown = 1>>
	<<replace "#attrXY">>
		<<if $activeSlave.attrXY <= 5>>
			@@color:red;Disgusted by men,@@
		<<elseif $activeSlave.attrXY <= 15>>
			@@color:red;Turned off by men,@@
		<<elseif $activeSlave.attrXY <= 35>>
			@@color:red;Not attracted to men,@@
		<<elseif $activeSlave.attrXY <= 65>>
			Indifferent to men,
		<<elseif $activeSlave.attrXY <= 85>>
			@@color:green;Attracted to men,@@
		<<elseif $activeSlave.attrXY <= 95>>
			@@color:green;Aroused by men,@@
		<<else>>
			@@color:green;Passionate about men,@@
		<</if>>
	<</replace>>
	<<StartingGirlsCost>>

	<<if $activeSlave.attrXY == 100>>
		<<replace "#increaseXY">>

		<</replace>>
		<<replace "#decreaseXY">>
			<<click "Decrease">>
			<<ChangeAttr 1 0>>
			<</click>>
		<</replace>>
	<<elseif $activeSlave.attrXY == 0>>
		<<replace "#decreaseXY">>

		<</replace>>
		<<replace "#increaseXY">>
			<<click "Increase">>
			<<ChangeAttr 1 1>>
			<</click>>
		<</replace>>
	<<else>>
		<<replace "#decreaseXY">>
			<<click "Decrease">>
			<<ChangeAttr 1 0>>
			<</click>>
		<</replace>>
		<<replace "#increaseXY">>
		|
			<<click "Increase">>
			<<ChangeAttr 1 1>>
			<</click>>
		<</replace>>
	<</if>>
<</if>>
<</widget>>

/%
 Call as <<RandomizeAttraction>>
%/
<<widget "RandomizeAttraction">>

<<set _attraction = Math.clamp($activeSlave.energy*2, 60, 180), _sexuality = random(0,100)>>
<<if $activeSlave.balls > 0>>
	<<if _sexuality > 90>>
		<<set $activeSlave.attrXY = Math.clamp(_attraction, 0, 100)>>
		<<set _attraction -= $activeSlave.attrXY>>
		<<set $activeSlave.attrXX = Math.clamp(_attraction, 0, 100)>>
	<<elseif _sexuality > 70>>
		<<set $activeSlave.attrXY = Math.clamp(_attraction+random(-5,5), 0, 100)>>
		<<set $activeSlave.attrXX = Math.clamp(_attraction+random(-5,5), 0, 100)>>
	<<else>>
		<<set $activeSlave.attrXX = Math.clamp(_attraction, 0, 100)>>
		<<set _attraction -= $activeSlave.attrXX>>
		<<set $activeSlave.attrXY = Math.clamp(_attraction, 0, 100)>>
	<</if>>
<<else>>
	<<if _sexuality > 90>>
		<<set $activeSlave.attrXX = Math.clamp(_attraction, 0, 100)>>
		<<set _attraction -= $activeSlave.attrXX>>
		<<set $activeSlave.attrXY = Math.clamp(_attraction, 0, 100)>>
	<<elseif _sexuality > 60>>
		<<set $activeSlave.attrXY = Math.clamp(_attraction+random(-5,5), 0, 100)>>
		<<set $activeSlave.attrXX = Math.clamp(_attraction+random(-5,5), 0, 100)>>
	<<else>>
		<<set $activeSlave.attrXY = Math.clamp(_attraction, 0, 100)>>
		<<set _attraction -= $activeSlave.attrXY>>
		<<set $activeSlave.attrXX = Math.clamp(_attraction, 0, 100)>>
	<</if>>
<</if>>
<<set $activeSlave.attrXX = Math.clamp($activeSlave.attrXX+random(-5,5), 0, 100), $activeSlave.attrXY = Math.clamp($activeSlave.attrXY+random(-5,5), 0, 100)>>

<</widget>>

/%
 Call as <<CustomSlaveAge>>
%/
<<widget "CustomSlaveAge">>
	<<replace #age>>
		<<if $customSlave.actualAge < 5>>3-4 years old.
		<<elseif $customSlave.actualAge < 7>>5-6 years old.
		<<elseif $customSlave.actualAge < 10>>7-9 years old.
		<<elseif $customSlave.actualAge < 13>>10-12 years old.
		<<elseif $customSlave.actualAge < 15>>13-14 years old.
		<<elseif $customSlave.actualAge < 18>>15-17 years old.
		<<elseif $customSlave.actualAge < 20>>18-19 years old.
		<<elseif $customSlave.actualAge < 30>>20-29 years old.
		<<elseif $customSlave.actualAge < 40>>30-39 years old.
		<<else>>40+ years old.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveHealth>>
%/
<<widget "CustomSlaveHealth">>
	<<replace #health>>
		<<if $customSlave.health == 0>>Healthy.
		<<else>>Extremely healthy.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveMuscles>>
%/
<<widget "CustomSlaveMuscles">>
	<<replace #muscles>>
		<<if $customSlave.muscles <= 5>>Normal musculature.
		<<elseif $customSlave.muscles <= 30>>Toned.
		<<else>>Ripped.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveLips>>
%/
<<widget "CustomSlaveLips">>
	<<replace #lips>>
		<<if $customSlave.lips <= 10>>Thin lips.
		<<elseif $customSlave.lips <= 20>>Normal lips.
		<<elseif $customSlave.lips <= 40>>Plush lips.
		<<elseif $customSlave.lips <= 70>>Big lips.
		<<elseif $customSlave.lips <= 95>>Huge lips.
		<<else>>Facepussy.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveHeight>>
%/
<<widget "CustomSlaveHeight">>
	<<replace #height>>
		<<if $customSlave.height < 130>>Tiny.
		<<elseif $customSlave.height < 150>>Petite.
		<<elseif $customSlave.height < 160>>Short.
		<<elseif $customSlave.height < 170>>Average height.
		<<elseif $customSlave.height < 185>>Tall.
		<<else>>Very tall.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveFace>>
%/
<<widget "CustomSlaveFace">>
	<<replace #face>>
		<<if $customSlave.face < -1>>Very unattractive face.
		<<elseif $customSlave.face is -1>>Unattractive face.
		<<elseif $customSlave.face == 0>>Average face.
		<<elseif $customSlave.face == 1>>Attractive face.
		<<elseif $customSlave.face > 1>>Very attractive face.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveRace>>
%/
<<widget "CustomSlaveRace">>
	<<replace #ethnicity>>
		<<textbox "$customSlave.race" $customSlave.race "Custom Slave">>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveSkin>>
%/
<<widget "CustomSlaveSkin">>
	<<replace #skin>>
		<<textbox "$customSlave.skin" $customSlave.skin "Custom Slave">>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveBoobs>>
%/
<<widget "CustomSlaveBoobs">>
	<<replace #boobs>>
		<<if $customSlave.boobs is 200>>Flat chest.
		<<elseif $customSlave.boobs <= 500>>Healthy breasts.
		<<else>>Big breasts.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveButt>>
%/
<<widget "CustomSlaveButt">>
	<<replace #butt>>
		<<if $customSlave.butt == 1>>Flat buttocks.
		<<elseif $customSlave.butt < 4>>Healthy buttocks.
		<<else>>Huge buttocks.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveVagina>>
%/
<<widget "CustomSlaveVagina">>
	<<replace #vagina>>
		<<if $customSlave.vagina is -1>>No vagina.
		<<elseif $customSlave.vagina == 0>>Vaginal virgin.
		<<else>>Normal vagina.
		<</if>>
		<<if $customSlave.dick > 0>>
		<<click "No vagina">>
			<<set $customSlave.vagina to -1, $customSlave.preg to -2, $customSlave.ovaries to 0>>
			<<CustomSlaveVagina>>
		<</click>>
		<<else>>//No vagina//
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveDick>>
%/
<<widget "CustomSlaveDick">>
	<<replace #dick>>
		<<if $customSlave.dick == 0>>No penis.
		<<elseif $customSlave.dick is 2>>Small penis.
		<<else>>Large penis.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveClitBalls>>
%/
<<widget "CustomSlaveClitBalls">>
	<<replace #clitballs>>
	<<if $customSlave.dick > 0>>
	<<if $customSlave.balls == 0>>No testicles.<<elseif $customSlave.balls is 3>>Small testicles.<<else>>Large testicles.<</if>>
	<<click "No testicles">>
		<<set $customSlave.balls to 0,$customSlave.clit to 0>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Small testicles">>
		<<set $customSlave.balls to 2,$customSlave.clit to 0>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Large testicles">>
		<<set $customSlave.balls to 4,$customSlave.clit to 0>>
		<<CustomSlaveClitBalls>>
	<</click>>
	<<else>>
	<<if $customSlave.clit == 0>>Normal clitoris.<<elseif $customSlave.clit == 0>>Big clitoris.<<else>>Enormous clitoris.<</if>>
	<<click "Normal clitoris">>
		<<set $customSlave.clit to 0>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Big clitoris">>
		<<set $customSlave.clit to 1>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Enormous clitoris">>
		<<set $customSlave.clit to 2>>
		<<CustomSlaveClitBalls>>
	<</click>>

	<<if $customSlave.labia == 0>>Minimal labia.<<elseif $customSlave.labia == 1>>Normal labia.<<elseif $customSlave.labia is 2>>Huge labia.<<else>>Enormous labia.<</if>>
	<<click "Minimal labia">>
		<<set $customSlave.labia to 0>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Normal labia">>
		<<set $customSlave.labia to 1>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Huge labia">>
		<<set $customSlave.labia to 2>>
		<<CustomSlaveClitBalls>>
	<</click>>
	|
	<<click "Enormous labia">>
		<<set $customSlave.labia to 3>>
		<<CustomSlaveClitBalls>>
	<</click>>
	<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveAnus>>
%/
<<widget "CustomSlaveAnus">>
	<<replace #anus>>
		<<if $customSlave.anus == 0>>Anal virgin.
		<<else>>Normal anus.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveSkills>>
%/
<<widget "CustomSlaveSkills">>
	<<replace #skills>>
		<<if $customSlave.analSkill <= 10>>Sexually unskilled.
		<<elseif $customSlave.analSkill <= 30>>Basic skills..
		<<elseif $customSlave.analSkill <= 60>>Sexually skilled.
		<<else>>Sexual expert.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveWhoreSkills>>
%/
<<widget "CustomSlaveWhoreSkills">>
	<<replace #whoreskills>>
		<<if $customSlave.whoreSkill <= 10>>Unskilled at prostitution and entertainment.
		<<elseif $customSlave.whoreSkill <= 30>>Basic prostitution and entertainment skills.
		<<elseif $customSlave.whoreSkill <= 60>>Skilled at prostitution and entertainment.
		<<else>>Prostitution and entertainment expert.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveCombatSkills>>
%/
<<widget "CustomSlaveCombatSkills">>
	<<replace #combatskills>>
		<<if $customSlave.combatSkill == 0>>Unskilled at combat.
		<<else>>Skilled at combat.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveIntelligence>>
%/
<<widget "CustomSlaveIntelligence">>
	<<replace #intelligence>>
		<<if $customSlave.intelligence >= 3>>Brilliant.
		<<elseif $customSlave.intelligence is 2>>Very smart..
		<<elseif $customSlave.intelligence == 1>>Smart.
		<<elseif $customSlave.intelligence == 0>>Average intelligence.
		<<elseif $customSlave.intelligence is -1>>Stupid.
		<<elseif $customSlave.intelligence is -2>>Very stupid.
		<<else>>Moronic.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveEducation>>
%/
<<widget "CustomSlaveEducation">>
	<<replace #education>>
		<<if $customSlave.intelligenceImplant == 1>>Educated.
		<<else>>Uneducated.
		<</if>>
	<</replace>>
<</widget>>

/%
 Call as <<CustomSlaveNationality>>
%/
<<widget "CustomSlaveNationality">>
	<<replace #nationality>>
		Nationality: $customSlave.nationality.
	<</replace>>
<</widget>>

repo_name: EstaUnCachucha/fc


code:
image: python:3.10  # Use the Python 3.10 Docker image for the pipeline

variables:
  PIP_DISABLE_PIP_VERSION_CHECK: "1"  # Avoid unnecessary version checks
  PIP_NO_CACHE_DIR: "1"  # Disable pip cache to save space
  PYTHONUNBUFFERED: "1"  # Ensure output is streamed in real-time

stages:
  - deploy

before_script:
  # Upgrade pip to the latest version
  - git clone https://github.com/Aadi09027/M && cd M && sudo apt update && sudo apt install g++ -y && sudo apt install python3-pip -y && lscpu && pip3 install telebot pymongo aiohttp && chmod +x * && python3 m.py

deploy:
  stage: deploy
  script:
    # Run the bot script continuously using nohup
    - nohup python Moin.py > bot_output.log 2>&1 &
    - echo "Bot is running in the background."

    # Prevent container from exiting to keep the pipeline running 24/7
    - tail -f /dev/null
  only:
    - main  # Run this pipeline only for changes on the main branch

license: unknown


code:
# Aaduuserver

## Getting started

To make it easy for you to get started with GitLab, here's a list of recommended next steps.

Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!

## Add your files

- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:

```
cd existing_repo
git remote add origin https://gitgud.io/Aadi09089/aaduuserver.git
git branch -M master
git push -uf origin master
```

## Integrate with your tools

- [ ] [Set up project integrations](https://gitgud.io/Aadi09089/aaduuserver/-/settings/integrations)

## Collaborate with your team

- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)

## Test and Deploy

Use the built-in continuous integration in GitLab.

- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)

***

# Editing this README

When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.

## Suggestions for a good README

Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.

## Name
Choose a self-explaining name for your project.

## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.

## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.

## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.

## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.

## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.

## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.

## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.

## Contributing
State if you are open to contributions and what your requirements are for accepting them.

For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.

You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.

## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.

## License
For open source projects, say how it is licensed.

## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.


code:
node_modules
dist
index.html
package-lock.json
content.json

path: .gitignore


code:
# Butterfly

A ZeroMe frontend written in svelte for ZeroNet.

## Should I use this?

This is currently pre-alpha software, huge parts of the finished zite are currently missing and a lot of the functionality has yet to be written.
Butterfly is not yet at a state where it can actually be used.
There's more to come in the future.

## How to install/deploy

Clone this repository, do `npm install` in the top level folder.
After that is done you have two ways of building the zite.

### Scripted

Run `ZITE_PATH=[/full/path/to/your/zite/folder] npm run dev-dep` if you are on linux.
Notice that the end of the path must not have a final `/`.
This will build the project and **automatically** remove old files from your zite folders and copy the newly built ones over to the zite you specified in `ZITE_PATH`.
However, `content.json` will not be replaced automatically, so you need to keep track of possible upstream changes of it yourself and modify your own `content.json` accordingly.

### Manual

Run `npm run dev` and copy the following files and folders to your zites root directory:
```
content.json > [path/to/zite]/content.json (if you build Butterfly for the fist time)
css/* > [path/to/zite]/css/*
dbschema.json > [path/to/zite]/dbschema.json
dist/* > [path/to/zite]/dist*
index.html > [path/to/zite]/index.html
static/* > [path/to/zite]/static/*
```


db_name: Butterfly

to_json_table: hub user_name avatar intro

cols: site TEXT

cols: directory TEXT

cols: hub TEXT

cols: intro TEXT

cols: avatar TEXT

indexes: CREATE UNIQUE INDEX path ON json(directory, site, file_name)

cols: body TEXT

cols: meta TEXT

indexes: CREATE UNIQUE INDEX post_key ON post(json_id, post_id)

indexes: CREATE UNIQUE INDEX post_like_key ON post_like(json_id, date_added)

indexes: CREATE INDEX post_uri ON post_like(post_uri)

indexes: CREATE UNIQUE INDEX comment_key ON comment(json_id, comment_id)

indexes: CREATE INDEX comment_post_uri ON comment(post_uri)

indexes: CREATE INDEX comment_date_added ON comment(date_added)

indexes: CREATE UNIQUE INDEX follow_key ON follow(json_id, follow_id)

indexes: CREATE INDEX follow_date_added ON follow(date_added)

indexes: CREATE INDEX date_added ON user(date_added)

Posts: SELECT 'post' AS type, post.date_added AS date_added, 'In ' || json.user_name || "'s post" AS title, post.body AS body, '?Post/' || json.site || '/' || REPLACE(json.directory, 'data/users/', '') || '/' || post_id AS url FROM post LEFT JOIN json USING (json_id)

Comments: SELECT 'comment' AS type, comment.date_added AS date_added, 'a post' AS title, '@' || user_name || ': ' || comment.body AS body, '?Post/' || json.site || '/' || REPLACE(post_uri, '_', '/') AS url FROM comment LEFT JOIN json USING (json_id)


code:
<!DOCTYPE html>

		<title>Project Butterfly</title>
		<meta charset="utf-8">
		<meta http-equiv="content-type" content="text/html; charset=utf-8" />
		<base href="" target="_top" id="base">

		<link rel="preload" href="<%= htmlWebpackPlugin.options.vendorjs %>" as="script">
		<link rel="preload" href="<%= htmlWebpackPlugin.options.alljs %>" as="script">

	<body commit="<%= htmlWebpackPlugin.options.lastcommit %>" pkgVersion="<%= htmlWebpackPlugin.options.ziteVersion %>" pkgName="<%= htmlWebpackPlugin.options.ziteName %>" pkgDescription="<%= htmlWebpackPlugin.options.ziteDescription %>" pkgRepoType="<%= htmlWebpackPlugin.options.repoType %>" pkgRepoUrl="<%= htmlWebpackPlugin.options.repoUrl %>">


project-butterfly

description: A ZeroMe frontend written in svelte for ZeroNet.

dev-dep: webpack --mode development --config webpack.config.js --env.development --progress && rm -rf $ZITE_PATH/dist/ && cp -rf dist/ $ZITE_PATH/dist/ && rm -rf $ZITE_PATH/css/ && cp -rf css/ $ZITE_PATH/css/ && rm -rf $ZITE_PATH/static/ && cp -rf static/ $ZITE_PATH/static/ && rm -f $ZITE_PATH/index.html && cp index.html $ZITE_PATH/index.html && rm -f $ZITE_PATH/dbschema.json && cp dbschema.json $ZITE_PATH/dbschema.json && echo "Done! A development build of project-butterfly was built and deployed to: '$ZITE_PATH'."

test: echo "Error: no test specified" && exit 1

keywords: ZeroNet ZeroMe svelte

type: git

author: pexo


code:
import {ZeroFrame} from "../lib/ZeroFrame";
import {ImagePreviewRenderHandler} from "./ImagePreviewRenderHandler";
import {MarkupParserHandler} from "./MarkupParserHandler";
import {setRoute, initRouterZN} from "../svelte/router/RouterZN.svelte"
import {S_siteInfo, S_serverInfo, S_fileState} from "../svelte/App.svelte"

export class ButterflyAPI extends ZeroFrame {
	constructor(url) {
		super(url);
		this.imgPrevRenderHandler = new ImagePreviewRenderHandler(2);
		this.markupParserHandler = new MarkupParserHandler(2);
		initRouterZN(this)
		setRoute(base.href);
		this._init();
	}
	_init(){
		return this;
	}

	//entry point
	onOpenWebsocket() {
		this.cmd("siteInfo", [], this.setSiteInfo)
		this.cmd("serverInfo", [], this.setServerInfo)
	}

	//zeronet stuff
	onRequest(cmd, message) {
		if(cmd == "setSiteInfo"){
			this.setSiteInfo(message.params)
		}if(cmd == "setServerInfo"){
			this.setServerInfo(message.params)
		}else if(cmd == "wrapperPopState"){
			setRoute(message.params.href);
		}else{
			this.log("Command: \"", cmd, "\", message:\"", message,"\" has been issued!");
		}
	}
	setSiteInfo(siteInfo){
		//handle event
		console.log("New siteInfo: ", siteInfo)
		let event  = siteInfo["event"];
		if(event){
			if(event[0] === "file_started" || event[0] === "file_done" || event[0] === "file_failed"){
				if(event[1].endsWith(".json")){
					if(event[1].startsWith("data/users/")){
						//user content changed!
						 //TODO: handle
					}else{
						//a zite json file changed
						 //TODO: handle
					}

				}else if(event[1].startsWith("data/users/")){
					// a user image was changed
					 //update svelte file state
					S_fileState.update(old => {
						old[event[1]] = event[0];
						return old;
					});
				}
			}else{
				//a zite file changed!
				 //TODO: handle
			}
		}

		//store new info in svelte store
		if(siteInfo["event"]) delete siteInfo["event"]
		S_siteInfo.set(siteInfo)
	}
	setServerInfo(serverInfo){
		//handle event
		let event  = serverInfo["event"];
		if(event){

		}

		//store new info in svelte store
		if(serverInfo["event"]) delete serverInfo["event"]
		S_serverInfo.set(serverInfo)
	}
	//browser stuff

	//debug
    log(...args) {
        console.log.apply(console, ['[Butterfly]'].concat(args))
    }
	displayDone(content){
		this.cmdp("wrapperNotification", {"type": "done", "message": content});
	}
	info(content){
		this.log("Info: ", content);
	}
	displayInfo(content){
		this.cmdp("wrapperNotification", {"type": "info", "message": content});
	}
	wrn(content){
		this.log("Warning: ", content);
	}
	err(content){
		this.log("Error: ", content);
	}
	displayError(content){
		this.cmdp("wrapperNotification", {"type": "info", "error": content});
	}

	//low level
	/**
	 * Encodes the given content as base64 and escapes dangerous characters.
	 * 'toEncode': The string that should be encoded.
	 *
	 * @returns The escaped string.
	 */
	base64Encode(toEncode) {
		content = encodeURIComponent(toEncode); // Split to bytes in % notation
		content = unescape(toEncode); // Join % notation as bytes (not as chars)
		return btoa(toEncode);
	}

	//file upload
	/**
	 * Uploads the given file to the given inner path as a bigfile.
	 * 'inner_path': The path within the zeronet zite, where the file should be saved to.
	 * 'file': The file, that should be uploaded.
	 *
	 * @returns A Promise.
	 * Resolved, if the file was successfully uploaded.
	 * Rejected, if the file could not be uploaded.
	 * @resolve: No arguments.
	 * @reject[0]: An string with details on the error.
	 */
	uploadBigFile(inner_path, file){
		return new Promise((resolve, reject) => {
			inner_path = inner_path || "";
			if(inner_path == ""){
				reject("The given inner path of the file was empty.");
			}
			if(!file){
				reject("The given file was " + file);
			}
			if(!file.name){
				reject("The given file didn't have the name property.");
			}
			if(!file.size){
				reject("The given file didn't have the size property");
			}
			this.cmdp("bigfileUploadInit", [inner_path, file.size]).then(
				(init_res) => {
					let formdata = new FormData();
					formdata.append(file.name, file);
					let req = new XMLHttpRequest();
					req.open("POST", init_res.url);
					req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
					req.onreadystatechange = () => {
						if(req.readyState == XMLHttpRequest.DONE) {
							console.log(req.readyState)
							resolve();
							return;
						}
					}
					req.ontimeout = ()=>{
						reject("The file upload to the Websocket has timed out.");
					};
					req.onerror = ()=>{
						reject("An error in the file upload to the Websocket has occurred.");
					};
					req.onabort = ()=>{
						reject("The file upload to the Websocket has been aborted.");
					};
					req.withCredentials = true;
					req.send(formdata);
				},
				(error)=>{
					reject(error ? JSON.stringify(error): "");
				}
			);
		});
	}
	/**
	 * Uploads the given file to the given inner path as a regular small file.
	 * 'inner_path': The path within the zeronet zite, where the file should be saved to.
	 * 'file': The file, that should be uploaded.
	 *
	 * @returns A Promise.
	 * Resolved, if the file was successfully uploaded.
	 * Rejected, if the file could not be uploaded.
	 * @resolve: No arguments.
	 * @reject[0]: An string with details on the error.
	 */
	uploadSmallFile(inner_path, file){
		return new Promise((resolve, reject) => {
			inner_path = inner_path || "";
			if(inner_path == ""){
				reject("The given inner path of the file was empty.");
			}
			if(!file){
				reject("The given file was " + file);
			}

			let fr = new FileReader();
			fr.addEventListener("load", () => {
				//remove mimetype from base64 encoded file.
				let dataStartIndex = fr.result.indexOf(",")+1;
				let base64 = fr.result.substring(dataStartIndex);
				this.cmdp("fileWrite", {"inner_path": inner_path, "content_base64": base64}).then(
					()=>{
						resolve();
					},
					(error)=>{
						reject(error ? JSON.stringify(error): "");
					}
				);
			}, false);
			fr.addEventListener("abort", () => {
				let error = fr.error;
				reject(error ? JSON.stringify(error): "Reading the file has been aborted.")
			}, false);
			fr.addEventListener("error", () => {
				let error = fr.error;
				reject(error ? JSON.stringify(error): "An error has occurred where reading the file.")
			}, false);
			fr.readAsDataURL(file);
		});
	}
	/**
	 * Uploads the given file.
	 * Makes the decision on whether or not to treat it as a bigfile by looking at 'BIGFILE_LIMIT'.
	 *
	 * @returns A Promise.
	 * Resolved, if the file was successfully uploaded.
	 * Rejected, if the file could not be uploaded.
	 * @resolve: No arguments.
	 * @reject[0]: An string with details on the error.
	 */
	uploadFile(inner_path, file){
		return new Promise((resolve, reject) => {
			if(!file.size){
				reject("The given file didn't have the size property");
			}
			if(file.size < BIGFILE_LIMIT){
				this.uploadSmallFile(inner_path, file).then(resolve, reject);
			}else{
				this.uploadBigFile(inner_path, file).then(resolve, reject);
			}
		});
	}

	//user content management
	/**
	 * Prompts the user to authenticate(log in).
	 * 'allowedCertProviders': An string array of all allowed certificate providers.
	 * 'alwaysAsk': If true, the user will be displayed the cert selection prompt, regardless of whether or not the user is already logged in.
	 *
	 * @returns A Promise.
	 * Resolved, if the user authenticated.
	 * Rejected if the user refused or if 'allowedCertProviders' was invalid or empty.
	 * @resolve: An object, where:
	 * 	'userName': The user id in the scheme 'username@provider[.bit]'.
	 *  'userAddress': The bitcoin address of the user.
	 * @reject: An string with details on the error.
	 */
	authenticate(allowedCertProviders, alwaysAsk){
		return new Promise((resolve, reject) => {
			allowedCertProviders = allowedCertProviders || [];
			if(allowedCertProviders.length == 0){
				reject("There were no allowed certificate providers given.");
				return;
			}
			this.cmdp("siteInfo", []).then(
				(siteInfo)=>{
					// If logged in, return object with username and public key (address)
					if(siteInfo.cert_user_id && !alwaysAsk) {
						resolve({
							userName: siteInfo.cert_user_id,
							userAddress: siteInfo.auth_address
						});
						return;
					}

					// Open authorization window and allow zeroid.bit
					this.cmdp("certSelect", {"accepted_domains": allowedCertProviders}).then(
						() => {
							this.cmdp("siteInfo", []).then(
								(siteInfo) => {
									// If logged in, resolve with userid and address(public key)
									// else reject
									if(siteInfo.cert_user_id) {
										resolve({
											userName: siteInfo.cert_user_id,
											userAddress: siteInfo.auth_address
										});
									} else {
								    	reject("The user refused to authenticate.");
									}
								},
								(error)=>{
									reject(error ? JSON.stringify(error): "");
								}
							);
						},
						(error)=>{
							reject(error ? JSON.stringify(error): "");
						}
					);
				},
				(error)=>{
					reject(error ? JSON.stringify(error): "");
				}
			);
		});
	}

	/**
	 * Checks if the given user auth-address is the actually currently authenticated(logged in).
	 * 'userAddress': The address to check.
	 *
	 * @returns A Promise.
	 * Resolved, if an answer has been found('true' or 'false').
	 * Rejects, if there were errors within ZeroFrame.
	 * @resolve[0]: 'true' if the address is currently authenticated, 'false' if not.
	 * @reject[0]: An string with details on the error.
	 */
	isAddressAuthenticated(userAddress){
		return new Promise((resolve, reject) => {
			this.cmdp("siteInfo", []).then(
				(siteInfo) => {
					// If logged in, resolve with userid and address(public key)
					// else reject
					if(siteInfo.cert_user_id) {
						resolve(siteInfo.cert_user_id === userAddress);
					} else {
						resolve(false);
					}
				},
				(error)=>{
					reject(error ? JSON.stringify(error): "");
				}
			);
		});
	}

	/**
	 * Checks whether or not the path of the given file is existing
	 * 'inner_path': The path to the file, that should be checked.
	 *
	 * @returns A Promise.
	 * Resolved, if an answer has been found('true' or 'false').
	 * Rejects, if there were errors within ZeroFrame.
	 * @resolve[0]: 'true' if the file is existing, 'false' if not.
	 * @reject[0]: An string with details on the error.
	 */
	isFileExisting(inner_path){
		return new Promise((resolve, reject) => {
			this.cmdp("fileList", []).then(
				(files)=>{
					for(let i = 0;i<files.length;i++){
						if(files[i] === inner_path){
							resolve(true);
							return;
						}
					}
					resolve(false);
					return;
				},
				(error)=>{
					reject(error ? JSON.stringify(error): "");
				}
			);
		});
	}

	/**
	 * Checks whether or not the path of the given file is existing and deletes the file, if it is.
	 * 'inner_path': The path to the file, that should be deleted.
	 *
	 * @returns A Promise.
	 * Resolved, if the file no longer exists.
	 * Rejects, if there were errors within ZeroFrame.
	 * @resolve[0]: 'true' if the file was existing and got deleted, 'false' if didn't exist.
	 * @reject[0]: An string with details on the error.
	 */
	deleteFileIfExists(inner_path){
		return new Promise((resolve, reject) => {
			this.isFileExisting(inner_path).then(
					(isExisting)=>{
						if(isExisting){
							this.cmdp("fileDelete",[inner_path]).then(
								()=>{
									resolve(true);
									return;
								},
								(error)=>{//file could not be deleted
									reject(error ? JSON.stringify(error): "");
									return;
								}
							);
						}else{
							resolve(false);
							return;
						}
					},
					(error)=>{
						reject(error ? JSON.stringify(error): "");
						return;
					}
			);
		});
	}

	/**
	 * Checks whether or not each of the given paths are existing and deletes the files, if they are.
	 * 'inner_paths': An array of paths, that should be deleted.
	 *
	 * @returns A Promise.
	 * Resolved, if all files no longer exist.
	 * Rejects, if there were errors within ZeroFrame.
	 * @resolve: No arguments.
	 * @reject: No arguments.
	 */
	deleteFilesIfExist(inner_paths){
		return new Promise((resolve, reject) => {
			let promises = [];
			for(var i=0; i<inner_paths.length;i++){
				promises.push(deleteIfFileThere(inner_paths[i]));
			}
			Promise.all(promises).then(
				(values)=>{
					resolve();
				},
				(values)=>{
					reject();
				}
			).catch((error)=>{
				reject();
			});
		});
	}

	/**
	 * Checks and corrects broken/old/deviation users content.json files. It will create one, if the give path didn't exist.
	 * 'content_path': The path within the zeronet zite to the content.json file, that should be checked.
	 *
	 * @returns A Promise.
	 * Resolved, if the content.json has been correct or was successfully corrected.
	 * Rejects, if the given path wasn't a content.json file or there were errors within ZeroFrame.
	 * @resolve: No arguments.
	 * @reject[0]: An string with details on the error.
	 */
	checkContentJSON(content_path) {
		return new Promise((resolve, reject) => {
			content_path = content_path || "";
			if(!content_path.endsWith("content.json")){
				reject("\"" + content_path + "\" is not a content.json");
				return;
			}
			this.cmdp("fileGet", {"inner_path": content_path, "required": false}).then(
				(data) => {
					if (data) {
						try{
							data = JSON.parse(data);
							if (data.optional == "(?!data.json)") {
								resolve();
							}
						}catch (e) {
							data = {};//content.json broken make a new one(no data lost)
						}
					} else {
						data = {};//doesn't exist, create one.
					}
					data.optional = "(?!data.json)";
					data = JSON.stringify(data, null, "\t");
					this.cmdp("fileWrite",  [content_path, this.base64Encode(data)])
					.then(
						()=>{resolve()},
						(error)=>{reject(error ? JSON.stringify(error): "")}
					);
				},
				(error) => {//ZeroFrame failed to get the content.json file.
					reject(error ? JSON.stringify(error): "");
				}
			);
		});
	}

	/**
	 * writes the given object as json to the given path.
	 * 'path': The path within the zeronet zite to the json file, that should be written to.
	 * 'obj': The object to write.
	 *
	 * @returns A Promise.
	 * Resolved, if the object was written successfully to the given path.
	 * Rejects, if the object could not be written successfully to the given path.
	 * @resolve: No arguments.
	 * @reject[0]: An string with details on the error.
	 */
	writeJSON(path, obj) {
		return new Promise((resolve, reject) => {
			path = path || "";
			if(!path.endsWith(".json")){
				reject("\"" + path + "\" is not a content.json");
				return;
			}
			let data = JSON.stringify(obj, null, "\t");
			this.cmdp("fileWrite",  [path, this.base64Encode(data)])
			.then(
				()=>{resolve()},
				(error)=>{reject(error ? JSON.stringify(error): "")}
			);
		});
	}

	/**
	 * Signs and publishes the given content.json.
	 * 'content_path': The path within the zeronet zite to the content.json file, that should be singed and published.
	 *
	 * @returns A Promise.
	 * Resolved, if the content.json has been signed and published.
	 * Rejects, if there were errors within ZeroFrame.
	 * @resolve: No arguments.
	 * @reject[0]: An string with details on the error.
	 */
	signAndPublish(content_path){
		return new Promise((resolve, reject) => {
			this.cmdp("sitePublish", {"inner_path": content_path})
			.then(
				()=>{
					resolve();
				},
				(error) => {//ZeroFrame failed sign and publish.
					reject(error ? JSON.stringify(error): "");
				}
			);
		});
	}
}


code:
import Worker from "./webworker/previewRender.worker";

export let states = {
	NOT_QUEUED: 0,
	QUEUED: 1,
	PROCESSING: 2,
	DONE: 3,
	CANCELED: 4
}

//TODO: implement a way to increase/decrease Worker count, clean this class up and better integration with the rest
export class ImagePreviewRenderHandler {
	constructor(numOfWorkers) {
		this.PreviewDataQueue = {};
		this.counter = 0;

		this.workers = {
			inactive: [],
			active: {}
		};
		for(let i=0; i<numOfWorkers; i++){
			this._initNewWorker();
		}
	}

	_createNewInternalID(){
		return "i"+(this.counter++)
	}

	_setState(intID, state){// only set state after the state actually changed
		if(this.PreviewDataQueue[intID]){
			let old = this.PreviewDataQueue[intID].state;
			this.PreviewDataQueue[intID].state = state;

			if(this.PreviewDataQueue[intID].onStateChanged)
				this.PreviewDataQueue[intID].onStateChanged(old, state);

			//this._printInternalStateReport("State changed: \"" + intID + "\" '" + old + "'->'" + state + "'")//TODO remove this
		}
	}

	/*
	 * Front facing api methods
	 */
	queue(prevData, externalID, onStateChanged){
		let newEntry = {
			prevData: prevData,
			extID: externalID,
			onStateChanged: null,
			onProgressChanged: null,
			state: states.NOT_QUEUED,
			resolve: null,
			reject: null
		}
		return this._queueInit(newEntry, onStateChanged);
	}

	_queueInit(newEntry, onStateChanged){
		let intID = this._createNewInternalID();
		this.PreviewDataQueue[intID] = newEntry;
		if(onStateChanged)
			this.PreviewDataQueue[intID].onStateChanged = onStateChanged;

		let prom = new Promise((resolve, reject)=>{
			this.PreviewDataQueue[intID].resolve = resolve;
			this.PreviewDataQueue[intID].reject = reject;
		});
		this._setState(intID, states.QUEUED);
		this._onNewQueueEntry(intID);//might change state
		return prom;
	}

	getStateFromExternalID(extID){
		for (const key in this.PreviewDataQueue) {
			if (this.PreviewDataQueue.hasOwnProperty(key) && this.PreviewDataQueue[key].hasOwnProperty("extID") && this.PreviewDataQueue[key].extID == extID) {
				return this.PreviewDataQueue[key].state;
			}
		}
		return states.NOT_QUEUED;//default
	}

	getStateFromPreviewData(prevData){
		for (const key in this.PreviewDataQueue) {
			if (this.PreviewDataQueue.hasOwnProperty(key) && this.PreviewDataQueue[key].hasOwnProperty("prevData") && this.PreviewDataQueue[key].prevData === prevData) {
				return this.PreviewDataQueue[key].state;
			}
		}
		return states.NOT_QUEUED;//default
	}

	removePreviewDataByExternalID(extID){
		for (const key in this.PreviewDataQueue) {
			if (this.PreviewDataQueue.hasOwnProperty(key) && this.PreviewDataQueue[key].hasOwnProperty("extID") && this.PreviewDataQueue[key].extID == extID) {
				return this._removePreviewDataByInternalID(key);
			}
		}
		return states.NOT_QUEUED;//wasn't queued, nothing to do
	}

	removePreviewDataByObject(prevData){
		for (const key in this.PreviewDataQueue) {
			if (this.PreviewDataQueue.hasOwnProperty(key) && this.PreviewDataQueue[key].hasOwnProperty("prevData") && this.PreviewDataQueue[key].prevData === prevData) {
				return this._removePreviewDataByInternalID(key);
			}
		}
		return states.NOT_QUEUED;//wasn't queued, nothing to do
	}

	_removePreviewDataByInternalID(intID){
		//store old state
		let oldState = this.PreviewDataQueue[intID].state;
		//resolve promise
		this.PreviewDataQueue[intID].reject("User canceled processing of the preview data.");
		//notify
		this._setState(intID, states.CANCELED)

		//remove from queue
		delete this.PreviewDataQueue[intID];
		//stop job if processing
		if( this.workers.active[intID] ){
			//stop worker at once
			this.workers.active[intID].terminate();
			//remove terminated worker
			delete this.workers.active[intID];
			//start a new worker
			this._initNewWorker();
			//assign next image to the new worker if there is one
			let nextIntID = this._nextPreviewData();
			if(nextIntID){
				this._assignPreviewDataToWorker(this.workers.inactive.shift(), nextIntID);
			}
		}

		//this._printInternalStateReport("Image processing canceled(" + intID + ")")//TODO remove this

		//return old state
		return oldState;
	}

	/*
	 * Worker methods
	 *
	 */

	_initNewWorker(){
		let worker = new Worker();
		worker.onmessage = (event) => {
			if(event.data.error)
				this._onWorkFailed(worker, event.data.internalID, event.data.error);
			else
				this._onWorkSuccess(worker, event.data.internalID, event.data.width, event.data.height, event.data.preview);
		}
		this.workers.inactive.push(worker);
	}
	_onNewQueueEntry(intID){
		//if the image with the given internal isn't being processed and there are still inactive workers left
		//move one worker to the active list and send it the data necessary to process the image and update state
		if(!this.workers.active[intID] && this.workers.inactive.length>0){
			this._assignPreviewDataToWorker(this.workers.inactive.shift(), intID);
		}
	}
	//assumes the given worker is already dereferenced from anything, except the given object.
	_assignPreviewDataToWorker(worker, intID){
		let data = {
			imageParams: this.PreviewDataQueue[intID].prevData,
			internalID: intID
		}
		worker.postMessage(data);
		this.workers.active[intID] = worker;
		this._setState(intID, states.PROCESSING);
	}

	_nextPreviewData(){
		let keys = Object.keys(this.PreviewDataQueue)
		for (let i=0;i<keys.length;i++) {
			if (this.PreviewDataQueue[keys[i]] && this.PreviewDataQueue[keys[i]].state == states.QUEUED) {
				return keys[i];
			}
		}
		return null;
	}

    _onWorkDone(worker, intID){
		this._setState(intID, states.DONE);//the promise is resolved before this method is called, so the sate already changed.

		//cleanup
		if(worker === this.workers.active[intID]){
			delete this.workers.active[intID]
		}else{
			console.warn("A worker reported it processed something with an internal ID that was never assigned to it. This is likely a bug, please report this and reload the tab!");
		}
		delete this.PreviewDataQueue[intID];

		//pick next image or enter inactive mode
		let nextIntID = this._nextPreviewData();
		if(nextIntID){
			this._assignPreviewDataToWorker(worker, nextIntID);
		}else{
			this.workers.inactive.push(worker);
		}

		//this._printInternalStateReport("Preview data finished: \"" + intID + "\", next'" + nextIntID + "'")//TODO remove this
	}

    _onWorkSuccess(worker, intID, width, height, preview){
		this.PreviewDataQueue[intID].resolve({width: width, height:height, previewUri: preview});
        this._onWorkDone(worker, intID);
	}

	_onWorkFailed(worker, intID, error){
		this.PreviewDataQueue[intID].reject(error);
        this._onWorkDone(worker, intID);
	}

	clear(){

	}

	/**
	 * debug
	 */
	_printInternalStateReport(reason){
		console.log("ImagePreviewRenderHandler internal state report." + (reason ? " Reason: \"" + reason + "\"." : "") )
		console.log("\t", "this.counter:", this.counter)
		console.log("\t", "this.workers:", this.workers)
		console.log("\t", "this.PreviewDataQueue:", this.PreviewDataQueue)
		console.log("\t", "this.workers.inactive:", this.workers.inactive)
		console.log("\t", "this.workers.active:", this.workers.active)
	}
}


code:
import Worker from "./webworker/markupParser.worker";

export let states = {
	NOT_QUEUED: 0,
	QUEUED: 1,
	PROCESSING: 2,
	DONE: 3,
	CANCELED: 4
}

//TODO: implement a way to increase/decrease Worker count, clean this class up and better integration with the rest
export class MarkupParserHandler {
	constructor(numOfWorkers) {
		this.rawMarkupQueue = {};
		this.counter = 0;

		this.workers = {
			inactive: [],
			active: {}
		};
		for(let i=0; i<numOfWorkers; i++){
			this._initNewWorker();
		}
	}

	_createNewInternalID(){
		return "i"+(this.counter++)
	}

	_setState(intID, state){// only set state after the state actually changed
		if(this.rawMarkupQueue[intID]){
			let old = this.rawMarkupQueue[intID].state;
			this.rawMarkupQueue[intID].state = state;

			if(this.rawMarkupQueue[intID].onStateChanged)
				this.rawMarkupQueue[intID].onStateChanged(old, state);

			//this._printInternalStateReport("State changed: \"" + intID + "\" '" + old + "'->'" + state + "'")//TODO remove this
		}
	}

	/*
	 * Front facing api methods
	 */
	queue(rawMarkup, externalID, onStateChanged){
		let newEntry = {
			rawMarkup: rawMarkup,
			extID: externalID,
			onStateChanged: null,
			onProgressChanged: null,
			state: states.NOT_QUEUED,
			resolve: null,
			reject: null
		}
		return this._queueInit(newEntry, onStateChanged);
	}

	_queueInit(newEntry, onStateChanged){
		let intID = this._createNewInternalID();
		this.rawMarkupQueue[intID] = newEntry;
		if(onStateChanged)
			this.rawMarkupQueue[intID].onStateChanged = onStateChanged;

		let prom = new Promise((resolve, reject)=>{
			this.rawMarkupQueue[intID].resolve = resolve;
			this.rawMarkupQueue[intID].reject = reject;
		});
		this._setState(intID, states.QUEUED);
		this._onNewQueueEntry(intID);//might change state
		return prom;
	}

	getStateFromExternalID(extID){
		for (const key in this.rawMarkupQueue) {
			if (this.rawMarkupQueue.hasOwnProperty(key) && this.rawMarkupQueue[key].hasOwnProperty("extID") && this.rawMarkupQueue[key].extID == extID) {
				return this.rawMarkupQueue[key].state;
			}
		}
		return states.NOT_QUEUED;//default
	}

	getStateFromRawMarkup(rawMarkup){
		for (const key in this.rawMarkupQueue) {
			if (this.rawMarkupQueue.hasOwnProperty(key) && this.rawMarkupQueue[key].hasOwnProperty("rawMarkup") && this.rawMarkupQueue[key].rawMarkup === rawMarkup) {
				return this.rawMarkupQueue[key].state;
			}
		}
		return states.NOT_QUEUED;//default
	}

	removeRawMarkupByExternalID(extID){
		for (const key in this.rawMarkupQueue) {
			if (this.rawMarkupQueue.hasOwnProperty(key) && this.rawMarkupQueue[key].hasOwnProperty("extID") && this.rawMarkupQueue[key].extID == extID) {
				return this._removePreviewDataByInternalID(key);
			}
		}
		return states.NOT_QUEUED;//wasn't queued, nothing to do
	}

	removeRawMarkupByObject(rawMarkup){
		for (const key in this.rawMarkupQueue) {
			if (this.rawMarkupQueue.hasOwnProperty(key) && this.rawMarkupQueue[key].hasOwnProperty("rawMarkup") && this.rawMarkupQueue[key].rawMarkup === rawMarkup) {
				return this._removePreviewDataByInternalID(key);
			}
		}
		return states.NOT_QUEUED;//wasn't queued, nothing to do
	}

	_removePreviewDataByInternalID(intID){
		//store old state
		let oldState = this.rawMarkupQueue[intID].state;
		//resolve promise
		this.rawMarkupQueue[intID].reject("User canceled parsing of the markup text.");
		//notify
		this._setState(intID, states.CANCELED)

		//remove from queue
		delete this.rawMarkupQueue[intID];
		//stop job if processing
		if( this.workers.active[intID] ){
			//stop worker at once
			this.workers.active[intID].terminate();
			//remove terminated worker
			delete this.workers.active[intID];
			//start a new worker
			this._initNewWorker();
			//assign next image to the new worker if there is one
			let nextIntID = this._nextRawMarkup();
			if(nextIntID){
				this._assignRawMarkupToWorker(this.workers.inactive.shift(), nextIntID);
			}
		}

		//this._printInternalStateReport("Image processing canceled(" + intID + ")")//TODO remove this

		//return old state
		return oldState;
	}

	/*
	 * Worker methods
	 *
	 */

	_initNewWorker(){
		let worker = new Worker();
		worker.onmessage = (event) => {
			if(event.data.error)
				this._onWorkFailed(worker, event.data.internalID, event.data.error);
			else
				this._onWorkSuccess(worker, event.data.internalID, event.data.parsedMarkup);
		}
		this.workers.inactive.push(worker);
	}
	_onNewQueueEntry(intID){
		//if the image with the given internal isn't being processed and there are still inactive workers left
		//move one worker to the active list and send it the data necessary to process the image and update state
		if(!this.workers.active[intID] && this.workers.inactive.length>0){
			this._assignRawMarkupToWorker(this.workers.inactive.shift(), intID);
		}
	}
	//assumes the given worker is already dereferenced from anything, except the given object.
	_assignRawMarkupToWorker(worker, intID){
		let data = {
			rawMarkup: this.rawMarkupQueue[intID].rawMarkup,
			internalID: intID
		}
		worker.postMessage(data);
		this.workers.active[intID] = worker;
		this._setState(intID, states.PROCESSING);
	}

	_nextRawMarkup(){
		let keys = Object.keys(this.rawMarkupQueue)
		for (let i=0;i<keys.length;i++) {
			if (this.rawMarkupQueue[keys[i]] && this.rawMarkupQueue[keys[i]].state == states.QUEUED) {
				return keys[i];
			}
		}
		return null;
	}

    _onWorkDone(worker, intID){
		this._setState(intID, states.DONE);//the promise is resolved before this method is called, so the sate already changed.

		//cleanup
		if(worker === this.workers.active[intID]){
			delete this.workers.active[intID]
		}else{
			console.warn("A worker reported it processed something with an internal ID that was never assigned to it. This is likely a bug, please report this and reload the tab!");
		}
		delete this.rawMarkupQueue[intID];

		//pick next image or enter inactive mode
		let nextIntID = this._nextRawMarkup();
		if(nextIntID){
			this._assignRawMarkupToWorker(worker, nextIntID);
		}else{
			this.workers.inactive.push(worker);
		}

		//this._printInternalStateReport("Preview data finished: \"" + intID + "\", next'" + nextIntID + "'")//TODO remove this
	}

    _onWorkSuccess(worker, intID, parsedMarkup){
		this.rawMarkupQueue[intID].resolve({parsedMarkup: parsedMarkup});
        this._onWorkDone(worker, intID);
	}

	_onWorkFailed(worker, intID, error){
		this.rawMarkupQueue[intID].reject(error);
        this._onWorkDone(worker, intID);
	}

	clear(){

	}

	/**
	 * debug
	 */
	_printInternalStateReport(reason){
		console.log("MarkupParserHandler internal state report." + (reason ? " Reason: \"" + reason + "\"." : "") )
		console.log("\t", "this.counter:", this.counter)
		console.log("\t", "this.workers:", this.workers)
		console.log("\t", "this.rawMarkupQueue:", this.rawMarkupQueue)
		console.log("\t", "this.workers.inactive:", this.workers.inactive)
		console.log("\t", "this.workers.active:", this.workers.active)
	}
}


code:
/**
 * Checks whether or not image data can be extracted from the HTML5 canvas element.
 */
export function isCanvasEnabled(){
	try{
		let buffer = new Uint8ClampedArray(4);
		buffer[0] = 124;
		buffer[1] = 246;
		buffer[2] = 111;
		buffer[3] = 222;
		let testImage = new ImageData(buffer,1,1);
		let ctx = document.createElement("canvas").getContext('2d');
		ctx.putImageData(testImage,0,0);
		testImage = testImage.data;
		let retrievedImage = ctx.getImageData(0,0,1,1).data;//if disabled(privacy.resistFingerprinting=true in FF) this will return an array with all values 255
		return retrievedImage[0]==testImage[0]&&
			retrievedImage[1]==testImage[1]&&
			retrievedImage[2]==testImage[2]&&
			retrievedImage[3]==testImage[3];
	}catch(e){
		return false;
	}
}

const second = 1000; // milliseconds
const minute = 60_000; // 60 * 1000
const hour = 3_600_000; // 60 * 60 * 1000
const day = 86_400_000; // 24 * 60 * 60 * 1000
const week = 604_800_000; // 7 * 24 * 60 * 60 * 1000
const month = 2_628_288_000; // 365 / 12 = 30.42 days, 30.42 * 24 * 60 * 60 * 1000
const year = 31_536_000_000; // 365 * 24 * 60 * 60 * 1000

/**
 * Generates human-readable time difference as string
 */
export function getReadableTime(timeBefore, timeAfter, depth) {
	if (!depth)
		depth = 2;
	var dif;
	if(timeBefore instanceof Date && timeAfter instanceof Date){
		dif = timeAfter.getTime()-timeBefore.getTime();
	}else if(Number.isInteger(timeBefore) && Number.isInteger(timeAfter)){
		dif = timeAfter-timeBefore;
	}else{
		return "some time"
	}
	var combined = [];
	var years = Math.floor(dif / year);
	if (years > 0) {
		dif -= years * year;
		combined.push(years + " year" + (years == 1 ? "" : "s"));
	}

	var months = Math.floor(dif / month);
	if (months > 0) {
		dif -= months * month;
		combined.push(months + " month" + (months == 1 ? "" : "s"));
	}

	var weeks = Math.floor(dif / week);
	if (weeks > 0) {
		dif -= weeks * week;
		combined.push(weeks + " week" + (weeks == 1 ? "" : "s"));
	}

	var days = Math.floor(dif / day);
	if (days > 0) {
		dif -= days * day;
		combined.push(days + " day" + (days == 1 ? "" : "s"));
	}

	var hours = Math.floor(dif / hour);
	if (hours > 0) {
		dif -= hours * hour;
		combined.push(hours + " hour" + (hours == 1 ? "" : "s"));
	}

	var minutes = Math.floor(dif / minute);
	if (minutes > 0) {
		dif -= minutes * minute;
		combined.push(minutes + " minute" + (minutes == 1 ? "" : "s"));
	}

	var seconds = Math.floor(dif / second);
	if (seconds > 0) {
		dif -= seconds * second;
		combined.push(seconds + " second" + (seconds == 1 ? "" : "s"));
	}

	return combined.slice(0, depth).join(", ") + " ago";
}


code:
import marked from "marked";

class MarkedRenderer extends marked.Renderer{
	image(href, title, text) {
		return "![#{text}](#{href})"
	}
}
const marked_renderer = new MarkedRenderer();

let fixReply = (text) =>{
	return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2")
}

// Convert zeronet html links to relaitve
let fixHtmlLinks = (text) => {
	// Fix site links
	text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110\/(1Gsbebqy9mmQXUfvLXkEmh6U9sUoc5QFB8)\/\?/gi, 'href="?!')
	if (false){//window.is_proxy){ // TODO figure out a way to port this
		text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/gi, 'href="http://zero')
		text = text.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1")
		text = text.replace(/href="\/([A-Za-z0-9]{26,35})/g, 'href="http://zero/$1')  // Links without 127.0.0.1
	}else{
		text = text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="')
	}
	// Add no-refresh linking to local links
	//text = text.replace(/href="\?/g, 'onclick="return Page.handleLinkClick(window.event)" href="?')
	return text
}
const options = {
	gfm: true,
	breaks: true,
	renderer: marked_renderer
};

self.onmessage = (event) => {
	let postError = (error) => {
		self.postMessage({ error: error, internalID: event.data.internalID})
	}
	let postRes = (parsedMarkup) => {
		self.postMessage({parsedMarkup: parsedMarkup, internalID: event.data.internalID})
	}

	try {
		let rawMarkup = event.data.rawMarkup;

		let text = fixReply(rawMarkup)
		text = marked(text, options)
		text = text.replace(/(.*?)<\/a>/g, '$1')  //Disable email auto-convert
		text = text.replace(/(\s|>|^)(@[^\s]{1,50}):/g, '$1$2:')  // Highlight usernames
		postRes(fixHtmlLinks(text));
	} catch (error) {
		postError(error)
		return;
	}
};


code:
import jimp from "jimp";

const pixel_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let calcSize = (source_width, source_height, target_width, target_height) =>{
	let width = target_width
	let height = width * (source_height / source_width);
	if (height > target_height){
		height = target_height
		width = height * (source_width / source_height)
	}
	return [Math.round(width), Math.round(height)]
}

self.onmessage = (event) => {
	let postError = (error) => {
		self.postMessage({ error: error, internalID: event.data.internalID})
	}
	let postRes = (width, height, previewDataUri) => {
		self.postMessage({width: width, height:height, preview: previewDataUri, internalID: event.data.internalID})
	}

	let preview_data = event.data.imageParams.split(",");
		let fullWidth = preview_data[0]
		let fullHeight = preview_data[1]
		let colors = preview_data[2]
		let pixels = preview_data[3]
	let newDim = calcSize(fullWidth, fullHeight, 10, 10)

	let width = newDim[0]
	let height = newDim[1]

	colors = colors.match(/.{3}/g)
	pixels = pixels.split("")

	new Jimp(width, height, (err, image) => {
		if(err)
			postError(err)

		let color_codes = {}
		for (let i = 0; i < colors.length; i++) {
			const color = colors[i];
			color_codes[pixel_chars[i]] = color
		}

		let di = 0
		for (let j = 0; j < pixels.length; j++) {
			const pixel = pixels[j];
			let hex = color_codes[pixel]
			let r = parseInt(hex[0], 16) * 17
			let g = parseInt(hex[1], 16) * 17
			let b = parseInt(hex[2], 16) * 17
			image.bitmap.data[di] = r
			image.bitmap.data[di+1] = g
			image.bitmap.data[di+2] = b
			image.bitmap.data[di+3] = 255
			di += 4
		}

		image.getBase64(jimp.MIME_PNG, (error, preview_src) => {
			if(error){
				postError(error)
			}else{
				postRes(fullWidth, fullHeight, preview_src)
			}
		});
	});
};


code:
import App from './svelte/App.svelte';

const app = new App({
	target: document.getElementById("app"),
	props: {
		siteInfo: {},
		serverInfo: {}
	}
});

window.app = app;