mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Updated icon for ODT
This commit is contained in:
commit
c7fb6159f3
@ -1,3 +1,157 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
'''
|
||||||
|
economist.com
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import time, re
|
||||||
|
|
||||||
|
class Economist(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = 'The Economist'
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
|
__author__ = "Kovid Goyal"
|
||||||
|
INDEX = 'http://www.economist.com/printedition'
|
||||||
|
description = ('Global news and current affairs from a European'
|
||||||
|
' perspective. Best downloaded on Friday mornings (GMT)')
|
||||||
|
extra_css = '.headline {font-size: x-large;} \n h2 { font-size: small; } \n h1 { font-size: medium; }'
|
||||||
|
oldest_article = 7.0
|
||||||
|
cover_url = 'http://media.economist.com/sites/default/files/imagecache/print-cover-thumbnail/print-covers/currentcoverus_large.jpg'
|
||||||
|
#cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||||
|
remove_tags = [
|
||||||
|
dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||||
|
dict(attrs={'class':['dblClkTrk', 'ec-article-info',
|
||||||
|
'share_inline_header', 'related-items']}),
|
||||||
|
{'class': lambda x: x and 'share-links-header' in x},
|
||||||
|
]
|
||||||
|
keep_only_tags = [dict(id='ec-article-body')]
|
||||||
|
needs_subscription = False
|
||||||
|
no_stylesheets = True
|
||||||
|
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
||||||
|
lambda x:'</html>')]
|
||||||
|
|
||||||
|
'''
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
br.open('http://www.economist.com')
|
||||||
|
req = mechanize.Request(
|
||||||
|
'http://www.economist.com/members/members.cfm?act=exec_login',
|
||||||
|
headers = {
|
||||||
|
'Referer':'http://www.economist.com/',
|
||||||
|
},
|
||||||
|
data=urllib.urlencode({
|
||||||
|
'logging_in' : 'Y',
|
||||||
|
'returnURL' : '/',
|
||||||
|
'email_address': self.username,
|
||||||
|
'fakepword' : 'Password',
|
||||||
|
'pword' : self.password,
|
||||||
|
'x' : '0',
|
||||||
|
'y' : '0',
|
||||||
|
}))
|
||||||
|
br.open(req).read()
|
||||||
|
return br
|
||||||
|
'''
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
try:
|
||||||
|
return self.economist_parse_index()
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
self.log.warn(
|
||||||
|
'Initial attempt to parse index failed, retrying in 30 seconds')
|
||||||
|
time.sleep(30)
|
||||||
|
return self.economist_parse_index()
|
||||||
|
|
||||||
|
def economist_parse_index(self):
|
||||||
|
soup = self.index_to_soup(self.INDEX)
|
||||||
|
div = soup.find('div', attrs={'class':'issue-image'})
|
||||||
|
if div is not None:
|
||||||
|
img = div.find('img', src=True)
|
||||||
|
if img is not None:
|
||||||
|
self.cover_url = img['src']
|
||||||
|
feeds = OrderedDict()
|
||||||
|
for section in soup.findAll(attrs={'class':lambda x: x and 'section' in
|
||||||
|
x}):
|
||||||
|
h4 = section.find('h4')
|
||||||
|
if h4 is None:
|
||||||
|
continue
|
||||||
|
section_title = self.tag_to_string(h4).strip()
|
||||||
|
if not section_title:
|
||||||
|
continue
|
||||||
|
self.log('Found section: %s'%section_title)
|
||||||
|
articles = []
|
||||||
|
for h5 in section.findAll('h5'):
|
||||||
|
article_title = self.tag_to_string(h5).strip()
|
||||||
|
if not article_title:
|
||||||
|
continue
|
||||||
|
data = h5.findNextSibling(attrs={'class':'article'})
|
||||||
|
if data is None: continue
|
||||||
|
a = data.find('a', href=True)
|
||||||
|
if a is None: continue
|
||||||
|
url = a['href']
|
||||||
|
if url.startswith('/'): url = 'http://www.economist.com'+url
|
||||||
|
url += '/print'
|
||||||
|
article_title += ': %s'%self.tag_to_string(a).strip()
|
||||||
|
articles.append({'title':article_title, 'url':url,
|
||||||
|
'description':'', 'date':''})
|
||||||
|
if not articles:
|
||||||
|
# We have last or first section
|
||||||
|
for art in section.findAll(attrs={'class':'article'}):
|
||||||
|
a = art.find('a', href=True)
|
||||||
|
if a is not None:
|
||||||
|
url = a['href']
|
||||||
|
if url.startswith('/'): url = 'http://www.economist.com'+url
|
||||||
|
url += '/print'
|
||||||
|
title = self.tag_to_string(a)
|
||||||
|
if title:
|
||||||
|
articles.append({'title':title, 'url':url,
|
||||||
|
'description':'', 'date':''})
|
||||||
|
|
||||||
|
if articles:
|
||||||
|
if section_title not in feeds:
|
||||||
|
feeds[section_title] = []
|
||||||
|
feeds[section_title] += articles
|
||||||
|
|
||||||
|
ans = [(key, val) for key, val in feeds.iteritems()]
|
||||||
|
if not ans:
|
||||||
|
raise Exception('Could not find any articles, either the '
|
||||||
|
'economist.com server is having trouble and you should '
|
||||||
|
'try later or the website format has changed and the '
|
||||||
|
'recipe needs to be updated.')
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def eco_find_image_tables(self, soup):
|
||||||
|
for x in soup.findAll('table', align=['right', 'center']):
|
||||||
|
if len(x.findAll('font')) in (1,2) and len(x.findAll('img')) == 1:
|
||||||
|
yield x
|
||||||
|
|
||||||
|
def postprocess_html(self, soup, first):
|
||||||
|
body = soup.find('body')
|
||||||
|
for name, val in body.attrs:
|
||||||
|
del body[name]
|
||||||
|
|
||||||
|
for table in list(self.eco_find_image_tables(soup)):
|
||||||
|
caption = table.find('font')
|
||||||
|
img = table.find('img')
|
||||||
|
div = Tag(soup, 'div')
|
||||||
|
div['style'] = 'text-align:left;font-size:70%'
|
||||||
|
ns = NavigableString(self.tag_to_string(caption))
|
||||||
|
div.insert(0, ns)
|
||||||
|
div.insert(1, Tag(soup, 'br'))
|
||||||
|
del img['width']
|
||||||
|
del img['height']
|
||||||
|
img.extract()
|
||||||
|
div.insert(2, img)
|
||||||
|
table.replaceWith(div)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
'''
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre.utils.threadpool import ThreadPool, makeRequests
|
from calibre.utils.threadpool import ThreadPool, makeRequests
|
||||||
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
|
from calibre.ebooks.BeautifulSoup import Tag, NavigableString
|
||||||
@ -145,3 +299,5 @@ class Economist(BasicNewsRecipe):
|
|||||||
div.insert(2, img)
|
div.insert(2, img)
|
||||||
table.replaceWith(div)
|
table.replaceWith(div)
|
||||||
return soup
|
return soup
|
||||||
|
'''
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]),
|
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow","articleTabs_tab_quotes","articleTabs_tab_document"]),
|
||||||
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
|
||||||
dict(rel='shortcut icon'),
|
dict(rel='shortcut icon'),
|
||||||
|
{'class':lambda x: x and 'sTools' in x},
|
||||||
]
|
]
|
||||||
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
dict(name='div', attrs={'data-flash-settings':True}),
|
dict(name='div', attrs={'data-flash-settings':True}),
|
||||||
{'class':['insetContent embedType-interactive insetCol3wide','insetCol6wide','insettipUnit']},
|
{'class':['insetContent embedType-interactive insetCol3wide','insetCol6wide','insettipUnit']},
|
||||||
dict(rel='shortcut icon'),
|
dict(rel='shortcut icon'),
|
||||||
|
{'class':lambda x: x and 'sTools' in x},
|
||||||
]
|
]
|
||||||
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.9 KiB |
@ -1710,7 +1710,6 @@ class MobiWriter(object):
|
|||||||
'''
|
'''
|
||||||
from calibre.ebooks.oeb.base import TOC
|
from calibre.ebooks.oeb.base import TOC
|
||||||
items = list(self._oeb.toc.iterdescendants())
|
items = list(self._oeb.toc.iterdescendants())
|
||||||
items = [i for i in items if i.depth == 1]
|
|
||||||
offsets = {i:self._id_offsets.get(i.href, -1) for i in items if i.href}
|
offsets = {i:self._id_offsets.get(i.href, -1) for i in items if i.href}
|
||||||
items = [i for i in items if offsets[i] > -1]
|
items = [i for i in items if offsets[i] > -1]
|
||||||
items.sort(key=lambda i:offsets[i])
|
items.sort(key=lambda i:offsets[i])
|
||||||
|
@ -106,6 +106,10 @@ class InterfaceAction(QObject):
|
|||||||
self.gui.addAction(self.menuless_qaction)
|
self.gui.addAction(self.menuless_qaction)
|
||||||
self.genesis()
|
self.genesis()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_name(self):
|
||||||
|
return u'%s(%s)'%(self.__class__.__name__, self.name)
|
||||||
|
|
||||||
def create_action(self, spec=None, attr='qaction'):
|
def create_action(self, spec=None, attr='qaction'):
|
||||||
if spec is None:
|
if spec is None:
|
||||||
spec = self.action_spec
|
spec = self.action_spec
|
||||||
@ -125,13 +129,20 @@ class InterfaceAction(QObject):
|
|||||||
a.setToolTip(text)
|
a.setToolTip(text)
|
||||||
a.setStatusTip(text)
|
a.setStatusTip(text)
|
||||||
a.setWhatsThis(text)
|
a.setWhatsThis(text)
|
||||||
if shortcut:
|
keys = ()
|
||||||
a = ma if attr == 'qaction' else action
|
shortcut_action = action
|
||||||
if isinstance(shortcut, list):
|
desc = tooltip if tooltip else None
|
||||||
a.setShortcuts(shortcut)
|
if attr == 'qaction':
|
||||||
else:
|
shortcut_action = ma
|
||||||
a.setShortcut(shortcut)
|
if shortcut is not None:
|
||||||
setattr(self, attr, action)
|
keys = ((shortcut,) if isinstance(shortcut, basestring) else
|
||||||
|
tuple(shortcut))
|
||||||
|
|
||||||
|
self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr,
|
||||||
|
unicode(shortcut_action.text()), default_keys=keys,
|
||||||
|
action=shortcut_action, description=desc)
|
||||||
|
if attr is not None:
|
||||||
|
setattr(self, attr, action)
|
||||||
if attr == 'qaction' and self.action_add_menu:
|
if attr == 'qaction' and self.action_add_menu:
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
action.setMenu(menu)
|
action.setMenu(menu)
|
||||||
@ -139,6 +150,30 @@ class InterfaceAction(QObject):
|
|||||||
menu.addAction(self.menuless_qaction)
|
menu.addAction(self.menuless_qaction)
|
||||||
return action
|
return action
|
||||||
|
|
||||||
|
def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None,
|
||||||
|
description=None, triggered=None):
|
||||||
|
ac = menu.addAction(text)
|
||||||
|
if icon is not None:
|
||||||
|
if not isinstance(icon, QIcon):
|
||||||
|
icon = QIcon(I(icon))
|
||||||
|
ac.setIcon(icon)
|
||||||
|
keys = ()
|
||||||
|
if shortcut is not None and shortcut is not False:
|
||||||
|
keys = ((shortcut,) if isinstance(shortcut, basestring) else
|
||||||
|
tuple(shortcut))
|
||||||
|
unique_name = '%s : menu action : %s'%(self.unique_name, unique_name)
|
||||||
|
if description is not None:
|
||||||
|
ac.setToolTip(description)
|
||||||
|
ac.setStatusTip(description)
|
||||||
|
ac.setWhatsThis(description)
|
||||||
|
if shortcut is not False:
|
||||||
|
self.gui.keyboard.register_shortcut(unique_name,
|
||||||
|
unicode(text), default_keys=keys,
|
||||||
|
action=ac, description=description)
|
||||||
|
if triggered is not None:
|
||||||
|
ac.triggered.connect(triggered)
|
||||||
|
return ac
|
||||||
|
|
||||||
def load_resources(self, names):
|
def load_resources(self, names):
|
||||||
'''
|
'''
|
||||||
If this plugin comes in a ZIP file (user added plugin), this method
|
If this plugin comes in a ZIP file (user added plugin), this method
|
||||||
|
@ -54,20 +54,22 @@ class AddAction(InterfaceAction):
|
|||||||
def genesis(self):
|
def genesis(self):
|
||||||
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
|
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
|
||||||
self.add_menu = self.qaction.menu()
|
self.add_menu = self.qaction.menu()
|
||||||
self.add_menu.addAction(_('Add books from directories, including '
|
ma = partial(self.create_menu_action, self.add_menu)
|
||||||
|
ma('recursive-single', _('Add books from directories, including '
|
||||||
'sub-directories (One book per directory, assumes every ebook '
|
'sub-directories (One book per directory, assumes every ebook '
|
||||||
'file is the same book in a different format)'),
|
'file is the same book in a different format)')).triggered.connect(
|
||||||
self.add_recursive_single)
|
self.add_recursive_single)
|
||||||
self.add_menu.addAction(_('Add books from directories, including '
|
ma('recursive-multiple', _('Add books from directories, including '
|
||||||
'sub directories (Multiple books per directory, assumes every '
|
'sub directories (Multiple books per directory, assumes every '
|
||||||
'ebook file is a different book)'), self.add_recursive_multiple)
|
'ebook file is a different book)')).triggered.connect(
|
||||||
|
self.add_recursive_multiple)
|
||||||
self.add_menu.addSeparator()
|
self.add_menu.addSeparator()
|
||||||
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
|
ma('add-empty', _('Add Empty book. (Book entry with no formats)'),
|
||||||
'formats)'), self.add_empty, _('Shift+Ctrl+E'))
|
shortcut=_('Shift+Ctrl+E')).triggered.connect(self.add_empty)
|
||||||
self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn)
|
ma('add-isbn', _('Add from ISBN')).triggered.connect(self.add_from_isbn)
|
||||||
self.add_menu.addSeparator()
|
self.add_menu.addSeparator()
|
||||||
self.add_menu.addAction(_('Add files to selected book records'),
|
ma('add-formats', _('Add files to selected book records'),
|
||||||
self.add_formats, _('Shift+A'))
|
triggered=self.add_formats, shortcut=_('Shift+A'))
|
||||||
|
|
||||||
self.qaction.triggered.connect(self.add_books)
|
self.qaction.triggered.connect(self.add_books)
|
||||||
|
|
||||||
@ -82,7 +84,8 @@ class AddAction(InterfaceAction):
|
|||||||
view = self.gui.library_view
|
view = self.gui.library_view
|
||||||
rows = view.selectionModel().selectedRows()
|
rows = view.selectionModel().selectedRows()
|
||||||
if not rows:
|
if not rows:
|
||||||
return
|
return error_dialog(self.gui, _('No books selected'),
|
||||||
|
_('Cannot add files as no books are selected'), show=True)
|
||||||
ids = [view.model().id(r) for r in rows]
|
ids = [view.model().id(r) for r in rows]
|
||||||
|
|
||||||
if len(ids) > 1 and not question_dialog(self.gui,
|
if len(ids) > 1 and not question_dialog(self.gui,
|
||||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QModelIndex, QIcon
|
from PyQt4.Qt import QModelIndex
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog, Dispatcher
|
from calibre.gui2 import error_dialog, Dispatcher
|
||||||
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook
|
||||||
@ -25,17 +25,19 @@ class ConvertAction(InterfaceAction):
|
|||||||
action_add_menu = True
|
action_add_menu = True
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
cm = self.qaction.menu()
|
m = self.convert_menu = self.qaction.menu()
|
||||||
cm.addAction(self.qaction.icon(), _('Convert individually'), partial(self.convert_ebook,
|
cm = partial(self.create_menu_action, self.convert_menu)
|
||||||
|
cm('convert-individual', _('Convert individually'),
|
||||||
|
icon=self.qaction.icon(), triggered=partial(self.convert_ebook,
|
||||||
False, bulk=False))
|
False, bulk=False))
|
||||||
cm.addAction(_('Bulk convert'),
|
cm('convert-bulk', _('Bulk convert'),
|
||||||
partial(self.convert_ebook, False, bulk=True))
|
triggered=partial(self.convert_ebook, False, bulk=True))
|
||||||
cm.addSeparator()
|
m.addSeparator()
|
||||||
ac = cm.addAction(QIcon(I('catalog.png')),
|
cm('create-catalog',
|
||||||
_('Create a catalog of the books in your calibre library'))
|
_('Create a catalog of the books in your calibre library'),
|
||||||
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog)
|
icon='catalog.png', shortcut=False,
|
||||||
|
triggered=self.gui.iactions['Generate Catalog'].generate_catalog)
|
||||||
self.qaction.triggered.connect(self.convert_ebook)
|
self.qaction.triggered.connect(self.convert_ebook)
|
||||||
self.convert_menu = cm
|
|
||||||
self.conversion_jobs = {}
|
self.conversion_jobs = {}
|
||||||
|
|
||||||
def location_selected(self, loc):
|
def location_selected(self, loc):
|
||||||
|
@ -90,21 +90,23 @@ class DeleteAction(InterfaceAction):
|
|||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.delete_books)
|
self.qaction.triggered.connect(self.delete_books)
|
||||||
self.delete_menu = self.qaction.menu()
|
self.delete_menu = self.qaction.menu()
|
||||||
self.delete_menu.addAction(
|
m = partial(self.create_menu_action, self.delete_menu)
|
||||||
|
m('delete-specific',
|
||||||
_('Remove files of a specific format from selected books..'),
|
_('Remove files of a specific format from selected books..'),
|
||||||
self.delete_selected_formats)
|
triggered=self.delete_selected_formats)
|
||||||
self.delete_menu.addAction(
|
m('delete-except',
|
||||||
_('Remove all formats from selected books, except...'),
|
_('Remove all formats from selected books, except...'),
|
||||||
self.delete_all_but_selected_formats)
|
triggered=self.delete_all_but_selected_formats)
|
||||||
self.delete_menu.addAction(
|
m('delete-all',
|
||||||
_('Remove all formats from selected books'),
|
_('Remove all formats from selected books'),
|
||||||
self.delete_all_formats)
|
triggered=self.delete_all_formats)
|
||||||
self.delete_menu.addAction(
|
m('delete-covers',
|
||||||
_('Remove covers from selected books'), self.delete_covers)
|
_('Remove covers from selected books'),
|
||||||
|
triggered=self.delete_covers)
|
||||||
self.delete_menu.addSeparator()
|
self.delete_menu.addSeparator()
|
||||||
self.delete_menu.addAction(
|
m('delete-matching',
|
||||||
_('Remove matching books from device'),
|
_('Remove matching books from device'),
|
||||||
self.remove_matching_books_from_device)
|
triggered=self.remove_matching_books_from_device)
|
||||||
self.qaction.setMenu(self.delete_menu)
|
self.qaction.setMenu(self.delete_menu)
|
||||||
self.delete_memory = {}
|
self.delete_memory = {}
|
||||||
|
|
||||||
|
@ -60,6 +60,15 @@ class ShareConnMenu(QMenu): # {{{
|
|||||||
|
|
||||||
self.email_actions = []
|
self.email_actions = []
|
||||||
|
|
||||||
|
if hasattr(parent, 'keyboard'):
|
||||||
|
r = parent.keyboard.register_shortcut
|
||||||
|
prefix = 'Share/Connect Menu '
|
||||||
|
for attr in ('folder', 'bambook', 'itunes'):
|
||||||
|
if not (iswindows or isosx) and attr == 'itunes':
|
||||||
|
continue
|
||||||
|
ac = getattr(self, 'connect_to_%s_action'%attr)
|
||||||
|
r(prefix + attr, unicode(ac.text()), action=ac)
|
||||||
|
|
||||||
def server_state_changed(self, running):
|
def server_state_changed(self, running):
|
||||||
text = _('Start Content Server')
|
text = _('Start Content Server')
|
||||||
if running:
|
if running:
|
||||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
|
from PyQt4.Qt import QMenu, QModelIndex, QTimer
|
||||||
|
|
||||||
from calibre.gui2 import error_dialog, Dispatcher, question_dialog
|
from calibre.gui2 import error_dialog, Dispatcher, question_dialog
|
||||||
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||||
@ -27,37 +27,38 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
action_add_menu = True
|
action_add_menu = True
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.create_action(spec=(_('Merge book records'), 'merge_books.png',
|
|
||||||
None, _('M')), attr='action_merge')
|
|
||||||
md = self.qaction.menu()
|
md = self.qaction.menu()
|
||||||
md.addAction(self.qaction.icon(), _('Edit metadata individually'),
|
cm = partial(self.create_menu_action, md)
|
||||||
partial(self.edit_metadata, False, bulk=False))
|
cm('individual', _('Edit metadata individually'), icon=self.qaction.icon(),
|
||||||
|
triggered=partial(self.edit_metadata, False, bulk=False))
|
||||||
md.addSeparator()
|
md.addSeparator()
|
||||||
md.addAction(_('Edit metadata in bulk'),
|
cm('bulk', _('Edit metadata in bulk'),
|
||||||
partial(self.edit_metadata, False, bulk=True))
|
triggered=partial(self.edit_metadata, False, bulk=True))
|
||||||
md.addSeparator()
|
md.addSeparator()
|
||||||
md.addAction(_('Download metadata and covers'), self.download_metadata,
|
cm('download', _('Download metadata and covers'),
|
||||||
Qt.ControlModifier+Qt.Key_D)
|
triggered=partial(self.download_metadata, ids=None),
|
||||||
|
shortcut='Ctrl+D')
|
||||||
self.metadata_menu = md
|
self.metadata_menu = md
|
||||||
|
|
||||||
mb = QMenu()
|
mb = QMenu()
|
||||||
mb.addAction(_('Merge into first selected book - delete others'),
|
cm2 = partial(self.create_menu_action, mb)
|
||||||
self.merge_books)
|
cm2('merge delete', _('Merge into first selected book - delete others'),
|
||||||
|
triggered=self.merge_books)
|
||||||
mb.addSeparator()
|
mb.addSeparator()
|
||||||
mb.addAction(_('Merge into first selected book - keep others'),
|
cm2('merge keep', _('Merge into first selected book - keep others'),
|
||||||
partial(self.merge_books, safe_merge=True),
|
triggered=partial(self.merge_books, safe_merge=True),
|
||||||
Qt.AltModifier+Qt.Key_M)
|
shortcut='Alt+M')
|
||||||
mb.addSeparator()
|
mb.addSeparator()
|
||||||
mb.addAction(_('Merge only formats into first selected book - delete others'),
|
cm2('merge formats', _('Merge only formats into first selected book - delete others'),
|
||||||
partial(self.merge_books, merge_only_formats=True),
|
triggered=partial(self.merge_books, merge_only_formats=True),
|
||||||
Qt.AltModifier+Qt.ShiftModifier+Qt.Key_M)
|
shortcut='Alt+Shift+M')
|
||||||
self.merge_menu = mb
|
self.merge_menu = mb
|
||||||
self.action_merge.setMenu(mb)
|
|
||||||
md.addSeparator()
|
md.addSeparator()
|
||||||
md.addAction(self.action_merge)
|
self.action_merge = cm('merge', _('Merge book records'), icon='merge_books.png',
|
||||||
|
shortcut=_('M'), triggered=self.merge_books)
|
||||||
|
self.action_merge.setMenu(mb)
|
||||||
|
|
||||||
self.qaction.triggered.connect(self.edit_metadata)
|
self.qaction.triggered.connect(self.edit_metadata)
|
||||||
self.action_merge.triggered.connect(self.merge_books)
|
|
||||||
|
|
||||||
def location_selected(self, loc):
|
def location_selected(self, loc):
|
||||||
enabled = loc == 'library'
|
enabled = loc == 'library'
|
||||||
@ -417,7 +418,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
db.set_custom(dest_id, dest_value, num=colnum)
|
db.set_custom(dest_id, dest_value, num=colnum)
|
||||||
if db.field_metadata[key]['datatype'] in \
|
if db.field_metadata[key]['datatype'] in \
|
||||||
('bool', 'int', 'float', 'rating', 'datetime') \
|
('bool', 'int', 'float', 'rating', 'datetime') \
|
||||||
and not dest_value:
|
and dest_value is None:
|
||||||
db.set_custom(dest_id, src_value, num=colnum)
|
db.set_custom(dest_id, src_value, num=colnum)
|
||||||
if db.field_metadata[key]['datatype'] == 'series' \
|
if db.field_metadata[key]['datatype'] == 'series' \
|
||||||
and not dest_value:
|
and not dest_value:
|
||||||
|
@ -5,6 +5,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QIcon, Qt
|
from PyQt4.Qt import QIcon, Qt
|
||||||
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
@ -21,18 +23,17 @@ class PreferencesAction(InterfaceAction):
|
|||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
pm = self.qaction.menu()
|
pm = self.qaction.menu()
|
||||||
|
cm = partial(self.create_menu_action, pm)
|
||||||
if isosx:
|
if isosx:
|
||||||
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
|
pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
|
||||||
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
|
cm('welcome wizard', _('Run welcome wizard'),
|
||||||
self.gui.run_wizard)
|
icon='wizard.png', triggered=self.gui.run_wizard)
|
||||||
pm.addAction(QIcon(I('plugins/plugin_updater.png')),
|
cm('plugin updater', _('Get plugins to enhance calibre'),
|
||||||
_('Get plugins to enhance calibre'), self.get_plugins)
|
icon='plugins/plugin_updater.png', triggered=self.get_plugins)
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
pm.addSeparator()
|
pm.addSeparator()
|
||||||
ac = pm.addAction(QIcon(I('debug.png')), _('Restart in debug mode'),
|
cm('restart', _('Restart in debug mode'), icon='debug.png',
|
||||||
self.debug_restart)
|
triggered=self.debug_restart, shortcut='Ctrl+Shift+R')
|
||||||
ac.setShortcut('Ctrl+Shift+R')
|
|
||||||
self.gui.addAction(ac)
|
|
||||||
|
|
||||||
self.preferences_menu = pm
|
self.preferences_menu = pm
|
||||||
for x in (self.gui.preferences_action, self.qaction):
|
for x in (self.gui.preferences_action, self.qaction):
|
||||||
|
@ -44,15 +44,16 @@ class SaveToDiskAction(InterfaceAction):
|
|||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.save_to_disk)
|
self.qaction.triggered.connect(self.save_to_disk)
|
||||||
self.save_menu = self.qaction.menu()
|
self.save_menu = self.qaction.menu()
|
||||||
self.save_menu.addAction(_('Save to disk in a single directory'),
|
cm = partial(self.create_menu_action, self.save_menu)
|
||||||
partial(self.save_to_single_dir, False))
|
cm('single dir', _('Save to disk in a single directory'),
|
||||||
self.save_menu.addAction(_('Save only %s format to disk')%
|
triggered=partial(self.save_to_single_dir, False))
|
||||||
|
cm('single format', _('Save only %s format to disk')%
|
||||||
prefs['output_format'].upper(),
|
prefs['output_format'].upper(),
|
||||||
partial(self.save_single_format_to_disk, False))
|
triggered=partial(self.save_single_format_to_disk, False))
|
||||||
self.save_menu.addAction(
|
cm('single dir and format',
|
||||||
_('Save only %s format to disk in a single directory')%
|
_('Save only %s format to disk in a single directory')%
|
||||||
prefs['output_format'].upper(),
|
prefs['output_format'].upper(),
|
||||||
partial(self.save_single_fmt_to_single_dir, False))
|
triggered=partial(self.save_single_fmt_to_single_dir, False))
|
||||||
self.save_sub_menu = SaveMenu(self.gui)
|
self.save_sub_menu = SaveMenu(self.gui)
|
||||||
self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu)
|
self.save_sub_menu_action = self.save_menu.addMenu(self.save_sub_menu)
|
||||||
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
|
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
|
||||||
@ -114,10 +115,7 @@ class SaveToDiskAction(InterfaceAction):
|
|||||||
opts.save_cover = False
|
opts.save_cover = False
|
||||||
opts.write_opf = False
|
opts.write_opf = False
|
||||||
opts.template = opts.send_template
|
opts.template = opts.send_template
|
||||||
if single_dir:
|
opts.single_dir = single_dir
|
||||||
opts.template = opts.template.split('/')[-1].strip()
|
|
||||||
if not opts.template:
|
|
||||||
opts.template = '{title} - {authors}'
|
|
||||||
self._saver = Saver(self.gui, self.gui.library_view.model().db,
|
self._saver = Saver(self.gui, self.gui.library_view.model().db,
|
||||||
Dispatcher(self._books_saved), rows, path, opts,
|
Dispatcher(self._books_saved), rows, path, opts,
|
||||||
spare_server=self.gui.spare_server)
|
spare_server=self.gui.spare_server)
|
||||||
|
@ -24,16 +24,21 @@ class StoreAction(InterfaceAction):
|
|||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.do_search)
|
self.qaction.triggered.connect(self.do_search)
|
||||||
self.store_menu = self.qaction.menu()
|
self.store_menu = self.qaction.menu()
|
||||||
self.load_menu()
|
cm = partial(self.create_menu_action, self.store_menu)
|
||||||
|
for x, t in [('author', _('author')), ('title', _('title')),
|
||||||
def load_menu(self):
|
('book', _('book'))]:
|
||||||
self.store_menu.clear()
|
func = getattr(self, 'search_%s'%('author_title' if x == 'book'
|
||||||
self.store_menu.addAction(self.menuless_qaction)
|
else x))
|
||||||
self.store_menu.addAction(_('Search for this author'), self.search_author)
|
ac = cm(x, _('Search for this %s'%t), triggered=func)
|
||||||
self.store_menu.addAction(_('Search for this title'), self.search_title)
|
setattr(self, 'action_search_by_'+x, ac)
|
||||||
self.store_menu.addAction(_('Search for this book'), self.search_author_title)
|
|
||||||
self.store_menu.addSeparator()
|
self.store_menu.addSeparator()
|
||||||
self.store_list_menu = self.store_menu.addMenu(_('Stores'))
|
self.store_list_menu = self.store_menu.addMenu(_('Stores'))
|
||||||
|
self.load_menu()
|
||||||
|
self.store_menu.addSeparator()
|
||||||
|
cm('choose stores', _('Choose stores'), triggered=self.choose)
|
||||||
|
|
||||||
|
def load_menu(self):
|
||||||
|
self.store_list_menu.clear()
|
||||||
icon = QIcon()
|
icon = QIcon()
|
||||||
icon.addFile(I('donate.png'), QSize(16, 16))
|
icon.addFile(I('donate.png'), QSize(16, 16))
|
||||||
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
|
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
|
||||||
@ -41,8 +46,6 @@ class StoreAction(InterfaceAction):
|
|||||||
self.store_list_menu.addAction(icon, n, partial(self.open_store, p))
|
self.store_list_menu.addAction(icon, n, partial(self.open_store, p))
|
||||||
else:
|
else:
|
||||||
self.store_list_menu.addAction(n, partial(self.open_store, p))
|
self.store_list_menu.addAction(n, partial(self.open_store, p))
|
||||||
self.store_menu.addSeparator()
|
|
||||||
self.store_menu.addAction(_('Choose stores'), self.choose)
|
|
||||||
|
|
||||||
def do_search(self):
|
def do_search(self):
|
||||||
return self.search()
|
return self.search()
|
||||||
|
@ -6,6 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, time
|
import os, time
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QAction, pyqtSignal
|
from PyQt4.Qt import Qt, QAction, pyqtSignal
|
||||||
|
|
||||||
@ -43,36 +44,33 @@ class ViewAction(InterfaceAction):
|
|||||||
self.qaction.triggered.connect(self.view_book)
|
self.qaction.triggered.connect(self.view_book)
|
||||||
self.view_action = self.menuless_qaction
|
self.view_action = self.menuless_qaction
|
||||||
self.view_menu = self.qaction.menu()
|
self.view_menu = self.qaction.menu()
|
||||||
ac = self.view_specific_action = QAction(_('View specific format'),
|
cm = partial(self.create_menu_action, self.view_menu)
|
||||||
self.gui)
|
self.view_specific_action = cm('specific', _('View specific format'),
|
||||||
ac.setShortcut(Qt.AltModifier+Qt.Key_V)
|
shortcut='Alt+V', triggered=self.view_specific_format)
|
||||||
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
|
self.action_pick_random = cm('pick random', _('Read a random book'),
|
||||||
ac = self.create_action(spec=(_('Read a random book'), 'random.png',
|
icon='random.png', triggered=self.view_random)
|
||||||
None, None), attr='action_pick_random')
|
self.clear_sep1 = self.view_menu.addSeparator()
|
||||||
ac.triggered.connect(self.view_random)
|
self.clear_sep2 = self.view_menu.addSeparator()
|
||||||
ac = self.clear_history_action = QAction(
|
self.clear_history_action = cm('clear history',
|
||||||
_('Clear recently viewed list'), self.gui)
|
_('Clear recently viewed list'), triggered=self.clear_history)
|
||||||
ac.triggered.connect(self.clear_history)
|
self.history_actions = [self.clear_sep1]
|
||||||
|
|
||||||
def initialization_complete(self):
|
def initialization_complete(self):
|
||||||
self.build_menus(self.gui.current_db)
|
self.build_menus(self.gui.current_db)
|
||||||
|
|
||||||
def build_menus(self, db):
|
def build_menus(self, db):
|
||||||
self.view_menu.clear()
|
for ac in self.history_actions:
|
||||||
self.view_menu.addAction(self.view_action)
|
self.view_menu.removeAction(ac)
|
||||||
self.view_menu.addAction(self.view_specific_action)
|
|
||||||
self.view_menu.addSeparator()
|
|
||||||
self.view_menu.addAction(self.action_pick_random)
|
|
||||||
self.history_actions = []
|
self.history_actions = []
|
||||||
history = db.prefs.get('gui_view_history', [])
|
history = db.prefs.get('gui_view_history', [])
|
||||||
if history:
|
if history:
|
||||||
self.view_menu.addSeparator()
|
self.view_menu.insertAction(self.clear_sep2, self.clear_sep1)
|
||||||
|
self.history_actions.append(self.clear_sep1)
|
||||||
for id_, title in history:
|
for id_, title in history:
|
||||||
ac = HistoryAction(id_, title, self.view_menu)
|
ac = HistoryAction(id_, title, self.view_menu)
|
||||||
self.view_menu.addAction(ac)
|
self.view_menu.insertAction(self.clear_sep2, ac)
|
||||||
ac.view_historical.connect(self.view_historical)
|
ac.view_historical.connect(self.view_historical)
|
||||||
self.view_menu.addSeparator()
|
self.history_actions.append(ac)
|
||||||
self.view_menu.addAction(self.clear_history_action)
|
|
||||||
|
|
||||||
def clear_history(self):
|
def clear_history(self):
|
||||||
db = self.gui.current_db
|
db = self.gui.current_db
|
||||||
|
@ -66,8 +66,8 @@ class EbookDownload(object):
|
|||||||
raise Exception(_('Not a support ebook format.'))
|
raise Exception(_('Not a support ebook format.'))
|
||||||
|
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
from calibre.ebooks.metadata.meta import get_metadata
|
||||||
with open(filename) as f:
|
with open(filename, 'rb') as f:
|
||||||
mi = get_metadata(f, ext)
|
mi = get_metadata(f, ext, force_read_metadata=True)
|
||||||
mi.tags.extend(tags)
|
mi.tags.extend(tags)
|
||||||
|
|
||||||
id = gui.library_view.model().db.create_book_entry(mi)
|
id = gui.library_view.model().db.create_book_entry(mi)
|
||||||
|
@ -218,7 +218,7 @@ class LayoutMixin(object): # {{{
|
|||||||
self.bd_splitter = Splitter('book_details_splitter',
|
self.bd_splitter = Splitter('book_details_splitter',
|
||||||
_('Book Details'), I('book.png'),
|
_('Book Details'), I('book.png'),
|
||||||
orientation=Qt.Vertical, parent=self, side_index=1,
|
orientation=Qt.Vertical, parent=self, side_index=1,
|
||||||
shortcut=_('Alt+D'))
|
shortcut=_('Shift+Alt+D'))
|
||||||
self.bd_splitter.addWidget(self.stack)
|
self.bd_splitter.addWidget(self.stack)
|
||||||
self.bd_splitter.addWidget(self.book_details)
|
self.bd_splitter.addWidget(self.book_details)
|
||||||
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
||||||
|
71
src/calibre/gui2/keyboard.py
Normal file
71
src/calibre/gui2/keyboard.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QObject, QKeySequence)
|
||||||
|
|
||||||
|
from calibre.utils.config import JSONConfig
|
||||||
|
from calibre.constants import DEBUG
|
||||||
|
from calibre import prints
|
||||||
|
|
||||||
|
class NameConflict(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Manager(QObject):
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QObject.__init__(self, parent)
|
||||||
|
|
||||||
|
self.config = JSONConfig('shortcuts/main')
|
||||||
|
self.custom_keys_map = {}
|
||||||
|
self.shortcuts = OrderedDict()
|
||||||
|
self.keys_map = {}
|
||||||
|
|
||||||
|
for unique_name, keys in self.config.get(
|
||||||
|
'map', {}).iteritems():
|
||||||
|
self.custom_keys_map[unique_name] = tuple(keys)
|
||||||
|
|
||||||
|
def register_shortcut(self, unique_name, name, default_keys=(),
|
||||||
|
description=None, action=None):
|
||||||
|
if unique_name in self.shortcuts:
|
||||||
|
name = self.shortcuts[unique_name]['name']
|
||||||
|
raise NameConflict('Shortcut for %r already registered by %s'%(
|
||||||
|
unique_name, name))
|
||||||
|
shortcut = {'name':name, 'desc':description, 'action': action,
|
||||||
|
'default_keys':tuple(default_keys)}
|
||||||
|
self.shortcuts[unique_name] = shortcut
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
seen = {}
|
||||||
|
for unique_name, shortcut in self.shortcuts.iteritems():
|
||||||
|
custom_keys = self.custom_keys_map.get(unique_name, None)
|
||||||
|
if custom_keys is None:
|
||||||
|
candidates = shortcut['default_keys']
|
||||||
|
else:
|
||||||
|
candidates = custom_keys
|
||||||
|
keys = []
|
||||||
|
for x in candidates:
|
||||||
|
ks = QKeySequence(x, QKeySequence.PortableText)
|
||||||
|
x = unicode(ks.toString(QKeySequence.PortableText))
|
||||||
|
if x in seen:
|
||||||
|
if DEBUG:
|
||||||
|
prints('Key %r for shortcut %s is already used by'
|
||||||
|
' %s, ignoring'%(x, shortcut['name'], seen[x]['name']))
|
||||||
|
continue
|
||||||
|
seen[x] = shortcut
|
||||||
|
keys.append(ks)
|
||||||
|
keys = tuple(keys)
|
||||||
|
#print (111111, unique_name, candidates, keys)
|
||||||
|
|
||||||
|
self.keys_map[unique_name] = keys
|
||||||
|
ac = shortcut['action']
|
||||||
|
if ac is not None:
|
||||||
|
ac.setShortcuts(list(keys))
|
||||||
|
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import (QIcon, Qt, QWidget, QSize,
|
from PyQt4.Qt import (QIcon, Qt, QWidget, QSize,
|
||||||
pyqtSignal, QToolButton, QMenu,
|
pyqtSignal, QToolButton, QMenu, QAction,
|
||||||
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
|
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
|
||||||
|
|
||||||
|
|
||||||
@ -178,7 +178,12 @@ class SearchBar(QWidget): # {{{
|
|||||||
x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
x.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
|
||||||
|
|
||||||
parent.advanced_search_button = x = QToolButton(self)
|
parent.advanced_search_button = x = QToolButton(self)
|
||||||
parent.advanced_search_button.setShortcut(_("Shift+Ctrl+F"))
|
parent.advanced_search_toggle_action = ac = QAction(parent)
|
||||||
|
parent.addAction(ac)
|
||||||
|
parent.keyboard.register_shortcut('advanced search toggle',
|
||||||
|
_('Advanced search'), default_keys=(_("Shift+Ctrl+F"),),
|
||||||
|
action=ac)
|
||||||
|
ac.triggered.connect(x.click)
|
||||||
x.setIcon(QIcon(I('search.png')))
|
x.setIcon(QIcon(I('search.png')))
|
||||||
l.addWidget(x)
|
l.addWidget(x)
|
||||||
x.setToolTip(_("Advanced search"))
|
x.setToolTip(_("Advanced search"))
|
||||||
|
@ -310,6 +310,12 @@ class CcNumberDelegate(QStyledItemDelegate): # {{{
|
|||||||
editor.setDecimals(2)
|
editor.setDecimals(2)
|
||||||
return editor
|
return editor
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
val = editor.value()
|
||||||
|
if val == editor.minimum():
|
||||||
|
val = None
|
||||||
|
model.setData(index, QVariant(val), Qt.EditRole)
|
||||||
|
|
||||||
def setEditorData(self, editor, index):
|
def setEditorData(self, editor, index):
|
||||||
m = index.model()
|
m = index.model()
|
||||||
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
|
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
|
||||||
|
@ -376,9 +376,12 @@ class SearchBoxMixin(object): # {{{
|
|||||||
self.search.clear()
|
self.search.clear()
|
||||||
self.search.setMaximumWidth(self.width()-150)
|
self.search.setMaximumWidth(self.width()-150)
|
||||||
self.action_focus_search = QAction(self)
|
self.action_focus_search = QAction(self)
|
||||||
shortcuts = QKeySequence.keyBindings(QKeySequence.Find)
|
shortcuts = list(
|
||||||
shortcuts = list(shortcuts) + [QKeySequence('/'), QKeySequence('Alt+S')]
|
map(lambda x:unicode(x.toString()),
|
||||||
self.action_focus_search.setShortcuts(shortcuts)
|
QKeySequence.keyBindings(QKeySequence.Find)))
|
||||||
|
shortcuts += ['/', 'Alt+S']
|
||||||
|
self.keyboard.register_shortcut('start search', _('Start search'),
|
||||||
|
default_keys=shortcuts, action=self.action_focus_search)
|
||||||
self.action_focus_search.triggered.connect(self.focus_search_box)
|
self.action_focus_search.triggered.connect(self.focus_search_box)
|
||||||
self.addAction(self.action_focus_search)
|
self.addAction(self.action_focus_search)
|
||||||
self.search.setStatusTip(re.sub(r'<\w+>', ' ',
|
self.search.setStatusTip(re.sub(r'<\w+>', ' ',
|
||||||
|
@ -16,7 +16,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
|
from PyQt4.Qt import (Qt, SIGNAL, QTimer, QHelpEvent, QAction,
|
||||||
QMenu, QIcon, pyqtSignal, QUrl,
|
QMenu, QIcon, pyqtSignal, QUrl,
|
||||||
QDialog, QSystemTrayIcon, QApplication, QKeySequence)
|
QDialog, QSystemTrayIcon, QApplication)
|
||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import __appname__, isosx
|
from calibre.constants import __appname__, isosx
|
||||||
@ -40,6 +40,7 @@ from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
|||||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||||
from calibre.gui2.tag_browser.ui import TagBrowserMixin
|
from calibre.gui2.tag_browser.ui import TagBrowserMixin
|
||||||
|
from calibre.gui2.keyboard import Manager
|
||||||
|
|
||||||
|
|
||||||
class Listener(Thread): # {{{
|
class Listener(Thread): # {{{
|
||||||
@ -104,6 +105,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
def __init__(self, opts, parent=None, gui_debug=None):
|
def __init__(self, opts, parent=None, gui_debug=None):
|
||||||
global _gui
|
global _gui
|
||||||
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
|
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
|
||||||
|
self.keyboard = Manager(self)
|
||||||
_gui = self
|
_gui = self
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.device_connected = None
|
self.device_connected = None
|
||||||
@ -238,7 +240,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.eject_action.setEnabled(False)
|
self.eject_action.setEnabled(False)
|
||||||
self.addAction(self.quit_action)
|
self.addAction(self.quit_action)
|
||||||
self.system_tray_menu.addAction(self.quit_action)
|
self.system_tray_menu.addAction(self.quit_action)
|
||||||
self.quit_action.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q))
|
self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
|
||||||
|
default_keys=('Ctrl+Q',), action=self.quit_action)
|
||||||
self.system_tray_icon.setContextMenu(self.system_tray_menu)
|
self.system_tray_icon.setContextMenu(self.system_tray_menu)
|
||||||
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
|
||||||
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
self.connect(self.donate_action, SIGNAL('triggered(bool)'), self.donate)
|
||||||
@ -249,7 +252,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
|
|
||||||
self.esc_action = QAction(self)
|
self.esc_action = QAction(self)
|
||||||
self.addAction(self.esc_action)
|
self.addAction(self.esc_action)
|
||||||
self.esc_action.setShortcut(QKeySequence(Qt.Key_Escape))
|
self.keyboard.register_shortcut('clear current search',
|
||||||
|
_('Clear the current search'), default_keys=('Esc',),
|
||||||
|
action=self.esc_action)
|
||||||
self.esc_action.triggered.connect(self.esc)
|
self.esc_action.triggered.connect(self.esc)
|
||||||
|
|
||||||
####################### Start spare job server ########################
|
####################### Start spare job server ########################
|
||||||
@ -340,6 +345,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
raise
|
raise
|
||||||
self.device_manager.set_current_library_uuid(db.library_id)
|
self.device_manager.set_current_library_uuid(db.library_id)
|
||||||
|
|
||||||
|
self.keyboard.finalize()
|
||||||
|
|
||||||
# Collect cycles now
|
# Collect cycles now
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
@ -1063,9 +1063,16 @@ class Splitter(QSplitter):
|
|||||||
self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label,
|
self.action_toggle = QAction(QIcon(icon), _('Toggle') + ' ' + label,
|
||||||
self)
|
self)
|
||||||
self.action_toggle.triggered.connect(self.toggle_triggered)
|
self.action_toggle.triggered.connect(self.toggle_triggered)
|
||||||
self.action_toggle.setShortcut(shortcut)
|
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
parent.addAction(self.action_toggle)
|
parent.addAction(self.action_toggle)
|
||||||
|
if hasattr(parent, 'keyboard'):
|
||||||
|
parent.keyboard.register_shortcut('splitter %s %s'%(name,
|
||||||
|
label), unicode(self.action_toggle.text()),
|
||||||
|
default_keys=(shortcut,), action=self.action_toggle)
|
||||||
|
else:
|
||||||
|
self.action_toggle.setShortcut(shortcut)
|
||||||
|
else:
|
||||||
|
self.action_toggle.setShortcut(shortcut)
|
||||||
|
|
||||||
def toggle_triggered(self, *args):
|
def toggle_triggered(self, *args):
|
||||||
self.toggle_side_pane()
|
self.toggle_side_pane()
|
||||||
|
@ -121,6 +121,9 @@ def config(defaults=None):
|
|||||||
help=_('Convert paths to lowercase.'))
|
help=_('Convert paths to lowercase.'))
|
||||||
x('replace_whitespace', default=False,
|
x('replace_whitespace', default=False,
|
||||||
help=_('Replace whitespace with underscores.'))
|
help=_('Replace whitespace with underscores.'))
|
||||||
|
x('single_dir', default=False,
|
||||||
|
help=_('Save into a single directory, ignoring the template'
|
||||||
|
' directory structure'))
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def preprocess_template(template):
|
def preprocess_template(template):
|
||||||
@ -131,7 +134,7 @@ def preprocess_template(template):
|
|||||||
template = template.decode(preferred_encoding, 'replace')
|
template = template.decode(preferred_encoding, 'replace')
|
||||||
return template
|
return template
|
||||||
|
|
||||||
class SafeFormat(TemplateFormatter):
|
class Formatter(TemplateFormatter):
|
||||||
'''
|
'''
|
||||||
Provides a format function that substitutes '' for any missing value
|
Provides a format function that substitutes '' for any missing value
|
||||||
'''
|
'''
|
||||||
@ -165,7 +168,7 @@ class SafeFormat(TemplateFormatter):
|
|||||||
|
|
||||||
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||||
sanitize_func=ascii_filename, replace_whitespace=False,
|
sanitize_func=ascii_filename, replace_whitespace=False,
|
||||||
to_lowercase=False):
|
to_lowercase=False, safe_format=True):
|
||||||
|
|
||||||
tsorder = tweaks['save_template_title_series_sorting']
|
tsorder = tweaks['save_template_title_series_sorting']
|
||||||
format_args = FORMAT_ARGS.copy()
|
format_args = FORMAT_ARGS.copy()
|
||||||
@ -225,8 +228,11 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
|||||||
format_args[key] = unicode(format_args[key])
|
format_args[key] = unicode(format_args[key])
|
||||||
else:
|
else:
|
||||||
format_args[key] = ''
|
format_args[key] = ''
|
||||||
components = SafeFormat().safe_format(template, format_args,
|
if safe_format:
|
||||||
|
components = Formatter().safe_format(template, format_args,
|
||||||
'G_C-EXCEPTION!', mi)
|
'G_C-EXCEPTION!', mi)
|
||||||
|
else:
|
||||||
|
components = Formatter().unsafe_format(template, format_args, mi)
|
||||||
components = [x.strip() for x in components.split('/')]
|
components = [x.strip() for x in components.split('/')]
|
||||||
components = [sanitize_func(x) for x in components if x]
|
components = [sanitize_func(x) for x in components if x]
|
||||||
if not components:
|
if not components:
|
||||||
@ -283,10 +289,20 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
|
|||||||
if not formats:
|
if not formats:
|
||||||
return True, id_, mi.title
|
return True, id_, mi.title
|
||||||
|
|
||||||
components = get_components(opts.template, mi, id_, opts.timefmt, length,
|
try:
|
||||||
|
components = get_components(opts.template, mi, id_, opts.timefmt, length,
|
||||||
ascii_filename if opts.asciiize else sanitize_file_name_unicode,
|
ascii_filename if opts.asciiize else sanitize_file_name_unicode,
|
||||||
to_lowercase=opts.to_lowercase,
|
to_lowercase=opts.to_lowercase,
|
||||||
replace_whitespace=opts.replace_whitespace)
|
replace_whitespace=opts.replace_whitespace, safe_format=False)
|
||||||
|
except Exception, e:
|
||||||
|
raise ValueError(_('Failed to calculate path for '
|
||||||
|
'save to disk. Template: %s\n'
|
||||||
|
'Error: %s'%(opts.template, e)))
|
||||||
|
if opts.single_dir:
|
||||||
|
components = components[-1:]
|
||||||
|
if not components:
|
||||||
|
raise ValueError(_('Template evaluation resulted in no'
|
||||||
|
' path components. Template: %s')%opts.template)
|
||||||
base_path = os.path.join(root, *components)
|
base_path = os.path.join(root, *components)
|
||||||
base_name = os.path.basename(base_path)
|
base_name = os.path.basename(base_path)
|
||||||
dirpath = os.path.dirname(base_path)
|
dirpath = os.path.dirname(base_path)
|
||||||
|
@ -547,6 +547,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
|
|||||||
- Toggle jobs list
|
- Toggle jobs list
|
||||||
* - :kbd:`Alt+Shift+B`
|
* - :kbd:`Alt+Shift+B`
|
||||||
- Toggle Cover Browser
|
- Toggle Cover Browser
|
||||||
|
* - :kbd:`Alt+Shift+B`
|
||||||
|
- Toggle Book Details panel
|
||||||
* - :kbd:`Alt+Shift+T`
|
* - :kbd:`Alt+Shift+T`
|
||||||
- Toggle Tag Browser
|
- Toggle Tag Browser
|
||||||
* - :kbd:`Alt+A`
|
* - :kbd:`Alt+A`
|
||||||
|
@ -273,7 +273,9 @@ The following functions are available in addition to those described in single-f
|
|||||||
* ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole.
|
* ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole.
|
||||||
* ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting.
|
* ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting.
|
||||||
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
|
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
|
||||||
|
* ``strcat_max(max, string1, prefix2, string2, ...)`` -- Returns a string formed by concatenating the arguments. The returned value is initialized to string1. `Prefix, string` pairs are added to the end of the value as long as the resulting string length is less than `max`. String1 is returned even if string1 is longer than max. You can pass as many `prefix, string` pairs as you wish.
|
||||||
* ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
|
* ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
|
||||||
|
* ``strlen(a)`` -- Returns the length of the string passed as the argument.
|
||||||
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
|
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
|
||||||
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
|
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
|
||||||
* ``today()`` -- return a date string for today. This value is designed for use in format_date or days_between, but can be manipulated like any other string. The date is in ISO format.
|
* ``today()`` -- return a date string for today. This value is designed for use in format_date or days_between, but can be manipulated like any other string. The date is in ISO format.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -310,7 +310,16 @@ class TemplateFormatter(string.Formatter):
|
|||||||
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
||||||
return self.compress_spaces.sub(' ', ans).strip()
|
return self.compress_spaces.sub(' ', ans).strip()
|
||||||
|
|
||||||
########## a formatter guaranteed not to throw and exception ############
|
########## a formatter that throws exceptions ############
|
||||||
|
|
||||||
|
def unsafe_format(self, fmt, kwargs, book):
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.book = book
|
||||||
|
self.composite_values = {}
|
||||||
|
self.locals = {}
|
||||||
|
return self.vformat(fmt, [], kwargs).strip()
|
||||||
|
|
||||||
|
########## a formatter guaranteed not to throw an exception ############
|
||||||
|
|
||||||
def safe_format(self, fmt, kwargs, error_value, book):
|
def safe_format(self, fmt, kwargs, error_value, book):
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
@ -136,6 +136,19 @@ class BuiltinStrcat(BuiltinFormatterFunction):
|
|||||||
res += args[i]
|
res += args[i]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
class BuiltinStrlen(BuiltinFormatterFunction):
|
||||||
|
name = 'strlen'
|
||||||
|
arg_count = 1
|
||||||
|
category = 'String Manipulation'
|
||||||
|
__doc__ = doc = _('strlen(a) -- Returns the length of the string passed as '
|
||||||
|
'the argument')
|
||||||
|
|
||||||
|
def evaluate(self, formatter, kwargs, mi, locals, a):
|
||||||
|
try:
|
||||||
|
return len(a)
|
||||||
|
except:
|
||||||
|
return -1
|
||||||
|
|
||||||
class BuiltinAdd(BuiltinFormatterFunction):
|
class BuiltinAdd(BuiltinFormatterFunction):
|
||||||
name = 'add'
|
name = 'add'
|
||||||
arg_count = 2
|
arg_count = 2
|
||||||
@ -345,6 +358,40 @@ class BuiltinSwitch(BuiltinFormatterFunction):
|
|||||||
return args[i+1]
|
return args[i+1]
|
||||||
i += 2
|
i += 2
|
||||||
|
|
||||||
|
class BuiltinStrcatMax(BuiltinFormatterFunction):
|
||||||
|
name = 'strcat_max'
|
||||||
|
arg_count = -1
|
||||||
|
category = 'String Manipulation'
|
||||||
|
__doc__ = doc = _('strcat_max(max, string1, prefix2, string2, ...) -- '
|
||||||
|
'Returns a string formed by concatenating the arguments. The '
|
||||||
|
'returned value is initialized to string1. `Prefix, string` '
|
||||||
|
'pairs are added to the end of the value as long as the '
|
||||||
|
'resulting string length is less than `max`. String1 is returned '
|
||||||
|
'even if string1 is longer than max. You can pass as many '
|
||||||
|
'`prefix, string` pairs as you wish.')
|
||||||
|
|
||||||
|
def evaluate(self, formatter, kwargs, mi, locals, *args):
|
||||||
|
if len(args) < 2:
|
||||||
|
raise ValueError(_('strcat_max requires 2 or more arguments'))
|
||||||
|
if (len(args) % 2) != 0:
|
||||||
|
raise ValueError(_('strcat_max requires an even number of arguments'))
|
||||||
|
try:
|
||||||
|
max = int(args[0])
|
||||||
|
except:
|
||||||
|
raise ValueError(_('first argument to strcat_max must be an integer'))
|
||||||
|
|
||||||
|
i = 2
|
||||||
|
result = args[1]
|
||||||
|
try:
|
||||||
|
while i < len(args):
|
||||||
|
if (len(result) + len(args[i]) + len(args[i+1])) > max:
|
||||||
|
break
|
||||||
|
result = result + args[i] + args[i+1]
|
||||||
|
i += 2
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result.strip()
|
||||||
|
|
||||||
class BuiltinInList(BuiltinFormatterFunction):
|
class BuiltinInList(BuiltinFormatterFunction):
|
||||||
name = 'in_list'
|
name = 'in_list'
|
||||||
arg_count = 5
|
arg_count = 5
|
||||||
@ -432,7 +479,7 @@ class BuiltinSwapAroundComma(BuiltinFormatterFunction):
|
|||||||
'returns val unchanged')
|
'returns val unchanged')
|
||||||
|
|
||||||
def evaluate(self, formatter, kwargs, mi, locals, val):
|
def evaluate(self, formatter, kwargs, mi, locals, val):
|
||||||
return re.sub(r'^(.*?),(.*$)', r'\2 \1', val, flags=re.I)
|
return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip()
|
||||||
|
|
||||||
class BuiltinIfempty(BuiltinFormatterFunction):
|
class BuiltinIfempty(BuiltinFormatterFunction):
|
||||||
name = 'ifempty'
|
name = 'ifempty'
|
||||||
@ -502,7 +549,7 @@ class BuiltinListitem(BuiltinFormatterFunction):
|
|||||||
index = int(index)
|
index = int(index)
|
||||||
val = val.split(sep)
|
val = val.split(sep)
|
||||||
try:
|
try:
|
||||||
return val[index]
|
return val[index].strip()
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -620,7 +667,8 @@ class BuiltinSublist(BuiltinFormatterFunction):
|
|||||||
return ''
|
return ''
|
||||||
si = int(start_index)
|
si = int(start_index)
|
||||||
ei = int(end_index)
|
ei = int(end_index)
|
||||||
val = val.split(sep)
|
# allow empty list items so counts are what the user expects
|
||||||
|
val = [v.strip() for v in val.split(sep)]
|
||||||
try:
|
try:
|
||||||
if ei == 0:
|
if ei == 0:
|
||||||
return sep.join(val[si:])
|
return sep.join(val[si:])
|
||||||
@ -955,7 +1003,8 @@ _formatter_builtins = [
|
|||||||
BuiltinLowercase(), BuiltinMultiply(), BuiltinNot(),
|
BuiltinLowercase(), BuiltinMultiply(), BuiltinNot(),
|
||||||
BuiltinOndevice(), BuiltinOr(), BuiltinPrint(), BuiltinRawField(),
|
BuiltinOndevice(), BuiltinOr(), BuiltinPrint(), BuiltinRawField(),
|
||||||
BuiltinRe(), BuiltinSelect(), BuiltinShorten(), BuiltinStrcat(),
|
BuiltinRe(), BuiltinSelect(), BuiltinShorten(), BuiltinStrcat(),
|
||||||
BuiltinStrcmp(), BuiltinStrInList(), BuiltinSubitems(),
|
BuiltinStrcatMax(),
|
||||||
|
BuiltinStrcmp(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),
|
||||||
BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundComma(),
|
BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundComma(),
|
||||||
BuiltinSwitch(), BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(),
|
BuiltinSwitch(), BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(),
|
||||||
BuiltinToday(), BuiltinUppercase(),
|
BuiltinToday(), BuiltinUppercase(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user