Piorjade

fsExpose (CC Filebrowser) [original]

Oct 21st, 2016
294
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 18.66 KB | None | 0 0
  1. --[[
  2.                                             fsExpose (name by supernicejohn - he is indeed nice)
  3.  
  4.   Made by Piorjade
  5.               [INFO(offtopic):]
  6.  
  7.   I did this while I couldn't access my home computer on my MacBook,
  8.   so I had many problems with the keyboard and maybe there are some
  9.   awkward spaces between functions here :P
  10.  
  11.               [INFO(on this program):]
  12.  
  13.   -Right window shows every file in the current directory
  14.  
  15.   -Left window shows the code of a file (click on a file in the right window)
  16.  
  17.   -Right click to:
  18.     -create a new folder
  19.     -create a new file
  20.     -delete a file/folder
  21.     -copy a file/a whole folder
  22.     -paste a file/a whole folder with the stuff inside
  23.     -edit a file
  24.  
  25.   -To add in new entries in the right-click menu OR edited the way an entry works:
  26.     -scroll down a bit to the function drawRightClick(), to add in new entries, or edit the text
  27.     -scroll down to _set = {}, and edit the functions or add new ones
  28.     -scroll down to drawScreen(), and down to the comment "Commands" and edit how to handle the return of drawRightClick()
  29.      (or just go to line 571, for the lazy peeps out there)
  30.  
  31.   -Click on the searchbar to get to a path directly
  32.  
  33.   -Click on the left arrow to go back to the folder you were before
  34.  
  35.   -Click on the right arrow to go to the folder, where you were, before you pressed back (much like a normal filebrowser, basically)
  36.  
  37.               [Current state:]
  38.  
  39.   -Works quite decent.
  40.  
  41.   -No bugs currently found.
  42.  
  43.   -(Yes, cat does not go to the next line if a line in a file is too long to display, this is intended (mainly to prevent glitches I had))
  44.  
  45.   -I'm going to implement this (with edited commands) into doorX, after I tested it out with the resizeable windows (I didn't test the resize handle yet)
  46. ]]--
  47.  
  48.  
  49.  
  50. --[[
  51.     listDir(Path[, true (if you want to get the full path)])
  52.  
  53.   lists the whole tree in the given directory
  54.  
  55.   Example:
  56.   listDir("/rom", true)
  57.  
  58.   Returns:
  59.  
  60.   /rom/autorun
  61.   /rom/programs/shell
  62.   /rom/programs/ls
  63.   [...]
  64. ]]
  65.  
  66. local function listDir(path, withP)
  67.   local t = {}
  68.   local files = fs.list(path)
  69.   for _, a in ipairs(files) do
  70.     if fs.isDir(path.."/"..a) then
  71.       local tab = listDir(path.."/"..a, true)
  72.       for _, a in ipairs(tab) do
  73.         table.insert(t, a)
  74.       end
  75.     else
  76.     if withP then
  77.         table.insert(t, path.."/"..a)
  78.     else
  79.         table.insert(t, a)
  80.     end
  81.     end
  82.   end
  83.   return t
  84. end
  85.  
  86. --[[
  87.     Preset settings!
  88.  
  89.     If you want to (e.g. open a file with a different command)
  90.     edit these commands, set the commands :)
  91. ]]
  92.  
  93. _set = {}
  94. function _set.open(f)
  95.   --runs the file or opens the folder
  96.   if not fs.isDir(f) then
  97. return shell.run(f)
  98.   else
  99. table.insert(_history, currentPath)
  100. currentPath = f
  101.   end
  102. end
  103. function _set.copy(f)
  104.   --inserts the file as fdat[FILENAME] = [TEXT]
  105.   fdat = {}
  106.   if fs.isDir(f) then
  107.     nf = string.sub(f, #currentPath)
  108.     _filename = nf
  109.     for _, a in ipairs(listDir(f, true)) do
  110.       local _, file = pcall(fs.open, a, "r")
  111.       if _ then
  112.         local inhalt = file.readAll()
  113.         file.close()
  114.           if string.find(a, currentPath, 1, #currentPath) then
  115.             a = string.sub(a, #currentPath)
  116.         end
  117.         fdat[a] = inhalt
  118.       else
  119.         printErr(file)
  120.       end
  121.     end
  122.   else
  123.     local file = fs.open(f, "r")
  124.     local inhalt = file.readAll()
  125.     file.close()
  126.     f = string.sub(f, #currentPath)
  127.     fdat[f] = inhalt
  128.   end
  129. end
  130.  
  131. function _set.delete(f)
  132.   --deletes the file
  133.   local _, ok, err = pcall(fs.delete, f)
  134.   if not _ then
  135.     printErr(ok)
  136.   end
  137. end
  138.  
  139.  
  140. function _set.edit(f)
  141.   --edit a file with 'edit <path>'
  142.   shell.run("edit", f)
  143. end
  144.  
  145.  
  146. function _set.paste(f)
  147.   --paste everything in the fdat table to the current directory
  148.  
  149.     if #_filename > 0 then
  150.         if string.find(_filename, "/", 1, 1) then _filename = string.sub(_filename, 2) end
  151.         local c = 1
  152.         local str = _filename
  153.         repeat
  154.             if fs.exists(currentPath..str) then
  155.                 str = _filename.."-"..tostring(c)
  156.                 c = c+1
  157.             else
  158.                 break
  159.             end
  160.         until not fs.exists(currentPath..str)
  161.         oldfname = _filename
  162.         _filename = str
  163.         fs.makeDir(currentPath.._filename)
  164.         table.insert(_history, currentPath)
  165.         currentPath = currentPath.._filename
  166.     end
  167.     for a, b in pairs(fdat) do
  168.         local c = 1
  169.         local str = a
  170.         if string.find(str, oldfname, 1, #oldfname) then
  171.             str = string.sub(str, #oldfname+2)
  172.         end
  173.         repeat
  174.             if fs.exists(currentPath..str) then
  175.                 str = a.."-"..tostring(c)
  176.                 c = c+1
  177.             end
  178.         until not fs.exists(currentPath..str)
  179.         a = str
  180.         local _, file = pcall(fs.open, currentPath..a, "w")
  181.         if _ then
  182.             file.write(b)
  183.             file.close()
  184.         else
  185.             printErr(file)
  186.         end
  187.     end
  188.   fdat = {}
  189.   oldfname = ""
  190.   _filename = ""
  191. end
  192.  
  193. local function clear(bg, fg)
  194.   --[[
  195.     This function clears the screen in the
  196.     desired Background (bg) and the desired
  197.     Foreground (fg) color. (This is very very
  198.     simple :D)
  199.   ]]--
  200.   term.setCursorPos(1,1)
  201.   term.setBackgroundColor(bg)
  202.   term.setTextColor(fg)
  203.   term.clear()
  204. end
  205.  
  206. --[[
  207.     Here is the right-click menu
  208.  
  209.     If you want to add stuff,
  210.     then write your new entry
  211.     and increase "height".
  212. ]]
  213.  
  214.  
  215. function drawRightClick(x, y)
  216.   --increase (or decrease) this variable to make the menu smaller/bigger (mainly for removing/adding entries)
  217.   local height = 7
  218.   local w = window.create(oldTerm, x, y, 15, height)
  219.   term.redirect(w)
  220.   clear(colors.white, colors.black)
  221.   term.write("     Open     ")
  222.   term.setCursorPos(1, 2)
  223.   term.setTextColor(colors.blue)
  224.   term.write("     Copy     ")
  225.   term.setCursorPos(1, 3)
  226.   term.setTextColor(colors.red)
  227.   term.write("    Delete    ")
  228.   term.setCursorPos(1, 4)
  229.   term.setTextColor(colors.lime)
  230.   term.write("     Paste    ")
  231.   term.setCursorPos(1, 5)
  232.   term.setTextColor(colors.black)
  233.   term.write("  New Folder  ")
  234.   term.setCursorPos(1, 6)
  235.   term.write("   New File   ")
  236.   term.setCursorPos(1, 7)
  237.   term.write("     Edit     ")
  238.   --[[
  239.             EXAMPLE NEW ENTRY:
  240.  
  241.   term.setCursorPos(1,8)
  242.   term.setTextColor(colors.yellow)
  243.   term.write("Secret function")
  244.   ]]
  245.   term.redirect(oldTerm)
  246.   while true do
  247.     local _, button, a, b = os.pullEvent("mouse_click")
  248.     if _ == "mouse_click" and button == 1 and a >= x and a <= x+15-1 and b >= y and b <= y+6 then
  249.       if b == y then
  250.         return "open"
  251.       elseif b == y+1 then
  252.         return "copy"
  253.       elseif b == y+2 then
  254.         return "delete"
  255.       elseif b == y+3 then
  256.         return "paste"
  257.       elseif b == y+4 then
  258.         return "newfolder"
  259.       elseif b == y+5 then
  260.         return "newfile"
  261.       elseif b == y+6 then
  262.         return "edit"
  263.       end
  264.       --[[
  265.                 CONTINUEATION OF THE EXAMPLE
  266.       elseif b == y+7 then
  267.         return "secretfunction"
  268.       ]]
  269.     elseif _ == "mouse_click" then
  270.       w.setVisible(false)
  271.       term.redirect(oldTerm)
  272.       w = nil
  273.       break
  274.     end
  275.   end
  276. end
  277.  
  278.  
  279.  
  280.  
  281.  
  282.  
  283. --variables
  284. _ver = 2.1
  285. _name = "doorX file manager"
  286. currentPath = "/" --self explaining
  287. selected = "" --stores the path of the selected file
  288. _cFiles = {} --stores the files in the current directory
  289. _history = {} --Stores every path the user is heading
  290. _last = nil --Stores the path that the user was in, before he pressed back
  291. _filename = "" --stores the original filename
  292. _fScroll = 0 --stores the number of times _files did scroll
  293. _cScroll = 0 --stores the number of times _tree did scroll
  294. _cMax = 0 --stores the maximum lines of cat
  295. _cX, _cY = 0, 0 --stores the size of the cat window
  296. oldfname = "" --saving purposes
  297. fdat = {} --stores all the listed files and their data
  298. catTab = {} --stores the cat'ed file in a table (one entry per line)
  299. maxFiles = 0
  300. missingFiles = 0
  301. leftFiles = 0
  302.  
  303.  
  304. --functions
  305.  
  306.  
  307. local function drawButton(text)
  308.   --[[
  309.     Function to draw a blue button with
  310.     the given text.
  311.     (This is basically to prevent from me
  312.     writing that code over and over again)
  313.   ]]--
  314.   local c = term.getBackgroundColor()
  315.   term.setBackgroundColor(colors.blue)
  316.   term.write(text)
  317.   term.setBackgroundColor(c)
  318. end
  319.  
  320.  
  321. local function redrawScreens()
  322.   local oMaxFiles = maxFiles
  323.   --[[
  324.     Put every file/folder in _cFiles and then
  325.     print them to the right panel and additionally
  326.     every folder to the left panel
  327.   ]]--
  328.   clear(colors.lightGray, colors.white)
  329.   local oldX, oldY = _files.getCursorPos()
  330.   _cFiles = {}
  331.   maxFiles = 0
  332.   missingFiles = 0
  333.   leftFiles = 0
  334.   --list files and clear screen
  335.   _cFiles = fs.list(currentPath)
  336.   term.redirect(_files)
  337.   clear(colors.gray, colors.lime)
  338.   _tree.redraw()
  339.   --print files and folders
  340.   for _, a in ipairs(_cFiles) do
  341.     if fs.isDir(currentPath..a) then
  342.       term.setTextColor(colors.blue)
  343.     end
  344.     term.write(a)
  345.     term.setTextColor(colors.lime)
  346.     local cx, cy = term.getCursorPos()
  347.     term.setCursorPos(1, cy+1)
  348.   end
  349.   _files.setCursorPos(1, 1)
  350.   --store new variables
  351.   term.redirect(oldTerm)
  352.   maxFiles = #_cFiles
  353.   local x, y = _tree.getSize()
  354.   x, y = _files.getSize()
  355.   leftFiles = maxFiles-y
  356.   if leftFiles < 0 then leftFiles = 0 end
  357.   term.redirect(searchBar)
  358.   clear(colors.gray, colors.lime)
  359.   if not string.find(currentPath, "/", 1, 1) then
  360.     currentPath = "/"..currentPath
  361.   end
  362.   if not string.find(currentPath, "/", #currentPath, #currentPath) then currentPath = currentPath.."/" end
  363.   term.write(currentPath)
  364.   local cScroll = 0
  365.   if _fScroll > cScroll and maxFiles >= oMaxFiles then
  366.     repeat
  367.       _files.scroll(1)
  368.       _files.setCursorPos(1, y)
  369.       _files.setTextColor(colors.lime)
  370.       if fs.isDir(currentPath.._cFiles[missingFiles+y+1]) then
  371.         _files.setTextColor(colors.blue)
  372.       end
  373.       _files.write(_cFiles[missingFiles+y+1])
  374.       missingFiles = missingFiles+1
  375.       leftFiles = leftFiles-1
  376.       cScroll = cScroll+1
  377.     until cScroll == _fScroll
  378.   else
  379.     _fScroll = 0
  380.   end
  381.   if leftFiles < 0 then leftFiles = 0 end
  382. end
  383.  
  384.  
  385.  
  386. local function rewriteCat()
  387. _cScroll = 0
  388. _cMax = 0
  389. catTab = {}
  390. _cX, _cY = _tree.getSize()
  391.   if #selected > 0 then
  392.     term.redirect(_tree)
  393.     clear(colors.gray, colors.lime)
  394.     local file, err = fs.open(selected, "r")
  395.     if not file then
  396.       printErr(err)
  397.       return
  398.     end
  399.     repeat
  400.       local str = file.readLine()
  401.       if str then
  402.         table.insert(catTab, str)
  403.         _cMax = _cMax+1
  404.       end
  405.     until str == nil
  406.     file.close()
  407.     term.setCursorPos(1,1)
  408.     clear(colors.gray, colors.lime)
  409.  
  410.     for _, a in ipairs(catTab) do
  411.       term.write(a)
  412.       local x, y = term.getCursorPos()
  413.       term.setCursorPos(1, y+1)
  414.     end
  415.     term.setCursorPos(1,1)
  416.     term.redirect(oldTerm)
  417.   end
  418. end
  419.  
  420. local function drawScreen()
  421.   --[[
  422.     This is basically the main function that
  423.     controls everything.
  424.   ]]--
  425.   clear(colors.lightBlue, colors.black)
  426.   --create the goback- and goforthbutton + searchbar
  427.   local maxX, maxY = term.getSize()
  428.   sBar = window.create(oldTerm, 1, 1, tonumber(maxX), 1)
  429.   term.redirect(sBar)
  430.   --draw the back and forth buttons
  431.   clear(colors.lightGray, colors.black)
  432.   term.setCursorPos(1,1)
  433.   drawButton("<")
  434.   term.write(" ")
  435.   drawButton(">")
  436.   term.write(" ")
  437.   --draw the searchbar, as a new window (and leave the last pixel again)
  438.   searchBar = window.create(sBar, 5, 1, maxX-5, 1)
  439.   term.redirect(searchBar)
  440.   clear(colors.gray, colors.lime)
  441.   term.write("Enter a path.")
  442.   --draw the left panel, to see the folders in the current path (to move to the folders)
  443.   term.redirect(oldTerm)
  444.   leftPanel = window.create(oldTerm, 1, 2, maxX/2-7, maxY-1)
  445.   term.redirect(leftPanel)
  446.   clear(colors.lightGray, colors.black)
  447.   local lx, ly = term.getSize()
  448.   _tree = window.create(leftPanel, 2, 2, lx-2, ly-2)
  449.   term.redirect(_tree)
  450.   clear(colors.gray, colors.lime)
  451.   term.redirect(oldTerm)
  452.   --draw the right panel, to manage files
  453.   rightPanel = window.create(oldTerm, lx+1, 2, maxX-lx+1, maxY-1)
  454.   term.redirect(rightPanel)
  455.   clear(colors.lightGray, colors.black)
  456.   local rx, ry = term.getSize()
  457.   _files = window.create(rightPanel, 2, 2, rx-2, ry-2)
  458.   term.redirect(_files)
  459.   clear(colors.gray, colors.lime)
  460.  
  461.   --start the main loop, managing everything
  462.   while true do
  463.     term.redirect(oldTerm)
  464.     redrawScreens()
  465.     local _, button, x, y = os.pullEvent()
  466.  
  467.     if _ == "term_resize" then
  468.       oMaxX, oMaxY = term.getSize()
  469.       --resize every window again
  470.       --complicated math, kappa
  471.       local x, y = term.getSize()
  472.       sBar.reposition(1, 1, x, 1)
  473.       sBar.redraw()
  474.       term.redirect(sBar)
  475.       x = term.getSize()
  476.       searchBar.reposition(5, 1, x-5, 1)
  477.       searchBar.redraw()
  478.       term.redirect(leftPanel)
  479.       leftPanel.reposition(1, 2, x/2-7, y-1)
  480.       leftPanel.redraw()
  481.       _tree.reposition(2, 2, x/2-9, y-3)
  482.       _tree.redraw()
  483.       term.redirect(rightPanel)
  484.       rightPanel.reposition((x/2-7)+1, 2, x-(x/2-7)+1, y-1)
  485.       rightPanel.redraw()
  486.       _files.reposition(2, 2, x-(x/2-7)-2, y-3)
  487.       _files.redraw()
  488.       term.redirect(oldTerm)
  489.     elseif _ == "mouse_click" and button == 1 and x >= 5 and x < oMaxX and y == 1 then
  490.       term.redirect(searchBar)
  491.       clear(colors.gray, colors.lime)
  492.       local oprint = print
  493.       _G.print = function(t)
  494.         return term.write(t)
  495.       end
  496.       local e = read()
  497.       _G.print = oprint
  498.       if fs.exists(e) and fs.isDir(e) then
  499.         table.insert(_history, currentPath)
  500.  
  501.         currentPath = e
  502.         term.redirect(oldTerm)
  503.       elseif e == "exit" then
  504.         term.redirect(oldTerm)
  505.         term.setCursorPos(1,1)
  506.         term.setBackgroundColor(colors.black)
  507.         term.setTextColor(colors.white)
  508.         term.clear()
  509.         break
  510.       else
  511.         clear(colors.gray, colors.red)
  512.         term.write("Invalid path.")
  513.         term.redirect(oldTerm)
  514.         sleep(1)
  515.       end
  516.     elseif _ == "mouse_click" and button == 1 and x == 1 and y == 1 then
  517.     if #_history > 0 then
  518.         _last = currentPath
  519.         currentPath = _history[#_history]
  520.         table.remove(_history, #_history)
  521.     end
  522.     elseif _ == "mouse_click" and button == 1 and x == 3 and y == 1 then
  523.     if _last ~= nil then
  524.         table.insert(_history, currentPath)
  525.         currentPath = _last
  526.         _last = nil
  527.     end
  528.     elseif _ == "mouse_scroll" and button == 1 and x >= 2 and x <= oMaxX/2-8 and y >= 3 and y <= oMaxY-2 and _cScroll+_cY <= _cMax then
  529.       _tree.scroll(1)
  530.       _tree.setCursorPos(1, _cY)
  531.       _tree.write(tostring(catTab[_cScroll+_cY-2]))
  532.       _cScroll = _cScroll+1
  533.     elseif _ == "mouse_scroll" and button == -1 and x >= 2 and x <= oMaxX/2-8 and y >= 3 and y <= oMaxY-2 and _cScroll > 0 then
  534.       _tree.scroll(-1)
  535.       _tree.setCursorPos(1, 1)
  536.       _tree.write(tostring(catTab[_cScroll]))
  537.       _cScroll = _cScroll-1
  538.     elseif _ == "mouse_scroll" and button == -1 and x >= (oMaxX/2-8)+2 and x <= oMaxX-2 and y >= 3 and y <= oMaxY-1 and missingFiles > 0 then
  539.       _files.scroll(-1)
  540.       local x, y = _files.getSize()
  541.       _files.setCursorPos(1, y)
  542.       if fs.isDir(currentPath.._cFiles[missingFiles]) then
  543.         _files.setTextColor(colors.blue)
  544.       end
  545.       _files.write(_cFiles[missingFiles])
  546.       _files.setTextColor(colors.lime)
  547.       missingFiles = missingFiles-1
  548.       leftFiles = leftFiles+1
  549.       _fScroll = _fScroll-1
  550.     elseif _ == "mouse_scroll" and button == 1 and x >= (oMaxX/2-8)+2 and x <= oMaxX-2 and y >= 3 and y <= oMaxY-1 and leftFiles > 0 then
  551.       _files.scroll(1)
  552.       local x, y = _files.getSize()
  553.       _files.setCursorPos(1, y)
  554.       if fs.isDir(currentPath.._cFiles[missingFiles+y+1]) then
  555.         _files.setTextColor(colors.blue)
  556.       end
  557.       _files.write(_cFiles[missingFiles+y+1])
  558.       _files.setTextColor(colors.lime)
  559.       missingFiles = missingFiles+1
  560.       leftFiles = leftFiles-1
  561.       _fScroll = _fScroll+1
  562.     elseif _ == "mouse_click" and button == 1 and x >= (oMaxX/2-8)+2 and x <= oMaxX-2 and y >= 3 and y <= oMaxY-1 then
  563.       local realY = y
  564.       local y = y-2
  565.       if _cFiles[missingFiles+y] then
  566.         selected = currentPath.._cFiles[missingFiles+y]
  567.         rewriteCat()
  568.       end
  569.     elseif _ == "mouse_click" and button == 2 and x >= (oMaxX/2-8)+2 and x <= oMaxX-2 and y >= 3 and y <= oMaxY-1 then
  570.  
  571. --[[
  572.     Commands
  573.  
  574.     If you added new entries,
  575.     Write your way of handling the click.
  576. ]]
  577.  
  578.  
  579.       local realY = y
  580.       local y = y-2
  581.       local invert = false
  582.       if 19-realY < 7 then invert = true end
  583.       local command = ""
  584.       if not invert then
  585.         command = drawRightClick(x, realY, false)
  586.       else
  587.         command = drawRightClick(x, 12, false)
  588.       end
  589.  
  590.       if command == "open" and _cFiles[missingFiles+y] then
  591.         _set.open(currentPath.._cFiles[missingFiles+y])
  592.  
  593.       elseif command == "copy" and _cFiles[missingFiles+y] then
  594.         fileName = _cFiles[missingFiles+y]
  595.         local byteinhalt = _set.copy(currentPath.._cFiles[missingFiles+y])
  596.           fileData = byteinhalt
  597.       elseif command == "delete" and _cFiles[missingFiles+y] then
  598.         _set.delete(currentPath.._cFiles[missingFiles+y])
  599.       elseif command == "newfolder" then
  600.         term.redirect(searchBar)
  601.         clear(colors.gray, colors.lime)
  602.         term.write(currentPath)
  603.         local e = read()
  604.         if string.find(e, "%.%.") then
  605.           printErr("Invalid path.")
  606.           sleep(2)
  607.         elseif string.find(e, "//") then
  608.           printErr("Invalid path.")
  609.           sleep(2)
  610.         elseif fs.exists(currentPath..e) then
  611.           printErr("Already exists.")
  612.           sleep(2)
  613.         else
  614.           fs.makeDir(currentPath..e)
  615.         end
  616.         term.redirect(oldTerm)
  617.       elseif command == "paste" then
  618.         _set.paste()
  619.       elseif command == "edit" and _cFiles[missingFiles+y] and not fs.isDir(_cFiles[missingFiles+y]) then
  620.         _set.edit(currentPath.._cFiles[missingFiles+y])
  621.       elseif command == "newfile" then
  622.         term.redirect(searchBar)
  623.         clear(colors.gray, colors.lime)
  624.         term.write(currentPath)
  625.         local oprint = _G.print
  626.         _G.print = function(f)
  627.           term.write(f)
  628.         end
  629.         _G.print = oprint
  630.         local e = read()
  631.         if string.find(e, "%.%.") then
  632.           printErr("Invalid path.")
  633.           sleep(2)
  634.         elseif string.find(e, "//") then
  635.           printErr("Invalid path.")
  636.           sleep(2)
  637.         elseif fs.exists(currentPath..e) then
  638.           printErr("Already exists.")
  639.           sleep(2)
  640.         else
  641.           local ff = fs.open(currentPath..e, "w")
  642.           ff.close()
  643.         end
  644.       end
  645.     end
  646.   end
  647. end
  648.  
  649.  
  650. --code
  651. --catch the original terminal
  652. oldTerm = term.current()
  653. oMaxX, oMaxY = term.getSize()
  654. function printErr(str)
  655.   term.redirect(searchBar)
  656.   clear(colors.gray, colors.red)
  657.   term.write(str)
  658.   term.redirect(oldTerm)
  659. end
  660. drawScreen()
Add Comment
Please, Sign In to add comment