Update network object

Update network object

--
-- Copyright (C) 2017 DBot
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

MODEL_BOX_PANEL = {
    SEQUENCE_STAND: 22
    PONY_VEC_Z: 64 * .7

    SEQUENCES: {
        'Standing':    22
        'Moving':      316
        'Walking':     232
        'Sit':         202
        'Swim':        370
        'Run':         328
        'Crouch walk': 286
        'Crouch':      76
        'Jump':        160
    }

    EDITOR_SEQUENCES: {
        -- idle
        {
            time: 5
            func: (dist, ang, delta) -> dist, ang
        }

        -- Slow move to left
        {
            time: 5
            func: (dist, ang, delta) ->
                ang.y += delta * 10
                return dist, ang
        }

        -- Eyes
        {
            time: 5
            func: (dist, ang, delta) ->
                return 40, Angle(0, 0, 0)
        }

        -- Left view
        {
            time: 3
            func: (dist, ang, delta) ->
                return 80, Angle(0, 90, 0)
        }

        -- Move to right
        {
            time: 7
            func: (dist, ang, delta) ->
                ang.y -= 10 * delta
                return 80, ang
        }

        -- Eyes (bottom)
        {
            time: 5
            func: (dist, ang, delta) ->
                return 20, Angle(40, 0, 0)
        }

        -- Look at top mane
        {
            time: 5
            func: (dist, ang, delta) ->
                return 50, Angle(-20, 180, 0)
        }
    }

    Init: =>
        @animRate = 1
        @seq = @SEQUENCE_STAND
        @targetAngle = Angle(0, 0, 0)
        @angle = Angle(0, 0, 0)
        @distToPony = 100
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @hold = false
        @holdLast = 0
        @mouseX, @mouseY = 0, 0
        @SetMouseInputEnabled(true)
        @editorSeq = 1
        @nextSeq = @EDITOR_SEQUENCES[@editorSeq].time + RealTime()
        @playing = true
        @lastTick = RealTime()
        @SetCursor('none')

        @animButton = vgui.Create('DButton', @)
        with @animButton
            \SetSize(120, 20)
            \SetText('Playing animation')
            .LastStatus = true
            .Think = ->
                if .LastStatus ~= @playing
                    .LastStatus = @playing
                    \SetText('Playing Animation') if @playing
                    \SetText('Play Animation') if not @playing
            .DoClick = ->
                @playing = not @playing
                if @playing
                    @editorSeq = 1
                    @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                    @ResetPosition()

        @seqButton = vgui.Create('DComboBox', @)
        with @seqButton
            \SetSize(120, 20)
            \SetValue('Standing')
            SEQUENCES
            \AddChoice(choice, num) for choice, num in pairs @SEQUENCES
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                @SetSequence(data)
    ResetPosition: =>
        @targetAngle = Angle(0, 0, 0)
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)

    PerformLayout: (w = 0, h = 0) =>
        @animButton\SetPos(w - 130, 10)
        @seqButton\SetPos(10, 10)

    OnMousePressed: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = true
        @SetCursor('sizeall')
        @holdLast = RealTime() + .1
        @oldPlaying = @playing
        @playing = false
        @mouseX, @mouseY = gui.MousePos()
    OnMouseReleased: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = false
        @SetCursor('none')
        if @holdLast > RealTime()
            @playing = true
            if not @oldPlaying
                @editorSeq = 1
                @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                @ResetPosition()

    SetController: (val) => @controller = val

    OnMouseWheeled: (wheelDelta = 0) =>
        @playing = false
        @editorSeq = 1
        @targetDistToPony = math.Clamp(@targetDistToPony - wheelDelta * 10, 20, 150)
    GetModel: => @model
    GetSequence: => @seq
    GetSeq: => @seq
    GetAnimRate: => @animRate
    SetAnimRate: (val = 1) => @animRate = val
    SetSeq: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    SetSequence: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    ResetSequence: => @SetSequence(@SEQUENCE_STAND)
    ResetSeq: => @SetSequence(@SEQUENCE_STAND)

    ResetModel: (ponydata, model = 'models/ppm/player_default_base.mdl') =>
        @model\Remove() if IsValid(@model)
        @model = ClientsideModel(model)
        with @model
            \SetNoDraw(true)
            .__PPM2_PonyData = ponydata
        @model\SetSequence(@seq)
        @model\FrameAdvance(0)
        return @model
    Think: =>
        rtime = RealTime()
        delta = rtime - @lastTick
        @lastTick = rtime
        if IsValid(@model)
            @model\FrameAdvance(delta * @animRate)
            @model\SetPlaybackRate(1)
            @model\SetPoseParameter('move_x', 1)

        @hold = @IsHovered() if @hold

        if @playing
            cseq = @EDITOR_SEQUENCES[@editorSeq]
            if @nextSeq < rtime
                @editorSeq += 1
                @editorSeq = 1 if not @EDITOR_SEQUENCES[@editorSeq]
                cseq = @EDITOR_SEQUENCES[@editorSeq]
                @nextSeq = rtime + cseq.time

            {:p, :y, :r} = @targetAngle
            @targetDistToPony, @targetAngle = cseq.func(@targetDistToPony, Angle(p, y, r), delta)
            @targetDistToPony = math.Clamp(@targetDistToPony, 20, 150)
            @targetAngle.p = math.Clamp(@targetAngle.p, -40, 10)
        else
            if @hold
                x, y = gui.MousePos()
                deltaX, deltaY = x - @mouseX, y - @mouseY
                @mouseX, @mouseY = x, y
                {:pitch, :yaw, :roll} = @targetAngle
                yaw -= deltaX * .5
                pitch = math.Clamp(pitch - deltaY * .5, -40, 10)
                @targetAngle = Angle(pitch, yaw, roll)

        @angle = LerpAngle(delta * 4, @angle, @targetAngle)
        @distToPony = Lerp(delta * 4, @distToPony, @targetDistToPony)
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @vectorPos\Rotate(@angle)
        @drawAngle = (Vector(0, 0, @PONY_VEC_Z * .7) - @vectorPos)\Angle()

    FLOOR_VECTOR: Vector(0, 0, -30)
    FLOOR_ANGLE: Vector(0, 0, 1)

    DRAW_WALLS: {
        {Vector(-4000, 0, 900), Vector(1, 0, 0), 8000, 2000}
        {Vector(4000, 0, 900), Vector(-1, 0, 0), 8000, 2000}
        {Vector(0, -4000, 900), Vector(0, 1, 0), 8000, 2000}
        {Vector(0, 4000, 900), Vector(0, -1, 0), 8000, 2000}
        {Vector(0, 0, 900), Vector(0, 0, -1), 8000, 8000}
    }

    WALL_COLOR: Color(98, 189, 176)
    FLOOR_COLOR: Color(53, 150, 84)

    Paint: (w = 0, h = 0) =>
        surface.SetDrawColor(0, 0, 0)
        surface.DrawRect(0, 0, w, h)
        return if not IsValid(@model)
        x, y = @LocalToScreen(0, 0)
        cam.Start3D(@vectorPos, @drawAngle, 90, x, y, w, h)

        render.DrawQuadEasy(@FLOOR_VECTOR, @FLOOR_ANGLE, 7000, 7000, @FLOOR_COLOR)

        for {pos, ang, w, h} in *@DRAW_WALLS
            render.DrawQuadEasy(pos, ang, w, h, @WALL_COLOR)

        render.SuppressEngineLighting(true)
        render.ResetModelLighting(1, 1, 1)
        render.SetColorModulation(1, 1, 1)
        @controller\GetRenderController()\PreDraw(@model) if @controller
        @model\DrawModel()
        @controller\GetRenderController()\PostDraw(@model) if @controller
        render.SuppressEngineLighting(false)

        cam.End3D()
    OnRemove: =>
        @model\Remove() if IsValid(@model)
}

vgui.Register('PPM2ModelPanel', MODEL_BOX_PANEL, 'EditablePanel')

PANEL_SETTINGS_BASE = {
    Init: =>
        @shouldSaveData = false
        @SetMouseInputEnabled(true)
        @SetKeyboardInputEnabled(true)
        @DockPadding(5, 5, 5, 5)
        @unsavedChanges = false
        @updateFuncs = {}
    ValueChanges: (valID, newVal, pnl) =>
        @unsavedChanges = true
        @frame.unsavedChanges = true
        @frame\SetTitle("#{@GetTargetData() and @GetTargetData()\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor; *Unsaved changes*")
    GetShouldSaveData: => @shouldSaveData
    ShouldSaveData: => @shouldSaveData
    SetShouldSaveData: (val = false) => @shouldSaveData = val
    GetTargetData: => @data
    TargetData: => @data
    SetTargetData: (val) => @data = val
    DoUpdate: => func() for func in *@updateFuncs
    NumSlider: (name = 'Slider', option = '', decimals = 0) =>
		with vgui.Create('DNumSlider', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 0, 2, 0)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetText(name)
			\SetMin(0)
			\SetMax(1)
			\SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
			\SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
			\SetDecimals(decimals)
            \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
			.TextArea\SetTextColor(color_white)
			.Label\SetTextColor(color_white)
            .OnValueChanged = (pnl, newVal = 1) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
                \SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
                \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    Label: (text = '') =>
        with vgui.Create('DLabel', @scroll or @)
            \SetText(text)
            \Dock(TOP)
            \SetTextColor(color_white)
            \SizeToContents()
            w, h = \GetSize()
            \SetSize(w, h + 5)
            @scroll\AddItem(_with_0) if IsValid(@scroll)
	CheckBox: (name = 'Label', option = '') =>
		with vgui.Create('DCheckBoxLabel', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 2, 2, 2)
			\SetText(name)
			\SetTextColor(color_white)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .OnChange = (pnl, newVal = false) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal and 1 or 0, @GetShouldSaveData())
                @ValueChanges(option, newVal and 1 or 0, pnl)
            table.insert @updateFuncs, ->
                \SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    ColorBox: (name = 'Colorful Box', option = '') =>
        collapse = vgui.Create('DCollapsibleCategory', @scroll or @)
        box = vgui.Create('DColorMixer', collapse)
        collapse.box = box
        with box
            \SetSize(250, 270)
			\SetTooltip("#{name}\nData value: #{option}")
            \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .ValueChanged = (pnl, newVal = Color(0, 0, 0)) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        with collapse
            \SetContents(box)
            \Dock(TOP)
            \DockMargin(2, 2, 2, 2)
            \SetSize(250, 270)
            \SetLabel(name)
            \SetExpanded(false)
        @scroll\AddItem(collapse) if IsValid(@scroll)
        return box, collapse
    Spoiler: (name = 'Mysterious spoiler') =>
        collapse = vgui.Create('DCollapsibleCategory', @scroll or @)
        canvas = vgui.Create('EditablePanel', collapse)
        with canvas
            \SetSize(0, 400)
            \Dock(FILL)
        with collapse
            \SetContents(canvas)
            \Dock(TOP)
            \DockMargin(2, 2, 2, 2)
            \SetSize(250, 270)
            \SetLabel(name)
            \SetExpanded(false)
        @scroll\AddItem(collapse) if IsValid(@scroll)
        return canvas, collapse
    ComboBox: (name = 'Combo Box', option = '', choices) =>
        label = vgui.Create('DLabel', @scroll or @)
        with label
            \SetText(name)
            \SetTextColor(color_white)
            \Dock(TOP)
            \SetSize(0, 20)
            \DockMargin(5, 0, 5, 0)
            \SetMouseInputEnabled(true)
        @scroll\AddItem(label) if IsValid(@scroll)
        box = vgui.Create('DComboBox', label)
        with box
            \Dock(RIGHT)
            \SetSize(170, 0)
            \DockMargin(0, 0, 5, 0)
            \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
            if choices
                \AddChoice(choice) for choice in *choices
            else
                \AddChoice(choice) for choice in *@GetTargetData()["Get#{option}Types"](@GetTargetData()) if @GetTargetData() and @GetTargetData()["Get#{option}Types"]
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                index -= 1
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, index, @GetShouldSaveData())
                @ValueChanges(option, index, pnl)
            table.insert @updateFuncs, ->
                \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
        return box, label
    URLInput: (option = '') =>
        wrapper = vgui.Create('EditablePanel', @scroll or @)
        with wrapper
            \Dock(TOP)
            \DockMargin(5, 10, 5, 10)
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            \SetSize(0, 20)
        textInput = vgui.Create('DTextEntry', wrapper)
        @scroll\AddItem(wrapper) if IsValid(@scroll)
        with textInput
            \Dock(FILL)
            \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            .OnEnter = ->
                text = \GetValue()
                if text\find('^https?://')
                    @GetTargetData()["Set#{option}"](@GetTargetData(), text)
                    @ValueChanges(option, text, textInput)
                else
                    @GetTargetData()["Set#{option}"](@GetTargetData(), '')
                    @ValueChanges(option, '', textInput)
            .OnKeyCodeTyped = (pnl, key = KEY_FIRST) ->
                switch key
                    when KEY_FIRST
                        return true
                    when KEY_NONE
                        return true
                    when KEY_TAB
                        return true
                    when KEY_ENTER
                        .OnEnter()
                        \KillFocus()
                        return true
                timer.Create "PPM2.EditorCodeChange.#{option}", 1, 1, ->
                    return if not IsValid(textInput)
                    .OnEnter()
            table.insert @updateFuncs, -> \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        return textInput
    ScrollPanel: =>
        return @scroll if IsValid(@scroll)
        @scroll = vgui.Create('DScrollPanel', @)
        @scroll\Dock(FILL)
        return @scroll
    Paint: (w = 0, h = 0) =>
		surface.SetDrawColor(130, 130, 130)
		surface.DrawRect(0, 0, w, h)
}

vgui.Register('PPM2SettingsBase', PANEL_SETTINGS_BASE, 'EditablePanel')

EditorPages = {
    {
        'name': 'Main'
        'internal': 'main'
        'func': (sheet) =>
            @ScrollPanel()
            @CheckBox('Gender', 'Gender')
            @ComboBox('Race', 'Race')
            @NumSlider('Weight', 'Weight', 2)
            @CheckBox('Socks (simple texture)', 'Socks')
            @ComboBox('Eyelashes', 'EyelashType')
            @ComboBox('Bodysuit', 'Bodysuit')
            @ColorBox('Body color', 'BodyColor')
            @CheckBox('Socks (as model)', 'SocksAsModel')
            @ColorBox('Socks model color', 'SocksColor')
            @CheckBox('No flexes on new model', 'NoFlex')
            @Label('You can disable separately any flex state controller\nSo these flexes can be modified with third-party addons (like PAC3)')
            flexes = @Spoiler('Flexes controls')
            for {:flex, :active} in *PPM2.PonyFlexController.FLEX_LIST
                @CheckBox("Disable #{flex} control", "DisableFlex#{flex}")\SetParent(flexes) if active
            flexes\SizeToContents()
    }

    {
        'name': 'Eyes'
        'internal': 'eyes'
        'func': (sheet) =>
            @ScrollPanel()
            @CheckBox('Use separated settings for eyes', 'SeparateEyes')
            for publicName in *{'', 'Left', 'Right'}
                @Label("'#{publicName}' Eye settings")
                @ComboBox("#{publicName} Eye type", "EyeType#{publicName}")
                @CheckBox("#{publicName} Eye lines", "EyeLines#{publicName}")
                @CheckBox("#{publicName} Derp eye", "DerpEyes#{publicName}")
                @NumSlider("#{publicName} Derp eye strength", "DerpEyesStrength#{publicName}", 2)
                @NumSlider("#{publicName} Eye size", "IrisSize#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole width", "HoleWidth#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole size", "HoleSize#{publicName}", 2)

                @ColorBox("#{publicName} Eye background", "EyeBackground#{publicName}")
                @ColorBox("#{publicName} Eye hole", "EyeHole#{publicName}")
                @ColorBox("#{publicName} Top eye iris", "EyeIrisTop#{publicName}")
                @ColorBox("#{publicName} Bottom eye iris", "EyeIrisBottom#{publicName}")
                @ColorBox("#{publicName} Eye line 1", "EyeIrisLine1#{publicName}")
                @ColorBox("#{publicName} Eye line 2", "EyeIrisLine2#{publicName}")
                @ColorBox("#{publicName} Eye reflection effect", "EyeReflection#{publicName}")
                @ColorBox("#{publicName} Eye effect", "EyeEffect#{publicName}")
    }

    {
        'name': 'Mane and tail'
        'internal': 'manetail'
        'func': (sheet) =>
            @ScrollPanel()
            @Label('"New" types affect only new model')
            @ComboBox('Tail type', 'TailType')
            @ComboBox('New Tail type', 'TailTypeNew')

            @ComboBox('Mane type', 'ManeType')
            @ComboBox('New Mane type', 'ManeTypeNew')

            @ComboBox('Lower Mane type', 'ManeTypeLower')
            @ComboBox('New Lower Mane type', 'ManeTypeLowerNew')

            for i = 1, 2
                @ColorBox("Mane color #{i}", "ManeColor#{i}")
                @ColorBox("Tail color #{i}", "TailColor#{i}")

            for i = 1, 6
                @ColorBox("Mane detail color #{i}", "ManeDetailColor#{i}")
                @ColorBox("Tail detail color #{i}", "TailDetailColor#{i}")

            for i = 1, 6
                @Label("Mane URL Detail #{i} input field")
                @URLInput("ManeURL#{i}")
                @ColorBox("Mane URL detail color #{i}", "ManeURLColor#{i}")
                @Label("Tail URL Detail #{i} input field")
                @URLInput("TailURL#{i}")
                @ColorBox("Tail URL detail color #{i}", "TailURLColor#{i}")

            @CheckBox('Separate upper and lower mane colors', 'SeparateMane')
            @Label('These options have effect only on new model')

            for i = 1, 2
                @ColorBox("Upper Mane color #{i}", "UpperManeColor#{i}")
                @ColorBox("Lower Mane color #{i}", "LowerManeColor#{i}")

            for i = 1, 6
                @ColorBox("Upper Mane detail color #{i}", "UpperManeDetailColor#{i}")
                @ColorBox("Lower Tail detail color #{i}", "LowerManeDetailColor#{i}")

            for i = 1, 6
                @Label("Upper mane URL Detail #{i} input field")
                @URLInput("UpperManeURL#{i}")
                @ColorBox("Upper Mane URL detail color #{i}", "UpperManeURLColor#{i}")
                @Label("Lower mane URL Detail #{i} input field")
                @URLInput("LowerManeURL#{i}")
                @ColorBox("Lower Tail URL detail color #{i}", "LowerManeURLColor#{i}")
    }

    {
        'name': 'Body details'
        'internal': 'bodydetail'
        'func': (sheet) =>
            @ScrollPanel()
            for i = 1, PPM2.MAX_BODY_DETAILS
                @ComboBox("Detail #{i}", "BodyDetail#{i}")
                @ColorBox("Detail color #{i}", "BodyDetailColor#{i}")

            @Label('Body detail URL image input fields\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')
            for i = 1, PPM2.MAX_BODY_DETAILS
                @Label("Body detail #{i}")
                @URLInput("BodyDetailURL#{i}")
                @ColorBox("URL Detail color #{i}", "BodyDetailURLColor#{i}")
    }

    {
        'name': 'Cutiemark'
        'internal': 'cmark'
        'func': (sheet) =>
            @CheckBox('Display cutiemark', 'CMark')
            @ComboBox('Cutiemark type', 'CMarkType')
            @markDisplay = vgui.Create('EditablePanel', @)
            with @markDisplay
                \Dock(TOP)
                \SetSize(320, 320)
                \DockMargin(20, 20, 20, 20)
                .Paint = (pnl, w = 0, h = 0) ->
                    data = @GetTargetData()
                    return if not data
                    controller = data\GetController()
                    return if not controller
                    rcontroller = controller\GetRenderController()
                    return if not rcontroller
                    tcontroller = rcontroller\GetTextureController()
                    return if not tcontroller
                    mat = tcontroller\GetCMarkGUI()
                    return if not mat
                    surface.SetDrawColor(0, 0, 0)
                    surface.DrawRect(0, 0, w, h)
                    surface.SetDrawColor(255, 255, 255)
                    surface.SetMaterial(mat)
                    surface.DrawTexturedRect(0, 0, w, h)

            @Label('Cutiemark URL image input field\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')\DockMargin(5, 10, 5, 10)
            @URLInput('CMarkURL')
    }

    {
        'name': 'Files'
        'internal': 'saves'
        'func': (sheet) =>
            @Label('Open file by double click')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    @frame.data\SetFilename(fil)
                    @frame.data\ReadFromDisk(true)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = false
                    @frame.unsavedChanges = false
                    @frame\SetTitle("#{fil} - PPM2 Pony Editor")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm2/*', 'DATA')
                for fil in *files
                    matchBak = '.bak.txt'
                    continue if fil\sub(-#matchBak) == matchBak
                    list\AddLine(fil\sub(1, #fil - 4))
            @rebuildFileList()
    }

    {
        'name': 'Old Files'
        'internal': 'oldsaves'
        'func': (sheet) =>
            @Label('!!! It may or may not work. You will be squished.')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    newData = PPM2.ReadFromOldData(fil)
                    if not newData
                        Derma_Message('Failed to import.', 'Onoh!', 'Okai ;w;')
                        return
                    @frame.data\SetFilename(newData\GetFilename())
                    newData\ApplyDataToObject(@frame.data, false)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = true
                    @frame.unsavedChanges = true
                    @frame\SetTitle("#{newData\GetFilename()} - PPM2 Pony Editor; *Unsaved changes*")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm/*', 'DATA')
                list\AddLine(fil\sub(1, #fil - 4)) for fil in *files
            @rebuildFileList()
    }
}

EditorModels = {
    'DEFAULT': 'models/ppm/player_default_base.mdl'
    'CPPM': 'models/cppm/player_default_base.mdl'
    'NEW': 'models/ppm/player_default_base_new.mdl'
}

USE_MODEL = CreateConVar('ppm2_editor_model', 'new', {FCVAR_ARCHIVE}, 'What model to use in editor. Valids are "default", "cppm", "new"')

if IsValid(PPM2.EditorFrame)
    PPM2.EditorFrame\Remove()
    net.Start('PPM2.EditorStatus')
    net.WriteBool(false)
    net.SendToServer()
PPM2.OpenEditor = ->
    if IsValid(PPM2.EditorFrame)
        PPM2.EditorFrame\SetVisible(true)
        PPM2.EditorFrame\Center()
        PPM2.EditorFrame\MakePopup()
        net.Start('PPM2.EditorStatus')
        net.WriteBool(true)
        net.SendToServer()
        return

    frame = vgui.Create('DFrame')
    self = frame
    W, H = ScrW() - 25, ScrH() - 25
    @SetSize(W, H)
    @Center()
    @MakePopup()
    @SetTitle('PPM2 Pony Editor')
    @SetDeleteOnClose(false)
    PPM2.EditorFrame = @

    @OnClose = ->
        net.Start('PPM2.EditorStatus')
        net.WriteBool(false)
        net.SendToServer()

    @menus = vgui.Create('DPropertySheet', @)
    @menus\Dock(LEFT)
    @menus\SetSize(370, 0)

    @model = vgui.Create('PPM2ModelPanel', @)
    @model\Dock(FILL)

    copy = PPM2.GetMainData()\Copy()
    ply = LocalPlayer()
    editorModelSelect = USE_MODEL\GetString()\upper()
    editorModelSelect = EditorModels[editorModelSelect] and editorModelSelect or 'DEFAULT'
    ent = @model\ResetModel(nil, EditorModels[editorModelSelect])
    controller = copy\CreateCustomController(ent)
    copy\SetController(controller)
    frame.controller = controller
    frame.data = copy
    frame.DoUpdate = -> pnl\DoUpdate() for i, pnl in pairs @panels

    saveAs = (callback = (->)) ->
        confirm = (txt = '') ->
            txt = txt\Trim()
            return if txt == ''
            copy\SetFilename(txt)
            copy\Save()
            @unsavedChanges = false
            @model.unsavedChanges = false
            @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")
            @panels.saves.rebuildFileList()
            callback(txt)
        Derma_StringRequest('Save as', 'Enter file name without ppm2/ and .txt', copy\GetFilename(), confirm)

    @saveButton = vgui.Create('DButton', @)
    with @saveButton
        \SetText('Save')
        \SetPos(W - 205, 5)
        \SetSize(90, 20)
        .DoClick = -> saveAs()

    @wearButton = vgui.Create('DButton', @)
    with @wearButton
        \SetText('Apply changes (wear)')
        \SetPos(W - 350, 5)
        \SetSize(140, 20)
        lastWear = 0
        .DoClick = ->
            return if RealTime() < lastWear
            lastWear = RealTime() + 5
            mainData = PPM2.GetMainData()
            nwdata = LocalPlayer()\GetPonyData()
            mainData\SetNetworkData(nwdata) if nwdata
            copy\ApplyDataToObject(mainData, false) -- no save on apply
    @selectModelBox = vgui.Create('DComboBox', @)
    with @selectModelBox
        \SetSize(120, 20)
        \SetPos(W - 475, 5)
        \SetValue(editorModelSelect)
        \AddChoice(choice) for choice in *{'default', 'cppm', 'new'}
        .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
            @SetDeleteOnClose(true)
            RunConsoleCommand('ppm2_editor_model', value)

            confirm = ->
                @Close()
                timer.Simple 0.1, PPM2.OpenEditor
            Derma_Query(
                'You should restart editor for applying change.\nRestart now?\nUnsaved data can be lost!',
                'Editor restart required',
                'Yas!',
                confirm,
                'Noh!'
            )

    @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")

    @model\SetController(controller)
    controller\SetupEntity(ent)

    @panels = {}

    for {:name, :func, :internal} in *EditorPages
        pnl = vgui.Create('PPM2SettingsBase', @menus)
        @menus\AddSheet(name, pnl)
        pnl\SetTargetData(copy)
        pnl\Dock(FILL)
        pnl.frame = @
        func(pnl, @menus)
        @panels[internal] = pnl

concommand.Add 'ppm2_editor', PPM2.OpenEditor

IconData =
	title: 'PPM V2.0',
	icon: 'gui/pped_icon.png',
	width: 960,
	height: 700,
	onewindow: true,
	init: (icon, window) ->
		window\Remove()
		RunConsoleCommand('ppm2_editor')

list.Set('DesktopWindows', 'PPM2', IconData)
CreateContextMenu() if IsValid(g_ContextMenu)

--
-- Copyright (C) 2017 DBot
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

MODEL_BOX_PANEL = {
    SEQUENCE_STAND: 22
    PONY_VEC_Z: 64 * .7

    SEQUENCES: {
        'Standing':    22
        'Moving':      316
        'Walking':     232
        'Sit':         202
        'Swim':        370
        'Run':         328
        'Crouch walk': 286
        'Crouch':      76
        'Jump':        160
    }

    EDITOR_SEQUENCES: {
        -- idle
        {
            time: 5
            func: (dist, ang, delta) -> dist, ang
        }

        -- Slow move to left
        {
            time: 5
            func: (dist, ang, delta) ->
                ang.y += delta * 10
                return dist, ang
        }

        -- Eyes
        {
            time: 5
            func: (dist, ang, delta) ->
                return 40, Angle(0, 0, 0)
        }

        -- Left view
        {
            time: 3
            func: (dist, ang, delta) ->
                return 80, Angle(0, 90, 0)
        }

        -- Move to right
        {
            time: 7
            func: (dist, ang, delta) ->
                ang.y -= 10 * delta
                return 80, ang
        }

        -- Eyes (bottom)
        {
            time: 5
            func: (dist, ang, delta) ->
                return 20, Angle(40, 0, 0)
        }

        -- Look at top mane
        {
            time: 5
            func: (dist, ang, delta) ->
                return 50, Angle(-20, 180, 0)
        }
    }

    Init: =>
        @animRate = 1
        @seq = @SEQUENCE_STAND
        @targetAngle = Angle(0, 0, 0)
        @angle = Angle(0, 0, 0)
        @distToPony = 100
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @hold = false
        @holdLast = 0
        @mouseX, @mouseY = 0, 0
        @SetMouseInputEnabled(true)
        @editorSeq = 1
        @nextSeq = @EDITOR_SEQUENCES[@editorSeq].time + RealTime()
        @playing = true
        @lastTick = RealTime()
        @SetCursor('none')

        @animButton = vgui.Create('DButton', @)
        with @animButton
            \SetSize(120, 20)
            \SetText('Playing animation')
            .LastStatus = true
            .Think = ->
                if .LastStatus ~= @playing
                    .LastStatus = @playing
                    \SetText('Playing Animation') if @playing
                    \SetText('Play Animation') if not @playing
            .DoClick = ->
                @playing = not @playing
                if @playing
                    @editorSeq = 1
                    @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                    @ResetPosition()

        @seqButton = vgui.Create('DComboBox', @)
        with @seqButton
            \SetSize(120, 20)
            \SetValue('Standing')
            SEQUENCES
            \AddChoice(choice, num) for choice, num in pairs @SEQUENCES
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                @SetSequence(data)
    ResetPosition: =>
        @targetAngle = Angle(0, 0, 0)
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)

    PerformLayout: (w = 0, h = 0) =>
        @animButton\SetPos(w - 130, 10)
        @seqButton\SetPos(10, 10)

    OnMousePressed: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = true
        @SetCursor('sizeall')
        @holdLast = RealTime() + .1
        @oldPlaying = @playing
        @playing = false
        @mouseX, @mouseY = gui.MousePos()
    OnMouseReleased: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = false
        @SetCursor('none')
        if @holdLast > RealTime()
            @playing = true
            if not @oldPlaying
                @editorSeq = 1
                @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                @ResetPosition()

    SetController: (val) => @controller = val

    OnMouseWheeled: (wheelDelta = 0) =>
        @playing = false
        @editorSeq = 1
        @targetDistToPony = math.Clamp(@targetDistToPony - wheelDelta * 10, 20, 150)
    GetModel: => @model
    GetSequence: => @seq
    GetSeq: => @seq
    GetAnimRate: => @animRate
    SetAnimRate: (val = 1) => @animRate = val
    SetSeq: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    SetSequence: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    ResetSequence: => @SetSequence(@SEQUENCE_STAND)
    ResetSeq: => @SetSequence(@SEQUENCE_STAND)

    ResetModel: (ponydata, model = 'models/ppm/player_default_base.mdl') =>
        @model\Remove() if IsValid(@model)
        @model = ClientsideModel(model)
        with @model
            \SetNoDraw(true)
            .__PPM2_PonyData = ponydata
        @model\SetSequence(@seq)
        @model\FrameAdvance(0)
        return @model
    Think: =>
        rtime = RealTime()
        delta = rtime - @lastTick
        @lastTick = rtime
        if IsValid(@model)
            @model\FrameAdvance(delta * @animRate)
            @model\SetPlaybackRate(1)
            @model\SetPoseParameter('move_x', 1)

        @hold = @IsHovered() if @hold

        if @playing
            cseq = @EDITOR_SEQUENCES[@editorSeq]
            if @nextSeq < rtime
                @editorSeq += 1
                @editorSeq = 1 if not @EDITOR_SEQUENCES[@editorSeq]
                cseq = @EDITOR_SEQUENCES[@editorSeq]
                @nextSeq = rtime + cseq.time

            {:p, :y, :r} = @targetAngle
            @targetDistToPony, @targetAngle = cseq.func(@targetDistToPony, Angle(p, y, r), delta)
            @targetDistToPony = math.Clamp(@targetDistToPony, 20, 150)
            @targetAngle.p = math.Clamp(@targetAngle.p, -40, 10)
        else
            if @hold
                x, y = gui.MousePos()
                deltaX, deltaY = x - @mouseX, y - @mouseY
                @mouseX, @mouseY = x, y
                {:pitch, :yaw, :roll} = @targetAngle
                yaw -= deltaX * .5
                pitch = math.Clamp(pitch - deltaY * .5, -40, 10)
                @targetAngle = Angle(pitch, yaw, roll)

        @angle = LerpAngle(delta * 4, @angle, @targetAngle)
        @distToPony = Lerp(delta * 4, @distToPony, @targetDistToPony)
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @vectorPos\Rotate(@angle)
        @drawAngle = (Vector(0, 0, @PONY_VEC_Z * .7) - @vectorPos)\Angle()

    FLOOR_VECTOR: Vector(0, 0, -30)
    FLOOR_ANGLE: Vector(0, 0, 1)

    DRAW_WALLS: {
        {Vector(-4000, 0, 900), Vector(1, 0, 0), 8000, 2000}
        {Vector(4000, 0, 900), Vector(-1, 0, 0), 8000, 2000}
        {Vector(0, -4000, 900), Vector(0, 1, 0), 8000, 2000}
        {Vector(0, 4000, 900), Vector(0, -1, 0), 8000, 2000}
        {Vector(0, 0, 900), Vector(0, 0, -1), 8000, 8000}
    }

    WALL_COLOR: Color(98, 189, 176)
    FLOOR_COLOR: Color(53, 150, 84)

    Paint: (w = 0, h = 0) =>
        surface.SetDrawColor(0, 0, 0)
        surface.DrawRect(0, 0, w, h)
        return if not IsValid(@model)
        x, y = @LocalToScreen(0, 0)
        cam.Start3D(@vectorPos, @drawAngle, 90, x, y, w, h)

        render.DrawQuadEasy(@FLOOR_VECTOR, @FLOOR_ANGLE, 7000, 7000, @FLOOR_COLOR)

        for {pos, ang, w, h} in *@DRAW_WALLS
            render.DrawQuadEasy(pos, ang, w, h, @WALL_COLOR)

        render.SuppressEngineLighting(true)
        render.ResetModelLighting(1, 1, 1)
        render.SetColorModulation(1, 1, 1)
        @controller\GetRenderController()\PreDraw(@model) if @controller
        @model\DrawModel()
        @controller\GetRenderController()\PostDraw(@model) if @controller
        render.SuppressEngineLighting(false)

        cam.End3D()
    OnRemove: =>
        @model\Remove() if IsValid(@model)
}

vgui.Register('PPM2ModelPanel', MODEL_BOX_PANEL, 'EditablePanel')

PANEL_SETTINGS_BASE = {
    Init: =>
        @shouldSaveData = false
        @SetMouseInputEnabled(true)
        @SetKeyboardInputEnabled(true)
        @DockPadding(5, 5, 5, 5)
        @unsavedChanges = false
        @updateFuncs = {}
    ValueChanges: (valID, newVal, pnl) =>
        @unsavedChanges = true
        @frame.unsavedChanges = true
        @frame\SetTitle("#{@GetTargetData() and @GetTargetData()\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor; *Unsaved changes*")
    GetShouldSaveData: => @shouldSaveData
    ShouldSaveData: => @shouldSaveData
    SetShouldSaveData: (val = false) => @shouldSaveData = val
    GetTargetData: => @data
    TargetData: => @data
    SetTargetData: (val) => @data = val
    DoUpdate: => func() for func in *@updateFuncs
    NumSlider: (name = 'Slider', option = '', decimals = 0) =>
		with vgui.Create('DNumSlider', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 0, 2, 0)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetText(name)
			\SetMin(0)
			\SetMax(1)
			\SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
			\SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
			\SetDecimals(decimals)
            \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
			.TextArea\SetTextColor(color_white)
			.Label\SetTextColor(color_white)
            .OnValueChanged = (pnl, newVal = 1) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
                \SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
                \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    Label: (text = '') =>
        with vgui.Create('DLabel', @scroll or @)
            \SetText(text)
            \Dock(TOP)
            \SetTextColor(color_white)
            \SizeToContents()
            w, h = \GetSize()
            \SetSize(w, h + 5)
            @scroll\AddItem(_with_0) if IsValid(@scroll)
	CheckBox: (name = 'Label', option = '') =>
		with vgui.Create('DCheckBoxLabel', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 2, 2, 2)
			\SetText(name)
			\SetTextColor(color_white)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .OnChange = (pnl, newVal = false) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal and 1 or 0, @GetShouldSaveData())
                @ValueChanges(option, newVal and 1 or 0, pnl)
            table.insert @updateFuncs, ->
                \SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    ColorBox: (name = 'Colorful Box', option = '') =>
        collapse = vgui.Create('DCollapsibleCategory', @scroll or @)
        box = vgui.Create('DColorMixer', collapse)
        collapse.box = box
        with box
            \SetSize(250, 270)
			\SetTooltip("#{name}\nData value: #{option}")
            \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .ValueChanged = (pnl, newVal = Color(0, 0, 0)) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        with collapse
            \SetContents(box)
            \Dock(TOP)
            \DockMargin(2, 2, 2, 2)
            \SetSize(250, 270)
            \SetLabel(name)
            \SetExpanded(false)
        @scroll\AddItem(collapse) if IsValid(@scroll)
        return box, collapse
    Spoiler: (name = 'Mysterious spoiler') =>
        collapse = vgui.Create('DCollapsibleCategory', @scroll or @)
        canvas = vgui.Create('EditablePanel', collapse)
        with canvas
            \SetSize(0, 400)
            \Dock(FILL)
        with collapse
            \SetContents(canvas)
            \Dock(TOP)
            \DockMargin(2, 2, 2, 2)
            \SetSize(250, 270)
            \SetLabel(name)
            \SetExpanded(false)
        @scroll\AddItem(collapse) if IsValid(@scroll)
        return canvas, collapse
    ComboBox: (name = 'Combo Box', option = '', choices) =>
        label = vgui.Create('DLabel', @scroll or @)
        with label
            \SetText(name)
            \SetTextColor(color_white)
            \Dock(TOP)
            \SetSize(0, 20)
            \DockMargin(5, 0, 5, 0)
            \SetMouseInputEnabled(true)
        @scroll\AddItem(label) if IsValid(@scroll)
        box = vgui.Create('DComboBox', label)
        with box
            \Dock(RIGHT)
            \SetSize(170, 0)
            \DockMargin(0, 0, 5, 0)
            \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
            if choices
                \AddChoice(choice) for choice in *choices
            else
                \AddChoice(choice) for choice in *@GetTargetData()["Get#{option}Types"](@GetTargetData()) if @GetTargetData() and @GetTargetData()["Get#{option}Types"]
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                index -= 1
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, index, @GetShouldSaveData())
                @ValueChanges(option, index, pnl)
            table.insert @updateFuncs, ->
                \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
        return box, label
    URLInput: (option = '') =>
        wrapper = vgui.Create('EditablePanel', @scroll or @)
        with wrapper
            \Dock(TOP)
            \DockMargin(5, 10, 5, 10)
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            \SetSize(0, 20)
        textInput = vgui.Create('DTextEntry', wrapper)
        @scroll\AddItem(wrapper) if IsValid(@scroll)
        with textInput
            \Dock(FILL)
            \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            .OnEnter = ->
                text = \GetValue()
                if text\find('^https?://')
                    @GetTargetData()["Set#{option}"](@GetTargetData(), text)
                    @ValueChanges(option, text, textInput)
                else
                    @GetTargetData()["Set#{option}"](@GetTargetData(), '')
                    @ValueChanges(option, '', textInput)
            .OnKeyCodeTyped = (pnl, key = KEY_FIRST) ->
                switch key
                    when KEY_FIRST
                        return true
                    when KEY_NONE
                        return true
                    when KEY_TAB
                        return true
                    when KEY_ENTER
                        .OnEnter()
                        \KillFocus()
                        return true
                timer.Create "PPM2.EditorCodeChange.#{option}", 1, 1, ->
                    return if not IsValid(textInput)
                    .OnEnter()
            table.insert @updateFuncs, -> \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        return textInput
    ScrollPanel: =>
        return @scroll if IsValid(@scroll)
        @scroll = vgui.Create('DScrollPanel', @)
        @scroll\Dock(FILL)
        return @scroll
    Paint: (w = 0, h = 0) =>
		surface.SetDrawColor(130, 130, 130)
		surface.DrawRect(0, 0, w, h)
}

vgui.Register('PPM2SettingsBase', PANEL_SETTINGS_BASE, 'EditablePanel')

EditorPages = {
    {
        'name': 'Main'
        'internal': 'main'
        'func': (sheet) =>
            @ScrollPanel()
            @CheckBox('Gender', 'Gender')
            @ComboBox('Race', 'Race')
            @NumSlider('Weight', 'Weight', 2)
            @CheckBox('Socks (simple texture)', 'Socks')
            @ComboBox('Eyelashes', 'EyelashType')
            @ComboBox('Bodysuit', 'Bodysuit')
            @ColorBox('Body color', 'BodyColor')
            @CheckBox('Socks (as model)', 'SocksAsModel')
            @ColorBox('Socks model color', 'SocksColor')
            @CheckBox('No flexes on new model', 'NoFlex')
            @Label('You can disable separately any flex state controller\nSo these flexes can be modified with third-party addons (like PAC3)')
            flexes = @Spoiler('Flexes controls')
            for {:flex, :active} in *PPM2.PonyFlexController.FLEX_LIST
                @CheckBox("Disable #{flex} control", "DisableFlex#{flex}")\SetParent(flexes) if active
            flexes\SizeToContents()
    }

    {
        'name': 'Eyes'
        'internal': 'eyes'
        'func': (sheet) =>
            @ScrollPanel()
            @CheckBox('Use separated settings for eyes', 'SeparateEyes')
            for publicName in *{'', 'Left', 'Right'}
                @Label("'#{publicName}' Eye settings")
                @ComboBox("#{publicName} Eye type", "EyeType#{publicName}")
                @CheckBox("#{publicName} Eye lines", "EyeLines#{publicName}")
                @CheckBox("#{publicName} Derp eye", "DerpEyes#{publicName}")
                @NumSlider("#{publicName} Derp eye strength", "DerpEyesStrength#{publicName}", 2)
                @NumSlider("#{publicName} Eye size", "IrisSize#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole width", "HoleWidth#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole size", "HoleSize#{publicName}", 2)

                @ColorBox("#{publicName} Eye background", "EyeBackground#{publicName}")
                @ColorBox("#{publicName} Eye hole", "EyeHole#{publicName}")
                @ColorBox("#{publicName} Top eye iris", "EyeIrisTop#{publicName}")
                @ColorBox("#{publicName} Bottom eye iris", "EyeIrisBottom#{publicName}")
                @ColorBox("#{publicName} Eye line 1", "EyeIrisLine1#{publicName}")
                @ColorBox("#{publicName} Eye line 2", "EyeIrisLine2#{publicName}")
                @ColorBox("#{publicName} Eye reflection effect", "EyeReflection#{publicName}")
                @ColorBox("#{publicName} Eye effect", "EyeEffect#{publicName}")
    }

    {
        'name': 'Mane and tail'
        'internal': 'manetail'
        'func': (sheet) =>
            @ScrollPanel()
            @Label('"New" types affect only new model')
            @ComboBox('Tail type', 'TailType')
            @ComboBox('New Tail type', 'TailTypeNew')

            @ComboBox('Mane type', 'ManeType')
            @ComboBox('New Mane type', 'ManeTypeNew')

            @ComboBox('Lower Mane type', 'ManeTypeLower')
            @ComboBox('New Lower Mane type', 'ManeTypeLowerNew')

            for i = 1, 2
                @ColorBox("Mane color #{i}", "ManeColor#{i}")
                @ColorBox("Tail color #{i}", "TailColor#{i}")

            for i = 1, 6
                @ColorBox("Mane detail color #{i}", "ManeDetailColor#{i}")
                @ColorBox("Tail detail color #{i}", "TailDetailColor#{i}")

            for i = 1, 6
                @Label("Mane URL Detail #{i} input field")
                @URLInput("ManeURL#{i}")
                @ColorBox("Mane URL detail color #{i}", "ManeURLColor#{i}")
                @Label("Tail URL Detail #{i} input field")
                @URLInput("TailURL#{i}")
                @ColorBox("Tail URL detail color #{i}", "TailURLColor#{i}")

            @CheckBox('Separate upper and lower mane colors', 'SeparateMane')
            @Label('These options have effect only on new model')

            for i = 1, 2
                @ColorBox("Upper Mane color #{i}", "UpperManeColor#{i}")
                @ColorBox("Lower Mane color #{i}", "LowerManeColor#{i}")

            for i = 1, 6
                @ColorBox("Upper Mane detail color #{i}", "UpperManeDetailColor#{i}")
                @ColorBox("Lower Tail detail color #{i}", "LowerManeDetailColor#{i}")

            for i = 1, 6
                @Label("Upper mane URL Detail #{i} input field")
                @URLInput("UpperManeURL#{i}")
                @ColorBox("Upper Mane URL detail color #{i}", "UpperManeURLColor#{i}")
                @Label("Lower mane URL Detail #{i} input field")
                @URLInput("LowerManeURL#{i}")
                @ColorBox("Lower Tail URL detail color #{i}", "LowerManeURLColor#{i}")
    }

    {
        'name': 'Body details'
        'internal': 'bodydetail'
        'func': (sheet) =>
            @ScrollPanel()
            for i = 1, PPM2.MAX_BODY_DETAILS
                @ComboBox("Detail #{i}", "BodyDetail#{i}")
                @ColorBox("Detail color #{i}", "BodyDetailColor#{i}")

            @Label('Body detail URL image input fields\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')
            for i = 1, PPM2.MAX_BODY_DETAILS
                @Label("Body detail #{i}")
                @URLInput("BodyDetailURL#{i}")
                @ColorBox("URL Detail color #{i}", "BodyDetailURLColor#{i}")
    }

    {
        'name': 'Cutiemark'
        'internal': 'cmark'
        'func': (sheet) =>
            @CheckBox('Display cutiemark', 'CMark')
            @ComboBox('Cutiemark type', 'CMarkType')
            @markDisplay = vgui.Create('EditablePanel', @)
            with @markDisplay
                \Dock(TOP)
                \SetSize(320, 320)
                \DockMargin(20, 20, 20, 20)
                .Paint = (pnl, w = 0, h = 0) ->
                    data = @GetTargetData()
                    return if not data
                    controller = data\GetController()
                    return if not controller
                    rcontroller = controller\GetRenderController()
                    return if not rcontroller
                    tcontroller = rcontroller\GetTextureController()
                    return if not tcontroller
                    mat = tcontroller\GetCMarkGUI()
                    return if not mat
                    surface.SetDrawColor(0, 0, 0)
                    surface.DrawRect(0, 0, w, h)
                    surface.SetDrawColor(255, 255, 255)
                    surface.SetMaterial(mat)
                    surface.DrawTexturedRect(0, 0, w, h)

            @Label('Cutiemark URL image input field\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')\DockMargin(5, 10, 5, 10)
            @URLInput('CMarkURL')
    }

    {
        'name': 'Files'
        'internal': 'saves'
        'func': (sheet) =>
            @Label('Open file by double click')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    @frame.data\SetFilename(fil)
                    @frame.data\ReadFromDisk(true)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = false
                    @frame.unsavedChanges = false
                    @frame\SetTitle("#{fil} - PPM2 Pony Editor")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm2/*', 'DATA')
                for fil in *files
                    matchBak = '.bak.txt'
                    continue if fil\sub(-#matchBak) == matchBak
                    list\AddLine(fil\sub(1, #fil - 4))
            @rebuildFileList()
    }

    {
        'name': 'Old Files'
        'internal': 'oldsaves'
        'func': (sheet) =>
            @Label('!!! It may or may not work. You will be squished.')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    newData = PPM2.ReadFromOldData(fil)
                    if not newData
                        Derma_Message('Failed to import.', 'Onoh!', 'Okai ;w;')
                        return
                    @frame.data\SetFilename(newData\GetFilename())
                    newData\ApplyDataToObject(@frame.data, false)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = true
                    @frame.unsavedChanges = true
                    @frame\SetTitle("#{newData\GetFilename()} - PPM2 Pony Editor; *Unsaved changes*")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm/*', 'DATA')
                list\AddLine(fil\sub(1, #fil - 4)) for fil in *files
            @rebuildFileList()
    }
}

EditorModels = {
    'DEFAULT': 'models/ppm/player_default_base.mdl'
    'CPPM': 'models/cppm/player_default_base.mdl'
    'NEW': 'models/ppm/player_default_base_new.mdl'
}

USE_MODEL = CreateConVar('ppm2_editor_model', 'new', {FCVAR_ARCHIVE}, 'What model to use in editor. Valids are "default", "cppm", "new"')

if IsValid(PPM2.EditorFrame)
    PPM2.EditorFrame\Remove()
    net.Start('PPM2.EditorStatus')
    net.WriteBool(false)
    net.SendToServer()
PPM2.OpenEditor = ->
    if IsValid(PPM2.EditorFrame)
        PPM2.EditorFrame\SetVisible(true)
        PPM2.EditorFrame\Center()
        PPM2.EditorFrame\MakePopup()
        net.Start('PPM2.EditorStatus')
        net.WriteBool(true)
        net.SendToServer()
        return

    frame = vgui.Create('DFrame')
    self = frame
    W, H = ScrW() - 25, ScrH() - 25
    @SetSize(W, H)
    @Center()
    @MakePopup()
    @SetTitle('PPM2 Pony Editor')
    @SetDeleteOnClose(false)
    PPM2.EditorFrame = @

    @OnClose = ->
        net.Start('PPM2.EditorStatus')
        net.WriteBool(false)
        net.SendToServer()

    @menus = vgui.Create('DPropertySheet', @)
    @menus\Dock(LEFT)
    @menus\SetSize(370, 0)

    @model = vgui.Create('PPM2ModelPanel', @)
    @model\Dock(FILL)

    copy = PPM2.GetMainData()\Copy()
    ply = LocalPlayer()
    editorModelSelect = USE_MODEL\GetString()\upper()
    editorModelSelect = EditorModels[editorModelSelect] and editorModelSelect or 'DEFAULT'
    ent = @model\ResetModel(nil, EditorModels[editorModelSelect])
    controller = copy\CreateCustomController(ent)
    copy\SetController(controller)
    frame.controller = controller
    frame.data = copy
    frame.DoUpdate = -> pnl\DoUpdate() for i, pnl in pairs @panels

    saveAs = (callback = (->)) ->
        confirm = (txt = '') ->
            txt = txt\Trim()
            return if txt == ''
            copy\SetFilename(txt)
            copy\Save()
            @unsavedChanges = false
            @model.unsavedChanges = false
            @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")
            @panels.saves.rebuildFileList()
            callback(txt)
        Derma_StringRequest('Save as', 'Enter file name without ppm2/ and .txt', copy\GetFilename(), confirm)

    @saveButton = vgui.Create('DButton', @)
    with @saveButton
        \SetText('Save')
        \SetPos(W - 205, 5)
        \SetSize(90, 20)
        .DoClick = -> saveAs()

    @wearButton = vgui.Create('DButton', @)
    with @wearButton
        \SetText('Apply changes (wear)')
        \SetPos(W - 350, 5)
        \SetSize(140, 20)
        lastWear = 0
        .DoClick = ->
            return if RealTime() < lastWear
            lastWear = RealTime() + 5
            mainData = PPM2.GetMainData()
            copy\ApplyDataToObject(mainData, false) -- no save on apply
    @selectModelBox = vgui.Create('DComboBox', @)
    with @selectModelBox
        \SetSize(120, 20)
        \SetPos(W - 475, 5)
        \SetValue(editorModelSelect)
        \AddChoice(choice) for choice in *{'default', 'cppm', 'new'}
        .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
            @SetDeleteOnClose(true)
            RunConsoleCommand('ppm2_editor_model', value)

            confirm = ->
                @Close()
                timer.Simple 0.1, PPM2.OpenEditor
            Derma_Query(
                'You should restart editor for applying change.\nRestart now?\nUnsaved data can be lost!',
                'Editor restart required',
                'Yas!',
                confirm,
                'Noh!'
            )

    @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")

    @model\SetController(controller)
    controller\SetupEntity(ent)

    @panels = {}

    for {:name, :func, :internal} in *EditorPages
        pnl = vgui.Create('PPM2SettingsBase', @menus)
        @menus\AddSheet(name, pnl)
        pnl\SetTargetData(copy)
        pnl\Dock(FILL)
        pnl.frame = @
        func(pnl, @menus)
        @panels[internal] = pnl

concommand.Add 'ppm2_editor', PPM2.OpenEditor

IconData =
	title: 'PPM V2.0',
	icon: 'gui/pped_icon.png',
	width: 960,
	height: 700,
	onewindow: true,
	init: (icon, window) ->
		window\Remove()
		RunConsoleCommand('ppm2_editor')

list.Set('DesktopWindows', 'PPM2', IconData)
CreateContextMenu() if IsValid(g_ContextMenu)


trigger: use namespace triggers instead of triggers

Former-commit-id: 99a906db4b16ce36903f349924aaf8847c3f21c6 [formerly 194d570d838883b08dea6a64f962a1530c473ffe]
Former-commit-id: c01c67e35b2085e88ccc4c483e1eeaeff254b409

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      name UNIQUE ON CONFLICT REPLACE,
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', destination, source, 'Usage: !trigger add <name>|<lua pattern>|<lua code>. Ex: !trigger add help|^!help (%w+)|say "%s, you need help"'

-- Construct a safe environ for trigger with two functions; say and reply
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Register the command
regCommand = (source, destination, name, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (name, pattern, funcstr) VALUES(?, ?, ?)"
  code = ins\bind_values name, pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand 'trigger', pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, name) =>
  db = openDb!
  -- Find pattern which is used for deletion of handler
  stmt = db\prepare "SELECT pattern FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  code = stmt\step!
  if code == sqlite3.DONE
    -- Invalid name
    db\close!
    return
  pattern = stmt\get_values()[1]

  -- Delete trigger from db
  ins = db\prepare "DELETE FROM trigger WHERE name = ?"
  code = ins\bind_values pattern
  code = ins\step!
  code = ins\finalize!
  db\close!

  @UnregisterCommand 'triggers', pattern

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  ivar2\RegisterCommand 'triggers', row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)
db\close!

PRIVMSG:
  '%ptrigger$': triggerHelp
  '%ptrigger add (.+)|(.+)|(.+)$': regCommand
  '%ptrigger del (.+)$': delCommand

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      name UNIQUE ON CONFLICT REPLACE,
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', destination, source, 'Usage: !trigger add <name>|<lua pattern>|<lua code>. Ex: !trigger add help|^!help (%w+)|say "%s, you need help"'

-- Construct a safe environ for trigger with two functions; say and reply
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Register the command
regCommand = (source, destination, name, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (name, pattern, funcstr) VALUES(?, ?, ?)"
  code = ins\bind_values name, pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand 'trigger', pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, name) =>
  db = openDb!
  -- Find pattern which is used for deletion of handler
  stmt = db\prepare "SELECT pattern FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  code = stmt\step!
  if code == sqlite3.DONE
    -- Invalid name
    db\close!
    return
  pattern = stmt\get_values()[1]

  -- Delete trigger from db
  ins = db\prepare "DELETE FROM trigger WHERE name = ?"
  code = ins\bind_values pattern
  code = ins\step!
  code = ins\finalize!
  db\close!

  @UnregisterCommand 'trigger', pattern

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  ivar2\RegisterCommand 'trigger', row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)
db\close!

PRIVMSG:
  '%ptrigger$': triggerHelp
  '%ptrigger add (.+)|(.+)|(.+)$': regCommand
  '%ptrigger del (.+)$': delCommand


ircsnap: some cleanup

ircsnap: some cleanup

Former-commit-id: d01c0abf17863e225c709e7ea9676b488cb0ce94
Former-commit-id: d910e784705d53d554cdee025a88a5facab4277a

{:simplehttp, :json, :urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
fs = require'nixio.fs'
ltn12 = require'ltn12'

-- All URLs in this module is under this prefix
urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\match '.png'
      content_type = 'image/png'
    if file\match '.svg'
      content_type = 'image/svg'
    if file\match '.mp4'
      content_type = 'video/mp4'
    if file\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn
    return

  -- Serve video player page
  video = req.url\match '/video/(.*)$'
  if video then
    return send video_html(video)

  channel = req.url\match('channel=(.+)%s*')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your name

        <input id="text" type="text" required>

        Attach text

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">
      🎥 Upload video! <input type="file" accept="video/*" id="capturevf" style="visibility:hidden;">

        What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools ->  Add to homescreen to create a convenient shortcut for easy and fast access to sharing

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."


ircsnap: use new web api function sig

ircsnap: use new web api function sig

Former-commit-id: 81af2fd175be700ae40b57cd79c13ff35aeebc82
Former-commit-id: 86bb405f9845b87c80327112a8f33c1e9cc1c3ea

{:urlEncode} = require'util'
html2unicode = require 'html'
lfs = require'lfs'

-- All URLs in this module is under this prefix
urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) =>
  url = req.url
  send = (body, code, content_type) ->
    if not code then code = "200"
    if not content_type then content_type = 'text/html'
    res\append ':status', code
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(#body)
    req\write_headers(res, false, 30)
    req\write_body_from_string(body, 30)

  file = url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = lfs.attributes(fn).size
    --body = fd\read '*a'
    content_type = 'image/jpeg'
    if file\lower!\match '.png'
      content_type = 'image/png'
    if file\lower!\match '.svg'
      content_type = 'image/svg'
    if file\lower!\match '.mp4'
      content_type = 'video/mp4'
    if file\lower!\match '.mov'
      content_type = 'video/quicktime'
    if file\lower!\match '.mp3'
      content_type = 'audio/mp3'

    res\append ':status', '200'
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(size)
    req\write_headers(res, false, 30)
    fd = io.open(fn, 'rb')
    req\write_body_from_file(fd, 5*60)
    fd\close!
    return

  -- Serve video player page
  video = url\match '/video/(.*)$'
  if video then
    return send video_html(video)

  channel = url\match('channel=(.+)%s*')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP ]]..unescaped_channel..[[</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your nickname

        <input id="text" type="text" required>

        Attach text, if you want

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">
      🎥 Upload video! <input type="file" accept="video/*" id="capturevf" style="visibility:hidden;">

      What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools ->  Add to homescreen to create a convenient shortcut for easy and fast access to sharing

Made by xt

  ]]
  if req.method == 'POST'
    fn = req.headers['x-filename']
    sender = req.headers['x-sender'] or ''
    text = req.headers['x-text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        -- Move tempfile to real location
        os.rename req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
lfs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."

{:urlEncode} = require'util'
html2unicode = require 'html'
lfs = require'lfs'
ltn12 = require'ltn12'

-- All URLs in this module is under this prefix
urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  url = req.url
  send = (body, code, content_type) ->
    if not code then code = "200"
    if not content_type then content_type = 'text/html'
    res\append ':status', code
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(#body)
    req\write_headers(res, false, 30)
    req\write_body_from_string(body, 30)

  file = url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = lfs.attributes(fn).size
    --body = fd\read '*a'
    content_type = 'image/jpeg'
    if file\lower!\match '.png'
      content_type = 'image/png'
    if file\lower!\match '.svg'
      content_type = 'image/svg'
    if file\lower!\match '.mp4'
      content_type = 'video/mp4'
    if file\lower!\match '.mov'
      content_type = 'video/quicktime'
    if file\lower!\match '.mp3'
      content_type = 'audio/mp3'

    res\append ':status', '200'
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(size)
    req\write_headers(res, false, 30)
    fd = io.open(fn, 'rb')
    req\write_body_from_file(fd, 5*60)
    fd\close!
    return

  -- Serve video player page
  video = url\match '/video/(.*)$'
  if video then
    return send video_html(video)

  channel = url\match('channel=(.+)%s*')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP ]]..unescaped_channel..[[</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your nickname

        <input id="text" type="text" required>

        Attach text, if you want

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">
      🎥 Upload video! <input type="file" accept="video/*" id="capturevf" style="visibility:hidden;">

      What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools ->  Add to homescreen to create a convenient shortcut for easy and fast access to sharing

Made by xt

  ]]
  if req.method == 'POST'
    fn = req.headers['x-filename']
    sender = req.headers['x-sender'] or ''
    text = req.headers['x-text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        -- Move tempfile to real location
        os.rename req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
lfs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."


update router constructor and routes as [{pattern: url, action: (req, res) ->} ...]

class Router
  new: (ctrls = {}) =>
    @routes = {}
    table.foreach ctrls, (k, v) ->
      ctls = require "controller"
      table.insert @routes,
        pattern: k
        action: ctls[v.controller][v.action]

  process: (req, res, next) =>
    for i, route in ipairs @routes
      {:pattern, :action} = route
      if "#{req.method} #{req.url}"\find(pattern) != nil
        return action req, res
    if next != nil
      next()

  METHOD: (method, path, mw) =>
    table.insert @routes,
      pattern: "#{method} #{path}"
      action: mw

  use: (path, mw) =>
    @METHOD '%a+', path, mw

  get: (path, mw) =>
    @METHOD 'GET', path, mw

return Router

class Router
  routes: {}

  new: (ctrls = {}) =>
    table.foreach ctrls, (k, v) ->
      ctls = require "controller"
      @routes[k] = ctls[v.controller][v.action]

  process: (req, res, next) =>
    for pattern, func in pairs @routes
      if "#{req.method} #{req.url}"\find(pattern) != nil
        return func req, res
    if next != nil
      next()

  METHOD: (method, path, mw) =>
    table.insert @routes, "#{method} #{path}": mw

  use: (path, mw) =>
    @METHOD 'GET', path, mw

  get: (path, mw) =>
    @METHOD 'GET', path, mw

return Router


Add console in dev environment

Add console in dev environment

lapis = require "lapis"
config = (require "lapis.config").get!
content = require "content"
logger = require "lapis.logging"
console = require "lapis.console" if config._name == 'development' or config._name == 'development-perftest'

import after_dispatch from require "lapis.nginx.context"
import to_json from require "lapis.util"
import capture_errors, assert_error, yield_error from require "lapis.application"

class CSWeek extends lapis.Application
    layout: require "views.layout"
    error_page: require "views.error"

    handle_error: (err, trace, code = 500) =>
        r = @app.Request @app, @req, @res

        with r
            .m, errpg = content\get "error"
            if errpg then
                .m = { }
                .m[code] = "Could not load the error page (code #{code}): #{errpg}
When handling #{err}
#{trace}"

            .m[code] = .m[code] or .m.default or { }
            .page_id = "error"
            .status = code
            .error = err
            .traceback = trace
            .m.title = .m[code].title

            \write {
                status: code
                content_type: "text/html"
                render: "error"
            }
            \render!
            logger.request r
        r

    try_render: (template, context) =>
        context = context or self
        with context
            .nav = assert_error content\get "nav"
            .footer = assert_error content\get "footer"
        render: template

    @before_filter =>
        if #[n for n in *{'production-perftest', 'development-perftest'} when n == config._name] > 0
            after_dispatch ->
                print to_json(ngx.ctx.performance)

    [home: "/"]: =>
        @page_id = "home"
        @m = assert_error content\get "home"
        @lecturers = assert_error content\get "lecturers"
        @app\try_render "home", self

    [about: "/o-nedelji"]: =>
        @page_id = "about"
        @m = assert_error content\get "about"
        @app\try_render "about", self

    [lecturers: "/predavaci"]: =>
        @page_id = "lecturers"
        @m = assert_error content\get "lecturers_page"
        @lecturers = assert_error content\get "lecturers"
        @app\try_render "lecturers", self

    [lecturer: "/predavaci/:name"]: =>
        @lecturers_page = assert_error content\get "lecturers_page"
        @page_id = "lecturer"
        lecturers = assert_error content\get "lecturers"
        results = [l for l in *lecturers when l.id == @params.name]
        if #results > 1
            yield_error "Multiple lecturer id's found for id `#{@params.name}`"
        else if #results == 0
            @app.handle_error self, "No such lecturer: `#{@params.name}`", nil, 404
            return

        @m = results[1]
        @app\try_render "lecturer", self

    [apply: "/prijava"]: =>
        @page_id = "apply"
        @m = assert_error content\get "apply"

        @app\try_render "apply", self

    [console: "/console"]: console.make!

    handle_404: =>
        @app.handle_error self, "Route `#{self.req.parsed_url.path or 'unknown'}` not found", nil, 404

lapis = require "lapis"
config = (require "lapis.config").get!
content = require "content"
logger = require "lapis.logging"

import after_dispatch from require "lapis.nginx.context"
import to_json from require "lapis.util"
import capture_errors, assert_error, yield_error from require "lapis.application"

class CSWeek extends lapis.Application
    layout: require "views.layout"
    error_page: require "views.error"

    handle_error: (err, trace, code = 500) =>
        r = @app.Request @app, @req, @res

        with r
            .m, errpg = content\get "error"
            if errpg then
                .m = { }
                .m[code] = "Could not load the error page (code #{code}): #{errpg}
When handling #{err}
#{trace}"

            .m[code] = .m[code] or .m.default or { }
            .page_id = "error"
            .status = code
            .error = err
            .traceback = trace
            .m.title = .m[code].title

            \write {
                status: code
                content_type: "text/html"
                render: "error"
            }
            \render!
            logger.request r
        r

    try_render: (template, context) =>
        context = context or self
        with context
            .nav = assert_error content\get "nav"
            .footer = assert_error content\get "footer"
        render: template

    @before_filter =>
        if #[n for n in *{'production-perftest', 'development-perftest'} when n == config._name] > 0
            after_dispatch ->
                print to_json(ngx.ctx.performance)

    [home: "/"]: =>
        @page_id = "home"
        @m = assert_error content\get "home"
        @lecturers = assert_error content\get "lecturers"
        @app\try_render "home", self

    [about: "/o-nedelji"]: =>
        @page_id = "about"
        @m = assert_error content\get "about"
        @app\try_render "about", self

    [lecturers: "/predavaci"]: =>
        @page_id = "lecturers"
        @m = assert_error content\get "lecturers_page"
        @lecturers = assert_error content\get "lecturers"
        @app\try_render "lecturers", self

    [lecturer: "/predavaci/:name"]: =>
        @lecturers_page = assert_error content\get "lecturers_page"
        @page_id = "lecturer"
        lecturers = assert_error content\get "lecturers"
        results = [l for l in *lecturers when l.id == @params.name]
        if #results > 1
            yield_error "Multiple lecturer id's found for id `#{@params.name}`"
        else if #results == 0
            @app.handle_error self, "No such lecturer: `#{@params.name}`", nil, 404
            return

        @m = results[1]
        @app\try_render "lecturer", self

    [apply: "/prijava"]: =>
        @page_id = "apply"
        @m = assert_error content\get "apply"

        @app\try_render "apply", self

    handle_404: =>
        @app.handle_error self, "Route `#{self.req.parsed_url.path or 'unknown'}` not found", nil, 404