You are not logged in.
Pages: 1
I'm Misko from Linux Lite and kind of on probation with TRIOS Linux.
Since this is my first time on the forum I'll share a small gift.
Here is a small py script for you, folks from around the world, to be used as a thunar custom action for audio files, and to modify as you whish.
It opens a GTK3 window to choose an option on selected files. Some attempt to organize my right click menu and make it easier to add custom actions on a new install. An idea I haven't worked much with.
I've added some commands to play and add audio files to playlist and in case an application is installed icon will show up in the list. Audacious, Clementine, SmPlayer, QMMP and VL all to play audio files. Sound converter to convert audio files and Thunar's bulk rename to rename mp3's. As an extra, slider to adjusts volume level, because you would probably want to do that when playing music. Hope it will be in useful. Cheers
# Thunar custom actions launcher for the audio files
# Milos Pavlovic 2015 <mpsrbija@gmail.com>
#
# Save this file to /usr/local/bin/zoose-audio.py or at your whish just remember to change the path in command
# Setting thunar custom action:
# Name: Audio options
# Description : Play Audio Files
# Command: python3 /usr/local/bin/zoose-audio.py "%F"
#
# File Pattern: *
# Appearance: Directories, Audio files
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# Feel free to add/change commands here
zoose_icons=[
["name","icon","command"],
["Play with Audacious","audacious",'audacious'],
["Enqueue in Audacious","audacious",'audacious -e'],
["Play with Clementine","clementine",'clementine -l'],
["Enqueue in Clementine","clementine",'clementine -a'],
["Play with SMPlayer","smplayer",'smplayer'],
["Enqueue in SMPlayer","smplayer",'smplayer -add-to-playlist'],
["Play with VLC","vlc",'vlc'],
["Play with Qmmp","qmmp",'qmmp'],
["Enqueue in Qmmp","qmmp",'qmmp -e'],
["Convert","soundconverter",'/usr/bin/soundconverter'],
["Bulk Rename (Thunar)","thunar",'/usr/bin/thunar --bulk-rename'],
]
import os
import sys
import string
import subprocess
import shlex
from gi.repository import Gtk as gtk
from gi.repository.GdkPixbuf import Pixbuf
# Which to identify wheather the program exists
def which(program):
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
class Zoose:
def destroy(self, widget, data=None):
# zoose mode
gtk.main_quit()
def action(self, widget, event, data=None):
if event.button==1:
pathinfo=widget.get_path_at_pos(int(event.x),int(event.y))
if pathinfo==None: return False
pathnr=pathinfo[0]
comm=" %s" % str(sys.argv[1])
execc="%s" % self.liststore[pathnr][2]
command=self.liststore[pathnr][2] + comm
if command=='':
gtk.main_quit()
subprocess.Popen(shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
gtk.main_quit()
return True
return False
def create_label(self):
label = gtk.Label()
label.set_markup("Select the action you whish to perform")
label.set_line_wrap(True)
label.set_size_request(740, -1)
self.grid.attach(label, 0, 0, 2, 1)
def create_icons(self):
# we make the icons
self.liststore=gtk.ListStore(Pixbuf,str,str)
self.iv=gtk.IconView(self.liststore)
self.iv.set_pixbuf_column(0)
self.iv.set_text_column(1)
#self.iv.set_columns(3)
self.iv.set_events(self.iv.get_events())
self.iv.connect("button-press-event", self.action)
sw = gtk.ScrolledWindow()
sw.set_size_request(-1, 250)
sw.add(self.iv)
self.grid.attach(sw, 0, 1, 2, 1)
it=gtk.IconTheme.get_default()
first=True
for line in zoose_icons:
if first:
first=False
continue
try:
if '/' in line[1]:
pixbuf=Pixbuf.new_from_file(line[1]) # if the icon is a path load it from file
else:
pixbuf=it.load_icon(line[1],48,0)
except:
pixbuf=it.load_icon('gtk-stop',48,0)
namen=(line[0])
execc=line[2]
chk = execc.split(' ')[0]
if chk == "gksudo":
chk = execc.split(' ')[1]
elif chk == "gksu":
chk = execc.split(' ')[1]
checking = which(chk)
chk = execc.split(' ')[0]
if chk == "gksudo":
chk = execc.split(' ')[1]
elif chk == "gksu":
chk = execc.split(' ')[1]
checking = which(chk)
if checking != None:
self.liststore.append([ pixbuf,namen,line[2] ])
def create_label_two(self):
# adjustment (initial value, min value, max value,
# step increment - press cursor keys to see!,
# page increment - click around the handle to see!,
# page size - not used here)
ad1 = gtk.Adjustment(0, 0, 100, 5, 10, 0)
cmd = "/usr/bin/amixer sget Master | grep '\[' "
result = os.popen(cmd)
result = result.readline()
find_start = result.find('[') + 1
find_end = result.find('%]', find_start)
audio_level = int(result[find_start:find_end])
ad1. set_value(audio_level)
# an horizontal scale
self.h_scale = gtk.Scale(
orientation=gtk.Orientation.HORIZONTAL, adjustment=ad1)
# of integers (no digits)
self.h_scale.set_digits(0)
# that can expand horizontally if there is space in the grid
self.h_scale.set_hexpand(True)
# that is aligned at the top of the space allowed in the grid
self.h_scale.set_valign(gtk.Align.START)
# we connect the signal "value-changed" emitted by the scale with the callback
# function scale_moved
self.h_scale.connect("value-changed", self.scale_moved)
self.grid.attach(self.h_scale, 1, 2, 1, 2)
# Let's set the voulume here
def scale_moved(self, event):
val = "{0}".format(int(self.h_scale.get_value()))
proc = subprocess.Popen('/usr/bin/amixer sset Master ' + str(val) + '% 2>&1 >/dev/null', shell=True, stdout=subprocess.PIPE)
proc.wait()
def __init__(self):
# create the window
window = gtk.Window()
self.window=window
window.set_border_width(10)
window.set_title('Audio File Actions')
window.connect("destroy", self.destroy)
window.set_border_width(5)
window.set_position(1)
window.set_resizable(False)
self.grid = gtk.Grid()
window.add(self.grid)
window.set_size_request(740, 300)
self.create_label()
self.create_icons()
self.create_label_two()
it=gtk.IconTheme.get_default()
try:
window.set_icon(it.load_icon(gtk.STOCK_HOME, 64, 0))
except:
pass
window.show_all()
def main(self):
# Cliche init
gtk.main()
# Go
if __name__ == "__main__":
app = Zoose()
app.main()
Do you want to exit the Circus?
https://www.youtube.com/watch?v=ZJwQicZHp_c
Offline
This looks really interesting - thanks for sharing.
A quick question: Is it possible to do this so that instead of creating a new window to display the options, that you popup a menu at the mouse cursor position that lists these options?
Please remember to mark your thread [SOLVED] to make it easier for others to find
--- How To Ask For Help | FAQ | Developer Wiki | Community | Contribute ---
Offline
A quick question: Is it possible to do this so that instead of creating a new window to display the options, that you popup a menu at the mouse cursor position that lists these options?
Yes, it is possible. Would have to create a GtkMenu and append Menu items instead of adding them to IconView. Then probably the best option to display the menu would be to create a submenu for the thunar to list those options.
I'm not sure would it be possible to create a scale and append it to the menu, so that volume could be set through the menu. That most likely wouldn't work.
Do you want to exit the Circus?
https://www.youtube.com/watch?v=ZJwQicZHp_c
Offline
Hey ToZ, I've tried to make it work as a menu.
The problem is that the program doesn't close when it is clicked outside of the menu.
In other words, when it looses the focus, the menu hides, but it's still running in the background.
I've made this "close" menu item as a kind of "workaround".
Also the name of the file must be zoose.py for some reason.
# Thunar custom actions launcher for the audio files
# Milos Pavlovic 2015 <mpsrbija@gmail.com>
#
# Save this file to /usr/local/bin/zoose.py
# Setting thunar custom action:
# Name: Audio options
# Description : Manipulate Audio Files
# Command: python3 /usr/local/bin/zoose.py "%F"
#
# File Pattern: *
# Appearance: Directories, Audio files
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
zoose_icons=[
["name","icon","command"],
["Audacious","audacious",'audacious'],
["Enqueue in Audacious","audacious",'audacious -e'],
["Clementine","clementine",'clementine -l'],
["Enqueue in Clementine","clementine",'clementine -a'],
["SMPlayer","smplayer",'smplayer'],
["Enqueue in SMPlayer","smplayer",'smplayer -add-to-playlist'],
["VLC","vlc",'vlc'],
["Qmmp","qmmp",'qmmp'],
["Enqueue in Qmmp","qmmp",'qmmp -e'],
["Convert","soundconverter",'/usr/bin/soundconverter'],
["Bulk Rename (Thunar)","file-manager",'/usr/bin/thunar --bulk-rename'],
["Close","gtk-close",'echo'],
]
import os
import sys
import string
import subprocess
import shlex
from gi.repository import Gtk as Gtk
from gi.repository.GdkPixbuf import Pixbuf
# Which
def which(program):
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
class Menu:
def destroy(self, widget, data=None):
# zoose mode
Gtk.main_quit()
def action(self, widget, event, x, data=None):
if len(sys.argv) > 1 and x != '':
comm=" {0}".format(str(sys.argv[1]))
command="{0}{1}". format(x, comm)
subprocess.Popen(shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
print("No file(s) passed. Exiting.")
Gtk.main_quit()
def __init__(self):
self.menu = Gtk.Menu()
self.menu.connect("destroy", self.destroy)
it=Gtk.IconTheme.get_default()
first=True
for line in zoose_icons:
if first:
first=False
continue
try:
if '/' in line[1]:
pixbuf=pixbuf_new_from_file(line[1])
else:
pixbuf=it.load_icon(line[1],24,0)
except:
pixbuf=it.load_icon('gtk-stop',24,0)
namen=(line[0])
execc=line[2]
chk = execc.split(' ')[0]
if chk == "gksudo":
chk = execc.split(' ')[1]
elif chk == "gksu":
chk = execc.split(' ')[1]
elif chk == "kdesu":
chk = execc.split(' ')[1]
x=execc
checking = which(chk)
if checking != None:
box = Gtk.Box()
box.set_spacing(10)
img = Gtk.Image()
img.set_from_pixbuf(pixbuf)
label = Gtk.Label(namen)
box.add(img)
box.add(label)
menuitem = Gtk.MenuItem()
menuitem.add(box)
menuitem.connect("button-press-event", self.action, x)
self.menu.append(menuitem)
self.menu.popup(None, None, None, None, 0, Gtk.get_current_event_time())
self.menu.show_all()
def main(self):
# Cliche init
Gtk.main()
# I swear zoose this better work
if __name__ == "__main__":
app = Menu()
app.main()
Any ideas on how to make it work better?
Do you want to exit the Circus?
https://www.youtube.com/watch?v=ZJwQicZHp_c
Offline
This is really interesting - thanks for sharing again.
There have been some requests to add the ability to group custom actions is sub-menus off of the main Thunar menu. At one time, we had the thunar actions plugin, but this has not been worked on for a number of years and no longer builds. The script you have provided here maybe a way to work around this limitation.
I'll try using it for a while to see how it works.
Please remember to mark your thread [SOLVED] to make it easier for others to find
--- How To Ask For Help | FAQ | Developer Wiki | Community | Contribute ---
Offline
Thanks for the idea.
This script may not be of much practical use but maybe it could be usefull as an idea for the devs.
Here is another idea for the xfdesktop icons. (Not sure if this is the right place to post ideas)
This is just a concept that runs xfconf-query command in the background. Doesn't work very well until the properties for all the icons are created.
Turning the switches on and off will create the properties and it should work after that. Gtk3+
#!/usr/bin/env python3
#xfce4-desktop icons switch, Misko, GPLv2
from gi.repository import Gtk, Gdk
import os
import sys
import shlex
import subprocess
from gi.repository.GdkPixbuf import Pixbuf
#Temporary window icon don't forget to change!
icon="/usr/share/trios-art/icons/t19-red.png"
def execute(command):
"""function to exec everything"""
p = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return p.stdout
class XfDesktopIconsWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Desktop Icons")
self.set_default_size(-1, 210)
self.set_border_width(10)
self.grid = Gtk.Grid()
self.grid.set_row_spacing(8)
self.grid.set_column_spacing(8)
self.add(self.grid)
self.label = Gtk.Label()
self.label.set_name('label')
self.label.set_markup("To show and hide desktop icons\nuse the switch next to the name")
self.label.set_line_wrap(True)
self.label.set_size_request(200, -1)
self.grid.attach(self.label, 1, 0, 2, 1)
self.label1 = Gtk.Label("File System", xalign=1)
self.label2 = Gtk.Label("Home", xalign=1)
self.label3 = Gtk.Label("Trash", xalign=1)
self.label4 = Gtk.Label("Removable Devices", xalign=1)
self.label5 = Gtk.Label("Network Shares", xalign=1)
self.label6 = Gtk.Label("Removable Drives", xalign=1)
self.label7 = Gtk.Label("Other Devices", xalign=1)
self.grid.attach(self.label1, 1, 1, 1, 1)
self.grid.attach(self.label2, 1, 2, 1, 1)
self.grid.attach(self.label3, 1, 3, 1, 1)
self.grid.attach(self.label4, 2, 4, 1, 1)
self.grid.attach(self.label5, 1, 6, 1, 1)
self.grid.attach(self.label6, 1, 7, 1, 1)
self.grid.attach(self.label7, 1, 8, 1, 1)
button1 = Gtk.Switch()
button1.connect("notify::active", self.on_switch_activated_filesystem)
cmd = 'xfconf-query --channel xfce4-desktop --property "/desktop-icons/file-icons/show-filesystem" | grep -c "true"'
result = os.popen(cmd)
result = result.readline()
if int(result) == 0:
button1.set_active(False)
elif int(result) == 1:
button1.set_active(True)
else:
button1.set_active(False)
self.grid.attach_next_to(button1, self.label1, Gtk.PositionType.RIGHT, 1, 1)
button2 = Gtk.Switch()
button2.connect("notify::active", self.on_switch_activatedb)
cmd = 'xfconf-query --channel xfce4-desktop --property "/desktop-icons/file-icons/show-home" | grep -c "true"'
if int(result) == 0:
button2.set_active(False)
else:
button2.set_active(True)
self.grid.attach_next_to(button2, self.label2, Gtk.PositionType.RIGHT, 1, 1)
button3= Gtk.Switch()
button3.connect("notify::active", self.on_switch_activatedc)
cmd = 'xfconf-query --channel xfce4-desktop --property "/desktop-icons/file-icons/show-trash" | grep -c "true"'
result = os.popen(cmd)
result = result.readline()
if int(result) == 0:
button3.set_active(False)
else:
button3.set_active(True)
self.grid.attach_next_to(button3, self.label3, Gtk.PositionType.RIGHT, 1, 1)
self.button4= Gtk.Switch()
self.button4.connect("notify::active", self.on_switch_activatede)
cmd = 'xfconf-query --channel xfce4-desktop -n --property "/desktop-icons/file-icons/show-removable" | grep -c "true"'
removable = os.popen(cmd)
removable = removable.readline()
if int(removable) == 0:
self.button4.set_active(False)
else:
self.button4.set_active(True)
self.grid.attach_next_to(self.button4, self.label4, Gtk.PositionType.LEFT, 1, 1)
self.button5 = Gtk.Switch()
self.button5.connect("notify::active", self.on_switch_activated_network)
cmd = 'xfconf-query --channel xfce4-desktop -n --property "/desktop-icons/file-icons/show-network-removable" | grep -c "true"'
result = os.popen(cmd)
result = result.readline()
if int(result) == 0:
self.button5.set_active(False)
else:
self.button5.set_active(True)
self.grid.attach_next_to(self.button5, self.label5, Gtk.PositionType.RIGHT, 1, 1)
if int(removable) == 0:
self.button5.set_sensitive(False)
self.button6= Gtk.Switch()
self.button6.connect("notify::active", self.on_switch_activatedf)
cmd = 'xfconf-query --channel xfce4-desktop -n --property "/desktop-icons/file-icons/show-device-removable" | grep -c "true"'
result = os.popen(cmd)
result = result.readline()
if int(result) == 0:
self.button6.set_active(False)
else:
self.button6.set_active(True)
self.grid.attach_next_to(self.button6, self.label6, Gtk.PositionType.RIGHT, 1, 1)
if int(removable) == 0:
self.button6.set_sensitive(False)
self.button7= Gtk.Switch()
self.button7.connect("notify::active", self.on_switch_activated_unknown)
cmd = 'xfconf-query --channel xfce4-desktop -n --property "/desktop-icons/file-icons/show-unknown-removable" | grep -c "true"'
result = os.popen(cmd)
result = result.readline()
if int(result) == 0:
self.button7.set_active(False)
else:
self.button7.set_active(True)
self.grid.attach_next_to(self.button7, self.label7, Gtk.PositionType.RIGHT, 1, 1)
if int(removable) == 0:
self.button7.set_sensitive(False)
buttonc = Gtk.Button(label="_Close", use_underline=True)
buttonc.set_border_width(10)
buttonc.connect("clicked", self.on_close_clicked)
self.grid.attach(buttonc, 2, 9, 1, 1)
button_about = Gtk.Button(label="_About", use_underline=True)
button_about.set_border_width(10)
button_about.connect("clicked", self.show_about_dialog)
self.grid.attach(button_about, 1, 9, 1, 1)
def on_switch_activated_filesystem(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-filesystem" --set "true"')
state = "on"
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-filesystem" --set "false"')
state = "off"
return print("File System icon is set", state)
def on_switch_activatedb(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-home" --set "true"')
state = "on"
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-home" --set "false"')
state = "off"
return print("Home icon is", state)
def on_switch_activatedc(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-trash" --set "true"')
state = "on"
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-trash" --set "false"')
state = "off"
return print("Trash icon is", state)
def on_switch_activated_network(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-network-removable" --set "true"')
state = "on"
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-network-removable" --set "false"')
state = "off"
return print("Network icon is", state)
def on_switch_activatede(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-removable" --set "true"')
state = "on"
try:
self.button5.set_sensitive(True)
self.button6.set_sensitive(True)
self.button7.set_sensitive(True)
except: pass
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-removable" --set "false"')
state = "off"
self.button5.set_sensitive(False)
self.button6.set_sensitive(False)
self.button7.set_sensitive(False)
return print("Show Removable is", state)
def on_switch_activatedf(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-device-removable" --set "true"')
state = "on"
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-device-removable" --set "false"')
state = "off"
return print("Show Removable drive is", state)
def on_switch_activated_unknown(self, switch, gparam):
if switch.get_active():
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-unknown-removable" --set "true"')
state = "on"
else:
execute('xfconf-query --channel xfce4-desktop --create --type="bool" --property "/desktop-icons/file-icons/show-unknown-removable" --set "false"')
state = "off"
return print("Show Unknown Device is", state)
def on_close_clicked(self, button):
print("Closing Xfce Desktop Icons")
Gtk.main_quit()
def show_about_dialog(self, widget):
about_dialog = Gtk.AboutDialog()
about_dialog.set_destroy_with_parent (True)
about_dialog.set_program_name("Desktop Icons")
#about_dialog.set_website('http://')
#about_dialog.set_website_label('')
about_dialog.set_icon(Pixbuf.new_from_file(icon))
about_dialog.set_logo(Pixbuf.new_from_file(icon))
about_dialog.set_copyright('Copyright 2015')
about_dialog.set_comments((u'A tool to show/hide icons on xfdesktop'))
about_dialog.set_license('''This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301, USA. ''')
about_dialog.set_authors([u'Milos Pavlovic <mpsrbija@gmail.com>'])
about_dialog.run()
about_dialog.destroy()
def main():
window = XfDesktopIconsWindow()
window.connect("delete-event", Gtk.main_quit)
window.set_resizable(False)
window.set_position(Gtk.WindowPosition.CENTER)
window.set_icon(Pixbuf.new_from_file("{0}".format(icon)))
window.set_name('DesktopIcons')
window.show_all()
Gtk.main()
if __name__ == '__main__':
try:
main()
except (Exception, AttributeError, FileNotFoundError) as e:
print("Exiting due to error: {0}".format(e))
sys.exit(1)
Do you want to exit the Circus?
https://www.youtube.com/watch?v=ZJwQicZHp_c
Offline
Pages: 1
[ Generated in 0.016 seconds, 7 queries executed - Memory usage: 660.39 KiB (Peak: 693.23 KiB) ]