Changeset - 2af6698b0566
[Not reviewed]
default
0 2 0
drewp - 23 years ago 2002-07-09 05:28:21

pulled Lightboard out of rsn.py (and fixed all the dependency problems i could find)
pulled Lightboard out of rsn.py (and fixed all the dependency problems i could find)
made some exceptions print better
2 files changed with 47 insertions and 415 deletions:
0 comments (0 inline, 0 general)
light8/Lightboard.py
Show inline comments
 
from __future__ import nested_scopes,division
 
from __future__ import nested_scopes
 

	
 
from Tix import *
 
from time import sleep
 
from signal import signal, SIGINT
 
from time import time
 
import sys, cPickle, random
 
import sys, cPickle
 

	
 
import io
 
from uihelpers import *
 
from panels import *
 
from Xfader import *
 
from subediting import Subediting
 
from Fader import Fader
 
from ExternalInput import ExternalSliders
 
import io, stage, Subs, Patch, ExtSliderMapper
 
import dmxclient
 
import stage
 
import Subs, Patch
 

	
 
class Pickles:
 
    def __init__(self, scalelevels, subs=None, windowpos=None):
 
    def __init__(self, scalelevels, subs=None):
 
        self.scalelevels = dict([(name, lev.get()) 
 
            for name, lev in scalelevels.items()])
 
        self.substate = dict([(name, subobj.get_state())
 
            for name, subobj in subs])
 
        self.windowpos = windowpos
 
        # print "substate", self.substate 
 

	
 
class Lightboard:
 
    def __init__(self, master, DUMMY):
 
    def __init__(self, master, parportdmx, DUMMY):
 
        self.master = master
 

	
 
        self.parportdmx = parportdmx
 
        self.DUMMY = DUMMY
 
        self.jostle_mode = 0
 
        self.lastline = None
 

	
 
        self.channel_levels = [] # these are actually the labels for the
 
                                 # channel levels, and should probably be moved
 
                                 # to panels.Leveldisplay
 
        self.channel_levels = []
 
        self.scalelevels = {}
 

	
 
        self.lastsublevels = {}  # to determine which subs changed
 
        self.unchangedeffect = {} # dict of levels for lights that didn't 
 
                                  # change last time
 
        self.lastunchangedgroup = {}
 

	
 
        # doesn't draw any UI yet-- look for self.xfader.setupwidget()
 
        self.xfader = Xfader(self.scalelevels) 
 
        self.xfader = Xfader(self.scalelevels) # doesn't draw any UI yet-- look for self.xfader.setupwidget()
 
        self.oldlevels = [None] * 68 # never replace this; just clear it
 
        self.subediting = Subediting(currentoutputlevels=self.oldlevels)
 

	
 
        self.windowpos = 0
 
        self.get_data()
 
        self.buildinterface()
 
        self.load()
 

	
 
        print "Light 8.8: Entering backgroundloop"
 
        self.backgroundloop()
 
        self.updatestagelevels()
 
        self.rec_file = open('light9.log', 'a')
 
        self.record_start()
 
        
 
    def buildinterface(self):
 
        print "Light 8.8: Constructing interface..."
 
        for w in self.master.winfo_children():
 
            w.destroy()
 

	
 
        print "\tstage"
 
        stage_tl = toplevelat('stage')
 
        stage_tl = toplevelat(22,30)
 
        s = stage.Stage(stage_tl)
 
        stage.createlights(s)
 
        s.setsubediting(self.subediting)
 
        s.pack()
 
        self.stage = s # save it
 

	
 
        sub_tl = toplevelat('sub')
 
        scene_tl = toplevelat('scenes')
 
        effect_tl = toplevelat('effect')
 
        sub_tl = toplevelat(0,0)
 
        effect_tl = toplevelat(462,4)
 

	
 
        print "\tslider patching -- It can't be turned off!"
 
        mapping_tl = toplevelat('mapping')
 
        self.slidermapper = ExtSliderMapper.ExtSliderMapper(mapping_tl, 
 
                                                            self.scalelevels, 
 
                                                            ExternalSliders(),
 
                                                            self)
 
        self.slidermapper.pack()
 
        self.subpanels = Subpanels(sub_tl, effect_tl, self, self.scalelevels,
 
                                   Subs, self.xfader, self.changelevel,
 
                                   self.subediting, Subs.longestsubname())
 

	
 
        print "\tsubmaster control"
 
        self.subpanels = Subpanels(sub_tl, effect_tl, scene_tl, self, 
 
                                   self.scalelevels, Subs, self.xfader, 
 
                                   self.changelevel, self.subediting, 
 
                                   Subs.longestsubname())
 

	
 
        for n, lev in self.scalelevels.items():
 
            self.lastsublevels[n] = lev.get()
 

	
 
        print "\tlevel display"
 
        leveldisplay_tl = toplevelat('leveldisplay')
 
        leveldisplay_tl = toplevelat(873,400)
 
        leveldisplay_tl.bind('<Escape>', sys.exit)
 

	
 
        self.leveldisplay = Leveldisplay(leveldisplay_tl, self.channel_levels)
 
        # I don't think we need this
 
        # for i in range(0,len(self.channel_levels)):
 
            # self.channel_levels[i].config(text=self.oldlevels[i])
 
            # colorlabel(self.channel_levels[i])
 
        for i in range(0,len(self.channel_levels)):
 
            self.channel_levels[i].config(text=self.oldlevels[i])
 
            colorlabel(self.channel_levels[i])
 

	
 
        print "\tconsole"
 
        Console(self)
 

	
 
        # root frame
 
        print "\tcontrol panel"
 
        self.master.configure(bg='black')
 
        controlpanel = Controlpanel(self.master, self.xfader, self.refresh, 
 
            self.quit, self.toggle_jostle, self.nonzerosubs)
 
        controlpanel = Controlpanel(self.master, self.xfader, self.refresh, self.quit)
 
        
 
        print "\tcrossfader"
 
        xf=Frame(self.master)
 
        xf.pack(side='right')
 

	
 
        self.master.bind('<q>', self.quit)
 
        self.master.bind('<r>', self.refresh)
 
        leveldisplay_tl.bind('<q>', self.quit)
 
        leveldisplay_tl.bind('<r>', self.refresh)
 

	
 
        self.xfader.setupwidget(xf)
 
        controlpanel.pack()
 

	
 
        print "\tcue fader (skipped)"
 
        # cuefader_tl = toplevelat('cuefader')
 
        # cuefader = Fader(cuefader_tl, Subs.cues, self.scalelevels)
 
        # cuefader.pack()
 
        print "Light 8.8: Everything's under control"
 

	
 
        cuefader_tl = toplevelat(78, 480)
 
        cuefader = Fader(cuefader_tl, Subs.cues, self.scalelevels)
 
        cuefader.pack()
 

	
 
    def get_data(self,*args):
 
        Subs.reload_data(self.DUMMY)
 
        Patch.reload_data(self.DUMMY)
 
        print "Light 8.8:", len(Patch.patch), "dimmers patched"
 
        print "Light 8.8:", len(Subs.subs), "submasters loaded"
 
        print "Patch:", Patch.patch
 
        print "Subs:", ', '.join(Subs.subs.keys())
 

	
 
    def refresh(self, *args):
 
        'rebuild interface, reload data'
 
        print "Light 8.8: Refresh initiated.  Cross your fingers."
 
        self.get_data()
 
        print "Light 8.8: Subediting refreshed"
 
        self.subediting.refresh()
 
        print "Light 8.8: Rebuilding interface..."
 
        self.buildinterface()
 
        bindkeys(self.master,'<Escape>', self.quit)
 
        print "Light 8.8: Setting up slider patching..."
 
        self.slidermapper.setup()
 
        # self.master.tk_setPalette('gray40')
 
        print "Light 8.8: Now back to your regularly scheduled Light 8"
 

	
 
    def stageassub(self):
 
        """returns the current onstage lighting as a levels
 
        dictionary, skipping the zeros, and using names where
 
        possible"""
 
        levs=self.oldlevels
 
        
 
        return dict([(Patch.get_channel_name(i),l) for i,l
 
                     in zip(range(1,len(levs)+1),levs)
 
                     if l>0])
 
    def save_sub(self, name, levels, refresh=1):
 
    def save_sub(self, name, levels):
 
        if not name:
 
            print "Enter sub name in console."
 
            return
 

	
 
        st = ''
 
        linebuf = 'subs["%s"] = {' % name
 
@@ -176,143 +125,49 @@ class Lightboard:
 
        else:
 
            filename = 'Config.py'
 
        f = open(filename, 'a')
 
        f.write(st)
 
        f.close()
 
        print 'Added sub:', st
 
        if refresh:
 
            self.refresh()
 
        self.refresh()
 

	
 
    # this is called on a loop, and ALSO by the Scales
 
    def changelevel(self, *args):
 
        'Amp trims slider'
 

	
 
        # load levels from external sliders
 
        extlevels = self.slidermapper.get_levels()
 
        for name, val in extlevels.items():
 
            if name in self.scalelevels:
 
                sl = self.scalelevels[name]
 
                sl.set(val)
 
        
 
        # newstart = time()
 

	
 
        # learn what changed
 
        unchangedgroup = {}
 
        changedgroup = {}
 
        for name, lastlevel in self.lastsublevels.items():
 
            newlevel = self.scalelevels[name].get()
 
            if lastlevel != newlevel:
 
                changedgroup[name] = newlevel
 
            else:
 
                unchangedgroup[name] = newlevel
 

	
 
        changedeffect = {}
 
        if not changedgroup:
 
            # should load levels from last time
 
            pass
 
        else:
 
            # calculate effect of new group.  this should take no time if 
 
            # nothing changed
 
            for name, level in changedgroup.items():
 
                newlevels = Subs.subs[name].get_levels(level=level)
 
                for (ch, fadelev) in newlevels.items():
 
                    changedeffect[ch-1] = \
 
                        max(changedeffect.get(ch-1, 0), fadelev)
 

	
 
        if unchangedgroup != self.lastunchangedgroup:
 
            # unchanged group changed! (confusing, huh?)
 
            # this means: the static subs from the last time are not the same 
 
            # as they are this time, so we recalculate effect of unchanged group
 
            self.unchangedeffect = {}
 
            for name, level in unchangedgroup.items():
 
                newlevels = Subs.subs[name].get_levels(level=level)
 
                for (ch, fadelev) in newlevels.items():
 
                    self.unchangedeffect[ch-1] = \
 
                        max(self.unchangedeffect.get(ch-1, 0), fadelev)
 
            self.lastunchangedgroup = unchangedgroup
 
                
 
        # record sublevels for future generations (iterations, that is)
 
        for name in self.lastsublevels:
 
            self.lastsublevels[name] = self.scalelevels[name]
 

	
 
        # merge effects together
 
        levels = [0] * 68
 
        if changedeffect:
 
            levels = [int(max(changedeffect.get(ch, 0), 
 
                              self.unchangedeffect.get(ch, 0)))
 
                        for ch in range(0, 68)]
 
        else:
 
            levels = [int(self.unchangedeffect.get(ch, 0))
 
                        for ch in range(0, 68)]
 

	
 
        '''
 
        newend = time()
 

	
 
        levels_opt = levels
 
        
 
        oldstart = time()
 
        # i tried to optimize this to a dictionary, but there was no speed
 
        # improvement
 
        levels = [0] * 68
 
        for name, s in Subs.subs.items():
 
            sublevel = self.scalelevels[name].get()
 
            newlevels = s.get_levels(level=sublevel)
 
            self.lastsublevels[name] = sublevel # XXX remove
 
            newlevels = s.get_levels(level=self.scalelevels[name].get())
 
            for (ch, fadelev) in newlevels.items():
 
                levels[ch-1] = max(levels[ch-1], fadelev)
 
        levels = [int(l) for l in levels]
 
        oldend = time()
 

	
 
        newtime = newend - newstart
 
        oldtime = oldend - oldstart
 
        print "new", newtime, 'old', (oldend - oldstart), 'sup', \
 
               oldtime / newtime
 
        
 
        if levels != levels_opt: 
 
            raise "not equal"
 
            # for l, lo in zip(levels, levels_opt):
 
                # print l, lo
 
        '''
 
        
 
        levels = [int(l) for l in levels]
 

	
 
        for lev,lab,oldlev,numlab in zip(levels, self.channel_levels, 
 
                                         self.oldlevels, 
 
                                         self.leveldisplay.number_labels):
 
            if lev != oldlev:
 
                lab.config(text="%d" % lev) # update labels in lev display
 
                colorlabel(lab)             # recolor labels
 
                lab.config(text="%d" % lev)
 
                colorlabel(lab)
 
                if lev < oldlev:
 
                    numlab['bg'] = 'blue'
 
                else:
 
                    numlab['bg'] = 'red'
 
            else:
 
                numlab['bg'] = 'grey40'
 
                numlab['bg'] = 'lightPink'
 

	
 
        # replace the elements in oldlevels - don't make a new list 
 
        # (Subediting is watching it)
 
        self.oldlevels[:] = levels[:]
 
        self.oldlevels[:] = levels[:] # replace the elements in oldlevels - don't make a new list (Subediting is watching it)
 
            
 
        if self.jostle_mode:
 
            delta = random.randrange(-1, 2, 1) # (-1, 0, or 1)
 
            # print "delta", delta
 
            levels = [min(100, max(x + delta, 0)) for x in levels]
 
            # print "jostled", levels
 

	
 
        dmxclient.outputlevels([l/100 for l in levels])
 
#        self.parportdmx.sendlevels(levels)
 

	
 
    def updatestagelevels(self):
 
        self.master.after(100, self.updatestagelevels)
 
        for lev, idx in zip(self.oldlevels, xrange(0, 68 + 1)):
 
            self.stage.updatelightlevel(Patch.get_channel_name(idx + 1), lev)
 
        self.parportdmx.sendlevels(levels)
 

	
 
    def load(self):
 
        try:
 
            filename = '/tmp/light9.prefs'
 
            if self.DUMMY:
 
                filename += '.dummy'
 
            print "Light 8.8: Loading from", filename
 
            print "Loading from", filename
 
            file = open(filename, 'r')
 
            p = cPickle.load(file)
 
            for s, v in p.scalelevels.items():
 
                try:
 
                    self.scalelevels[s].set(v)
 
                except Exception,e:
 
@@ -325,63 +180,24 @@ class Lightboard:
 
        except IOError, e:
 
            print "IOError: Couldn't load prefs (%s): %s" % (filename,e)
 
        except EOFError, e:
 
            print "EOFrror: Couldn't load prefs (%s): %s" % (filename,e)
 
        except Exception,e:
 
            print "Couldn't load prefs (%s): %s" % (filename,e)
 
        self.slidermapper.setup()
 

	
 
    def backgroundloop(self, *args):
 
        self.master.after(150, self.backgroundloop, ())
 
        self.master.after(50, self.backgroundloop, ())
 
        self.changelevel()
 
    def quit(self, *args):
 
        print "Light 8.8: And that's my cue to exit..."
 
        self.save()
 
        self.record_end()
 
        self.master.destroy()
 
        sys.exit()
 
    def save(self, *args):
 
        filename = '/tmp/light9.prefs'
 
        if self.DUMMY:
 
            filename += '.dummy'
 
        print "Light 8.8: Saving to", filename
 
        print "Saving to", filename
 
        file = open(filename, 'w')
 

	
 
        try:
 
            cPickle.dump(Pickles(self.scalelevels, Subs.subs.items()), file)
 
        except cPickle.UnpickleableError:
 
            print "UnpickleableError!  There's yer problem."
 
    def toggle_jostle(self, *args):
 
        self.jostle_mode = not self.jostle_mode
 
        if self.jostle_mode:
 
            print 'Light 8.8: Perhaps we can jost-le your memory?'
 
        else:
 
            print 'Light 8.8: He remembers! (jostle off)'
 
    def nonzerosubs(self, *args):
 
        print "Light 8.8: Active subs:"
 
        for n, dv in self.scalelevels.items():
 
            if dv.get() > 0:
 
                print "%-.4f: %s" % (dv.get(), n)
 
    def record_start(self):
 
        print "Light 8.8: Recorder started"
 
        self.rec_file.write("%s:\t%s\n" % (time(), "--- Start ---"))
 
        self.record_stamp()
 
    def record_end(self):
 
        print "Light 8.8: Recorder shutdown"
 
        self.rec_file.write("%s:\t%s\n" % (time(), "--- End ---"))
 
    def record_stamp(self):
 
        'Record the current submaster levels, continue this loop'
 
        levels = []
 
        for n, v in self.scalelevels.items():
 
            lev = v.get()
 
            if lev:
 
                levels.append('%s\t%s' % (n, lev))
 

	
 

	
 
        newdata = '\t'.join(levels) 
 
        if newdata!=self.lastline:
 
            template = "%s:\t%s\n" % (time(), newdata)
 
            self.rec_file.write(template)
 
            self.lastline = newdata
 
        self.master.after(100, self.record_stamp)
 
    def highlight_sub(self, name, color):
 
        self.subediting.colorsub(name, color)
light8/rsn.py
Show inline comments
 
@@ -10,12 +10,17 @@ import io
 
from uihelpers import *
 
from panels import *
 
from Xfader import *
 
from subediting import Subediting
 
from Fader import Fader
 
import stage
 
from Lightboard import Lightboard
 

	
 
import Subs, Patch
 

	
 

	
 

	
 
if len(sys.argv) >= 2:
 
    DUMMY = 0
 
    print "This is the real thing, baby"
 
    window_title = "Light 8.8 (On Air)"
 
else:
 
@@ -25,204 +30,15 @@ else:
 

	
 
root = Tk()
 
root.wm_title(window_title)
 
root.wm_geometry('+462+470')
 
root.tk_focusFollowsMouse()
 

	
 
import Subs, Patch
 

	
 
def get_data(*args):
 
    Subs.reload_data(DUMMY)
 
    Patch.reload_data(DUMMY)
 
    print "Patch:", Patch.patch
 
    print "Subs:", ', '.join(Subs.subs.keys())
 

	
 
get_data()
 

	
 
parportdmx = io.ParportDMX(DUMMY)
 

	
 
class Lightboard:
 
    def __init__(self, master):
 
        self.master = master
 

	
 
        self.channel_levels = []
 
        self.scalelevels = {}
 
        self.xfader = Xfader(self.scalelevels) # doesn't draw any UI yet-- look for self.xfader.setupwidget()
 
        self.oldlevels = [None] * 68 # never replace this; just clear it
 
        self.subediting = Subediting(currentoutputlevels=self.oldlevels)
 

	
 
        self.buildinterface()
 
        self.load()
 
        self.backgroundloop()
 
        
 
    def buildinterface(self):
 
        global DUMMY
 
        for w in self.master.winfo_children():
 
            w.destroy()
 

	
 
        stage_tl = toplevelat(22,30)
 
        s = stage.Stage(stage_tl)
 
        stage.createlights(s)
 
        s.setsubediting(self.subediting)
 
        s.pack()
 

	
 
        sub_tl = toplevelat(0,0)
 
        effect_tl = toplevelat(462,4)
 

	
 
        self.subpanels = Subpanels(sub_tl, effect_tl, self, self.scalelevels,
 
                                   Subs, self.xfader, self.changelevel,
 
                                   self.subediting, Subs.longestsubname())
 

	
 
        leveldisplay_tl = toplevelat(873,400)
 
        leveldisplay_tl.bind('<Escape>', sys.exit)
 

	
 
        self.leveldisplay = Leveldisplay(leveldisplay_tl, self.channel_levels)
 
        for i in range(0,len(self.channel_levels)):
 
            self.channel_levels[i].config(text=self.oldlevels[i])
 
            colorlabel(self.channel_levels[i])
 

	
 
        Console(self)
 

	
 
        # root frame
 
        controlpanel = Controlpanel(root, self.xfader, self.refresh, self.quit)
 
        
 
        xf=Frame(root)
 
        xf.pack(side='right')
 

	
 
        root.bind('<q>', self.quit)
 
        root.bind('<r>', self.refresh)
 
        leveldisplay_tl.bind('<q>', self.quit)
 
        leveldisplay_tl.bind('<r>', self.refresh)
 

	
 
        self.xfader.setupwidget(xf)
 
        controlpanel.pack()
 

	
 
        cuefader_tl = toplevelat(78, 480)
 
        cuefader = Fader(cuefader_tl, Subs.cues, self.scalelevels)
 
        cuefader.pack()
 

	
 
    def refresh(self, *args):
 
        'rebuild interface, reload data'
 
        get_data()
 
        self.subediting.refresh()
 
        self.buildinterface()
 
        bindkeys(root,'<Escape>', self.quit)
 

	
 
    def stageassub(self):
 
        """returns the current onstage lighting as a levels
 
        dictionary, skipping the zeros, and using names where
 
        possible"""
 
        levs=self.oldlevels
 
        
 
        return dict([(Patch.get_channel_name(i),l) for i,l
 
                     in zip(range(1,len(levs)+1),levs)
 
                     if l>0])
 
    def save_sub(self, name, levels):
 
        if not name:
 
            print "Enter sub name in console."
 
            return
 

	
 
        st = ''
 
        linebuf = 'subs["%s"] = {' % name
 
        for channame,lev in levels.items():
 
            if len(linebuf) > 60: 
 
                st += linebuf + '\n   '
 
                linebuf = ''
 

	
 
            linebuf += ' "%s" : %d,' % (channame, lev)
 
        st += linebuf + '}\n'
 
        if DUMMY:
 
            filename = 'ConfigDummy.py'
 
        else:
 
            filename = 'Config.py'
 
        f = open(filename, 'a')
 
        f.write(st)
 
        f.close()
 
        print 'Added sub:', st
 
        self.refresh()
 

	
 
    # this is called on a loop, and ALSO by the Scales
 
    def changelevel(self, *args):
 
        'Amp trims slider'
 

	
 
        levels = [0] * 68
 
        for name, s in Subs.subs.items():
 
            newlevels = s.get_levels(level=self.scalelevels[name].get())
 
            for (ch, fadelev) in newlevels.items():
 
                levels[ch-1] = max(levels[ch-1], fadelev)
 

	
 
        levels = [int(l) for l in levels]
 

	
 
        for lev,lab,oldlev,numlab in zip(levels, self.channel_levels, 
 
                                         self.oldlevels, 
 
                                         self.leveldisplay.number_labels):
 
            if lev != oldlev:
 
                lab.config(text="%d" % lev)
 
                colorlabel(lab)
 
                if lev < oldlev:
 
                    numlab['bg'] = 'blue'
 
                else:
 
                    numlab['bg'] = 'red'
 
            else:
 
                numlab['bg'] = 'lightPink'
 

	
 
        self.oldlevels[:] = levels[:] # replace the elements in oldlevels - don't make a new list (Subediting is watching it)
 
            
 
        parportdmx.sendlevels(levels)
 

	
 
    def load(self):
 
        try:
 
            filename = '/tmp/light9.prefs'
 
            if DUMMY:
 
                filename += '.dummy'
 
            print "Loading from", filename
 
            file = open(filename, 'r')
 
            p = cPickle.load(file)
 
            for s, v in p.scalelevels.items():
 
                try:
 
                    self.scalelevels[s].set(v)
 
                except:
 
                    print "Couldn't set %s -> %s" % (s, v)
 
            for name, substate in p.substate.items():
 
                try:
 
                    Subs.subs[name].set_state(substate)
 
                except:
 
                    print "Couldn't set sub %s state" % name
 
        except IOError:
 
            print "IOError: Couldn't load prefs (%s)" % filename
 
        except EOFError:
 
            print "EOFrror: Couldn't load prefs (%s)" % filename
 
        except:
 
            print "BigTrouble: Couldn't load prefs (%s)" % filename
 

	
 
    def backgroundloop(self, *args):
 
        self.master.after(50, self.backgroundloop, ())
 
        self.changelevel()
 
    def quit(self, *args):
 
        self.save()
 
        root.destroy()
 
        sys.exit()
 
    def save(self, *args):
 
        filename = '/tmp/light9.prefs'
 
        if DUMMY:
 
            filename += '.dummy'
 
        print "Saving to", filename
 
        file = open(filename, 'w')
 
        try:
 
            cPickle.dump(Pickles(self.scalelevels, Subs.subs.items()), file)
 
        except cPickle.UnpickleableError:
 
            print "UnpickleableError!  There's yer problem."
 

	
 
class Pickles:
 
    def __init__(self, scalelevels, subs=None):
 
        self.scalelevels = dict([(name, lev.get()) 
 
            for name, lev in scalelevels.items()])
 
        self.substate = dict([(name, subobj.get_state())
 
            for name, subobj in subs])
 
        # print "substate", self.substate
 

	
 
mr_lightboard = Lightboard(root)
 
mr_lightboard = Lightboard(root,parportdmx,DUMMY)
 

	
 
signal(SIGINT, mr_lightboard.quit)
 
bindkeys(root,'<Escape>', mr_lightboard.quit)
 

	
 
root.mainloop() # Receiver switches main
0 comments (0 inline, 0 general)