# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2015, Kovid Goyal 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