calibre/src/pyj/session.pyj
2024-03-11 08:53:43 +05:30

415 lines
16 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
from uuid import short_uuid
from ajax import ajax_send
all_settings = {
'copy_to_library_dupes': {'default': 'add;overwrite', 'category': 'book_list'},
'last_sort_order': {'default': {}, 'category': 'book_list'},
'show_all_metadata': {'default': False, 'category': 'book_list'}, # show all metadata fields in the book details panel
'sort': {'default': 'timestamp.desc', 'category': 'book_list'}, # comma separated list of items of the form: field.order
'view_mode': {'default': 'cover_grid', 'category': 'book_list'},
'fts_related_words': {'default': True, 'category': 'book_list'},
# Tag Browser settings
'and_search_terms': {'default': False, 'category': 'tag_browser'}, # how to add search terms to the search expression from the Tag Browser
'collapse_at': {'default': 25, 'category': 'tag_browser'}, # number of items at which sub-groups are created, 0 to disable
'dont_collapse': {'default': '', 'category': 'tag_browser'}, # comma separated list of category names
'hide_empty_categories': {'default': 'no', 'category': 'tag_browser'},
'partition_method': {'default': 'first letter', 'category': 'tag_browser'}, # other choices: 'disable', 'partition'
'sort_tags_by': {'default': 'name', 'category': 'tag_browser'}, # other choices: popularity, rating
# Book reader settings
'background_image_fade': {'default': 0, 'category': 'read_book', 'is_local': True},
'background_image_style': {'default': 'scaled', 'category': 'read_book', 'is_local': True},
'background_image': {'default': None, 'category': 'read_book', 'is_local': True},
'base_font_size': {'default': 16, 'category': 'read_book', 'is_local': True},
'book_scrollbar': {'default': False, 'category': 'read_book'},
'columns_per_screen': {'default': {'portrait':0, 'landscape':0}, 'category': 'read_book', 'is_local': True},
'controls_help_shown_count': {'default': 0, 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
'controls_help_shown_count_rtl_page_progression': {'default': 0, 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
'cover_preserve_aspect_ratio': {'default': True, 'category': 'read_book'},
'current_color_scheme': {'default': 'system', 'category': 'read_book'},
'footer': {'default': {'right': 'progress'}, 'category': 'read_book'},
'header': {'default': {}, 'category': 'read_book'},
'controls_footer': {'default': {'right': 'progress'}, 'category': 'read_book'},
'left-margin': {'default': {}, 'category': 'read_book'},
'right-margin': {'default': {}, 'category': 'read_book'},
'hide_tooltips': {'default': False, 'category': 'read_book'},
'keyboard_shortcuts': {'default': {}, 'category': 'read_book'},
'lines_per_sec_auto': {'default': 1, 'category': 'read_book', 'is_local': True},
'lines_per_sec_smooth': {'default': 20, 'category': 'read_book', 'is_local': True},
'margin_bottom': {'default': 20, 'category': 'read_book', 'is_local': True},
'margin_left': {'default': 20, 'category': 'read_book', 'is_local': True},
'margin_right': {'default': 20, 'category': 'read_book', 'is_local': True},
'margin_top': {'default': 20, 'category': 'read_book', 'is_local': True},
'max_text_height': {'default': 0, 'category': 'read_book', 'is_local': True},
'max_text_width': {'default': 0, 'category': 'read_book', 'is_local': True},
'override_book_colors': {'default': 'never', 'category': 'read_book'},
'paged_margin_clicks_scroll_by_screen': {'default': True, 'category': 'read_book'},
'paged_wheel_scrolls_by_screen': {'default': False, 'category': 'read_book'},
'paged_wheel_section_jumps': {'default': True, 'category': 'read_book'},
'paged_pixel_scroll_threshold': {'default': 60, 'category': 'read_book'},
'read_mode': {'default': 'paged', 'category': 'read_book', 'is_local': True},
'scroll_auto_boundary_delay': {'default': 5, 'category': 'read_book', 'is_local': True},
'scroll_stop_boundaries': {'default': False, 'category': 'read_book', 'is_local': True},
'standalone_font_settings': {'default': {}, 'category': 'read_book', 'is_local': True},
'standalone_misc_settings': {'default': {}, 'category': 'read_book', 'is_local': True},
'standalone_recently_opened': {'default': v'[]', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
'user_color_schemes': {'default': {}, 'category': 'read_book'},
'user_stylesheet': {'default': '', 'category': 'read_book', 'is_local': True},
'word_actions': {'default': v'[]', 'category': 'read_book'},
'highlight_style': {'default': None, 'category': 'read_book', 'is_local': True},
'highlights_export_format': {'default': 'text', 'category': 'read_book', 'is_local': True},
'custom_highlight_styles': {'default': v'[]', 'category': 'read_book'},
'show_selection_bar': {'default': True, 'category': 'read_book'},
'net_search_url': {'default': 'https://google.com/search?q={q}', 'category': 'read_book'},
'selection_bar_actions': {'default': v"['copy', 'lookup', 'highlight', 'remove_highlight', 'search_net', 'clear']", 'category': 'read_book'},
'selection_bar_quick_highlights': {'default': v"[]", 'category': 'read_book'},
'skipped_dialogs': {'default': v'{}', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
'tts': {'default': v'{}', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
'tts_backend': {'default': v'{}', 'category': 'read_book', 'is_local': True, 'disallowed_in_profile': True},
'fullscreen_when_opening': {'default': 'auto', 'category': 'read_book', 'is_local': True},
'book_search_mode': {'default': 'contains', 'category': 'read_book', 'is_local': True},
'book_search_case_sensitive': {'default': False, 'category': 'read_book', 'is_local': True},
'reverse_page_turn_zones': {'default': False, 'category': 'read_book', 'is_local': True},
'gesture_overrides': {'default': {}, 'category': 'read_book'},
}
defaults = {}
for x in Object.entries(all_settings):
defaults[x[0]] = x[1].default
def session_defaults():
return defaults
def storage_available(which):
which = which or 'localStorage'
try:
storage = window[which]
x = '__storage__test__'
storage.setItem(x, x)
storage.removeItem(x)
return True
except:
return False
class FakeStorage:
def __init__(self):
self.data = {}
def getItem(self, key):
return self.data[key]
def setItem(self, key, value):
if jstype(value) is not 'string':
value = JSON.stringify(value)
self.data[key] = value
def clear(self):
self.data = {}
def get_session_storage():
if not get_session_storage.ans:
if storage_available('localStorage'):
get_session_storage.ans = window.localStorage
elif storage_available('sessionStorage'):
get_session_storage.ans = window.sessionStorage
console.error('localStorage not available using sessionStorage instead')
else:
get_session_storage.ans = FakeStorage()
console.error('sessionStorage and localStorage not available using a temp cache instead')
return get_session_storage.ans
class SessionData:
def __init__(self, global_prefix=None):
self.global_prefix = global_prefix or 'calibre-session-'
self.storage = get_session_storage()
self.overflow_storage = {}
self.has_overflow = False
def get(self, key, defval):
key = self.global_prefix + key
if self.has_overflow:
ans = self.overflow_storage[key]
if ans is undefined:
ans = self.storage.getItem(key)
else:
ans = self.storage.getItem(key)
if ans is undefined or ans is None:
if defval is undefined:
defval = None
return defval
try:
return JSON.parse(ans)
except:
if defval is undefined:
defval = None
return defval
def set(self, key, value):
key = self.global_prefix + key
if value is None:
self.storage.removeItem(key)
v'delete self.overflow_storage[key]'
return True
value = JSON.stringify(value)
try:
self.storage.setItem(key, value)
v'delete self.overflow_storage[key]'
return True
except:
self.overflow_storage[key] = value
self.has_overflow = True
console.error('session storage has overflowed, using a temp cache instead')
return False
def set_bulk(self, changes):
for key in Object.keys(changes):
self.set(key, changes[key])
def clear(self):
self.storage.clear()
self.overflow_storage = {}
self.has_overflow = False
def local_storage():
if not local_storage.storage:
local_storage.storage = SessionData('calibre-local-')
return local_storage.storage
def get_device_uuid():
if not get_device_uuid.ans:
s = local_storage()
ans = s.get('device_uuid')
if not ans:
ans = short_uuid()
s.set('device_uuid', ans)
get_device_uuid.ans = ans
return get_device_uuid.ans
def deep_eq(a, b):
if a is b:
return True
if Array.isArray(a) and Array.isArray(b):
if a.length is not b.length:
return False
for i in range(a.length):
if not deep_eq(a[i], b[i]):
return False
return True
if a is None or b is None or a is undefined or b is undefined:
return False
if jstype(a) is not 'object' or jstype(b) is not 'object':
return False
ka = Object.keys(a)
if ka.length is not Object.keys(b).length:
return False
for key in ka:
if not deep_eq(a[key], b[key]):
return False
return True
def setting_val_is_default(setting_name, curval):
defval = defaults[setting_name]
return deep_eq(defval, curval)
def get_subset_of_settings(sd, filter_func):
ans = {}
for setting_name in Object.keys(all_settings):
curval = sd.get(setting_name, ans)
metadata = all_settings[setting_name]
is_set = curval is not ans
if filter_func(setting_name, metadata, is_set, curval):
ans[setting_name] = window.structuredClone(curval if is_set else metadata.default)
return ans
def apply_profile(sd, profile, filter_func):
changes = {}
for setting_name in Object.keys(all_settings):
metadata = all_settings[setting_name]
curval = sd.get(setting_name, changes)
is_set = curval is not changes
if filter_func(setting_name, metadata, is_set, curval):
newval = None
if Object.prototype.hasOwnProperty.call(profile, setting_name):
newval = profile[setting_name]
if deep_eq(newval, metadata.default):
newval = None
changes[setting_name] = window.structuredClone(newval)
sd.set_bulk(changes)
return changes
standalone_reader_defaults = {
'remember_window_geometry': False,
'remember_last_read': True,
'show_actions_toolbar': False,
'show_actions_toolbar_in_fullscreen': False,
'save_annotations_in_ebook': True,
'sync_annots_user': '',
'singleinstance': False,
'auto_hide_mouse': True,
'restore_docks': True,
}
def settings_for_reader_profile(sd):
margin_text_settings = {'footer': True, 'header': True, 'controls_footer': True, 'left-margin': True, 'right-margin': True}
def filter_func(setting_name, metadata, is_set, curval):
if margin_text_settings[setting_name]:
curval = window.structuredClone(curval)
for x in v"['left', 'right', 'middle']":
if curval[x] is 'empty':
v'delete curval[x]'
elif setting_name is 'standalone_misc_settings':
curval = window.structuredClone(curval)
for key in Object.keys(curval):
if curval[key] is standalone_reader_defaults[key]:
v'delete curval[key]'
if not is_set or metadata.category is not 'read_book' or metadata.disallowed_in_profile or deep_eq(curval, metadata.default):
return False
return True
return get_subset_of_settings(sd, filter_func)
def apply_reader_profile(sd, profile):
def filter_func(setting_name, metadata, is_set, curval):
return metadata.category is 'read_book' and not metadata.disallowed_in_profile
apply_profile(sd, profile, filter_func)
default_interface_data = {
'username': None,
'output_format': 'EPUB',
'input_formats': {'EPUB', 'MOBI', 'AZW3'},
'gui_pubdate_display_format': 'MMM yyyy',
'gui_timestamp_display_format': 'dd MMM yyyy',
'gui_last_modified_display_format': 'dd MMM yyyy',
'use_roman_numerals_for_series_number': True,
'default_library_id': None,
'default_book_list_mode': session_defaults().view_mode,
'library_map': None,
'search_the_net_urls': [],
'donate_link': 'https://calibre-ebook.com/donate',
'icon_map': {},
'icon_path': '',
'custom_list_template': None,
'num_per_page': 50,
'lang_code_for_user_manual': '',
}
def get_interface_data():
if not get_interface_data.storage:
get_interface_data.storage = SessionData('calibre-interface-data-')
ans = get_interface_data.storage.get('current')
if ans:
ans.is_default = False
else:
ans = {'is_default': True}
for k in default_interface_data:
ans[k] = default_interface_data[k]
return ans
def update_interface_data(new_data):
data = get_interface_data()
for k in default_interface_data:
nval = new_data[k]
if k is not undefined:
data[k] = nval
if not get_interface_data.storage:
get_interface_data.storage = SessionData('calibre-interface-data-')
get_interface_data.storage.set('current', data)
def get_translations(newval):
if not get_translations.storage:
get_translations.storage = SessionData('calibre-translations-')
if newval?:
get_translations.storage.set('current', newval)
else:
return get_translations.storage.get('current')
def is_setting_local(name):
m = all_settings[name]
return m.is_local if m else True
class UserSessionData(SessionData):
def __init__(self, username, saved_data):
self.prefix = (username or '') + '-'
self.has_user = bool(username)
self.username = username
SessionData.__init__(self)
self.echo_changes = False
self.changes = {}
self.has_changes = False
self.push_timer_id = None
if saved_data:
for key in saved_data:
if not is_setting_local(key):
self.set(key, saved_data[key])
self.echo_changes = True
def defval(self, key):
ans = session_defaults()[key]
if ans:
ans = window.structuredClone(ans)
return ans
def get(self, key, defval):
if defval is undefined:
defval = self.defval(key)
return SessionData.get(self, (self.prefix + key), defval)
def get_library_option(self, library_id, key, defval):
if not library_id:
return self.get(key, defval)
lkey = key + '-||-' + library_id
if defval is undefined:
defval = self.defval(key)
return self.get(lkey, defval)
def set(self, key, value):
if self.echo_changes and self.has_user and not is_setting_local(key):
self.changes[key] = value
self.has_changes = True
if self.push_timer_id is not None:
clearTimeout(self.push_timer_id)
self.push_timer_id = setTimeout(self.push_to_server.bind(self), 1000)
return SessionData.set(self, (self.prefix + key), value)
def set_library_option(self, library_id, key, value):
if library_id:
key = key + '-||-' + library_id
return self.set(key, value)
def push_to_server(self):
if self.has_changes:
ajax_send('interface-data/set-session-data', self.changes, def(end_type, xhr, ev):
if end_type is not 'load':
console.error('Failed to send session data to server: ' + xhr.error_html)
)
self.changes = {}
self.has_changes = False