[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# custom\n*cache\n*.pyproj\n*Qt*\n*__pycache__*\nversion/build\nversion/dist\n*.sln\n*Thumbs.db\nsettings.json\nbuild\ndist\nHappypanda\nsetup.py\n*.ini\n*.swp\n.DS_Store\ndownloads\n.happypanda\n# pycharm\n.idea/\n\n# User-specific files\n*.suo\n*.user\n*.sln.docstates\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbuild/\nbld/\n[Bb]in/\n[Oo]bj/\n\n# Roslyn cache directories\n*.ide/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n#NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opensdf\n*.sdf\n*.cachefile\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding addin-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings \n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# If using the old MSBuild-Integrated Package Restore, uncomment this:\n#!**/packages/repositories.config\n\n# Windows Azure Build Output\ncsx/\n*.build.csdef\n\n# Windows Store app package directory\nAppPackages/\n\n# Others\nsql/\n*.Cache\nClientBin/\n[Ss]tyle[Cc]op.*\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.pfx\n*.publishsettings\nnode_modules/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n/db\n/thumbnails\n/version/gui/static/gallery_def_ico.ico\n/version/gui/static/gallery_ext_ico.ico\n*.pyperf\n/temp\n/main.build\n/main.dist\n/version/db\n/test_g.zip\n/version/temp\n/version/modeltest.py\n/[Yamatogawa] Power Play!.zip\n/test.zip\n/test.py\n/setup.spec\n/roundcorner.png\n/nuitka.txt\n/models.py\n/info.txt\n/info.json\n/horopic2.jpg\n/happypanda-2015-11-10 23-51-59.hpdb\n/happypanda-2015-11-10 21-29-04.hpdb\n/happypanda-2015-11-09 21-08-42.hpdb\n/failg.zip\n/exHtmlRandomFileName123.html\n/deploy.bat\n/Attameaikko.01.zip\n/[Yamatogawa] Power Play!.zip\n/[Yamatogawa] Power Play!.zip\n*.zip\n/res/typicons\n/res/gallery_ext_ico.ico\n/res/sample.png\n*.hpdb\n/plugins\n/compilerconfig.json\n/compilerconfig.json.defaults\n/happypanda.VC.db\n/happypanda.VC.VC.opendb\n/.vs/config/applicationhost.config\n/data/happypanda.db\n/main.spec\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "*Happypanda v1.1*\n- Fixes\n   - Fixed HP settings unusable without internet connection\n   \n*Happypanda v1.0*\n* New stuff\n\t- New GUI look\n\t- New helpful color widgets added to `Settings -> Visual` [rachmadaniHaryono]\n\t- Gallery Contextmenu:\n\t\t- Added `Set rating` to quickly set gallery rating\n\t\t- Added `Lookup Artist` to open a new tab on preffered site with the artist's galleries\n\t\t- Added `Reset read count` under `Advanced`\n\t- Gallery Lists are now included when exporting gallery data\n\t- New sorting option: Gallery Rating\n\t- It is now possible to also append tags to galleries instead of replacing when editing\n\t- New gallery download source: `asmhentai.com` [rachmadaniHaryono]\n\t- New [special namespaced tag](https://github.com/Pewpews/happypanda/wiki/Gallery-Searching#special-namespaced-tags): `URL`\n\t\t- Use like this: `url:none` or `url:g.e-hentai`\n\t- Many quality of life changes\n\n* Changed stuff\n\t- `g.e-hentai` to `e-hentai`\n\t\t- Old URLs will automatically be converted to new on metadata fetch\n\t- Displaying rating on galleries is now optional\n\t- Improved search history\n\t- Improved gallery downloader (now very reliable) [rachmadaniHaryono]\n\t- Galleries will automatically be rated 5 stars on favorite\n\t- Gallery List edit popup will now appear in the middle of the application\n\t- Added a way to relogin into website\n\n* Fixes\n\t- E-Hentai login & gallery downloading\n\t- `date added` metadata wasn't included when exporting gallery data\n\t- `last read` Metadata wasn't included when importing gallery data\n\t- backup database name would get unusually long [rachmadaniHaryono]\n\t- Fixed HDoujin `info.txt` parsing\n\t- Newly downloaded galleries would sometimes cause a crash\n\t- Attempting to download exhentai links without being logged in would cause a crash\n\t- Using the random gallery opener would in rare cases cause a crash\n\t- Moving a gallery would cause a crash from a raised PermissionError exception\n\t- Fetching metadata for galleries would return multiple unrelated galleries to choose among\n\t- Fetching metadata for galleries with a colored cover whose gallery source is an archive would sometimes cause a crash\n\t- Galleries with an empty tag field wouldn't show up on a `tag:none` filter search\n\t- Gallery Deleted popup would appear when deleting gallery files from the application\n\t- Attempting to download removed galleries would cause a crash\n\t- Some gallery importing issues\n\n\n*Happypanda v0.30*\n- Someone finally convinced me into adding star ratings\n    - *Note:* Ratings won't be fetched from EH since I find them useless... Though I might make it an option later on. \n    - External viewer icon on galleries has been removed in favor of this\n- Visual make-over\n- Improved how thumbnails are loaded in gridview\n- Moving files into a monitored folder will now automatically add the galleries into your inbox \n- Added the following special namespaced tags:\n    - `path:none` to filter galleries that has a broken source\n    - `rating:integer` to filter galleries that has been rated `integer`\n    - read more about them [here](https://github.com/Pewpews/happypanda/wiki/Gallery-Searching)\n- Updated DB to version 0.26\n    - Added `Rating` metadata\n- Fixed bugs:\n    - Attempting to add galleries on a fresh install was causing an exception\n    - Moving files into a monitored folder, and then accepting the pop-up would cause an exception\n\n*Happypanda v0.29*\n- Increased and improved stability and perfomance\n- Shortened startup time\n- Galleries are now added dynamically\n- New feature: Tabs\n\t- New inbox tab for new gallery additions\n\t- Checking for duplicates will also make a new tab\n- Gallery deletion will now process smoothly\n- It is now possible to edit multiple galleries\n- Type and Langauge in metadata popup window are now clickable to issue a search\n- Updated DB to version 0.25\n\t- Added views in series table\n- Visual changes in gridview\n\t- Added a recently added indicator\n\t- Gallery filetypes will now be displayed with text\n- Added new options in settings\n\t- Removed option: autoadd scanned galleries\n- Fixed bugs:\n\t+ Fixed some metadata fetchings bugs\n\t+ Fixed database import and export issues\n\t+ Closing gallery metadata popup window caused an exception\n\t+ Fetching metadata with no internet connection caused an exception\n\t+ Invalid folders/archives were being picked up by the monitor\n\t+ Fixed default language issues\n\t+ Metadata would sometimes fail when doing a filesearch\n\t+ Thumbnail cache dir was not being cleared\n\t+ Adding from directory was not possible with single gallery add method\n\n*Happypanda v0.28.1*\n- Fixed bugs:\n\t+ Fixed typo in external viewer args\n\t+ Fixed regex not working when in namespace\n\t+ Fixed thumbnail generation causing an unhandled exception\n\t+ Moved directories kept their old path\n\t+ Fixed auto metdata fetcher failing when mixing galleries with colored covers and galleries with greyscale covers\n\t+ Gallery Metadata window wouldn't stay open\n\t+ Fixed a DB bug causing all kinds of errors, including:\n\t\t+ Editing a gallery while fetching its metadata would cause an exception\n\t+ Closing the gallery dialog while fetching metadata would cause an exception\n\n\n*Happypanda v0.28*\n- Improved perfomance of grid view significantly\n- Galleries are now draggable\n\t+ It is now possible to add galleries to a list by dragging them to the list\n- Improved metadata fetching accuracy from EH\n- Improved gallery lists with new options\n\t+ It is now possible to enable several search options on per gallerylist basis\n\t+ A new *Enforce* option to enforce the gallerylist filter\n- Improved gallery search\n\t+ New special namespaced tags: `read_count`, `date_added` and `last_read`\n\t\t+ Read more about them in the gallery searching guide\n\t+ New `<` less than and `>` greater than operator to be used with some special namespaced tags\n\t\t+ Read about it in the special namespaced tags section in the gallery searching guide\n- Brought back the old way of gaining access to EX\n\t- Only to be used if you can't gain access to EX by logging in normally\n- Added ability to specify arguments sent to viewer in settings (in the `Advanced` section)\n\t+ If when opening a gallery only the first image was viewable by your viewer, try change the arguments sent to the viewer\n- Updated the database to version 0.24 with the addition of new gallerylist fields\n- Moved regex search option to searchbar\n- Added grid spacing option in settings (`Visual->Grid View`)\n- Added folder and file extensions ignoring in settings (`Application->Ignore`)\n\t+ Folder and file extensions ignoring will work for the directory monitor and *Add gallery..* and *Populate from folder* gallery adding methods\n- Added new default language: Chinese\n- Improved and fixed URL parser in gallery-downloader\n- Custom languages will now be parsed from filenames along with the default languages\n- Tags are now sorted alphabetically everywhere\n- Gallerylists in contextmenu are also now sorted\n- Reason for why metdata fecthing failed is now shown in the failed-to-get-metadata-gallery popup\n- The current search term will now be upkeeped (upkept?) when switching between views\n- Disabled some tray messages on linux to prevent crash\n- The current gallerylist context will now be shown on the statusbar\n- The keys `del` and `shift + del` are now bound to gallery deletion\n- Added *exclude/include in auto metadata fetcher* in contextmenu for selection\n- Bug fixes:\n\t+ No thumbnails were created for images with incorrect extensions (namely png images with .jpg extension)\n\t+ Only accounts with access to EX were able to login\n\t+ Some filesystem events were not being detected\n\t+ Name parser was not parsing languages\n\t+ Some gallery attributes to not be added to the db on initial gallery creation\n\t+ Attempting to fetch metadata while an instance of auto metadata fetcher was already running caused an exception\n\t+ Gallery wasn't removed in view when removing from the duplicate-galleries popup\n\t+ Other minor bugs\n\n*Happypanda v0.27*\n- Many visual changes\n\t+ Including new ribbon indicating gallery type in gridview\n- New sidebar widget:\n\t+ New feature: Gallery lists\n\t+ New feature: Artists list\n\t+ Moved *NS & Tags* treelist from settings to sidebar widget\n- Metadata fetcher:\n\t+ Galleries with multiple hits found will now come last in the fetching process\n\t+ Added fallback system to fetch metadata from other sources than EH\n\t\t+ Currently supports panda.chaika.moe\n- Gallery downloader should now be more tolerant to mistakes in URLs\n- Added a \"gallery source is missing\" indicator in grid view\n- Removed EH member_id and pass_hash in favor for EH login method\n- Added new sort option: *last read*\n- Added option to exclude/include gallery from auto metadata fetcher in the contextmenu\n- Added general key shortcuts (read about the not so obvious shortcuts [here](https://github.com/Pewpews/happypanda/wiki/Keyboard-Shortcuts))\n- Added support for new metafile: *HDoujin downloader*'s default into.txt file\n- Added support for panda.chaika.moe URLs when fetching metadata\n- Updated database to version 0.23:\n\t- Gallery lists addition\n\t- New unique indexes in some tables\n\t- Thumbnail paths are now relative (removing the need to rebuild thumbs when moving Happypanda folder)\n- Settings:\n\t+ Added option to force support for high DPI displays\n\t+ Added option to control the gallery size in grid view\n\t+ Enabled most *Gallery* options in the *Visual* section for OSX\n\t+ Added options to customize gallery type ribbon colors\n\t+ Added options to set default gallery values\n\t+ Added a way to add custom languages in settings\n\t+ Added option to send deleted files  to recycle bin\n\t+ Added option to hide the sidebar widget on startup\n- Bug fixes:\n\t+ Fixed a bug causing some external viewers to only be able to view the first image\n\t+ Fixed metadata disappearance bug (hopefully, for real this time!)\n\t+ Fixed decoding issues preventing some galleries from getting imported\n\t+ Fixed lots of critical database issues requiring a rebuild for updating users\n\t+ Fixed gallery downloading from g.e-hentai\n\t+ Fixed bug causing \"Show in library\" to not work properly\n\t+ Fixed a bug causing a hang while fetching metadata\n\t+ Fixed a bug causing autometadata fetcher to sometimes fail fetching for some galleries\n\t+ Fixed hand when checking for duplicates\n\t+ Fixed database rebuild issues\n\t+ Potentially fixed a bug preventing archives from being imported, courtesy of KuroiKitsu\n\t+ Many other minor bugs\n\n*Happypanda v0.26*\n- Startup is now slighty faster\n- New redesigned gallery metadata window!\n\t+ New chapter view in the metadata window\n\t+ Artist field is now clickable to issue a search for galleries with same artist\n- Some GUI changes\n- New advanced gallery search **(make sure to read the search guide found in `Settings -> About -> Search Guide`)**\n\t+ Case sensitive searching\n\t+ Whole terms match searching\n\t+ Terms excluding\n\t+ New special namespaced tags (Read about them in `Settings -> About -> Search Guide`)\n- New import/export database feature found in `Settings -> About -> Database`\n- Added new column in `Skipped paths` window to show what reason caused a file to be skipped\n- Gallery downloader\n\t+ Added new batch urls window to gallery downloader\n\t+ Gallery downloading from `panda.chaika.moe` is now using its new api\n\t+ Added context menu's to download items\n\t+ Added download progress on download items\n\t+ Doubleclicking on finished download items will open its containing folder\n- Added autocomplete on the artist field in gallery edit dialog\n- Activated the `last read` attribute on galleries\n- Improved hash generation\n- Introducing metafiles:\n\t+ Files containing gallery metadata in same folder/archive is now detected on import\n\t+ Only supports [eze](https://github.com/dnsev-h/eze)'s `info.json` files for now\n- Settings\n\t+ Moved alot of options around. **Note: Some options will be reset**\n\t+ Reworded some options and fixed typos\n\t+ Enabled the `Database` tab in *About* section with import/export database feature\n- Updated the database to version 0.22\n\t+ Database will now be backed up before upgrading\n- Clicking on the tray icon ballon will now activate Happypanda\n- Thumbnail regenerating\n\t+ Added confirmation popup when about to regenerate thumbnails\n\t+ Application restart is no longer required after regenerating thumbnails\n\t+ Added confirmation popup asking about if the thumbnail cache should be cleaned before regenerating\n- Renamed `Random Gallery Opener` to `Open random gallery` and also moved it to the Gallery menu on the toolbar\n- `Open random gallery` will now only pick a random gallery in current view.\n\t+ *E.g. switching to the favorite view will make it pick a random gallery among the favorites*\n- Fixed bugs:\n\t+ Fixed a bug causing archives downloaded from g.e/ex to fail when trying to add to library\n\t+ Fixed a bug where fetching galleries from the database would sometimes throw an exception\n\t+ Fixed a bug causing people running from source to never see the new update notification\n\t+ Fixed some popup blur bug\n\t+ Fixed an annoyance where the text cursor would always move to the end when searching\n\t+ Fixed a bug where `Show in Folder` and `Open folder/archive` in gallery context menu was doing the same thing\n\t+ Fixed a bug where tags fetched from chaika included underscores\n\t+ Fixed bug where the notification widget would sometimes not show messages\n\t+ Fixed bug where chapters added to gallery with directory source would not open correctly\n\n*Happypanda v0.25*\n- Added *Show in folder* entry in gallery contextmenu\n- Gallery popups\n\t+ A contextmenu will now be shown when you rightclick a gallery\n\t+ Added *Skip* button in the metadata gallery chooser popup (the one asking you which gallery you want to extract metadata from)\n\t+ The text in metadata gallery chooser popups will now wrap\n\t+ Added tooltips displaying title and artist when hovering galleries in some popups\n- Settings\n\t+ A new button allowing you to recreate your thumbnail cache is now in *Settings* -> *Advanced* -> *Gallery*\n\t+ Added new tab *Downloader* in *Web* section\n\t+ Renamed *General* tab in *Web* section to *Metadata*\n\t+ Some options in settings will now show a tooltip explaining the option on hover\n- You can now go back to previous or to next search terms with the two new buttons beside the search bar (hidden until you actually search something)\n\t+ Back and Forward keys has been bound to these two buttons (very OS dependent but something like `ALT + LEFT ARROW` etc.) Back and Forward buttons on your mouse should also probably work (*shrugs*)\n\t+ Added *Use current gallery link* checkbox option in *Web* section\n- Toolbar\n\t+ Renamed *Misc* to *Tools*\n\t+ New *Scan for new galleries* entry in *Gallery*\n\t+ New *Gallery Downloader* entry in *Tools*\n- Gallery downloading\n\t+ Supports archive and torrent downloading\n\t+ archives will be automatically imported while torrents will be sent to your torrent client\n\t+ Currently supports ex/g.e gallery urls and panda.chaika.moe gallery/archive urls\n\t\t- Note: downloading archives from ex/g.e will be handled the same way as if you did it in your browser, i.e. it will cost you GP/credits.\n- Tray icon\n\t+ You can now manually check for a new update by right clicking on the tray icon\n\t+ Triggering the tray icon, i.e. clicking on it, will now activate (showing it) the Happypanda window\n- Fixed bugs:\n\t+ Fixed a bug where skipped galleries/paths would get moved\n\t+ Fixed a bug where gallery archives/folders containing images with `.jpeg` and/or capitalized (`.JPG`, etc.) extensions were treated as invalid gallery sources, or causing the program to behave very weird if they managed to get imported somehow\n\t+ Fixed a bug where you couldn't search with the Regex option turned on\n\t+ Fixed a bug where changing gallery covers would fail if the previous cover was not deleted or found.\n\t+ Fixed a bug where non-existent monitored folders were not detected\n\t+ Fixed a bug in the user settings (*settings.ini*) parsing, hence the reset\n\t+ Fixed other minor misc. bugs\n\n*Happypanda v0.24.1*\n- Fixed bugs:\n\t+ Removing a gallery and its files should now work\n\t+ Popups was staying on top of all windows\n\n*Happypanda v0.24*\n- Mostly gui fixes/improvements\n\t+ Changed toolbar style and icons\n\t+ Added new native spinners\n\t+ Added spinner for the metadata fetching process\n\t+ Added spinner for initial load\n\t+ Added spinner for DB activity\n\t+ Removed sort contextmenu and added it to the toolbar\n\t+ Removed some space around galleries in grid view\n\t+ Added kinetic scrolling when scrolling with middlemouse button\n- New DB Overview window and tab in settings dialog\n\t+ you can now see all namespaces and tags in the `Namespace and Tags` tab\n- Pressing the return-key will now open selected galleries\n- New options in settings dialog\n\t+ Make extracting archives before opening optional in `Application -> General`\n\t+ Open chapters sequentially or all at once in `Application -> General`\n- Added a confirmation when closing while there is still DB activity to avoid data loss\n- Added log file rotation\n\t+ When happypanda.log reaches `10 mb` a new file will be made (rotating between 3 files)\n- Fixed bugs:\n\t+ Temporarily fixed a critical bug where galleries wouldn't load\n\t+ Fixed a bug where the tray icon would stay even after closing the application\n\t+ Fixed a bug where clicking on a tag with no namespace in the Gallery Metdata Popup would search the tag with a blank namespace\n\t+ Fixed a minor bug where when opening the settings dialog a small window would appear first in a split second\n\n*Happypanda v0.23*\n- Stability and perfomance increase for very large libraries\n\t+ Instant startup: Galleries are now lazily loaded\n\t+ Application now supports very large galleries (tested with 10k galleries)\n\t+ Gallery searching will now scale with amount of galleries (means, no freezes when searching)\n\t+ Same with adding new galleries.\n- The gallery window appearing when you click on a gallery is now interactable\n\t+ Clicking on a link will open it in your default browser\n\t+ Clicking on a tag will search for the tag\n- Added some animation and a spinner\n- Fixed bugs:\n\t+ Fixed critical bug where slected galleries were not mapped properly. (Which sometimes resulted in wrong galleries being removed)\n\t+ Fixed a bug where pressing CTRL + A to select all galleries would tell that i has selected the total amount of galleries multipled by 3\n\t+ Fixed a bug where the notificationbar would sometiems not hide itself\n\t+ & other minor bugs\n\n*Happypanda v0.22*\n- Added support for .rar files.\n\t+ To enable rar support, specify the path to unrar in Settings -> Application -> General. Follow the instructions for your OS.\n- Fixed most (if not all) gallery importing issues\n- Added a way to populate form archive.\n\t+ Note: Subfolders will always be treated as galleries when populating from an archive.\n- Fixed a bug where users who tries Happypanda for the first time would see the 'rebuilding galleries' dialog.\n- & other misc. changes\n\n*Happypanda v0.21*\n- The application will now ask if you want to view skipped paths after searching for galleries\n- Added 'delete successful' in the notificationbar\n- Bugfixes:\n\t+ Fixed critical bug: Could not open chapters\n\t\t+ If your gallery still won't open then please try re-adding the gallery.\n\t+ Fixed bug: Covers for archives with no folder in-between were not being found\n\t+ & other minor bugs\n\n*Happypanda v0.20*\n- Added support for recursively importing of galleries (applies to archives)\n\t+ Directories in archives will now be noticed when importing\n\t+ Directories with archives as chapters will now be properly imported\n- Added drag and drop feature for directories and archives\n- Galleries that was unsuccesful during gallery fetching will now be displayed in a popup\n- Added support for directory or archive ignoring\n- Added support for changing gallery covers\n- Added: move imported galleries to a specified folder feature\n- Increased speed of Populate from folder and Add galleries...\n- Improved title parser to now remove unneecessary whitespaces\n- Improved gallery hashing to avoid unnecessary hashing\n- Added 'Add archive' button in chapter dialog\n- Popups will now center on parent window correctly\n\t+ It is now possible to move popups by leftclicking and dragging\n\t+ Added background blur effect when popups are shown\n- The rebuild galleries popup will now show real progress\n- Settings:\n\t+ Added new option: Treat subfolders as galleries\n\t+ Added new option: Move imported galleries\n\t+ Added new option: Scroll to new galleries (disabled)\n\t+ Added new option: Open random gallery chapters\n\t+ Added new option: Rename gallery source (disabled)\n\t+ Added new tab in Advanced section: Gallery\n\t+ Added new options: Gallery renamer (disabled)\n\t+ Added new tab in Application section: Ignore\n\t+ Enabled General tab in Application section\n\t+ Reenabled Display on gallery options\n- Contextmenu:\n\t+ When selecting more galleries only options that apply to selected galleries will be shown\n\t+ It is now possible to favourite/Unfavourite selected galleries\n\t+ Reenabled removing of selected galleries\n\t+ Added: Advanced and Change cover\n- Updated database to version 0.2\n- Bugfixes:\n\t+ Fixed critical bug: not being able to add chapters\n\t+ Fixed bug: removing a chapter would always remove the first chapter\n\t+ Fixed bug: fetched metadata title and artist would not be formatted correctly\n\t+ & other minor bugs\n\n*Happypanda v0.19*\n- Improved stability\n- Updated and fixed auto metadata fetcher:\n    + Now twice as fast\n    + No more need to restart application because it froze\n    + Updated to support namespace fetching directly from the official API\n- Improved tag autocompletion in gallery dialog\n- Added a system tray to notify you about events such as auto metadata fetcher being done\n- Sorting:\n    + Added a new sort option: Publication Date\n    + Added an indicator to the current sort option.\n    + Your current sort option will now be saved\n    + Increased pecision of date added\n- Settings:\n    + Added new options:\n        * Continue auto metadata fetcher from where it left off\n        * Use japanese title\n    + Enabled option:\n        * Auto add new galleries on startup\n    + Removed options:\n        * HTML Parsing or API\n- Bugfixes:\n    + Fixed critical bug: Fetching metadata from exhentai not working\n    + Fixed critical bug: Duplicates were being created in database\n    + Fixed a bug causing the update checker to always fail.\n\n*Happypanda v0.18*\n- Greatly improved stability\n- Added numbers to show how many galleries are left when fetching for metadata\n- Possibly fixed a bug causing the *\"big changes are about to occur\"* popup to never disappear\n- Fixed auto metadata fetcher (did not work before)\n\n*Happypanda v0.17*\n- Improved UI\n- Improved stability\n- Improved the toolbar\n-\t+ Added a way to find duplicate galleries\n\t+ Added a random gallery opener\n\t+ Added a way to fetch metadata for all your galleries\n- Added a way to automagically fetch metadata from g.e-/exhentai\n\t+ Fetching metadata is now safer, and should not get you banned\n- Added a new sort option: Date added\n- Added a place for gallery hashes in the database\n- Added folder monitoring support\n\t+ You will now be informed when you rename, remove or add a gallery source in one of your monitored folders\n\t+ The application will scan for new galleries in all of your monitored folders on startup\n- Added a new section in settings dialog: Application\n\t+ Added new options in settings dialog\n\t+ Enabled the 'General' tab in the Web section\n- Bugfixes:\n\t+ Fixed a bug where you could only open the first chapter of a gallery\n\t+ Fixed a bug causing the application to crash when populating new galleries\n\t+ Fixed some issues occuring when adding archive files\n\t+ Fixed some issues occuring when editing galleries\n\t+ other small bugfixes\n- Disabled gallery source type and external program viewer icons because of memory leak (will be reenabled in a later version)\n- Cleaned up some code\n\n*Happypanda v0.16*\n- A more proper way to search for namespace and tags is now available\n- Added support for external image viewers\n- Added support for CBZ\n- The settings button will now open up a real settings dialog\n\t+ Tons of new options are now available in the settings dialog\n- Restyled the grid view\n- Restyled the tooltip to now show other metadata in grid view\n- Added troubleshoot, regex and search guides\n- Fixed bugs:\n\t+ Application crashing when adding a gallery\n\t+ Application crashing when refreshing\n\t+ Namespace & tags not being shown correctly\n\t+ & other small bugs\n\n*Happypanda v0.15*\n- More options are now available in contextmenu when rightclicking a gallery\n- It's now possible to add and remove chapters from a gallery\n- Added a way to select more galleries\n\t+ More options are now available in contextmenu for selected galleries\n- Added more columns to tableview\n\t+ Language\n\t+ Link\n\t+ Chapters\n- Tweaked the grid view to reduce the lag when scrolling\n- Added 1 more way to add galleries\n- Already exisiting galleries will now be ignored\n- Database will now try to auto update to newest version\n- Updated Database to version 0.16 (breaking previous versions)\n- Bugfixes\n\n*Happypanda v0.14*\n- New tableview. Switch easily between grid view and table view with the new button beside the searchbar\n- Now able to add and read ZIP archives (You don't need to extract anymore).\n\t+ Added temp folder for when opening a chapter\n- Changed icons to white icons\n- Added tag autocomplete in series dialog\n- Searchbar is now enabled for searching\n\t+ Autocomplete will complete series' titles\n\t+ Search for title or author\n\t+ Tag searching is only partially supported.\n- Added sort options in contextmenu\n- Title of series is now included in the 'Opening chapter' string\n- Happypanda will now check for new version on startup\n- Happypanda will now log errors.\n\t+ Added a --debug or -d option to create a detailed log\n- Updated Database version to 0.15 (supports 0.14 & 0.13)\n\t+ Now with unique tag mappings\n\t+ A new metadata: times_read\n\n*Happypanda v0.13*\n- First public release\n"
  },
  {
    "path": "INSTALL.md",
    "content": "This guide will show you how to run from source.\n\nA better option is dowloading the latest version\nfor your OS from\nhttps://github.com/Pewpews/happypanda/releases\n\nIf you have any questions, please find me here\n[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Pewpews/happypanda?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)  I'll try to answer as soon as possible.\n\nFirst make sure you have python of minimum version 3.4 installed.\n\nDownload from here https://www.python.org/downloads/\n- arch: sudo pacman -S python3\n- ubuntu: apt-get install python3\n- OSX: see below\n\n*Note: make sure to mark the 'Add to path' checkbox when available on windows*\n\n# Linux\n1. Go where you want happypanda to be downloaded (E.g. `cd ~`), and write `git clone https://github.com/Pewpews/happypanda.git`\n  - If it fails with something like 'unrecognized command 'git'' then do: `sudo pacman -S git` (`apt-get install git` on Ubuntu), and try again\n2. Install these dependencies:\n  - Qt5 (Install this first) >= 5.4\n    + `sudo pacman -S qt5-base` (`apt-get install qt5-default` on Ubuntu)\n  - pip\n    + Python 3.4 should've included pip on install. Incase it didn't: `sudo pacman -S python-pip`\n    + Enter the happypanda folder and write `pip3 install -r requirements.txt`\n  - PyQt5\n    + I'm pretty sure you can install this through pip3, but if not then just `sudo pacman -S python-pyqt5` on Arch\n    + On Ubuntu\n        - `apt-get install python3-pyqt5`\n        - `apt-get install PyQt5`\n        - `apt-get install python3-pyqt5`\n        - `apt-get install python3-pyqt5.qtsql`\n3. In the happypanda directory go to the *version* directory and write `python3 main.py`\n4. The program should now be running\n\n# Windows\n1. Go to the frontpage of the happypanda repo and click Download Zip\n2. Extract to desired location\n3. Install these dependencies:\n  - Qt5 (Install this first) >= 5.4\n    + Download from https://www.qt.io/download-open-source/#section-2\n  - pip\n    + Python 3.4 should've included pip on install. Incase it didn't https://pip.pypa.io/en/latest/installing.html\n    Make sure python is in your PATH. (http://stackoverflow.com/questions/6318156/adding-python-path-on-windows-7)\n    + Now open cmd and `cd` to the happypanda folder\n    + Write: `pip install -r requirements.txt` and press enter\n  - PyQt5\n    + I'm pretty sure you can install this through pip, but here is the download location\n    http://www.riverbankcomputing.com/software/pyqt/download5 (see Binary Packages for windows)\n4. Finally, write `python main.py` to run the program\n5. The program should now be running.\n\nNote: Try renaming the 'main.py' file to 'main.pyw' and then just doubleclick on it to try running without console (not guaranteed to work)\n\n# Mac OS X\n(Note: PyQt5 MUST be installed via Homebrew and NOT via Pip)\n\n1. Install Homebrew (this makes everything easier)\n  - Open Terminal\n  - Run the following\n     + `ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"`\n     + `brew update && brew upgrade --all`\n    \n2. To install Python3, PyQt5, and sip (*still in Terminal*)\n     + `brew install PyQt5`\n3. To install other dependencies\n     - Download HappyPanda\n        + Go to github.com/Pewpews/happypanda\n        + Press the \"Download ZIP\" button\n        + UnZip happypanda-master.zip\n  - In Terminal, navigate to the happypanda-master folder (E.g.: `cd /where/ever/you/put/the/folder/happypanda-master`)\n    + Write `pip3 install -r requirements.txt`\n5. Running HappyPanda\n  - Run the following\n   + `cd /where/ever/you/put/the/folder/happypanda-master/version`\n     + (For example `cd /Users/username/Downloads/happypanda-master/version`)\n   + `python3 main.py`\n"
  },
  {
    "path": "LICENSE",
    "content": "Happypanda is a cross platform manga/doujinshi manager with namespace & tag support;\nCopyright (C) 2016  Pewpews\n\nThis program is free software; you can redistribute it and/or\nmodify it under the terms of the GNU General Public License\nas published by the Free Software Foundation; either version 2\nof the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program; if not, write to the Free Software\nFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA."
  },
  {
    "path": "LICENSE-3RD-PARTY",
    "content": "-----------------------------------------------------------------------------\n                        The MIT License (MIT)\n        applies to:\n\t\t- Beautiful Soup\n\t\t- Robobrowser\n-----------------------------------------------------------------------------\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n-----------------------------------------------------------------------------\n                 Apache License, Version 2.0 (the \"License\")\n        applies to:\n        - requests\n\t\t- watchdog\n-----------------------------------------------------------------------------\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n\n-----------------------------------------------------------------------------\n        applies to:\n        - scandir\n-----------------------------------------------------------------------------\nCopyright (c) 2012, Ben Hoyt\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n* Neither the name of Ben Hoyt nor the names of its contributors may be used\nto endorse or promote products derived from this software without specific\nprior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n-----------------------------------------------------------------------------\n\t\t\tThe ISC License\n        applies to:\n        - rarfile\n-----------------------------------------------------------------------------\nPermission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n-----------------------------------------------------------------------------\n\t\t\tThe GPL v3\n        applies to:\n        - PyQt5\n-----------------------------------------------------------------------------\nThis program is free software; you can redistribute it and/or\nmodify it under the terms of the GNU General Public License\nas published by the Free Software Foundation; either version 2\nof the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program; if not, write to the Free Software\nFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n\n-----------------------------------------------------------------------------\n\t\t\tThe LGPL\n        applies to:\n        - Qt5\n-----------------------------------------------------------------------------\nThis library is free software; you can redistribute it and/or\nmodify it under the terms of the GNU Lesser General Public\nLicense as published by the Free Software Foundation; either\nversion 2.1 of the License, or (at your option) any later version.\n\nThis library is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\nLesser General Public License for more details.\n\nYou should have received a copy of the GNU Lesser General Public\nLicense along with this library; if not, write to the Free Software\nFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.\n\n-----------------------------------------------------------------------------\n\t\t\tThe \"BSD\" License\n        applies to:\n        - Send2Trash\n-----------------------------------------------------------------------------\nCopyright (c) 2013, Hardcoded Software, http://www.hardcoded.net\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n    * Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n---------------------------------------------------------------------------\nThe Python Imaging Library (PIL) is\n\n    Copyright © 1997-2011 by Secret Labs AB\n    Copyright © 1995-2011 by Fredrik Lundh\n---------------------------------------------------------------------------\n\nBy obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions:\n\nPermission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission.\n\nSECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE."
  },
  {
    "path": "README.rst",
    "content": "\nWork on this program has been halted in favor of its successor `HappyPanda X <https://github.com/happypandax/server>`__ (bugs and such won't be fixed).\n===========\n\n   Follow me on twitter to keep up to date with HPX:\n\n   .. image:: https://img.shields.io/twitter/follow/pewspew.svg?style=social&label=Follow\n     :target: https://twitter.com/twiddly_\n\nThis is a cross platform manga/doujinshi manager with namespace & tag\nsupport.\n\nFeatures\n========\n\n-  Portable, self-contained in folder and cross-platform\n-  Low memory footprint\n-  Advanced gallery search with regex support (`learn more about it\n   here <https://github.com/Pewpews/happypanda/wiki/Gallery-Searching>`__)\n-  Gallery tagging: userdefined namespaces and tags\n-  Gallery metadata fetching from the web (supports various sources)\n-  Gallery downloading from the web (supports various sources) \\*\n-  Folder monitoring that'll notify you of filesystem changes\n-  Multiple ways of adding galleries to make it as convienient as\n   possible!\n-  Recursive directory/archive scanning\n-  Supports ZIP/CBZ, RAR/CBR and directories with loose files\n-  Very customizable\n-  And lots more...\n\n\\* Gallery downloading from E-Hentai costs Credits/GP\n\nScreenshots\n===========\n.. image:: https://github.com/Pewpews/happypanda/raw/master/misc/screenshot1.png\n    :width: 100%\n    :align: center\n.. image:: https://github.com/Pewpews/happypanda/raw/master/misc/screenshot2.png\n    :width: 100%\n    :align: center\n.. image:: https://github.com/Pewpews/happypanda/raw/master/misc/screenshot3.png\n    :width: 100%\n    :align: center\n\nHow to install and run\n======================\n\nWindows\n^^^^^^^\n\n#. Download the archive from\n   `releases <https://github.com/Pewpews/happypanda/releases>`__\n#. Extract the archive to its own folder\n#. Find Happypanda.exe and double click on it!\n\nMac and Linux\n^^^^^^^^^^^^^\n\nInstall from PYPI or see `INSTALL.md <https://github.com/Pewpews/happypanda/blob/master/INSTALL.md>`__\n\nPYPI\n^^^^^^^^^^^^^\n``pip install happypanda`` (thanks `@Evolution0 <https://github.com/Evolution0>`__)\nand then run with ``happypanda --home``\n\nNote: use of the ``--home`` flag will make happypanda create required files and directories at:\n\nOn windows:\n``'C:\\Users\\YourName\\AppData\\Local\\Pewpew\\Happypanda'``\n\nOn mac:\n``'/Users/YourName/Library/Application Support/Happypanda'``\n\nOn linux:\n``'/home/YourName/.local/share/Happypanda'``\n\n\nUpdating\n========\n\n| Overwrite your previous installation.\n| More info in the `wiki <https://github.com/Pewpews/happypanda/wiki>`__\n\n\nPYPI\n^^^^^^^^^^^^^\n``pip install --upgrade happypanda``\n\n\nMisc.\n=====\n\nFor general documentation (how to add galleries and usage of the\nsearch), check the\n`wiki <https://github.com/Pewpews/happypanda/wiki>`__.\n\nPeople wanting to import galleries from the Pururin database torrent\nshould find `this <https://github.com/Exedge/Convertor>`__ useful.\n\nDependencies\n============\n\n-  Qt5 (Install this first) >= 5.4\n-  PyQt5 (pip)\n-  requests (pip)\n-  beautifulsoup4 (pip)\n-  watchdog (pip)\n-  scandir (pip)\n-  rarfile (pip)\n-  robobrowser (pip)\n-  Send2Trash (pip)\n-  Pillow (pip) or PIL\n-  python-dateutil (pip)\n-  QtAwesome (pip)\n-  appdirs (pip)\n\nContributing\n============\n\nPlease refer to ``HappypandaX`` instead.\n"
  },
  {
    "path": "VS.txt",
    "content": "1.1\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "-r requirements.txt\n\npytest==3.0.3\n"
  },
  {
    "path": "requirements.txt",
    "content": "pyqt5\nrequests\nbeautifulsoup4\nscandir\nrarfile\nwatchdog\nrobobrowser\nSend2Trash\npillow\npython-dateutil\nQtAwesome==0.3.3"
  },
  {
    "path": "res/license.txt",
    "content": "btn_star2.png |\u001f\u001f________________________________________\r\nhttps://www.iconfinder.com/iconsets/woothemesiconset  |\r\n------------------------------------------------------"
  },
  {
    "path": "res/style.css",
    "content": "DoNotDelete {\n}\n\nQLabel#author {\n\tfont-weight:lighter;\n}\n\nAppWindow > QToolBar, QStatusBar, SideBarWidget {\n\tbackground-color: #2A2D31;\n\tborder: none;\n}\n\nAppWindow > QToolBar::sunken, AppWindow > QStatusBar::item {\n\tborder: none;\n}\n\nAppWindow > QStatusBar > QWidget, QStatusBar > QLabel {\n\tcolor:white;\n\tborder: none;\n}\n\nLoading > QWidget {\n\tbackground-color:rgba(0, 0, 0, 0.65);\n}\n\nBasePopup QLabel, BaseUserChoice QLabel{\n\tcolor:white;\n}\n\nBasePopup > QFrame, BaseUserChoice > QFrame{\n\tbackground-color:rgba(0, 0, 0, 0.85);\n\tborder-radius: 1em;\n}\n\nQScrollBar:vertical {\n\twidth:1em;\n}\n\nQScrollBar:horizontal {\n\theight:1em;\n\t}\n\nQScrollBar:vertical, QScrollBar:horizontal {\n\tborder: 0px solid #2A2D31;\n\tbackground:none;\n\tmargin: 0px 0px 0px 0px;\n\t}\n\nQScrollBar::handle {\n\tbackground: #2A2D31;\n}\n\nQScrollBar::handle:vertical, QScrollBar::handle:horizontal {\n\tmin-height: 7em;\n}\n\nQScrollBar::add-line:vertical {\n\tbackground: #2A2D31;\n\theight: 0px;\n\tsubcontrol-position: bottom;\n\tsubcontrol-origin: margin;\n}\n\nQScrollBar::add-line:horizontal {\n\tbackground: #2A2D31;\n\twidth: 0px;\n\tsubcontrol-position: right;\n\tsubcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:vertical {\n\tbackground: #2A2D31;\n\theight: 0px;\n\tsubcontrol-position: top;\n\tsubcontrol-origin: margin;\n}\n\nQScrollBar::sub-line:horizontal {\n\tbackground: #2A2D31;\n\twidth: 0px;\n\tsubcontrol-position: left;\n\tsubcontrol-origin: margin;\n}\n\nQScrollBar::add-page, QScrollBar::sub-page {\n\tbackground: none;\n}\n\nQMenu {\n\tbackground-color: #2A2D31;\n\tcolor: white;\n}\n\nQMenu::item:selected {\n\tbackground-color: #d64933;\n}\n\nMangaView {\n\tborder: 0px solid;\n}\n\nQLabel {\n\tcolor: #d64933;\n}\n\nQPushButton {\n\tborder: 1px solid #3E4249;\n    border-radius: 1px;\n    padding: 5px;\n}\n\nQPushButton, QToolButton {\n\tbackground-color: #3E4249;\n\tcolor: white;\n}\n\nQPushButton:hover, QToolButton:hover {\n\tborder: 1px solid #d64933;\n}\n\nQPushButton:pressed, QPushButton:checked {\n\tbackground-color: #d64933;\n}\n\nQToolTip {\n\tborder-style: none;\n\tbackground-color: #2A2D31;\n\tcolor: white;\n}\n\nNotificationOverlay > QLabel {\n\tcolor: white;\n    background-color: #3E4249;\n\tborder-style: none;\n}\n\nTagText {\n\tpadding-right: 0.7em;\n\tpadding-left: 0.7em;\n\tpadding-bottom: 0.2em;\n\tpadding-top: 0.1em;\n\tborder-radius: 0.45em;\n\tborder: 0.1em solid #d64933;\n\tbackground-color: rgba(62, 66, 73,0.50) !important;\n}\n\nArrowWindow {\n    background-color: #2A2D31;\n    color: #d64933;\n    border: 1px solid #d64933;\n    border-radius: 5px;\n}\n\nGalleryMetaWindow QScrollArea QWidget, QHeaderView::section, QHeaderView::section::checked {\n    background-color: #2A2D31;\n}\n\nSettingsDialog, SettingsDialog QScrollArea QWidget, GalleryDialog, GalleryDialog QScrollArea QWidget {\n    color: #2A2D31;\n}\n\nGalleryDialog QScrollArea QWidget QPushButton, SettingsDialog QScrollArea QWidget QPushButton{\n    color: white;\n}\n\nQHeaderView::section {\n    color: white;\n}\n\nQHeaderView::section {\n    border-style: none;\n    padding: 5px;\n}\n\nQGroupBox::title, QHeaderView::down-arrow, QHeaderView::up-arrow {\n\tcolor: #d64933;\n}\n\nQListView, QTableView {\n   border-style: none;\n}\n\nQProgressBar {\n    border: 2px solid #3E4249;\n    border-radius: 5px;\n}\n\nQProgressBar::chunk {\n    background-color: #d64933;\n    width: 20px;\n}"
  },
  {
    "path": "tests/database/test_db.py",
    "content": "\"\"\"test db module.\"\"\"\nfrom itertools import product\nfrom unittest import mock\n\nimport pytest\n\n\n@pytest.mark.parametrize(\n    'path_isfile_retval, check_dbv_retval, path_is_dbc_path',\n    product([False, True], repeat=3)\n)\ndef test_init_db(path_isfile_retval, check_dbv_retval, path_is_dbc_path):\n    \"\"\"test sqlite generation and db creation\"\"\"\n    with mock.patch('version.database.db.db_constants') as m_dbc, \\\n            mock.patch('version.database.db.sqlite3') as m_sl3, \\\n            mock.patch('version.database.db.os') as m_os, \\\n            mock.patch('version.database.db.create_db_path') as m_create_db_path, \\\n            mock.patch('version.database.db.check_db_version') \\\n            as m_check_dbv:\n        from version.database import db\n        m_os.path.isfile.return_value = path_isfile_retval\n        m_check_dbv.return_value = check_dbv_retval\n        if path_is_dbc_path:\n            path = m_dbc.DB_PATH\n        else:\n            path = mock.Mock()\n        # run\n        res = db.init_db(path)\n        # test\n        if path_isfile_retval:\n            if path == m_dbc.DB_PATH and not check_dbv_retval:\n                m_sl3.assert_has_calls([\n                    mock.call.connect(path, check_same_thread=False),\n                ])\n                assert res is None\n                return\n            else:\n                m_sl3.assert_has_calls([\n                    mock.call.connect(path, check_same_thread=False),\n                    mock.call.connect().execute('PRAGMA foreign_keys = on')\n                ])\n        else:\n            m_create_db_path.assert_called_once_with()\n\n            m_sl3.assert_has_calls([\n                mock.call.connect(path, check_same_thread=False),\n                mock.call.connect().cursor(),\n                mock.call.connect().cursor().execute(\n                    'CREATE TABLE IF NOT EXISTS version(version REAL)'),\n                mock.call.connect().cursor().execute(\n                    'INSERT INTO version(version) VALUES(?)',\n                    (m_dbc.CURRENT_DB_VERSION,)\n                ),\n                mock.call.connect().cursor().executescript(db.STRUCTURE_SCRIPT),\n                mock.call.connect().commit(),\n                mock.call.connect().execute('PRAGMA foreign_keys = on')\n            ])\n        assert res == m_sl3.connect.return_value\n        assert res.isolation_level is None\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "\"\"\"test utils module.\"\"\"\nfrom unittest import mock\nfrom itertools import product\n\nimport pytest\n\nfrom version.utils import backup_database\n\n\n@pytest.mark.parametrize(\n    'mock_exists_retval, mock_isdir_retval',\n    product([True, False], repeat=2)\n)\ndef test_run_backup_database(mock_exists_retval, mock_isdir_retval):\n    \"\"\"test run with mock obj as input.\"\"\"\n    mock_db_path = mock.Mock()\n    mock_base_path = mock.Mock()\n    mock_name = mock.Mock()\n    with mock.patch('version.utils.os') as mock_os, \\\n            mock.patch('version.utils.shutil') as mock_shutil, \\\n            mock.patch('version.utils.datetime') as mock_datetime:\n        mock_datetime.datetime.today.return_value = '2016-10-25 15:42:47.649416'\n        mock_os.path.split.return_value = (mock_base_path, mock_name)\n        mock_os.path.exists.return_value = mock_exists_retval\n        mock_os.path.isdir.return_value = mock_isdir_retval\n        res = backup_database(mock_db_path)\n        assert res\n        mock_datetime.datetime.today.assert_called_once_with()\n        os_calls = [\n            mock.call.path.split(mock_db_path),\n            mock.call.path.join(mock_base_path, 'backup'),\n            mock.call.path.isdir(mock_os.path.join.return_value),\n            mock.call.path.join(\n                mock_os.path.join.return_value,\n                \"2016-10-25-{}\".format(mock_name)),\n            mock.call.path.exists(mock_os.path.join.return_value),\n        ]\n        if mock_exists_retval:\n            if mock_isdir_retval:\n                assert len(mock_os.mock_calls) == 103\n            else:\n                assert len(mock_os.mock_calls) == 104\n            os_calls.extend([\n                mock.call.path.join(\n                    mock_os.path.join.return_value,\n                    \"2016-10-25(1)-2016-10-25-{}\".format(mock_name)),\n                mock.call.path.join(\n                    mock_os.path.join.return_value,\n                    \"2016-10-25(2)-2016-10-25-{}\".format(mock_name)),\n            ])\n            assert not mock_shutil.mock_calls\n        else:\n            if mock_isdir_retval:\n                assert len(mock_os.mock_calls) == 5\n            else:\n                assert len(mock_os.mock_calls) == 6\n            mock_shutil.copyfile.assert_called_once_with(\n                mock_db_path, mock_os.path.join.return_value)\n\n        if mock_isdir_retval:\n            assert not mock_os.mkdir.called\n        else:\n            mock_os.mkdir.assert_called_once_with(mock_os.path.join.return_value)\n        mock_os.assert_has_calls(os_calls, any_order=True)\n"
  },
  {
    "path": "version/app.py",
    "content": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport sys\nimport logging\nimport os\nimport threading\nimport re\nimport requests\nimport scandir\nimport random\nimport traceback\n\nfrom PyQt5.QtCore import (Qt, QSize, pyqtSignal, QThread, QEvent, QTimer,\n                          QObject, QPoint, QPropertyAnimation)\nfrom PyQt5.QtGui import (QPixmap, QIcon, QMoveEvent, QCursor,\n                         QKeySequence)\nfrom PyQt5.QtWidgets import (QMainWindow, QListView,\n                             QHBoxLayout, QFrame, QWidget, QVBoxLayout,\n                             QLabel, QStackedLayout, QToolBar, QMenuBar,\n                             QSizePolicy, QMenu, QAction, QLineEdit,\n                             QSplitter, QMessageBox, QFileDialog,\n                             QDesktopWidget, QPushButton, QCompleter,\n                             QListWidget, QListWidgetItem, QToolTip,\n                             QProgressBar, QToolButton, QSystemTrayIcon,\n                             QShortcut, QGraphicsBlurEffect, QTableWidget,\n                             QTableWidgetItem, QActionGroup)\n\nfrom executors import Executors\n\nimport app_constants\nimport misc\nimport gallery\nimport io_misc\nimport settingsdialog\nimport gallerydialog\nimport fetch\nimport gallerydb\nimport settings\nimport pewnet\nimport utils\nimport misc_db\nimport database\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass AppWindow(QMainWindow):\n    \"The application's main window\"\n\n    move_listener = pyqtSignal()\n    login_check_invoker = pyqtSignal()\n    db_startup_invoker = pyqtSignal(list)\n    duplicate_check_invoker = pyqtSignal(gallery.GalleryModel)\n    admin_db_method_invoker = pyqtSignal(object)\n    db_activity_checker = pyqtSignal()\n    graphics_blur = QGraphicsBlurEffect()\n\n    def __init__(self, disable_excepthook=False):\n        super().__init__()\n        if not disable_excepthook:\n            sys.excepthook = self.excepthook\n        app_constants.GENERAL_THREAD = QThread(self)\n        app_constants.GENERAL_THREAD.finished.connect(app_constants.GENERAL_THREAD.deleteLater)\n        app_constants.GENERAL_THREAD.start()\n        self.check_site_logins()\n        self._db_startup_thread = QThread(self)\n        self._db_startup_thread.finished.connect(self._db_startup_thread.deleteLater)\n        self.db_startup = gallerydb.DatabaseStartup()\n        self._db_startup_thread.start()\n        self.db_startup.moveToThread(self._db_startup_thread)\n        self.db_startup.DONE.connect(lambda: self.scan_for_new_galleries() if app_constants.LOOK_NEW_GALLERY_STARTUP else None)\n        self.db_startup_invoker.connect(self.db_startup.startup)\n        self.setAcceptDrops(True)\n        self.initUI()\n        self.startup()\n        QTimer.singleShot(3000, self._check_update)\n        self.setFocusPolicy(Qt.NoFocus)\n        self.set_shortcuts()\n        self.graphics_blur.setParent(self)\n\n    def set_shortcuts(self):\n        quit = QShortcut(QKeySequence('Ctrl+Q'), self, self.close)\n        search_focus = QShortcut(QKeySequence(QKeySequence.Find), self, lambda:self.search_bar.setFocus(Qt.ShortcutFocusReason))\n        prev_view = QShortcut(QKeySequence(QKeySequence.PreviousChild), self, self.switch_display)\n        next_view = QShortcut(QKeySequence(QKeySequence.NextChild), self, self.switch_display)\n        help = QShortcut(QKeySequence(QKeySequence.HelpContents), self, lambda:utils.open_web_link(\"https://github.com/Pewpews/happypanda/wiki\"))\n\n    def check_site_logins(self):\n        # checking logins\n        # need to do this to avoid settings dialog locking up\n        class LoginCheck(QObject):\n            def __init__(self):\n                super().__init__()\n            def check(self):\n                for s in settings.ExProperties.sites:\n                    ex = settings.ExProperties(s)\n                    if ex.cookies:\n                        if s == settings.ExProperties.EHENTAI:\n                            pewnet.EHen.check_login(ex.cookies)\n        logincheck = LoginCheck()\n        self.login_check_invoker.connect(logincheck.check)\n        logincheck.moveToThread(app_constants.GENERAL_THREAD)\n        self.login_check_invoker.emit()\n\n    def init_watchers(self):\n\n        def remove_gallery(g):\n            index = gallery.CommonView.find_index(self.get_current_view(), g.id, True)\n            if index:\n                gallery.CommonView.remove_gallery(self.get_current_view(), [index])\n            else:\n                log_e('Could not find gallery to remove from watcher')\n\n        def update_gallery(g):\n            index = gallery.CommonView.find_index(self.get_current_view(), g.id)\n            if index:\n                gal = index.data(gallery.GalleryModel.GALLERY_ROLE)\n                gal.path = g.path\n                gal.chapters = g.chapters\n            else:\n                log_e('Could not find gallery to update from watcher')\n            self.default_manga_view.replace_gallery(g, False)\n\n        def created(path):\n            self.gallery_populate([path])\n\n        def modified(path, gallery):\n            mod_popup = io_misc.ModifiedPopup(path, gallery, self)\n        def deleted(path, gallery):\n            d_popup = io_misc.DeletedPopup(path, gallery, self)\n            d_popup.UPDATE_SIGNAL.connect(update_gallery)\n            d_popup.REMOVE_SIGNAL.connect(remove_gallery)\n        def moved(new_path, gallery):\n            mov_popup = io_misc.MovedPopup(new_path, gallery, self)\n            mov_popup.UPDATE_SIGNAL.connect(update_gallery)\n\n        self.watchers = io_misc.Watchers()\n        self.watchers.gallery_handler.CREATE_SIGNAL.connect(created)\n        self.watchers.gallery_handler.MODIFIED_SIGNAL.connect(modified)\n        self.watchers.gallery_handler.MOVED_SIGNAL.connect(moved)\n        self.watchers.gallery_handler.DELETED_SIGNAL.connect(deleted)\n\n    def startup(self):\n        def normalize_first_time():\n            settings.set(app_constants.INTERNAL_LEVEL, 'Application', 'first time level')\n            settings.save()\n\n        def done(status=True):\n            self.db_startup_invoker.emit(gallery.MangaViews.manga_views)\n            #self.db_startup.startup()\n            if app_constants.FIRST_TIME_LEVEL != app_constants.INTERNAL_LEVEL:\n                normalize_first_time()\n            if app_constants.UPDATE_VERSION != app_constants.vs:\n                settings.set(app_constants.vs, 'Application', 'version')\n\n            if app_constants.UPDATE_VERSION != app_constants.vs:\n                pop = misc.BasePopup(self, blur=False)\n                ml = QVBoxLayout(pop.main_widget)\n                ml.addWidget(QLabel(\"\\nGoodbye Happypanda!\\n\\n\\nHello, this is the last release of 'old' Happypanda.\\n\"+\n                    \"This means that I (personally) won't be adding any new features or fix bugs.\\n\\n\"+\n                    \"I have started a new project where I (with the help of others)\\n try to create a better Happypanda from scratch.\\n\\n\"+\n                    \"Please follow me on twitter (@pewspew) to keep yourself updated!\\n\"))\n                ml.addLayout(pop.buttons_layout)\n                pop.add_buttons(\"close\")[0].clicked.connect(pop.close)\n                pop.adjustSize()\n                pop.show()\n\n            if app_constants.ENABLE_MONITOR and \\\n                app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS):\n                self.init_watchers()\n            self.download_manager = pewnet.Downloader()\n            app_constants.DOWNLOAD_MANAGER = self.download_manager\n            self.download_manager.start_manager(4)\n\n        eh_url = app_constants.DEFAULT_EHEN_URL\n        if 'g.e-h' in eh_url or 'http://' in eh_url: # reset default hen\n            eh_url_n = 'https://e-hentai.org/'\n            settings.set(eh_url_n, 'Web', 'default ehen url')\n            settings.save()\n            app_constants.DEFAULT_EHEN_URL = eh_url_n\n\n        done()\n\n    def initUI(self):\n        self.center = QWidget()\n        self._main_layout = QHBoxLayout(self.center)\n        self._main_layout.setSpacing(0)\n        self._main_layout.setContentsMargins(0,0,0,0)\n\n        self.init_stat_bar()\n        self.manga_views = {}\n        self._current_manga_view = None\n        self.default_manga_view = gallery.MangaViews(app_constants.ViewType.Default, self, True)\n        def refresh_view():\n            self.current_manga_view.sort_model.refresh()\n        self.db_startup.DONE.connect(refresh_view)\n        self.manga_list_view = self.default_manga_view.list_view\n        self.manga_table_view = self.default_manga_view.table_view\n        self.manga_list_view.gallery_model.STATUSBAR_MSG.connect(self.stat_temp_msg)\n        self.manga_list_view.STATUS_BAR_MSG.connect(self.stat_temp_msg)\n        self.manga_table_view.STATUS_BAR_MSG.connect(self.stat_temp_msg)\n\n        self.sidebar_list = misc_db.SideBarWidget(self)\n        self.db_startup.DONE.connect(self.sidebar_list.tags_tree.setup_tags)\n        self._main_layout.addWidget(self.sidebar_list)\n        self.current_manga_view = self.default_manga_view\n\n        #self.display_widget.setSizePolicy(QSizePolicy.Expanding,\n        #QSizePolicy.Preferred)\n        self.download_window = io_misc.GalleryDownloader(self)\n        self.download_window.hide()\n        # init toolbar\n        self.init_toolbar()\n\n        log_d('Create statusbar: OK')\n\n        self.system_tray = misc.SystemTray(QIcon(app_constants.APP_ICO_PATH), self)\n        app_constants.SYSTEM_TRAY = self.system_tray\n        tray_menu = QMenu(self)\n        self.system_tray.setContextMenu(tray_menu)\n        self.system_tray.setToolTip('Happypanda {}'.format(app_constants.vs))\n        tray_quit = QAction('Quit', tray_menu)\n        tray_update = tray_menu.addAction('Check for update')\n        tray_update.triggered.connect(self._check_update)\n        tray_menu.addAction(tray_quit)\n        tray_quit.triggered.connect(self.close)\n        self.system_tray.show()\n        def tray_activate(r=None):\n            if not r or r == QSystemTrayIcon.Trigger:\n                self.showNormal()\n                self.activateWindow()\n        self.system_tray.messageClicked.connect(tray_activate)\n        self.system_tray.activated.connect(tray_activate)\n        log_d('Create system tray: OK')\n        #self.display.addWidget(self.chapter_main)\n\n        self.setCentralWidget(self.center)\n        self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))\n\n        props = settings.win_read(self, 'AppWindow')\n        if props.resize:\n            x, y = props.resize\n            self.resize(x, y)\n        else:\n            self.resize(app_constants.MAIN_W, app_constants.MAIN_H)\n        self.setMinimumWidth(600)\n        self.setMinimumHeight(400)\n        misc.centerWidget(self)\n        self.init_spinners()\n        self.show()\n        log_d('Show window: OK')\n\n        self.notification_bar = misc.NotificationOverlay(self)\n        p = self.toolbar.pos()\n        self.notification_bar.move(p.x(), p.y() + self.toolbar.height())\n        self.notification_bar.resize(self.width())\n        self.notif_bubble = misc.AppBubble(self)\n        app_constants.NOTIF_BAR = self.notification_bar\n        app_constants.NOTIF_BUBBLE = self.notif_bubble\n\n        log_d('Create notificationbar: OK')\n\n        log_d('Window Create: OK')\n\n    def _check_update(self):\n        class upd_chk(QObject):\n            UPDATE_CHECK = pyqtSignal(str)\n            def __init__(self, **kwargs):\n                super().__init__(**kwargs)\n            def fetch_vs(self):\n                import requests\n                import time\n                log_d('Checking Update')\n                time.sleep(1.5)\n                try:\n                    r = requests.get(\"https://raw.githubusercontent.com/Pewpews/happypanda/master/VS.txt\")\n                    a = r.text\n                    vs = a.strip()\n                    self.UPDATE_CHECK.emit(vs)\n                except:\n                    log.exception('Checking Update: FAIL')\n                    self.UPDATE_CHECK.emit('this is a very long text which is sure to be over limit')\n\n        def check_update(vs):\n            log_i('Received version: {}\\nCurrent version: {}'.format(vs, app_constants.vs))\n            if vs != app_constants.vs:\n                if len(vs) < 10:\n                    self.notification_bar.begin_show()\n                    self.notification_bar.add_text(\"Version {} of Happypanda is\".format(vs) + \" available. Click here to update!\", False)\n                    self.notification_bar.clicked.connect(lambda: utils.open_web_link('https://github.com/Pewpews/happypanda/releases'))\n                    self.notification_bar.set_clickable(True)\n                else:\n                    self.notification_bar.add_text(\"An error occurred while checking for new version\")\n\n        self.update_instance = upd_chk()\n        thread = QThread(self)\n        self.update_instance.moveToThread(thread)\n        thread.started.connect(self.update_instance.fetch_vs)\n        self.update_instance.UPDATE_CHECK.connect(check_update)\n        self.update_instance.UPDATE_CHECK.connect(self.update_instance.deleteLater)\n        thread.finished.connect(thread.deleteLater)\n        thread.start()\n\n    def _web_metadata_picker(self, gallery, title_url_list, queue, parent=None):\n        if not parent:\n            parent = self\n        text = \"Which gallery do you want to extract metadata from?\"\n        s_gallery_popup = misc.SingleGalleryChoices(gallery, title_url_list,\n                                              text, parent)\n        s_gallery_popup.USER_CHOICE.connect(queue.put)\n\n    def get_metadata(self, gal=None):\n        if not app_constants.GLOBAL_EHEN_LOCK:\n            metadata_spinner = misc.Spinner(self)\n            metadata_spinner.set_text(\"Metadata\")\n            metadata_spinner.set_size(55)\n            thread = QThread(self)\n            thread.setObjectName('App.get_metadata')\n            fetch_instance = fetch.Fetch()\n            if gal:\n                if not isinstance(gal, list):\n                    galleries = [gal]\n                else:\n                    galleries = gal\n            else:\n                if app_constants.CONTINUE_AUTO_METADATA_FETCHER:\n                    galleries = [g for g in self.current_manga_view.gallery_model._data if not g.exed]\n                else:\n                    galleries = self.current_manga_view.gallery_model._data\n                if not galleries:\n                    self.notification_bar.add_text('All galleries has already been processed!')\n                    return None\n            fetch_instance.galleries = galleries\n\n            self.notification_bar.begin_show()\n            fetch_instance.moveToThread(thread)\n\n            def done(status):\n                self.notification_bar.end_show()\n                gallerydb.execute(database.db.DBBase.end, True)\n                try:\n                    fetch_instance.deleteLater()\n                except RuntimeError:\n                    pass\n                if not isinstance(status, bool):\n                    galleries = []\n                    for tup in status:\n                        galleries.append(tup[0])\n\n                    class GalleryContextMenu(QMenu):\n                        app_instance = self\n                        def __init__(self, parent=None):\n                            super().__init__(parent)\n                            show_in_library_act = self.addAction('Show in library')\n                            show_in_library_act.triggered.connect(self.show_in_library)\n\n                        def show_in_library(self):\n                            index = gallery.CommonView.find_index(self.app_instance.get_current_view(), self.gallery_widget.gallery.id, True)\n                            if index:\n                                gallery.CommonView.scroll_to_index(self.app_instance.get_current_view(), index)\n\n                    g_popup = io_misc.GalleryPopup(('Fecthing metadata for these galleries failed.' + ' Check happypanda.log for details.', galleries), self, menu=GalleryContextMenu)\n                    errors = {g[0].id: g[1] for g in status}\n                    for g_item in g_popup.get_all_items():\n                        g_item.extra_text.setText(\"<font color='red'>{}</font>\".format(errors[g_item.gallery.id]))\n                        g_item.extra_text.show()\n                    g_popup.graphics_blur.setEnabled(False)\n                    close_button = g_popup.add_buttons('Close')[0]\n                    close_button.clicked.connect(g_popup.close)\n\n            database.db.DBBase.begin()\n            fetch_instance.GALLERY_PICKER.connect(self._web_metadata_picker)\n            fetch_instance.GALLERY_EMITTER.connect(self.default_manga_view.replace_gallery)\n            fetch_instance.AUTO_METADATA_PROGRESS.connect(self.notification_bar.add_text)\n            thread.started.connect(fetch_instance.auto_web_metadata)\n            fetch_instance.FINISHED.connect(done)\n            fetch_instance.FINISHED.connect(metadata_spinner.before_hide)\n            thread.finished.connect(thread.deleteLater)\n            thread.start()\n            #fetch_instance.auto_web_metadata()\n            metadata_spinner.show()\n        else:\n            self.notif_bubble.update_text(\"Oops!\", \"Auto metadata fetcher is already running...\")\n\n    def init_stat_bar(self):\n        self.status_bar = self.statusBar()\n        self.status_bar.setSizeGripEnabled(False)\n        self.stat_info = QLabel()\n        self.stat_info.setIndent(5)\n        self.sort_main = QAction(\"Asc\", self)\n        sort_menu = QMenu()\n        self.sort_main.setMenu(sort_menu)\n        s_by_title = QAction(\"Title\", sort_menu)\n        s_by_artist = QAction(\"Artist\", sort_menu)\n        sort_menu.addAction(s_by_title)\n        sort_menu.addAction(s_by_artist)\n        self.status_bar.addPermanentWidget(self.stat_info)\n        #self.status_bar.addAction(self.sort_main)\n        self.temp_msg = QLabel()\n        self.temp_timer = QTimer()\n\n        app_constants.STAT_MSG_METHOD = self.stat_temp_msg\n\n    def stat_temp_msg(self, msg):\n        self.temp_timer.stop()\n        self.temp_msg.setText(msg)\n        self.status_bar.addWidget(self.temp_msg)\n        self.temp_timer.timeout.connect(self.temp_msg.clear)\n        self.temp_timer.setSingleShot(True)\n        self.temp_timer.start(5000)\n\n    def stat_row_info(self):\n        r = self.current_manga_view.get_current_view().sort_model.rowCount()\n        t = self.current_manga_view.get_current_view().gallery_model.rowCount()\n        g_l = self.get_current_view().sort_model.current_gallery_list\n        if g_l:\n            self.stat_info.setText(\"<b><i>{}</i></b> | Showing {} of {} \".format(g_l.name, r, t))\n        else:\n            self.stat_info.setText(\"Showing {} of {} \".format(r, t))\n\n    def set_current_manga_view(self, v):\n        self.current_manga_view = v\n\n    @property\n    def current_manga_view(self):\n        return self._current_manga_view\n\n    @current_manga_view.setter\n    def current_manga_view(self, new_view):\n        if self._current_manga_view:\n            self._main_layout.takeAt(1)\n        self._current_manga_view = new_view\n        self._main_layout.insertLayout(1, new_view.view_layout, 1)\n        self.stat_row_info()\n\n    def init_spinners(self):\n        # fetching spinner\n        self.data_fetch_spinner = misc.Spinner(self, \"center\")\n        self.data_fetch_spinner.set_size(80)\n        \n        self.manga_list_view.gallery_model.ADD_MORE.connect(self.data_fetch_spinner.show)\n        self.db_startup.START.connect(self.data_fetch_spinner.show)\n        self.db_startup.PROGRESS.connect(self.data_fetch_spinner.set_text)\n        self.manga_list_view.gallery_model.ADDED_ROWS.connect(self.data_fetch_spinner.before_hide)\n        self.db_startup.DONE.connect(self.data_fetch_spinner.before_hide)\n\n        ## deleting spinner\n        #self.gallery_delete_spinner = misc.Spinner(self)\n        #self.gallery_delete_spinner.set_size(40,40)\n        ##self.gallery_delete_spinner.set_text('Removing...')\n        #self.manga_list_view.gallery_model.rowsAboutToBeRemoved.connect(self.gallery_delete_spinner.show)\n        #self.manga_list_view.gallery_model.rowsRemoved.connect(self.gallery_delete_spinner.before_hide)\n\n\n    def search(self, srch_string):\n        \"Args should be Search Enums\"\n        self.search_bar.setText(srch_string)\n        self.search_backward.setVisible(True)\n        args = []\n        if app_constants.GALLERY_SEARCH_REGEX:\n            args.append(app_constants.Search.Regex)\n        if app_constants.GALLERY_SEARCH_CASE:\n            args.append(app_constants.Search.Case)\n        if app_constants.GALLERY_SEARCH_STRICT:\n            args.append(app_constants.Search.Strict)\n        self.current_manga_view.get_current_view().sort_model.init_search(srch_string, args)\n        old_cursor_pos = self._search_cursor_pos[0]\n        self.search_bar.end(False)\n        if self.search_bar.cursorPosition() != old_cursor_pos + 1:\n            self.search_bar.setCursorPosition(old_cursor_pos)\n\n    def switch_display(self):\n        \"Switches between fav and catalog display\"\n        if self.current_manga_view.fav_is_current():\n            self.tab_manager.library_btn.click()\n        else:\n            self.tab_manager.favorite_btn.click()\n\n    def settings(self):\n        sett = settingsdialog.SettingsDialog(self)\n        sett.scroll_speed_changed.connect(self.manga_list_view.updateGeometries)\n        #sett.show()\n\n    def init_toolbar(self):\n        self.toolbar = QToolBar()\n        self.toolbar.adjustSize()\n        #self.toolbar.setFixedHeight()\n        self.toolbar.setWindowTitle(\"Show\") # text for the contextmenu\n        #self.toolbar.setStyleSheet(\"QToolBar {border:0px}\") # make it user\n                                                   #defined?\n        self.toolbar.setMovable(False)\n        self.toolbar.setFloatable(False)\n        #self.toolbar.setIconSize(QSize(20,20))\n        self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        self.toolbar.setIconSize(QSize(20,20))\n\n        def switch_view(fav):\n            if fav:\n                self.default_manga_view.get_current_view().sort_model.fav_view()\n            else:\n                self.default_manga_view.get_current_view().sort_model.catalog_view()\n\n        self.tab_manager = misc_db.ToolbarTabManager(self.toolbar, self)\n        self.tab_manager.favorite_btn.clicked.connect(lambda: switch_view(True))\n        self.tab_manager.library_btn.click()\n        self.tab_manager.library_btn.clicked.connect(lambda: switch_view(False))\n\n        self.addition_tab = self.tab_manager.addTab(\"Inbox\", app_constants.ViewType.Addition, icon=app_constants.INBOX_ICON)\n\n        gallery_k = QKeySequence('Alt+G')\n        new_gallery_k = QKeySequence('Ctrl+N')\n        new_galleries_k = QKeySequence('Ctrl+Shift+N')\n        new_populate_k = QKeySequence('Ctrl+Alt+N')\n        scan_galleries_k = QKeySequence('Ctrl+Alt+S')\n        open_random_k = QKeySequence(QKeySequence.Open)\n        get_all_metadata_k = QKeySequence('Ctrl+Alt+M')\n        gallery_downloader_k = QKeySequence('Ctrl+Alt+D')\n\n        gallery_menu = QMenu()\n        gallery_action = QToolButton()\n        gallery_action.setIcon(app_constants.PLUS_ICON)\n        gallery_action.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        gallery_action.setShortcut(gallery_k)\n        gallery_action.setText('Gallery ')\n        gallery_action.setPopupMode(QToolButton.InstantPopup)\n        gallery_action.setToolTip('Contains various gallery related features')\n        gallery_action.setMenu(gallery_menu)\n        add_gallery_icon = QIcon(app_constants.PLUS_ICON)\n        gallery_action_add = QAction(add_gallery_icon, \"Add a gallery...\", self)\n        gallery_action_add.triggered.connect(lambda: gallery.CommonView.spawn_dialog(self))\n        gallery_action_add.setToolTip('Add a single gallery thoroughly')\n        gallery_action_add.setShortcut(new_gallery_k)\n        gallery_menu.addAction(gallery_action_add)\n        add_more_action = QAction(add_gallery_icon, \"Add galleries...\", self)\n        add_more_action.setStatusTip('Add galleries from different folders')\n        add_more_action.setShortcut(new_galleries_k)\n        add_more_action.triggered.connect(lambda: self.populate(True))\n        gallery_menu.addAction(add_more_action)\n        populate_action = QAction(add_gallery_icon, \"Populate from directory/archive...\", self)\n        populate_action.setStatusTip('Populates the DB with galleries from a single folder or archive')\n        populate_action.triggered.connect(self.populate)\n        populate_action.setShortcut(new_populate_k)\n        gallery_menu.addAction(populate_action)\n        gallery_menu.addSeparator()\n        scan_galleries_action = QAction('Scan for new galleries', self)\n        scan_galleries_action.setIcon(app_constants.SPINNER_ICON)\n        scan_galleries_action.triggered.connect(self.scan_for_new_galleries)\n        scan_galleries_action.setStatusTip('Scan monitored folders for new galleries')\n        scan_galleries_action.setShortcut(scan_galleries_k)\n        gallery_menu.addAction(scan_galleries_action)\n\n        duplicate_check_simple = QAction(\"Check for duplicate galleries\", self)\n        duplicate_check_simple.setIcon(app_constants.DUPLICATE_ICON)\n        duplicate_check_simple.triggered.connect(lambda: self.duplicate_check()) # triggered emits False\n        gallery_menu.addAction(duplicate_check_simple)\n\n        self.toolbar.addWidget(gallery_action)\n\n        spacer_tool = QWidget() \n        spacer_tool.setFixedSize(QSize(5, 1))\n        self.toolbar.addWidget(spacer_tool)\n\n        metadata_action = QToolButton()\n        metadata_action.setText('Fetch all metadata')\n        metadata_action.clicked.connect(self.get_metadata)\n        metadata_action.setIcon(app_constants.DOWNLOAD_ICON)\n        metadata_action.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        metadata_action.setShortcut(get_all_metadata_k)\n        self.toolbar.addWidget(metadata_action)\n\n        spacer_tool2 = QWidget() \n        spacer_tool2.setFixedSize(QSize(1, 1))\n        self.toolbar.addWidget(spacer_tool2)\n        \n        gallery_action_random = QToolButton()\n        gallery_action_random.setText(\"Open random gallery\")\n        gallery_action_random.clicked.connect(lambda: gallery.CommonView.open_random_gallery(self.get_current_view()))\n        gallery_action_random.setIcon(app_constants.RANDOM_ICON)\n        gallery_action_random.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        gallery_action_random.setShortcut(open_random_k)\n        self.toolbar.addWidget(gallery_action_random)\n\n        spacer_tool3 = QWidget() \n        spacer_tool3.setFixedSize(QSize(1, 1))\n        self.toolbar.addWidget(spacer_tool3)\n\n        gallery_downloader = QToolButton()\n        gallery_downloader.setText(\"Downloader\")\n        gallery_downloader.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        gallery_downloader.clicked.connect(self.download_window.show)\n        gallery_downloader.setShortcut(gallery_downloader_k)\n        gallery_downloader.setIcon(app_constants.MANAGER_ICON)\n        self.toolbar.addWidget(gallery_downloader)\n\n        spacer_tool4 = QWidget() \n        spacer_tool4.setFixedSize(QSize(5, 1))\n        self.toolbar.addWidget(spacer_tool4)\n\n        # debug specfic code\n        if app_constants.DEBUG:\n            def debug_func():\n                pass\n        \n            debug_btn = QToolButton()\n            debug_btn.setText(\"DEBUG BUTTON\")\n            self.toolbar.addWidget(debug_btn)\n            debug_btn.clicked.connect(debug_func)\n\n        spacer_middle = QWidget() # aligns buttons to the right\n        spacer_middle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n        self.toolbar.addWidget(spacer_middle)\n\n        sort_k = QKeySequence('Alt+S')\n\n        sort_action = QToolButton()\n        sort_action.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)\n        sort_action.setShortcut(sort_k)\n        sort_action.setIcon(app_constants.SORT_ICON_DESC)\n        sort_menu = misc.SortMenu(self, self.toolbar, sort_action)\n        sort_menu.set_toolbutton_text()\n        sort_action.setMenu(sort_menu)\n        sort_action.setPopupMode(QToolButton.InstantPopup)\n        self.toolbar.addWidget(sort_action)\n\n        def set_new_sort(s):\n            sort_menu.set_toolbutton_text()\n            self.current_manga_view.list_view.sort(s)\n        sort_menu.new_sort.connect(set_new_sort)\n\n        spacer_tool4 = QWidget() \n        spacer_tool4.setFixedSize(QSize(5, 1))\n        self.toolbar.addWidget(spacer_tool4)\n        \n        togle_view_k = QKeySequence('Alt+Space')\n\n        self.grid_toggle_g_icon = app_constants.GRID_ICON\n        self.grid_toggle_l_icon = app_constants.LIST_ICON\n        self.grid_toggle = QToolButton()\n        self.grid_toggle.setToolButtonStyle(Qt.ToolButtonIconOnly)\n        self.grid_toggle.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)\n        self.grid_toggle.setShortcut(togle_view_k)\n        if self.current_manga_view.current_view == gallery.MangaViews.View.List:\n            self.grid_toggle.setIcon(self.grid_toggle_l_icon)\n        else:\n            self.grid_toggle.setIcon(self.grid_toggle_g_icon)\n        self.grid_toggle.setObjectName('gridtoggle')\n        self.grid_toggle.clicked.connect(self.toggle_view)\n        self.toolbar.addWidget(self.grid_toggle)\n\n        spacer_mid2 = QWidget()\n        spacer_mid2.setFixedSize(QSize(5, 1))\n        self.toolbar.addWidget(spacer_mid2)\n\n        search_options = QToolButton()\n        search_options.setIconSize(QSize(15,15))\n        search_options.setPopupMode(QToolButton.InstantPopup)\n        self.toolbar.addWidget(search_options)\n        search_options.setIcon(app_constants.SEARCH_ICON)\n        search_options_menu = QMenu(self)\n        search_options.setMenu(search_options_menu)\n        case_search_option = search_options_menu.addAction('Case Sensitive')\n        case_search_option.setCheckable(True)\n        case_search_option.setChecked(app_constants.GALLERY_SEARCH_CASE)\n\n\n        def set_search_case(b):\n            app_constants.GALLERY_SEARCH_CASE = b\n            settings.set(b, 'Application', 'gallery search case')\n            settings.save()\n\n        case_search_option.toggled.connect(set_search_case)\n        search_options_menu.addSeparator()\n        strict_search_option = search_options_menu.addAction('Match whole terms')\n        strict_search_option.setCheckable(True)\n        strict_search_option.setChecked(app_constants.GALLERY_SEARCH_STRICT)\n\n        regex_search_option = search_options_menu.addAction('Regex')\n        regex_search_option.setCheckable(True)\n        regex_search_option.setChecked(app_constants.GALLERY_SEARCH_REGEX)\n\n        def set_search_strict(b):\n            if b:\n                if regex_search_option.isChecked():\n                    regex_search_option.toggle()\n            app_constants.GALLERY_SEARCH_STRICT = b\n            settings.set(b, 'Application', 'gallery search strict')\n            settings.save()\n\n        strict_search_option.toggled.connect(set_search_strict)\n\n        def set_search_regex(b):\n            if b:\n                if strict_search_option.isChecked():\n                    strict_search_option.toggle()\n            app_constants.GALLERY_SEARCH_REGEX = b\n            settings.set(b, 'Application', 'allow search regex')\n            settings.save()\n\n        regex_search_option.toggled.connect(set_search_regex)\n\n        self.search_bar = misc.LineEdit()\n\n        remove_txt = self.search_bar.addAction(app_constants.CROSS_ICON, QLineEdit.LeadingPosition)\n        refresh_search = self.search_bar.addAction(app_constants.REFRESH_ICON, QLineEdit.TrailingPosition)\n        refresh_search.triggered.connect(self.current_manga_view.get_current_view().sort_model.refresh)\n        remove_txt.setVisible(False)\n        def clear_txt():\n            self.search_bar.setText(\"\")\n            self.search_bar.returnPressed.emit()\n        remove_txt.triggered.connect(clear_txt)\n        def hide_cross(txt):\n            remove_txt.setVisible(bool(txt))\n        self.search_bar.textChanged.connect(hide_cross)\n\n        self.search_bar.setObjectName('search_bar')\n        self.search_timer = QTimer(self)\n        self.search_timer.setSingleShot(True)\n        self.search_timer.timeout.connect(lambda: self.search(self.search_bar.text()))\n        self._search_cursor_pos = [0, 0]\n        def set_cursor_pos(old, new):\n            self._search_cursor_pos[0] = old\n            self._search_cursor_pos[1] = new\n        self.search_bar.cursorPositionChanged.connect(set_cursor_pos)\n\n        if app_constants.SEARCH_AUTOCOMPLETE:\n            completer = QCompleter(self)\n            completer_view = misc.CompleterPopupView()\n            completer.setPopup(completer_view)\n            completer_view._setup()\n            completer.setModel(self.manga_list_view.gallery_model)\n            completer.setCaseSensitivity(Qt.CaseInsensitive)\n            completer.setCompletionMode(QCompleter.PopupCompletion)\n            completer.setCompletionRole(Qt.DisplayRole)\n            completer.setCompletionColumn(app_constants.TITLE)\n            completer.setFilterMode(Qt.MatchContains)\n            completer.activated[str].connect(lambda a: self.search(a))\n            self.search_bar.setCompleter(completer)\n            self.search_bar.returnPressed.connect(lambda: self.search(self.search_bar.text()))\n        if not app_constants.SEARCH_ON_ENTER:\n            self.search_bar.textEdited.connect(lambda: self.search_timer.start(800))\n        self.search_bar.setPlaceholderText(\"Search title, artist, namespace & tags\")\n        self.search_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n        self.manga_list_view.sort_model.HISTORY_SEARCH_TERM.connect(lambda a: self.search_bar.setText(a))\n        self.toolbar.addWidget(self.search_bar)\n\n        def search_history(_, back=True): # clicked signal passes a bool\n            sort_model = self.manga_list_view.sort_model\n            nav = sort_model.PREV if back else sort_model.NEXT\n            history_term = sort_model.navigate_history(nav)\n            if back:\n                self.search_forward.setVisible(True)\n\n        back_k = QKeySequence(QKeySequence.Back)\n        forward_k = QKeySequence(QKeySequence.Forward)\n\n        search_backbutton = QToolButton(self.toolbar)\n        search_backbutton.setIcon(app_constants.ARROW_LEFT_ICON)\n        search_backbutton.setFixedWidth(20)\n        search_backbutton.clicked.connect(search_history)\n        search_backbutton.setShortcut(back_k)\n        self.search_backward = self.toolbar.addWidget(search_backbutton)\n        self.search_backward.setVisible(False)\n        search_forwardbutton = QToolButton(self.toolbar)\n        search_forwardbutton.setIcon(app_constants.ARROW_RIGHT_ICON)\n        search_forwardbutton.setFixedWidth(20)\n        search_forwardbutton.clicked.connect(lambda: search_history(None, False))\n        search_forwardbutton.setShortcut(forward_k)\n        self.search_forward = self.toolbar.addWidget(search_forwardbutton)\n        self.search_forward.setVisible(False)\n\n        spacer_end = QWidget() # aligns settings action properly\n        spacer_end.setFixedSize(QSize(10, 1))\n        self.toolbar.addWidget(spacer_end)\n\n        settings_k = QKeySequence(\"Ctrl+P\")\n\n        settings_act = QToolButton(self.toolbar)\n        settings_act.setShortcut(settings_k)\n        settings_act.setIcon(QIcon(app_constants.SETTINGS_PATH))\n        settings_act.clicked.connect(self.settings)\n        self.toolbar.addWidget(settings_act)\n\n        self.addToolBar(self.toolbar)\n\n    def get_current_view(self):\n        return self.current_manga_view.get_current_view()\n\n    def toggle_view(self):\n        \"\"\"\n        Toggles the current display view\n        \"\"\"\n        if self.current_manga_view.current_view == gallery.MangaViews.View.Table:\n            self.current_manga_view.changeTo(self.current_manga_view.m_l_view_index)\n            self.grid_toggle.setIcon(self.grid_toggle_l_icon)\n        else:\n            self.current_manga_view.changeTo(self.current_manga_view.m_t_view_index)\n            self.grid_toggle.setIcon(self.grid_toggle_g_icon)\n\n    # TODO: Improve this so that it adds to the gallery dialog,\n    # so user can edit data before inserting (make it a choice)\n    def populate(self, mixed=None):\n        \"Populates the database with gallery from local drive'\"\n\n        if mixed:\n            gallery_view = misc.GalleryListView(self, True)\n            gallery_view.SERIES.connect(self.gallery_populate)\n            gallery_view.show()\n        else:\n            msg_box = misc.BasePopup(self)\n            l = QVBoxLayout()\n            msg_box.main_widget.setLayout(l)\n            l.addWidget(QLabel('Directory or Archive?'))\n            l.addLayout(msg_box.buttons_layout)\n\n            def from_dir():\n                path = QFileDialog.getExistingDirectory(self, \"Choose a directory containing your galleries\")\n                if not path:\n                    return\n                msg_box.close()\n                app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True\n                self.gallery_populate(path, True)\n            def from_arch():\n                path = QFileDialog.getOpenFileName(self, 'Choose an archive containing your galleries',\n                                       filter=utils.FILE_FILTER)\n                path = [path[0]]\n                if not all(path) or not path:\n                    return\n                msg_box.close()\n                app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True\n                self.gallery_populate(path, True)\n\n            buttons = msg_box.add_buttons('Directory', 'Archive', 'Close')\n            buttons[2].clicked.connect(msg_box.close)\n            buttons[0].clicked.connect(from_dir)\n            buttons[1].clicked.connect(from_arch)\n            msg_box.adjustSize()\n            msg_box.show()\n\n    def gallery_populate(self, path, validate=False):\n        \"Scans the given path for gallery to add into the DB\"\n        if len(path) is not 0:\n            data_thread = QThread(self)\n            data_thread.setObjectName('General gallery populate')\n            self.addition_tab.click()\n            self.g_populate_inst = fetch.Fetch()\n            self.g_populate_inst.series_path = path\n            self._g_populate_count = 0\n\n            fetch_spinner = misc.Spinner(self)\n            fetch_spinner.set_size(60)\n            fetch_spinner.set_text(\"Populating\")\n            fetch_spinner.show()\n\n            def finished(status):\n                fetch_spinner.hide()\n                if not status:\n                    log_e('Populating DB from gallery folder: Nothing was added!')\n                    self.notif_bubble.update_text(\"Gallery Populate\",\n                                   \"<font color='red'>Nothing was added. Check happypanda_log for details..</font>\")\n\n            def skipped_gs(s_list):\n                \"Skipped galleries\"\n                msg_box = QMessageBox(self)\n                msg_box.setIcon(QMessageBox.Question)\n                msg_box.setText('Do you want to view skipped paths?')\n                msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)\n                msg_box.setDefaultButton(QMessageBox.No)\n                if msg_box.exec() == QMessageBox.Yes:\n                    list_wid = QTableWidget(self)\n                    list_wid.setAttribute(Qt.WA_DeleteOnClose)\n                    list_wid.setRowCount(len(s_list))\n                    list_wid.setColumnCount(2)\n                    list_wid.setAlternatingRowColors(True)\n                    list_wid.setEditTriggers(list_wid.NoEditTriggers)\n                    list_wid.setHorizontalHeaderLabels(['Reason', 'Path'])\n                    list_wid.setSelectionBehavior(list_wid.SelectRows)\n                    list_wid.setSelectionMode(list_wid.SingleSelection)\n                    list_wid.setSortingEnabled(True)\n                    list_wid.verticalHeader().hide()\n                    list_wid.setAutoScroll(False)\n                    for x, g in enumerate(s_list):\n                        list_wid.setItem(x, 0, QTableWidgetItem(g[1]))\n                        list_wid.setItem(x, 1, QTableWidgetItem(g[0]))\n                    list_wid.resizeColumnsToContents()\n                    list_wid.setWindowTitle('{} skipped paths'.format(len(s_list)))\n                    list_wid.setWindowFlags(Qt.Window)\n                    list_wid.resize(900,400)\n\n                    list_wid.doubleClicked.connect(lambda i: utils.open_path(list_wid.item(i.row(), 1).text(), list_wid.item(i.row(), 1).text()))\n\n                    list_wid.show()\n\n            def a_progress(prog):\n                fetch_spinner.set_text(\"Populating... {}/{}\".format(prog, self._g_populate_count))\n\n            def add_to_model(gallery):\n                self.addition_tab.view.add_gallery(gallery, app_constants.KEEP_ADDED_GALLERIES)\n\n            def set_count(c):\n                self._g_populate_count = c\n\n            self.g_populate_inst.moveToThread(data_thread)\n            self.g_populate_inst.PROGRESS.connect(a_progress)\n            self.g_populate_inst.DATA_COUNT.connect(set_count)\n            self.g_populate_inst.LOCAL_EMITTER.connect(add_to_model)\n            self.g_populate_inst.FINISHED.connect(finished)\n            self.g_populate_inst.FINISHED.connect(self.g_populate_inst.deleteLater)\n            self.g_populate_inst.SKIPPED.connect(skipped_gs)\n            data_thread.finished.connect(data_thread.deleteLater)\n            data_thread.started.connect(self.g_populate_inst.local)\n            data_thread.start()\n            #self.g_populate_inst.local()\n            log_i('Populating DB from directory/archive')\n\n    def scan_for_new_galleries(self):\n        available_folders = app_constants.ENABLE_MONITOR and \\\n                                    app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS)\n        if available_folders and not app_constants.SCANNING_FOR_GALLERIES:\n            app_constants.SCANNING_FOR_GALLERIES = True\n            self.notification_bar.add_text(\"Scanning for new galleries...\")\n            log_i('Scanning for new galleries...')\n            try:\n                class ScanDir(QObject):\n                    finished = pyqtSignal()\n                    fetch_inst = fetch.Fetch(self)\n                    def __init__(self, addition_view, addition_tab, parent=None):\n                        super().__init__(parent)\n                        self.addition_view = addition_view\n                        self.addition_tab = addition_tab\n                        self._switched = False\n\n                    def switch_tab(self):\n                        if not self._switched:\n                            self.addition_tab.click()\n                            self._switched = True\n\n                    def scan_dirs(self):\n                        paths = []\n                        for p in app_constants.MONITOR_PATHS:\n                            if os.path.exists(p):\n                                dir_content = scandir.scandir(p)\n                                for d in dir_content:\n                                    paths.append(d.path)\n                            else:\n                                log_e(\"Monitored path does not exists: {}\".format(p.encode(errors='ignore')))\n\n                        self.fetch_inst.series_path = paths\n                        self.fetch_inst.LOCAL_EMITTER.connect(lambda g:self.addition_view.add_gallery(g, app_constants.KEEP_ADDED_GALLERIES))\n                        self.fetch_inst.LOCAL_EMITTER.connect(self.switch_tab)\n                        self.fetch_inst.local()\n                        #contents = []\n                        #for g in self.scanned_data:\n                        #\tcontents.append(g)\n\n                        #paths = sorted(paths)\n                        #new_galleries = []\n                        #for x in contents:\n                        #\ty = utils.b_search(paths, os.path.normcase(x.path))\n                        #\tif not y:\n                        #\t\tnew_galleries.append(x)\n                        self.finished.emit()\n                        self.deleteLater()\n                    #if app_constants.LOOK_NEW_GALLERY_AUTOADD:\n                    #\tQTimer.singleShot(10000,\n                    #\tself.gallery_populate(final_paths))\n                    #\treturn\n\n\n                def finished(): app_constants.SCANNING_FOR_GALLERIES = False\n\n                new_gall_spinner = misc.Spinner(self)\n                new_gall_spinner.set_text(\"Gallery Scan\")\n                new_gall_spinner.show()\n\n                thread = QThread(self)\n                self.scan_inst = ScanDir(self.addition_tab.view, self.addition_tab)\n                self.scan_inst.moveToThread(thread)\n                self.scan_inst.finished.connect(finished)\n                self.scan_inst.finished.connect(new_gall_spinner.before_hide)\n                thread.started.connect(self.scan_inst.scan_dirs)\n                #self.scan_inst.scan_dirs()\n                thread.finished.connect(thread.deleteLater)\n                thread.start()\n            except:\n                self.notification_bar.add_text('An error occured while attempting to scan for new galleries. Check happypanda.log.')\n                log.exception('An error occured while attempting to scan for new galleries.')\n                app_constants.SCANNING_FOR_GALLERIES = False\n        else:\n            self.notification_bar.add_text(\"Please specify directory in settings to scan for new galleries!\")\n\n    def dragEnterEvent(self, event):\n        if event.mimeData().hasUrls():\n            event.acceptProposedAction()\n        else:\n            super().dragEnterEvent(event)\n\n    def dropEvent(self, event):\n        acceptable = []\n        unaccept = []\n        for u in event.mimeData().urls():\n            path = u.toLocalFile()\n            if os.path.isdir(path) or path.endswith(utils.ARCHIVE_FILES):\n                acceptable.append(path)\n            else:\n                unaccept.append(path)\n        log_i('Acceptable dropped items: {}'.format(len(acceptable)))\n        log_i('Unacceptable dropped items: {}'.format(len(unaccept)))\n        log_d('Dropped items: {}\\n{}'.format(acceptable, unaccept).encode(errors='ignore'))\n        if acceptable:\n            self.notification_bar.add_text('Adding dropped items...')\n            log_i('Adding dropped items')\n            l = len(acceptable) == 1\n            f_item = acceptable[0]\n            if f_item.endswith(utils.ARCHIVE_FILES):\n                f_item = utils.check_archive(f_item)\n            else:\n                f_item = utils.recursive_gallery_check(f_item)\n            f_item_l = len(f_item) < 2\n            subfolder_as_c = not app_constants.SUBFOLDER_AS_GALLERY\n            if l and subfolder_as_c or l and f_item_l:\n                g_d = gallerydialog.GalleryDialog(self, acceptable[0])\n                g_d.show()\n            else:\n                self.gallery_populate(acceptable, True)\n            event.accept()\n        else:\n            text = 'File not supported' if len(unaccept) < 2 else 'Files not supported'\n            self.notification_bar.add_text(text)\n\n        if unaccept:\n            self.notification_bar.add_text('Some unsupported files did not get added')\n        super().dropEvent(event)\n\n    def resizeEvent(self, event):\n        try:\n            self.notification_bar.resize(event.size().width())\n        except AttributeError:\n            pass\n        self.move_listener.emit()\n        return super().resizeEvent(event)\n\n    def moveEvent(self, event):\n        self.move_listener.emit()\n        return super().moveEvent(event)\n\n    def showEvent(self, event):\n        return super().showEvent(event)\n\n    def cleanup_exit(self):\n        self.system_tray.hide()\n        # watchers\n        try:\n            self.watchers.stop_all()\n        except AttributeError:\n            pass\n\n        # settings\n        settings.set(self.manga_list_view.current_sort, 'General', 'current sort')\n        settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths')\n        if not self.isMaximized():\n            settings.win_save(self, 'AppWindow')\n\n        # temp dir\n        try:\n            for root, dirs, files in scandir.walk('temp', topdown=False):\n                for name in files:\n                    os.remove(os.path.join(root, name))\n                for name in dirs:\n                    os.rmdir(os.path.join(root, name))\n            log_d('Flush temp on exit: OK')\n        except:\n            log.exception('Flush temp on exit: FAIL')\n\n        # DB\n        try:\n            log_i(\"Analyzing database...\")\n            gallerydb.GalleryDB.analyze()\n            log_i(\"Closing database...\")\n            gallerydb.GalleryDB.close()\n        except:\n            pass\n        self.download_window.close()\n\n        # check if there is db activity\n        if not gallerydb.method_queue.empty():\n            class DBActivityChecker(QObject):\n                FINISHED = pyqtSignal()\n                def __init__(self, **kwargs):\n                    super().__init__(**kwargs)\n\n                def check(self):\n                    gallerydb.method_queue.join()\n                    self.FINISHED.emit()\n                    self.deleteLater()\n\n            db_activity = DBActivityChecker()\n            db_spinner = misc.Spinner(self)\n            self.db_activity_checker.connect(db_activity.check)\n            db_activity.moveToThread(app_constants.GENERAL_THREAD)\n            db_activity.FINISHED.connect(db_spinner.close)\n            db_spinner.set_text('DB Activity')\n            db_spinner.show()\n            self.db_activity_checker.emit()\n            msg_box = QMessageBox(self)\n            msg_box.setText('Database activity detected!')\n            msg_box.setInformativeText(\"Closing now might result in data loss.\" + \" Do you still want to close?\\n(Wait for the activity spinner to hide before closing)\")\n            msg_box.setIcon(QMessageBox.Critical)\n            msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)\n            msg_box.setDefaultButton(QMessageBox.No)\n            if msg_box.exec() == QMessageBox.Yes:\n                return 1\n            else:\n                return 2\n        else:\n            return 0\n\n    def duplicate_check(self, simple=True):\n        try:\n            self.duplicate_check_invoker.disconnect()\n        except TypeError:\n            pass\n        mode = 'simple' if simple else 'advanced'\n        log_i('Checking for duplicates in mode: {}'.format(mode))\n        notifbar = app_constants.NOTIF_BAR\n        notifbar.add_text('Checking for duplicates...')\n        duplicate_spinner = misc.Spinner(self)\n        duplicate_spinner.set_text(\"Duplicate Check\")\n        duplicate_spinner.show()\n        dup_tab = self.tab_manager.addTab(\"Duplicate\", app_constants.ViewType.Duplicate)\n        dup_tab.view.set_delete_proxy(self.default_manga_view.gallery_model)\n\n        class DuplicateCheck(QObject):\n            found_duplicates = pyqtSignal(tuple)\n            finished = pyqtSignal()\n            def __init__(self):\n                super().__init__()\n\n            def checkSimple(self, model):\n                galleries = model._data\n\n                duplicates = []\n                for n, g in enumerate(galleries, 1):\n                    notifbar.add_text('Checking gallery {}'.format(n))\n                    log_d('Checking gallery {}'.format(g.title.encode(errors=\"ignore\")))\n                    for y in galleries:\n                        title = g.title.strip().lower() == y.title.strip().lower()\n                        path = os.path.normcase(g.path) == os.path.normcase(y.path)\n                        if g.id != y.id and (title or path):\n                            if g not in duplicates:\n                                duplicates.append(y)\n                                duplicates.append(g)\n                                self.found_duplicates.emit((g, y))\n                self.finished.emit()\n\n        self._d_checker = DuplicateCheck()\n        self._d_checker.moveToThread(app_constants.GENERAL_THREAD)\n        self._d_checker.found_duplicates.connect(lambda t: dup_tab.view.add_gallery(t, record_time=True))\n        self._d_checker.finished.connect(dup_tab.click)\n        self._d_checker.finished.connect(self._d_checker.deleteLater)\n        self._d_checker.finished.connect(duplicate_spinner.before_hide)\n        if simple:\n            self.duplicate_check_invoker.connect(self._d_checker.checkSimple)\n        self.duplicate_check_invoker.emit(self.default_manga_view.gallery_model)\n\n    def excepthook(self, ex_type, ex, tb):\n        log_c(''.join(traceback.format_tb(tb)))\n        log_c('{}: {}'.format(ex_type, ex))\n        traceback.print_exception(ex_type, ex, tb)\n        w = QMessageBox(self)\n        w.setWindowTitle(\"Critical Error\")\n        w.setIcon(QMessageBox.Critical)\n        w.setText('A critical error has ben encountered. Stability from this point onward cannot be guaranteed.')\n        w.setStandardButtons(QMessageBox.Ok)\n        w.setDefaultButton(QMessageBox.Ok)\n        w.exec_()\n\n    def closeEvent(self, event):\n        r_code = self.cleanup_exit()\n        if r_code == 1:\n            log_d('Force Exit App: OK')\n            super().closeEvent(event)\n        elif r_code == 2:\n            log_d('Ignore Exit App')\n            event.ignore()\n        else:\n            log_d('Normal Exit App: OK')\n            super().closeEvent(event)\n\nif __name__ == '__main__':\n    raise NotImplementedError(\"Unit testing not implemented yet!\")"
  },
  {
    "path": "version/app_constants.py",
    "content": "﻿#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n\n\"\"\"Contains constants to be used by several modules\"\"\"\n\nimport os, sys, enum\nimport qtawesome as qta\n\ntry:\n    import settings\n    from database import db_constants\nexcept ImportError:\n    from . import settings\n    from .database import db_constants\n\n# Version number\nvs  = '1.1'\nDEBUG = False\n\nOS_NAME = ''\nif sys.platform.startswith('darwin'):\n\tOS_NAME = \"darwin\"\nelif os.name == 'nt':\n\tOS_NAME = \"windows\"\nelif os.name == 'posix':\n\tOS_NAME = \"linux\"\n\nAPP_RESTART_CODE = 0\n\nget = settings.get\n\nposix_program_dir = os.path.dirname(os.path.realpath(__file__))\nif os.name == 'posix':\n\t static_dir = os.path.join(posix_program_dir, '../res')\n\t bin_dir = os.path.join(posix_program_dir, 'bin')\n\t temp_dir = os.path.join(posix_program_dir, 'temp')\nelse:\n\tbin_dir = os.path.join(os.getcwd(), 'bin')\n\tstatic_dir = os.path.join(os.getcwd(), \"res\")\n\ttemp_dir = os.path.join('temp')\n# path to unrar tool binary\nunrar_tool_path = get('', 'Application', 'unrar tool path')\n\n# type of download needed by download manager for each site parser\n# NOTE define here if any new type will be supported in the future.\nDOWNLOAD_TYPE_ARCHIVE = 0\nDOWNLOAD_TYPE_TORRENT = 1 # Note: With this type, file will be sent to torrent program\nDOWNLOAD_TYPE_OTHER = 2\n\nVALID_GALLERY_CATEGORY = (\n    'Doujinshi',\n    'Manga',\n    'Artist CG',\n    'Game CG',\n    'Western',\n    'Non-H',\n    'Image Set',\n    'Cosplay',\n    'Miscellaneous',\n    'Private'\n)\n\n#default stylesheet path\ndefault_stylesheet_path = os.path.join(static_dir,\"style.css\")\nuser_stylesheet_path = \"\"\n\nINTERNAL_LEVEL = 8\nFIRST_TIME_LEVEL = get(INTERNAL_LEVEL, 'Application', 'first time level', int)\nUPDATE_VERSION = get('0.30', 'Application', 'version', str)\nFORCE_HIGH_DPI_SUPPORT = get(False, 'Advanced', 'force high dpi support', bool)\n\n# sizes\nMAIN_W = 1061 # main window\nMAIN_H = 650 # main window\nSIZE_FACTOR = get(10, 'Visual', 'size factor', int)\nGRID_SPACING = get(15, 'Visual', 'grid spacing', int)\nLISTBOX_H_SIZE = 190\nLISTBOX_W_SIZE = 950\nGRIDBOX_LBL_H = 60\nTHUMB_H_SIZE = 190 + SIZE_FACTOR\nTHUMB_W_SIZE = 133 + SIZE_FACTOR\n\nTHUMB_DEFAULT = (THUMB_W_SIZE, THUMB_H_SIZE)\nTHUMB_SMALL = (140, 93)\n\n# Columns\nCOLUMNS = tuple(range(11))\nTITLE = 0\nARTIST = 1\nDESCR = 2\nTAGS = 3\nTYPE = 4\nFAV = 5\nCHAPTERS = 6\nLANGUAGE = 7\nLINK = 8\nPUB_DATE = 9\nDATE_ADDED = 10\n\n@enum.unique\nclass ViewType(enum.IntEnum):\n\tDefault = 1\n\tAddition = 2\n\tDuplicate = 3\n\n@enum.unique\nclass ProfileType(enum.Enum):\n\tDefault = 1\n\tSmall = 2\n\n# Application\nSYSTEM_TRAY = None\nNOTIF_BAR = None\nNOTIF_BUBBLE = None\nSTAT_MSG_METHOD = None\nGENERAL_THREAD = None\nWHEEL_SCROLL_EFFECT = 10\nDOWNLOAD_MANAGER = None\n\n# ICONS\n\n# IMPORTANT: Neccessary because qtawesome can't function without an instanced QApplication\n# IMPORTANT: called after instancing qApplication in main.py\ndef load_icons():\n    global G_LISTS_ICON_WH\n    global G_LISTS_ICON\n    global LIST_ICON\n    global ARTISTS_ICON\n    global ARTIST_ICON\n    global NSTAGS_ICON\n    global PLUS_ICON\n    global ARROW_RIGHT_ICON\n    global ARROW_LEFT_ICON\n    global GRID_ICON\n    global GRIDL_ICON\n    global SEARCH_ICON\n    global CROSS_ICON\n    global CROSS_ICON_WH\n    global MANAGER_ICON\n    global DOWNLOAD_ICON\n    global RANDOM_ICON\n    global DUPLICATE_ICON\n    global SORT_ICON_DESC\n    global SORT_ICON_ASC\n    global REFRESH_ICON\n    global STAR_ICON\n    global CIRCLE_ICON\n    global INBOX_ICON\n    global SPINNER_ICON\n\n    G_LISTS_ICON_WH = qta.icon(\"fa.bars\", color=\"white\")\n    G_LISTS_ICON = qta.icon(\"fa.bars\", color=\"black\")\n    LIST_ICON = qta.icon(\"fa.bars\", color=\"white\")\n    ARTISTS_ICON = qta.icon(\"fa.users\", color=\"white\")\n    ARTIST_ICON = qta.icon(\"fa.user\", color=\"black\")\n    NSTAGS_ICON = qta.icon(\"fa.sitemap\", color=\"white\")\n    PLUS_ICON = qta.icon(\"fa.plus\", color=\"white\")\n    ARROW_RIGHT_ICON = qta.icon(\"fa.angle-double-right\", color=\"white\")\n    ARROW_LEFT_ICON = qta.icon(\"fa.angle-double-left\", color=\"white\")\n    GRID_ICON = qta.icon(\"fa.th\", color=\"white\")\n    GRIDL_ICON = qta.icon(\"fa.th-large\", color=\"white\")\n    SEARCH_ICON = qta.icon(\"fa.search\", color=\"white\")\n    CROSS_ICON = qta.icon(\"fa.times\", color=\"black\")\n    CROSS_ICON_WH = qta.icon(\"fa.times\", color=\"white\")\n    MANAGER_ICON = qta.icon(\"fa.tasks\", color=\"white\")\n    DOWNLOAD_ICON = qta.icon(\"fa.arrow-circle-o-down\", color=\"white\")\n    RANDOM_ICON = qta.icon(\"fa.random\", color=\"white\")\n    DUPLICATE_ICON = qta.icon(\"fa.files-o\", color=\"white\")\n    SORT_ICON_DESC = qta.icon(\"fa.sort-amount-desc\", color=\"white\")\n    SORT_ICON_ASC = qta.icon(\"fa.sort-amount-asc\", color=\"white\")\n    REFRESH_ICON = qta.icon(\"fa.refresh\", color=\"black\")\n    STAR_ICON = qta.icon(\"fa.star\", color=\"white\")\n    CIRCLE_ICON = qta.icon(\"fa.circle\", color=\"white\")\n    INBOX_ICON = qta.icon(\"fa.inbox\", color=\"white\")\n    SPINNER_ICON = qta.icon(\"fa.spinner\", color=\"white\")\n\n# image paths\nGALLERY_DEF_ICO_PATH = os.path.join(static_dir, \"gallery_def_ico.ico\")\nGALLERY_EXT_ICO_PATH = os.path.join(static_dir, \"gallery_ext_ico.ico\")\nAPP_ICO_PATH = os.path.join(static_dir, \"happypanda.ico\")\nSETTINGS_PATH = os.path.join(static_dir, \"settings.png\")\nNO_IMAGE_PATH = os.path.join(static_dir, \"default.jpg\")\n\n# Monitored Paths\nOVERRIDE_MONITOR = False # set true to make watchers to ignore next item (will be set to False)\nLOOK_NEW_GALLERY_STARTUP = get(True, 'Application', 'look new gallery startup', bool)\nENABLE_MONITOR = get(True, 'Application', 'enable monitor', bool)\nMONITOR_PATHS = [p for p in get([], 'Application', 'monitor paths', list) if os.path.exists(p)]\nIGNORE_PATHS = get([], 'Application', 'ignore paths', list)\nIGNORE_EXTS = get([], 'Application', 'ignore exts', list)\nSCANNING_FOR_GALLERIES = False # if a scan for new galleries is being done\nTEMP_PATH_IGNORE = []\n\n# GENERAL\nOVERRIDE_MOVE_IMPORTED_IN_FETCH = False # set to true to make a fetch instance ignore moving files (will be set to false)\nMOVE_IMPORTED_GALLERIES = get(False, 'Application', 'move imported galleries', bool)\nIMPORTED_GALLERY_DEF_PATH = get('', 'Application', 'imported gallery def path', str)\nOPEN_RANDOM_GALLERY_CHAPTERS = get(False, 'Application', 'open random gallery chapters', bool)\nOVERRIDE_SUBFOLDER_AS_GALLERY = False # set to true to make a fetch instance treat subfolder as galleries (will be set to false)\nSUBFOLDER_AS_GALLERY = get(False, 'Application', 'subfolder as gallery', bool)\nRENAME_GALLERY_SOURCE = get(False, 'Application', 'rename gallery source', bool)\nEXTRACT_CHAPTER_BEFORE_OPENING = get(True, 'Application', 'extract chapter before opening', bool)\nOPEN_GALLERIES_SEQUENTIALLY = get(False, 'Application', 'open galleries sequentially', bool)\nSEND_FILES_TO_TRASH = get(True, 'Application', 'send files to trash', bool)\nSHOW_SIDEBAR_WIDGET = get(False, 'Application', 'show sidebar widget', bool)\n\n# ADVANCED\nGALLERY_DATA_FIX_REGEX = get(\"\", 'Advanced', 'gallery data fix regex', str)\nGALLERY_DATA_FIX_TITLE = get(True, 'Advanced', 'gallery data fix title', bool)\nGALLERY_DATA_FIX_ARTIST = get(True, 'Advanced', 'gallery data fix artist', bool)\nGALLERY_DATA_FIX_REPLACE = get(\"\", 'Advanced', 'gallery data fix replace', str)\n\nEXTERNAL_VIEWER_ARGS = get(\"{$file}\", 'Advanced', 'external viewer args', str)\n\n# Import/Export\nEXPORT_FORMAT = get(1, 'Advanced', 'export format', int)\nEXPORT_PATH = ''\n\n# HASH\nHASH_GALLERY_PAGES = get('all', 'Advanced', 'hash gallery pages', int, str)\n\n# WEB\nINCLUDE_EH_EXPUNGED = get(False, 'Web', 'include eh expunged', bool)\nGLOBAL_EHEN_TIME = get(5, 'Web', 'global ehen time offset', int)\nGLOBAL_EHEN_LOCK = False\nDEFAULT_EHEN_URL = get('https://e-hentai.org/', 'Web', 'default ehen url', str)\nREPLACE_METADATA = get(False, 'Web', 'replace metadata', bool)\nALWAYS_CHOOSE_FIRST_HIT = get(False, 'Web', 'always choose first hit', bool)\nUSE_GALLERY_LINK = get(True, 'Web', 'use gallery link', bool)\nUSE_JPN_TITLE = get(False, 'Web', 'use jpn title', bool)\nCONTINUE_AUTO_METADATA_FETCHER = get(True, 'Web', 'continue auto metadata fetcher', bool)\nHEN_DOWNLOAD_TYPE = get(DOWNLOAD_TYPE_ARCHIVE, 'Web', 'hen download type', int)\nDOWNLOAD_DIRECTORY = get('downloads', 'Web', 'download directory', str)\nTORRENT_CLIENT = get('', 'Web', 'torrent client', str)\nHEN_LIST = get(['chaikahen'], 'Web', 'hen list', list)\nDOWNLOAD_GALLERY_TO_LIB = get(False, 'Web', 'download galleries to library', bool)\n\n# External Viewer\nEXTERNAL_VIEWER_SUPPORT = {'honeyview':['Honeyview.exe']}\nUSE_EXTERNAL_VIEWER = get(False, 'Application', 'use external viewer', bool)\nEXTERNAL_VIEWER_PATH = os.path.normcase(get('', 'Application', 'external viewer path', str))\n_REFRESH_EXTERNAL_VIEWER = False\n\n# controls\nTHUMBNAIL_CACHE_SIZE = (1024, get(200, 'Advanced', 'cache size', int)) #1024 is 1mib\nPREFETCH_ITEM_AMOUNT = get(50, 'Advanced', 'prefetch item amount', int)# amount of items to prefetch\nSCROLL_SPEED = get(7, 'Advanced', 'scroll speed', int) # controls how many steps it takes when scrolling\n\n# POPUP\nPOPUP_WIDTH = get(500, 'Visual', 'popup.w', int)\nPOPUP_HEIGHT = get(300, 'Visual', 'popup.h', int)\n\n# Gallery\nAPPEND_TAGS_GALLERIES = get(True, 'Application', 'append tags to gallery', bool)\nKEEP_ADDED_GALLERIES = get(True, 'Application', 'keep added galleries', bool)\nGALLERY_METAFILE_KEYWORDS = ('info.json', 'info.txt')\nCURRENT_SORT = get('title', 'General', 'current sort')\nHIGH_QUALITY_THUMBS = get(False, 'Visual', 'high quality thumbs', bool)\nDISPLAY_RATING = get(True, 'Visual', 'display gallery rating', bool)\nDISPLAY_GALLERY_TYPE = get(False, 'Visual', 'display gallery type', bool) if not sys.platform.startswith('darwin') else False\nDISPLAY_GALLERY_RIBBON = get(True, 'Visual', 'display gallery ribbon', bool)\nGALLERY_FONT = (get('Segoe UI', 'Visual', 'gallery font family', str),\n\t\t\t\tget(11, 'Visual', 'gallery font size', int))\nGALLERY_FONT_ELIDE = get(True, 'Visual', 'gallery font elide', bool)\n\nG_DEF_LANGUAGE = get('English', 'General', 'gallery default language', str)\nG_CUSTOM_LANGUAGES = get([], 'General', 'gallery custom languages', list)\nG_DEF_STATUS = get('Completed', 'General', 'gallery default status', str)\nG_DEF_TYPE = get('Doujinshi', 'General', 'gallery default type', str)\nG_LANGUAGES = [\"English\", \"Japanese\", \"Chinese\", \"Other\"]\nG_STATUS = [\"Ongoing\", \"Completed\", \"Unknown\"]\nG_TYPES = [\"Manga\", \"Doujinshi\", \"Artist CG Sets\", \"Game CG Sets\", \"Western\", \"Image Sets\", \"Non-H\", \"Cosplay\", \"Other\"]\n\n@enum.unique\nclass GalleryState(enum.Enum):\n\tDefault = 1\n\tNew = 2\n\n# Colors\nGRID_VIEW_TITLE_COLOR = get('#ffffff', 'Visual', 'grid view title color', str)\nGRID_VIEW_ARTIST_COLOR = get('#e2e2e2', 'Visual', 'grid view artist color', str)\nGRID_VIEW_LABEL_COLOR = get('#d64933', 'Visual', 'grid view label color', str)\n\nGRID_VIEW_T_MANGA_COLOR = get('#3498db', 'Visual', 'grid view t manga color', str)\nGRID_VIEW_T_DOUJIN_COLOR = get('#e74c3c', 'Visual', 'grid view t doujin color', str)\nGRID_VIEW_T_ARTIST_CG_COLOR = get('#16a085', 'Visual', 'grid view t artist cg color', str)\nGRID_VIEW_T_GAME_CG_COLOR = get('#2ecc71', 'Visual', 'grid view t game cg color', str)\nGRID_VIEW_T_WESTERN_COLOR = get('#ecf0f1', 'Visual', 'grid view t western color', str)\nGRID_VIEW_T_IMAGE_COLOR = get('#f39c12', 'Visual', 'grid view t image color', str)\nGRID_VIEW_T_NON_H_COLOR = get('#f1c40f', 'Visual', 'grid view t non-h color', str)\nGRID_VIEW_T_COSPLAY_COLOR = get('#9b59b6', 'Visual', 'grid view t cosplay color', str)\nGRID_VIEW_T_OTHER_COLOR = get('#34495e', 'Visual', 'grid view t other color', str)\n\n# Search\nSEARCH_AUTOCOMPLETE = get(True, 'Application', 'search autocomplete', bool)\nGALLERY_SEARCH_REGEX = get(False, 'Application', 'allow search regex', bool)\nSEARCH_ON_ENTER = get(False, 'Application', 'search on enter', bool)\nGALLERY_SEARCH_STRICT = get(False, 'Application', 'gallery search strict', bool)\nGALLERY_SEARCH_CASE = get(False, 'Application', 'gallery search case', bool)\n\n@enum.unique\nclass Search(enum.Enum):\n\tStrict = 1\n\tCase = 2\n\tRegex = 3\n\n# Grid Tooltip\nGRID_TOOLTIP = get(True, 'Visual', 'grid tooltip', bool)\nTOOLTIP_TITLE = get(False, 'Visual', 'tooltip title', bool)\nTOOLTIP_AUTHOR = get(False, 'Visual', 'tooltip author', bool)\nTOOLTIP_CHAPTERS = get(True, 'Visual', 'tooltip chapters', bool)\nTOOLTIP_STATUS = get(True, 'Visual', 'tooltip status', bool)\nTOOLTIP_TYPE = get(True, 'Visual', 'tooltip type', bool)\nTOOLTIP_LANG = get(False, 'Visual', 'tooltip lang', bool)\nTOOLTIP_DESCR = get(False, 'Visual', 'tooltip descr', bool)\nTOOLTIP_TAGS = get(False, 'Visual', 'tooltip tags', bool)\nTOOLTIP_LAST_READ = get(True, 'Visual', 'tooltip last read', bool)\nTOOLTIP_TIMES_READ = get(True, 'Visual', 'tooltip times read', bool)\nTOOLTIP_PUB_DATE = get(False, 'Visual', 'tooltip pub date', bool)\nTOOLTIP_DATE_ADDED = get(True, 'Visual', 'tooltip date added', bool)\n\nGALLERY_ADDITION_DATA = []\nGALLERY_DATA = [] # contains the most up to date gallery data\nGALLERY_LISTS = set() # contains the most up to dat gallery lists\n\n# Exceptions\nclass MetadataFetchFail(Exception): pass\nclass InternalPagesMismatch(Exception): pass\nclass ChapterExists(Exception): pass\nclass ChapterWrongParentGallery(Exception): pass\nclass CreateArchiveFail(Exception): pass\nclass FileNotFoundInArchive(Exception): pass\nclass WrongURL(Exception): pass\nclass NeedLogin(Exception): pass\nclass WrongLogin(Exception): pass\nclass HTMLParsing(Exception): pass\nclass GNotAvailable(Exception): pass\nclass TitleParsingError(Exception): pass\n\nEXTERNAL_VIEWER_INFO =\\\n\t\"\"\"{$folder} = path to folder\n{$file} = path to first image\n\nTip: IrfanView uses {$file}\n\t\"\"\"\n\nWHAT_IS_FILTER =\\\n\t\"\"\"[FILTER]\nFilters are basically predefined gallery search terms.\nEvery time a gallery matches the specific filter it gets automatically added to the list!\n\nFilter works the same way a gallery search does so make sure to read the guide in\nSettings -> About -> Search Guide.\nYou can write any valid gallery search term.\n\n[ENFORCE]\nWith Enforce enabled the list will only allow galleries that match the specified filter into the list.\n\"\"\"\n\nSUPPORTED_DOWNLOAD_URLS=\\\n\t\"\"\"Supported URLs:\n- exhentai/g.e-hentai/e-hentai gallery urls, e.g.: https://e-hentai.org/g/618395/0439fa3666/\n- panda.chaika.moe gallery and archive urls\n\thttp://panda.chaika.moe/[0]/[1]/ where [0] is 'gallery' or 'archive' and [1] are numbers\n- asmhentai.com gallery urls, e.g: http://asmhentai.com/g/102845/\n\t\"\"\"\n\nSUPPORTED_METADATA_URLS=\\\n\t\"\"\"Supported gallery URLs:\n- exhentai/g.e-hentai gallery urls, e.g.: http://g.e-hentai.org/g/618395/0439fa3666/\n- panda.chaika.moe gallery and archive urls\n\thttp://panda.chaika.moe/[0]/[1]/ where [0] is 'gallery' or 'archive' and [1] is numbers\n\t\"\"\"\n\nEXHEN_COOKIE_TUTORIAL =\\\n\t\"\"\"\nHow do I find these two values? <br \\>\n<b>All browsers</b> <br \\>\n1. Navigate to e-hentai.org (needs to be logged in) or exhentai.org <br \\>\n2. Right click on page --> Inspect element <br \\>\n3. Go on 'Console' tab <br \\>\n4. Write : 'document.cookie' <br \\>\n5. A line of values should appear that correspond to active cookies <br \\>\n6. Look for the 'ipb_member_id' and 'ipb_pass_hash' values <br \\>\n\"\"\"\n\nREGEXCHEAT =\\\n\t\"\"\"\n\t<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Untitled Document.md</title><style>@import 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.2.0/katex.min.css';code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}code,kbd{padding:2px 4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;margin:0 0 10px;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}fieldset{border:0;min-width:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=\"radio\"],input[type=\"checkbox\"]{margin:1px 0 0;line-height:normal}input[type=\"file\"]{display:block}input[type=\"range\"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=\"file\"]:focus,input[type=\"radio\"]:focus,input[type=\"checkbox\"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}output,.form-control{display:block;font-size:14px;line-height:1.4285714;color:#555}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=\"date\"],input[type=\"time\"],input[type=\"datetime-local\"],input[type=\"month\"]{line-height:34px;line-height:1.4285714 \\0}input[type=\"date\"].input-sm,.form-horizontal .form-group-sm input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-sm,.form-horizontal .form-group-sm input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-sm,.form-horizontal .form-group-sm input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-sm,.form-horizontal .form-group-sm input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"month\"].btn{line-height:30px}input[type=\"date\"].input-lg,.form-horizontal .form-group-lg input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-lg,.form-horizontal .form-group-lg input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-lg,.form-horizontal .form-group-lg input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-lg,.form-horizontal .form-group-lg input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"month\"].btn{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=\"radio\"],.radio-inline input[type=\"radio\"],.checkbox input[type=\"checkbox\"],.checkbox-inline input[type=\"checkbox\"]{position:absolute;margin-left:-20px;margin-top:4px \\9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=\"radio\"][disabled],input[type=\"radio\"].disabled,fieldset[disabled] input[type=\"radio\"],input[type=\"checkbox\"][disabled],input[type=\"checkbox\"].disabled,fieldset[disabled] input[type=\"checkbox\"],.radio-inline.disabled,fieldset[disabled] .radio-inline,.checkbox-inline.disabled,fieldset[disabled] .checkbox-inline,.radio.disabled label,fieldset[disabled] .radio label,.checkbox.disabled label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-horizontal .form-group-lg .form-control-static.form-control,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.form-control-static.input-sm,.form-horizontal .form-group-sm .form-control-static.form-control,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-left:0;padding-right:0}.input-sm,.form-horizontal .form-group-sm .form-control,.input-group-sm>.form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.input-group-sm>.input-group-addon{height:30px;line-height:1.5}.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.form-horizontal .form-group-sm select.form-control,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn{height:30px;line-height:30px}textarea.input-sm,.form-horizontal .form-group-sm textarea.form-control,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,select[multiple].input-sm,.form-horizontal .form-group-sm select[multiple].form-control,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control,.input-group-lg>.form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.input-group-lg>.input-group-addon{height:46px;line-height:1.33}.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,.form-horizontal .form-group-lg select.form-control,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn{height:46px;line-height:46px}textarea.input-lg,.form-horizontal .form-group-lg textarea.form-control,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,select[multiple].input-lg,.form-horizontal .form-group-lg select[multiple].form-control,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback,.form-horizontal .form-group-lg .form-control+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.form-horizontal .form-group-sm .form-control+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:before{content:\" \";display:table}.form-horizontal .form-group:after{content:\" \";display:table;clear:both}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px}.btn-sm,.btn-xs{font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=\"submit\"].btn-block,input[type=\"reset\"].btn-block,input[type=\"button\"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=\"col-\"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon{white-space:nowrap}.input-group-addon,.input-group-btn{width:1%;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm,.form-horizontal .form-group-sm .input-group-addon.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.form-horizontal .form-group-lg .input-group-addon.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=\"radio\"],.input-group-addon input[type=\"checkbox\"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn,.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.4285714;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>a:focus,.pagination>li>span:hover,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:hover,.pagination>.active>a:focus,.pagination>.active>span,.pagination>.active>span:hover,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open,.modal{overflow:hidden}.modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0);-webkit-transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.4285714px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.4285714}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{content:\" \";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.clearfix:before,.clearfix:after{content:\" \";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496;-webkit-text-size-adjust:none}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-doctype,.hljs-pi,.lisp .hljs-string,.hljs-javadoc{color:#586e75}.hljs-keyword,.hljs-winutils,.method,.hljs-addition,.css .hljs-tag,.hljs-request,.hljs-status,.nginx .hljs-title{color:#859900}.hljs-number,.hljs-command,.hljs-string,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula,.hljs-regexp,.hljs-hexcolor,.hljs-link_url{color:#2aa198}.hljs-title,.hljs-localvars,.hljs-chunk,.hljs-decorator,.hljs-built_in,.hljs-identifier,.vhdl .hljs-literal,.hljs-id,.css .hljs-function{color:#268bd2}.hljs-attribute,.hljs-variable,.lisp .hljs-body,.smalltalk .hljs-number,.hljs-constant,.hljs-class .hljs-title,.hljs-parent,.hljs-type,.hljs-link_reference{color:#b58900}.hljs-preprocessor,.hljs-preprocessor .hljs-keyword,.hljs-pragma,.hljs-shebang,.hljs-symbol,.hljs-symbol .hljs-string,.diff .hljs-change,.hljs-special,.hljs-attr_selector,.hljs-subst,.hljs-cdata,.css .hljs-pseudo,.hljs-header{color:#cb4b16}.hljs-deletion,.hljs-important{color:#dc322f}.hljs-link_label{color:#6c71c4}.tex .hljs-formula{background:#073642}*,*:before,*:after{box-sizing:border-box}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}images{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd{font-size:1em}code,kbd,pre,samp{font-family:monospace,monospace}samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=\"button\"],input[type=\"reset\"],input[type=\"submit\"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=\"checkbox\"],input[type=\"radio\"]{box-sizing:border-box;padding:0}input[type=\"number\"]::-webkit-inner-spin-button,input[type=\"number\"]::-webkit-outer-spin-button{height:auto}input[type=\"search\"]{-webkit-appearance:textfield;box-sizing:content-box}input[type=\"search\"]::-webkit-search-cancel-button,input[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}.debug{background-color:#ffc0cb!important}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ir{background-color:transparent;border:0;overflow:hidden}.ir::before{content:'';display:block;height:150%;width:0}html{font-size:.875em;background:#fafafa;color:#373D49}html,body{font-family:Georgia,Cambria,serif;height:100%}body{font-size:1rem;font-weight:400;line-height:2rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}li{-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-left:1rem}li>ul,li>ol{margin-bottom:0}p{padding-top:.66001rem;-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-top:0}p,pre{margin-bottom:1.33999rem}pre{font-size:1rem;padding:.66001rem 9.5px 9.5px;line-height:2rem;background:-webkit-linear-gradient(top,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background:linear-gradient(to bottom,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background-size:100% 4rem;border-color:#D3DAEA}blockquote{margin:0}blockquote p{font-size:1rem;margin-bottom:.33999rem;font-style:italic;padding:.66001rem 1rem 1rem;border-left:3px solid #A0AABF}th,td{padding:12px}h1,h2,h3,h4,h5,h6{font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;-webkit-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;-moz-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-style:normal;font-weight:600;margin-top:0}h1{line-height:3rem;font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h2,h3{line-height:3rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}a{cursor:pointer;color:#35D7BB;text-decoration:none}a:hover,a:focus{border-bottom-color:#35D7BB;color:#dff9f4}img{height:auto;max-width:100%}.g{display:block}.g:after{clear:both;content:'';display:table}.g-b{float:left;margin:0;width:100%}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--center{display:block;float:none;margin:0 auto}.g-b--right{float:right}.g-b--1of1{width:100%}.g-b--1of2,.g-b--2of4,.g-b--3of6,.g-b--4of8,.g-b--5of10,.g-b--6of12{width:50%}.g-b--1of3,.g-b--2of6,.g-b--4of12{width:33.333%}.g-b--2of3,.g-b--4of6,.g-b--8of12{width:66.666%}.g-b--1of4,.g-b--2of8,.g-b--3of12{width:25%}.g-b--3of4,.g-b--6of8,.g-b--9of12{width:75%}.g-b--1of5,.g-b--2of10{width:20%}.g-b--2of5,.g-b--4of10{width:40%}.g-b--3of5,.g-b--6of10{width:60%}.g-b--4of5,.g-b--8of10{width:80%}.g-b--1of6,.g-b--2of12{width:16.666%}.g-b--5of6,.g-b--10of12{width:83.333%}.g-b--1of8{width:12.5%}.g-b--3of8{width:37.5%}.g-b--5of8{width:62.5%}.g-b--7of8{width:87.5%}.g-b--1of10{width:10%}.g-b--3of10{width:30%}.g-b--7of10{width:70%}.g-b--9of10{width:90%}.g-b--1of12{width:8.333%}.g-b--5of12{width:41.666%}.g-b--7of12{width:58.333%}.g-b--11of12{width:91.666%}.g-b--push--1of1{margin-left:100%}.g-b--push--1of2,.g-b--push--2of4,.g-b--push--3of6,.g-b--push--4of8,.g-b--push--5of10,.g-b--push--6of12{margin-left:50%}.g-b--push--1of3,.g-b--push--2of6,.g-b--push--4of12{margin-left:33.333%}.g-b--push--2of3,.g-b--push--4of6,.g-b--push--8of12{margin-left:66.666%}.g-b--push--1of4,.g-b--push--2of8,.g-b--push--3of12{margin-left:25%}.g-b--push--3of4,.g-b--push--6of8,.g-b--push--9of12{margin-left:75%}.g-b--push--1of5,.g-b--push--2of10{margin-left:20%}.g-b--push--2of5,.g-b--push--4of10{margin-left:40%}.g-b--push--3of5,.g-b--push--6of10{margin-left:60%}.g-b--push--4of5,.g-b--push--8of10{margin-left:80%}.g-b--push--1of6,.g-b--push--2of12{margin-left:16.666%}.g-b--push--5of6,.g-b--push--10of12{margin-left:83.333%}.g-b--push--1of8{margin-left:12.5%}.g-b--push--3of8{margin-left:37.5%}.g-b--push--5of8{margin-left:62.5%}.g-b--push--7of8{margin-left:87.5%}.g-b--push--1of10{margin-left:10%}.g-b--push--3of10{margin-left:30%}.g-b--push--7of10{margin-left:70%}.g-b--push--9of10{margin-left:90%}.g-b--push--1of12{margin-left:8.333%}.g-b--push--5of12{margin-left:41.666%}.g-b--push--7of12{margin-left:58.333%}.g-b--push--11of12{margin-left:91.666%}.g-b--pull--1of1{margin-right:100%}.g-b--pull--1of2,.g-b--pull--2of4,.g-b--pull--3of6,.g-b--pull--4of8,.g-b--pull--5of10,.g-b--pull--6of12{margin-right:50%}.g-b--pull--1of3,.g-b--pull--2of6,.g-b--pull--4of12{margin-right:33.333%}.g-b--pull--2of3,.g-b--pull--4of6,.g-b--pull--8of12{margin-right:66.666%}.g-b--pull--1of4,.g-b--pull--2of8,.g-b--pull--3of12{margin-right:25%}.g-b--pull--3of4,.g-b--pull--6of8,.g-b--pull--9of12{margin-right:75%}.g-b--pull--1of5,.g-b--pull--2of10{margin-right:20%}.g-b--pull--2of5,.g-b--pull--4of10{margin-right:40%}.g-b--pull--3of5,.g-b--pull--6of10{margin-right:60%}.g-b--pull--4of5,.g-b--pull--8of10{margin-right:80%}.g-b--pull--1of6,.g-b--pull--2of12{margin-right:16.666%}.g-b--pull--5of6,.g-b--pull--10of12{margin-right:83.333%}.g-b--pull--1of8{margin-right:12.5%}.g-b--pull--3of8{margin-right:37.5%}.g-b--pull--5of8{margin-right:62.5%}.g-b--pull--7of8{margin-right:87.5%}.g-b--pull--1of10{margin-right:10%}.g-b--pull--3of10{margin-right:30%}.g-b--pull--7of10{margin-right:70%}.g-b--pull--9of10{margin-right:90%}.g-b--pull--1of12{margin-right:8.333%}.g-b--pull--5of12{margin-right:41.666%}.g-b--pull--7of12{margin-right:58.333%}.g-b--pull--11of12{margin-right:91.666%}.splashscreen{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#373D49;z-index:22}.splashscreen-dillinger{width:260px;height:auto;display:block;margin:0 auto;padding-bottom:3rem}.splashscreen p{font-size:1.25rem;padding-top:.56251rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;text-align:center;max-width:500px;margin:0 auto;color:#FFF}.sp-center{position:relative;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);top:50%}.open-menu>.wrapper{overflow-x:hidden}.page{margin:0 auto;position:relative;top:0;left:0;width:100%;height:100%;z-index:2;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;background-color:#fff;padding-top:51px;will-change:left}.open-menu .page{left:270px}.title{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;padding-left:16px;padding-right:16px;margin-top:1rem}.split-preview .title{padding-left:0}.title-document{line-height:1rem;font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem;font-weight:400;font-family:\"Ubuntu Mono\",Monaco;color:#373D49;padding-left:16px;padding-right:16px;width:80%;min-width:300px;outline:0;border:none}.icon{display:block;margin:0 auto;width:36px;height:36px;border-radius:3px;text-align:center}.icon svg{display:inline-block;margin-left:auto;margin-right:auto}.icon-preview{background-color:#373D49;line-height:40px}.icon-preview svg{width:19px;height:12px}.icon-settings{background-color:#373D49;line-height:44px}.icon-settings svg{width:18px;height:18px}.icon-link{width:16px;height:16px;line-height:1;margin-right:24px;text-align:right}.navbar{background-color:#373D49;height:51px;width:100%;position:fixed;top:0;left:0;z-index:6;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;will-change:left}.navbar:after{content:\"\";display:table;clear:both}.open-menu .navbar{left:270px}.navbar-brand{float:left;margin:0 0 0 24px;padding:0;line-height:42px}.navbar-brand svg{width:85px;height:11px}.nav-left{float:left}.nav-right{float:right}.nav-sidebar{width:100%}.menu{list-style:none;margin:0;padding:0}.menu a{border:0;color:#A0AABF;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;outline:none;text-transform:uppercase}.menu a:hover{color:#35D7BB}.menu .menu-item{border:0;display:none;float:left;margin:0;position:relative}.menu .menu-item>a{display:block;font-size:12px;height:51px;letter-spacing:1px;line-height:51px;padding:0 24px}.menu .menu-item--settings,.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar,.menu .menu-item--link-unlink.in-sidebar,.menu .menu-item--documents.in-sidebar{display:block}.menu .menu-item--documents{padding-bottom:1rem}.menu .menu-item.open>a{background-color:#1D212A}.menu .menu-item-icon>a{height:auto;padding:0}.menu .menu-item-icon:hover>a{background-color:transparent}.menu .menu-link.open i{background-color:#1D212A}.menu .menu-link.open g{fill:#35D7BB}.menu .menu-link-preview,.menu .menu-link-settings{margin-top:8px;width:51px}.menu-sidebar{width:100%}.menu-sidebar .menu-item{float:none;margin-bottom:1px;width:100%}.menu-sidebar .menu-item.open>a{background-color:#373D49}.menu-sidebar .open .caret{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.menu-sidebar>.menu-item:hover .dropdown a,.menu-sidebar>.menu-item:hover .settings a{background-color:transparent}.menu-sidebar .menu-link{background-color:#373D49;font-weight:600}.menu-sidebar .menu-link:after{content:\"\";display:table;clear:both}.menu-sidebar .menu-link>span{float:left}.menu-sidebar .menu-link>.caret{float:right;text-align:right;top:22px}.menu-sidebar .dropdown,.menu-sidebar .settings{background-color:transparent;position:static;width:100%}.dropdown{position:absolute;right:0;top:51px;width:188px}.dropdown,.settings{display:none;background-color:#1D212A}.dropdown{padding:0}.dropdown,.settings,.sidebar-list{list-style:none;margin:0}.sidebar-list{padding:0}.dropdown li{margin:32px 0;padding:0 0 0 32px}.dropdown li,.settings li{line-height:1}.sidebar-list li{line-height:1;margin:32px 0;padding:0 0 0 32px}.dropdown a{color:#D0D6E2}.dropdown a,.settings a,.sidebar-list a{display:block;text-transform:none}.sidebar-list a{color:#D0D6E2}.dropdown a:after,.settings a:after,.sidebar-list a:after{content:\"\";display:table;clear:both}.dropdown .icon,.settings .icon,.sidebar-list .icon{float:right}.open .dropdown,.open .settings,.open .sidebar-list{display:block}.open .dropdown.collapse,.open .collapse.settings,.open .sidebar-list.collapse{display:none}.open .dropdown.collapse.in,.open .collapse.in.settings,.open .sidebar-list.collapse.in{display:block}.dropdown .unlinked .icon,.settings .unlinked .icon,.sidebar-list .unlinked .icon{opacity:.3}.dropdown.documents li,.documents.settings li,.sidebar-list.documents li{background-image:url(\"../img/icons/file.svg\");background-position:240px center;background-repeat:no-repeat;background-size:14px 16px;padding:3px 32px}.dropdown.documents li.octocat,.documents.settings li.octocat,.sidebar-list.documents li.octocat{background-image:url(\"../img/icons/octocat.svg\");background-position:234px center;background-size:24px 24px}.dropdown.documents li:last-child,.documents.settings li:last-child,.sidebar-list.documents li:last-child{margin-bottom:1rem}.dropdown.documents li.active a,.documents.settings li.active a,.sidebar-list.documents li.active a{color:#35D7BB}.settings{position:fixed;top:67px;right:16px;border-radius:3px;width:288px;background-color:#373D49;padding:16px;z-index:7}.show-settings .settings{display:block}.settings .has-checkbox{float:left}.settings a{font-size:1.25rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;-webkit-font-smoothing:antialiased;line-height:28px;color:#D0D6E2}.settings a:after{content:\"\";display:table;clear:both}.settings a:hover{color:#35D7BB}.settings li{border-bottom:1px solid #4F535B;margin:0;padding:16px 0}.settings li:last-child{border-bottom:none}.brand{border:none;display:block}.brand:hover g{fill:#35D7BB}.toggle{display:block;float:left;height:16px;padding:25px 16px 26px;width:40px}.toggle span:after,.toggle span:before{content:'';left:0;position:absolute;top:-6px}.toggle span:after{top:6px}.toggle span{display:block;position:relative}.toggle span,.toggle span:after,.toggle span:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#D3DAEA;height:2px;-webkit-transition:all .3s;transition:all .3s;width:20px}.open-menu .toggle span{background-color:transparent}.open-menu .toggle span:before{-webkit-transform:rotate(45deg)translate(3px,3px);-ms-transform:rotate(45deg)translate(3px,3px);transform:rotate(45deg)translate(3px,3px)}.open-menu .toggle span:after{-webkit-transform:rotate(-45deg)translate(5px,-6px);-ms-transform:rotate(-45deg)translate(5px,-6px);transform:rotate(-45deg)translate(5px,-6px)}.caret{display:inline-block;width:0;height:0;margin-left:6px;vertical-align:middle;position:relative;top:-1px;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.sidebar{overflow:auto;height:100%;padding-right:15px;padding-bottom:15px;width:285px}.sidebar-wrapper{-webkit-overflow-scrolling:touch;background-color:#2B2F36;left:0;height:100%;overflow-y:hidden;position:fixed;top:0;width:285px;z-index:1}.sidebar-branding{width:160px;padding:0;margin:16px auto}.header{border-bottom:1px solid #E8E8E8;position:relative}.words{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;z-index:5;position:absolute;right:16px;top:0}.words span{color:#000}.btn{text-align:center;display:inline-block;width:100%;text-transform:uppercase;font-weight:600;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:14px;text-shadow:0 1px 0 #1b8b77;padding:16px 24px;background-color:#35D7BB;border-radius:3px;margin:0 auto 16px;line-height:1;color:#fff;-webkit-transition:all .15s linear;transition:all .15s linear;-webkit-font-smoothing:antialiased}.btn--new,.btn--save{display:block;width:238px}.btn--new:hover,.btn--new:focus,.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 3px #24b59c;text-shadow:0 1px 0 #24b59c}.btn--save{background-color:#4A5261;text-shadow:0 1px 1px #1e2127}.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 5px #08090a;text-shadow:none}.btn--delete{display:block;width:238px;background-color:transparent;font-size:12px;text-shadow:none}.btn--delete:hover,.btn--delete:focus{color:#fff;border-bottom-color:transparent;text-shadow:0 1px 0 #08090a;opacity:.8}.btn--ok,.btn--close{border-top:0;background-color:#4A5261;text-shadow:0 1px 0 #08090a;margin:0}.btn--ok:hover,.btn--ok:focus,.btn--close:hover,.btn--close:focus{color:#fff;background-color:#292d36;text-shadow:none}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(55,61,73,.8);-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;-webkit-transition-timing-function:ease-out;transition-timing-function:ease-out;will-change:left,opacity,visibility;z-index:5;opacity:0;visibility:hidden}.show-settings .overlay{visibility:visible;opacity:1}.switch{float:right;line-height:1}.switch input{display:none}.switch small{display:inline-block;cursor:pointer;padding:0 24px 0 0;-webkit-transition:all ease .2s;transition:all ease .2s;background-color:#2B2F36;border-color:#2B2F36}.switch small,.switch small:before{border-radius:30px;box-shadow:inset 0 0 2px 0 #14171F}.switch small:before{display:block;content:'';width:28px;height:28px;background:#fff}.switch.checked small{padding-right:0;padding-left:24px;background-color:#35D7BB;box-shadow:none}.modal--dillinger.about .modal-dialog{font-size:1.25rem;max-width:500px}.modal--dillinger .modal-dialog{max-width:600px;width:auto;margin:5rem auto}.modal--dillinger .modal-content{background:#373D49;border-radius:3px;box-shadow:0 2px 5px 0 #2C3B59;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;padding:2rem}.modal--dillinger ul{list-style-type:disc;margin:1rem 0;padding:0 0 0 1rem}.modal--dillinger li{padding:0;margin:0}.modal--dillinger .modal-header{border:0;padding:0}.modal--dillinger .modal-body{padding:0}.modal--dillinger .modal-footer{border:0;padding:0}.modal--dillinger .close{color:#fff;opacity:1}.modal-backdrop{background-color:#373D49}.pagination--dillinger{padding:0!important;margin:1.5rem 0!important;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-align-content:stretch;-ms-flex-line-pack:stretch;align-content:stretch}.pagination--dillinger,.pagination--dillinger li{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.pagination--dillinger li{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.pagination--dillinger li:first-child>a,.pagination--dillinger li.disabled>a,.pagination--dillinger li.disabled>a:hover,.pagination--dillinger li.disabled>a:focus,.pagination--dillinger li>a{background-color:transparent;border-color:#4F535B;border-right-color:transparent}.pagination--dillinger li.active>a,.pagination--dillinger li.active>a:hover,.pagination--dillinger li.active>a:focus{border-color:#4A5261;background-color:#4A5261;color:#fff}.pagination--dillinger li>a{float:none;color:#fff;width:100%;display:block;text-align:center;margin:0;border-right-color:transparent;padding:6px}.pagination--dillinger li>a:hover,.pagination--dillinger li>a:focus{border-color:#35D7BB;background-color:#35D7BB;color:#fff}.pagination--dillinger li:last-child a{border-color:#4F535B}.pagination--dillinger li:first-child a{border-right-color:transparent}.diNotify{position:absolute;z-index:9999;left:0;right:0;top:0;margin:0 auto;max-width:400px;text-align:center;-webkit-transition:top .5s ease-in-out,opacity .5s ease-in-out;transition:top .5s ease-in-out,opacity .5s ease-in-out;visibility:hidden}.diNotify-body{-webkit-font-smoothing:antialiased;background-color:#35D7BB;background:#666E7F;border-radius:3px;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;overflow:hidden;padding:1rem 2rem .5rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-webkit-align-items:baseline;-ms-flex-align:baseline;align-items:baseline;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.diNotify-icon{display:block;width:16px;height:16px;line-height:16px;position:relative;top:3px}.diNotify-message{padding-left:1rem}.zen-wrapper{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;z-index:10;background-color:#FFF;opacity:0;-webkit-transition:opacity .25s ease-in-out;transition:opacity .25s ease-in-out}.zen-wrapper.on{opacity:1}.enter-zen-mode{background-image:url(\"../img/icons/enter-zen.svg\");right:.5rem;top:.5rem;display:none}.enter-zen-mode,.close-zen-mode{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;background-repeat:no-repeat;width:32px;height:32px;display:block;position:absolute}.close-zen-mode{background-image:url(\"../img/icons/exit-zen.svg\");right:1rem;top:1rem}.zen-page{position:relative;top:0;bottom:0;z-index:11;height:100%;width:100%}#zen{font-size:1.25rem;width:300px;height:80%;margin:0 auto;position:relative;top:10%}#zen:before,#zen:after{content:\"\";position:absolute;height:10%;width:100%;z-index:12;pointer-events:none}.split{overflow:scroll;padding:0!important}.split-editor{padding-left:0;padding-right:0;position:relative}.show-preview .split-editor{display:none}.split-preview{background-color:#fff;display:none;top:0;position:relative;z-index:4}.show-preview .split-preview{display:block}#editor{font-size:1rem;font-family:\"Ubuntu Mono\",Monaco;font-weight:400;line-height:2rem;width:100%;height:100%}#editor .ace_gutter{-webkit-font-smoothing:antialiased}#preview a{color:#A0AABF;text-decoration:underline}.sr-only{visibility:hidden;text-overflow:110%;overflow:hidden;top:-100px;position:absolute}.mnone{margin:0!important}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=\"radio\"],.form-inline .checkbox input[type=\"checkbox\"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}.form-horizontal .form-group-lg .control-label{padding-top:14.3px}.form-horizontal .form-group-sm .control-label{padding-top:6px}.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}@media screen and (min-width:27.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--m1of1{width:100%}.g-b--m1of2,.g-b--m2of4,.g-b--m3of6,.g-b--m4of8,.g-b--m5of10,.g-b--m6of12{width:50%}.g-b--m1of3,.g-b--m2of6,.g-b--m4of12{width:33.333%}.g-b--m2of3,.g-b--m4of6,.g-b--m8of12{width:66.666%}.g-b--m1of4,.g-b--m2of8,.g-b--m3of12{width:25%}.g-b--m3of4,.g-b--m6of8,.g-b--m9of12{width:75%}.g-b--m1of5,.g-b--m2of10{width:20%}.g-b--m2of5,.g-b--m4of10{width:40%}.g-b--m3of5,.g-b--m6of10{width:60%}.g-b--m4of5,.g-b--m8of10{width:80%}.g-b--m1of6,.g-b--m2of12{width:16.666%}.g-b--m5of6,.g-b--m10of12{width:83.333%}.g-b--m1of8{width:12.5%}.g-b--m3of8{width:37.5%}.g-b--m5of8{width:62.5%}.g-b--m7of8{width:87.5%}.g-b--m1of10{width:10%}.g-b--m3of10{width:30%}.g-b--m7of10{width:70%}.g-b--m9of10{width:90%}.g-b--m1of12{width:8.333%}.g-b--m5of12{width:41.666%}.g-b--m7of12{width:58.333%}.g-b--m11of12{width:91.666%}.g-b--push--m1of1{margin-left:100%}.g-b--push--m1of2,.g-b--push--m2of4,.g-b--push--m3of6,.g-b--push--m4of8,.g-b--push--m5of10,.g-b--push--m6of12{margin-left:50%}.g-b--push--m1of3,.g-b--push--m2of6,.g-b--push--m4of12{margin-left:33.333%}.g-b--push--m2of3,.g-b--push--m4of6,.g-b--push--m8of12{margin-left:66.666%}.g-b--push--m1of4,.g-b--push--m2of8,.g-b--push--m3of12{margin-left:25%}.g-b--push--m3of4,.g-b--push--m6of8,.g-b--push--m9of12{margin-left:75%}.g-b--push--m1of5,.g-b--push--m2of10{margin-left:20%}.g-b--push--m2of5,.g-b--push--m4of10{margin-left:40%}.g-b--push--m3of5,.g-b--push--m6of10{margin-left:60%}.g-b--push--m4of5,.g-b--push--m8of10{margin-left:80%}.g-b--push--m1of6,.g-b--push--m2of12{margin-left:16.666%}.g-b--push--m5of6,.g-b--push--m10of12{margin-left:83.333%}.g-b--push--m1of8{margin-left:12.5%}.g-b--push--m3of8{margin-left:37.5%}.g-b--push--m5of8{margin-left:62.5%}.g-b--push--m7of8{margin-left:87.5%}.g-b--push--m1of10{margin-left:10%}.g-b--push--m3of10{margin-left:30%}.g-b--push--m7of10{margin-left:70%}.g-b--push--m9of10{margin-left:90%}.g-b--push--m1of12{margin-left:8.333%}.g-b--push--m5of12{margin-left:41.666%}.g-b--push--m7of12{margin-left:58.333%}.g-b--push--m11of12{margin-left:91.666%}.g-b--pull--m1of1{margin-right:100%}.g-b--pull--m1of2,.g-b--pull--m2of4,.g-b--pull--m3of6,.g-b--pull--m4of8,.g-b--pull--m5of10,.g-b--pull--m6of12{margin-right:50%}.g-b--pull--m1of3,.g-b--pull--m2of6,.g-b--pull--m4of12{margin-right:33.333%}.g-b--pull--m2of3,.g-b--pull--m4of6,.g-b--pull--m8of12{margin-right:66.666%}.g-b--pull--m1of4,.g-b--pull--m2of8,.g-b--pull--m3of12{margin-right:25%}.g-b--pull--m3of4,.g-b--pull--m6of8,.g-b--pull--m9of12{margin-right:75%}.g-b--pull--m1of5,.g-b--pull--m2of10{margin-right:20%}.g-b--pull--m2of5,.g-b--pull--m4of10{margin-right:40%}.g-b--pull--m3of5,.g-b--pull--m6of10{margin-right:60%}.g-b--pull--m4of5,.g-b--pull--m8of10{margin-right:80%}.g-b--pull--m1of6,.g-b--pull--m2of12{margin-right:16.666%}.g-b--pull--m5of6,.g-b--pull--m10of12{margin-right:83.333%}.g-b--pull--m1of8{margin-right:12.5%}.g-b--pull--m3of8{margin-right:37.5%}.g-b--pull--m5of8{margin-right:62.5%}.g-b--pull--m7of8{margin-right:87.5%}.g-b--pull--m1of10{margin-right:10%}.g-b--pull--m3of10{margin-right:30%}.g-b--pull--m7of10{margin-right:70%}.g-b--pull--m9of10{margin-right:90%}.g-b--pull--m1of12{margin-right:8.333%}.g-b--pull--m5of12{margin-right:41.666%}.g-b--pull--m7of12{margin-right:58.333%}.g-b--pull--m11of12{margin-right:91.666%}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:400px}#editor{font-size:1rem}}@media screen and (min-width:46.25em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--t1of1{width:100%}.g-b--t1of2,.g-b--t2of4,.g-b--t3of6,.g-b--t4of8,.g-b--t5of10,.g-b--t6of12{width:50%}.g-b--t1of3,.g-b--t2of6,.g-b--t4of12{width:33.333%}.g-b--t2of3,.g-b--t4of6,.g-b--t8of12{width:66.666%}.g-b--t1of4,.g-b--t2of8,.g-b--t3of12{width:25%}.g-b--t3of4,.g-b--t6of8,.g-b--t9of12{width:75%}.g-b--t1of5,.g-b--t2of10{width:20%}.g-b--t2of5,.g-b--t4of10{width:40%}.g-b--t3of5,.g-b--t6of10{width:60%}.g-b--t4of5,.g-b--t8of10{width:80%}.g-b--t1of6,.g-b--t2of12{width:16.666%}.g-b--t5of6,.g-b--t10of12{width:83.333%}.g-b--t1of8{width:12.5%}.g-b--t3of8{width:37.5%}.g-b--t5of8{width:62.5%}.g-b--t7of8{width:87.5%}.g-b--t1of10{width:10%}.g-b--t3of10{width:30%}.g-b--t7of10{width:70%}.g-b--t9of10{width:90%}.g-b--t1of12{width:8.333%}.g-b--t5of12{width:41.666%}.g-b--t7of12{width:58.333%}.g-b--t11of12{width:91.666%}.g-b--push--t1of1{margin-left:100%}.g-b--push--t1of2,.g-b--push--t2of4,.g-b--push--t3of6,.g-b--push--t4of8,.g-b--push--t5of10,.g-b--push--t6of12{margin-left:50%}.g-b--push--t1of3,.g-b--push--t2of6,.g-b--push--t4of12{margin-left:33.333%}.g-b--push--t2of3,.g-b--push--t4of6,.g-b--push--t8of12{margin-left:66.666%}.g-b--push--t1of4,.g-b--push--t2of8,.g-b--push--t3of12{margin-left:25%}.g-b--push--t3of4,.g-b--push--t6of8,.g-b--push--t9of12{margin-left:75%}.g-b--push--t1of5,.g-b--push--t2of10{margin-left:20%}.g-b--push--t2of5,.g-b--push--t4of10{margin-left:40%}.g-b--push--t3of5,.g-b--push--t6of10{margin-left:60%}.g-b--push--t4of5,.g-b--push--t8of10{margin-left:80%}.g-b--push--t1of6,.g-b--push--t2of12{margin-left:16.666%}.g-b--push--t5of6,.g-b--push--t10of12{margin-left:83.333%}.g-b--push--t1of8{margin-left:12.5%}.g-b--push--t3of8{margin-left:37.5%}.g-b--push--t5of8{margin-left:62.5%}.g-b--push--t7of8{margin-left:87.5%}.g-b--push--t1of10{margin-left:10%}.g-b--push--t3of10{margin-left:30%}.g-b--push--t7of10{margin-left:70%}.g-b--push--t9of10{margin-left:90%}.g-b--push--t1of12{margin-left:8.333%}.g-b--push--t5of12{margin-left:41.666%}.g-b--push--t7of12{margin-left:58.333%}.g-b--push--t11of12{margin-left:91.666%}.g-b--pull--t1of1{margin-right:100%}.g-b--pull--t1of2,.g-b--pull--t2of4,.g-b--pull--t3of6,.g-b--pull--t4of8,.g-b--pull--t5of10,.g-b--pull--t6of12{margin-right:50%}.g-b--pull--t1of3,.g-b--pull--t2of6,.g-b--pull--t4of12{margin-right:33.333%}.g-b--pull--t2of3,.g-b--pull--t4of6,.g-b--pull--t8of12{margin-right:66.666%}.g-b--pull--t1of4,.g-b--pull--t2of8,.g-b--pull--t3of12{margin-right:25%}.g-b--pull--t3of4,.g-b--pull--t6of8,.g-b--pull--t9of12{margin-right:75%}.g-b--pull--t1of5,.g-b--pull--t2of10{margin-right:20%}.g-b--pull--t2of5,.g-b--pull--t4of10{margin-right:40%}.g-b--pull--t3of5,.g-b--pull--t6of10{margin-right:60%}.g-b--pull--t4of5,.g-b--pull--t8of10{margin-right:80%}.g-b--pull--t1of6,.g-b--pull--t2of12{margin-right:16.666%}.g-b--pull--t5of6,.g-b--pull--t10of12{margin-right:83.333%}.g-b--pull--t1of8{margin-right:12.5%}.g-b--pull--t3of8{margin-right:37.5%}.g-b--pull--t5of8{margin-right:62.5%}.g-b--pull--t7of8{margin-right:87.5%}.g-b--pull--t1of10{margin-right:10%}.g-b--pull--t3of10{margin-right:30%}.g-b--pull--t7of10{margin-right:70%}.g-b--pull--t9of10{margin-right:90%}.g-b--pull--t1of12{margin-right:8.333%}.g-b--pull--t5of12{margin-right:41.666%}.g-b--pull--t7of12{margin-right:58.333%}.g-b--pull--t11of12{margin-right:91.666%}.splashscreen-dillinger{width:500px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--save-to,.menu .menu-item--import-from{display:block}.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog{font-size:1.25rem}.enter-zen-mode{display:block}.close-zen-mode{right:3rem;top:3rem}#zen{font-size:1.25rem;width:500px}.split-editor{border-right:1px solid #E8E8E8;float:left;height:calc(100vh - 130px);-webkit-overflow-scrolling:touch;padding-right:16px;width:50%}.show-preview .split-editor{display:block}.split-preview{display:block;float:right;height:calc(100vh - 130px);-webkit-overflow-scrolling:touch;position:relative;top:0;width:50%}#editor{font-size:1rem}}@media screen and (min-width:62.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--d1of1{width:100%}.g-b--d1of2,.g-b--d2of4,.g-b--d3of6,.g-b--d4of8,.g-b--d5of10,.g-b--d6of12{width:50%}.g-b--d1of3,.g-b--d2of6,.g-b--d4of12{width:33.333%}.g-b--d2of3,.g-b--d4of6,.g-b--d8of12{width:66.666%}.g-b--d1of4,.g-b--d2of8,.g-b--d3of12{width:25%}.g-b--d3of4,.g-b--d6of8,.g-b--d9of12{width:75%}.g-b--d1of5,.g-b--d2of10{width:20%}.g-b--d2of5,.g-b--d4of10{width:40%}.g-b--d3of5,.g-b--d6of10{width:60%}.g-b--d4of5,.g-b--d8of10{width:80%}.g-b--d1of6,.g-b--d2of12{width:16.666%}.g-b--d5of6,.g-b--d10of12{width:83.333%}.g-b--d1of8{width:12.5%}.g-b--d3of8{width:37.5%}.g-b--d5of8{width:62.5%}.g-b--d7of8{width:87.5%}.g-b--d1of10{width:10%}.g-b--d3of10{width:30%}.g-b--d7of10{width:70%}.g-b--d9of10{width:90%}.g-b--d1of12{width:8.333%}.g-b--d5of12{width:41.666%}.g-b--d7of12{width:58.333%}.g-b--d11of12{width:91.666%}.g-b--push--d1of1{margin-left:100%}.g-b--push--d1of2,.g-b--push--d2of4,.g-b--push--d3of6,.g-b--push--d4of8,.g-b--push--d5of10,.g-b--push--d6of12{margin-left:50%}.g-b--push--d1of3,.g-b--push--d2of6,.g-b--push--d4of12{margin-left:33.333%}.g-b--push--d2of3,.g-b--push--d4of6,.g-b--push--d8of12{margin-left:66.666%}.g-b--push--d1of4,.g-b--push--d2of8,.g-b--push--d3of12{margin-left:25%}.g-b--push--d3of4,.g-b--push--d6of8,.g-b--push--d9of12{margin-left:75%}.g-b--push--d1of5,.g-b--push--d2of10{margin-left:20%}.g-b--push--d2of5,.g-b--push--d4of10{margin-left:40%}.g-b--push--d3of5,.g-b--push--d6of10{margin-left:60%}.g-b--push--d4of5,.g-b--push--d8of10{margin-left:80%}.g-b--push--d1of6,.g-b--push--d2of12{margin-left:16.666%}.g-b--push--d5of6,.g-b--push--d10of12{margin-left:83.333%}.g-b--push--d1of8{margin-left:12.5%}.g-b--push--d3of8{margin-left:37.5%}.g-b--push--d5of8{margin-left:62.5%}.g-b--push--d7of8{margin-left:87.5%}.g-b--push--d1of10{margin-left:10%}.g-b--push--d3of10{margin-left:30%}.g-b--push--d7of10{margin-left:70%}.g-b--push--d9of10{margin-left:90%}.g-b--push--d1of12{margin-left:8.333%}.g-b--push--d5of12{margin-left:41.666%}.g-b--push--d7of12{margin-left:58.333%}.g-b--push--d11of12{margin-left:91.666%}.g-b--pull--d1of1{margin-right:100%}.g-b--pull--d1of2,.g-b--pull--d2of4,.g-b--pull--d3of6,.g-b--pull--d4of8,.g-b--pull--d5of10,.g-b--pull--d6of12{margin-right:50%}.g-b--pull--d1of3,.g-b--pull--d2of6,.g-b--pull--d4of12{margin-right:33.333%}.g-b--pull--d2of3,.g-b--pull--d4of6,.g-b--pull--d8of12{margin-right:66.666%}.g-b--pull--d1of4,.g-b--pull--d2of8,.g-b--pull--d3of12{margin-right:25%}.g-b--pull--d3of4,.g-b--pull--d6of8,.g-b--pull--d9of12{margin-right:75%}.g-b--pull--d1of5,.g-b--pull--d2of10{margin-right:20%}.g-b--pull--d2of5,.g-b--pull--d4of10{margin-right:40%}.g-b--pull--d3of5,.g-b--pull--d6of10{margin-right:60%}.g-b--pull--d4of5,.g-b--pull--d8of10{margin-right:80%}.g-b--pull--d1of6,.g-b--pull--d2of12{margin-right:16.666%}.g-b--pull--d5of6,.g-b--pull--d10of12{margin-right:83.333%}.g-b--pull--d1of8{margin-right:12.5%}.g-b--pull--d3of8{margin-right:37.5%}.g-b--pull--d5of8{margin-right:62.5%}.g-b--pull--d7of8{margin-right:87.5%}.g-b--pull--d1of10{margin-right:10%}.g-b--pull--d3of10{margin-right:30%}.g-b--pull--d7of10{margin-right:70%}.g-b--pull--d9of10{margin-right:90%}.g-b--pull--d1of12{margin-right:8.333%}.g-b--pull--d5of12{margin-right:41.666%}.g-b--pull--d7of12{margin-right:58.333%}.g-b--pull--d11of12{margin-right:91.666%}.splashscreen-dillinger{width:700px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--export-as{display:block}.menu .menu-item--preview{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:700px}#editor{font-size:1rem}}@media screen and (min-width:87.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.splashscreen-dillinger{width:800px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#editor{font-size:1rem}}</style></head><body id=\"preview\">\n<h4><a id=\"Characters_I_1\"></a>Characters I</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>.</td>\n<td>Match any character except newline</td>\n</tr>\n<tr>\n<td>^</td>\n<td>Match the start of the string</td>\n</tr>\n<tr>\n<td>$</td>\n<td>Match the end of the string</td>\n</tr>\n<tr>\n<td>*</td>\n<td>Match 0 or more repetitions</td>\n</tr>\n<tr>\n<td>+</td>\n<td>Match 1 or more repetitions</td>\n</tr>\n<tr>\n<td>?</td>\n<td>Match 0 or 1 repetitions</td>\n</tr>\n</tbody>\n</table>\n<h4><a id=\"Special_Sequences_I_12\"></a>Special Sequences I</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>\\A</td>\n<td>Match only at start of string</td>\n</tr>\n<tr>\n<td>\\\\b</td>\n<td>Match empty string, only at beginning or end of a word</td>\n</tr>\n<tr>\n<td>\\B</td>\n<td>Match empty string, only when it is not at beginning or end of word</td>\n</tr>\n<tr>\n<td>\\d</td>\n<td>Match digits # same as [0-9]</td>\n</tr>\n<tr>\n<td>\\D</td>\n<td>Match any non digit # same as [^0-9]</td>\n</tr>\n</tbody>\n</table>\n<h4><a id=\"Characters_II_22\"></a>Characters II</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>*?</td>\n<td>Match 0 or more repetitions non-greedy</td>\n</tr>\n<tr>\n<td>+?</td>\n<td>Match 1 or more repetitions non-greedy</td>\n</tr>\n<tr>\n<td>??</td>\n<td>Match 0 or 1 repetitions non-greedy</td>\n</tr>\n<tr>\n<td>\\</td>\n<td>Escape special characters</td>\n</tr>\n<tr>\n<td>[]</td>\n<td>Match a set of characters</td>\n</tr>\n<tr>\n<td>[a-z]</td>\n<td>Match any lowercase ASCII letter</td>\n</tr>\n<tr>\n<td>[lower-upper]</td>\n<td>Match a set of characters from lower to upper</td>\n</tr>\n<tr>\n<td>[^]</td>\n<td>Match characters NOT in a set</td>\n</tr>\n<tr>\n<td>A|B</td>\n<td>Match either A or B regular expressions (non-greedy)</td>\n</tr>\n</tbody>\n</table>\n<h4><a id=\"Special_Sequences_II_36\"></a>Special Sequences II</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>\\s</td>\n<td>Match whitespace characters # same as [ \\t\\n\\r\\f\\v]</td>\n</tr>\n<tr>\n<td>\\S</td>\n<td>Match non whitespace characters #same as [^ \\t\\n\\r\\f\\v]</td>\n</tr>\n<tr>\n<td>\\w</td>\n<td>Match unicode word characters # same as [a-zA-Z0-9_]</td>\n</tr>\n<tr>\n<td>\\W</td>\n<td>Match any character not a Unicode word character # same as [^a-zA-Z0-9_]</td>\n</tr>\n<tr>\n<td>\\Z</td>\n<td>Match only at end of string</td>\n</tr>\n</tbody>\n</table>\n<h4><a id=\"Characters_III_46\"></a>Characters III</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>{m}</td>\n<td>Match exactly m copies</td>\n</tr>\n<tr>\n<td>{m,n}</td>\n<td>Match from m to n repetitions</td>\n</tr>\n<tr>\n<td>{,n}</td>\n<td>Match from 0 to n repetitions</td>\n</tr>\n<tr>\n<td>{m,}</td>\n<td>Match from m to infinite repetitions</td>\n</tr>\n<tr>\n<td>{m,n}?</td>\n<td>Match from m to n repetitions non-greedy (as few as possible)</td>\n</tr>\n</tbody>\n</table>\n<h4><a id=\"Groups_I_56\"></a>Groups I</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>(match)</td>\n<td>Use to specify a group for which match can be retrieved later</td>\n</tr>\n<tr>\n<td>(?:match)</td>\n<td>Non-capturing version parenthesis (match cannot be retrieved later)</td>\n</tr>\n<tr>\n<td>(?P&lt;name&gt;)</td>\n<td>Capture group with name “name”</td>\n</tr>\n<tr>\n<td>(?P=name)</td>\n<td>Back reference group named “name” in same pattern</td>\n</tr>\n<tr>\n<td>(?#comment)</td>\n<td>Comment</td>\n</tr>\n</tbody>\n</table>\n<h4><a id=\"Lookahead__Behind_I_66\"></a>Lookahead / Behind I</h4>\n<table>\n<thead>\n<tr>\n<th>Expression</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>(?=match)</td>\n<td>Lookahead assertion - match if contents matches next, but don’t consume any of the string.</td>\n</tr>\n<tr>\n<td>(?!match)</td>\n<td>Negative lookahead assertion - match if contents do not match next</td>\n</tr>\n<tr>\n<td>(?&lt;=match)</td>\n<td>Positive lookbehind assertion - match if current position in string is preceded by match</td>\n</tr>\n<tr>\n<td>(?&lt;!match)</td>\n<td>Negative lookbehind assertion - match if current position is not preceded by match</td>\n</tr>\n<tr>\n<td>(?(id/name)yes|no)</td>\n<td>Match “yes” pattern if id or name exists, otherwise match “no” pattern</td>\n</tr>\n</tbody>\n</table>\n\n</body></html>\n\t\"\"\"\n\nABOUT =\\\n\t\"\"\"\n<!DOCTYPE html><html><head></head><body>\n<p><strong>Creator</strong>: <a href=\"https://github.com/Pewpews\">Pewpews</a></p>\n<p>Twitter: <a href=\"https://twitter.com/pewspew\">@pewspew</a></p>\n<p>Chat: <a href=\"https://gitter.im/Pewpews/happypanda\">Gitter chat</a></p>\n<p>Email: <code>happypandabugs@gmail.com</code></p>\n<p><strong>Current version</strong>: {}</p>\n<p><strong>Current database version</strong>: {}</p>\n<p>License: <a href=\"https://www.gnu.org/licenses/gpl-2.0.txt\"> GENERAL PUBLIC LICENSE, Version 2</a></p>\n<p>Happypanda was created using:</p>\n<ul>\n<li>Python 3.5</li>\n<li>The Qt5 Framework</li>\n<li>Various python libraries (see github repo)</li>\n</ul>\n<p>Contributors (github):\nrachmadaniHaryono (big thanks!), nonamethanks, ImoutoChan, Moshidesu, peaceanpizza, utterbull, LePearlo</p>\n\n</body></html>\n\t\"\"\".format(vs, db_constants.CURRENT_DB_VERSION)\n\nTROUBLE_GUIDE =\\\n\t\"\"\"\n<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Untitled Document.md</title><style>@import 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.2.0/katex.min.css';code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}code,kbd{padding:2px 4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;margin:0 0 10px;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}fieldset{border:0;min-width:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=\"radio\"],input[type=\"checkbox\"]{margin:1px 0 0;line-height:normal}input[type=\"file\"]{display:block}input[type=\"range\"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=\"file\"]:focus,input[type=\"radio\"]:focus,input[type=\"checkbox\"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}output,.form-control{display:block;font-size:14px;line-height:1.4285714;color:#555}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=\"date\"],input[type=\"time\"],input[type=\"datetime-local\"],input[type=\"month\"]{line-height:34px;line-height:1.4285714 \\0}input[type=\"date\"].input-sm,.form-horizontal .form-group-sm input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-sm,.form-horizontal .form-group-sm input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-sm,.form-horizontal .form-group-sm input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-sm,.form-horizontal .form-group-sm input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"month\"].btn{line-height:30px}input[type=\"date\"].input-lg,.form-horizontal .form-group-lg input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-lg,.form-horizontal .form-group-lg input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-lg,.form-horizontal .form-group-lg input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-lg,.form-horizontal .form-group-lg input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"month\"].btn{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=\"radio\"],.radio-inline input[type=\"radio\"],.checkbox input[type=\"checkbox\"],.checkbox-inline input[type=\"checkbox\"]{position:absolute;margin-left:-20px;margin-top:4px \\9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=\"radio\"][disabled],input[type=\"radio\"].disabled,fieldset[disabled] input[type=\"radio\"],input[type=\"checkbox\"][disabled],input[type=\"checkbox\"].disabled,fieldset[disabled] input[type=\"checkbox\"],.radio-inline.disabled,fieldset[disabled] .radio-inline,.checkbox-inline.disabled,fieldset[disabled] .checkbox-inline,.radio.disabled label,fieldset[disabled] .radio label,.checkbox.disabled label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-horizontal .form-group-lg .form-control-static.form-control,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.form-control-static.input-sm,.form-horizontal .form-group-sm .form-control-static.form-control,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-left:0;padding-right:0}.input-sm,.form-horizontal .form-group-sm .form-control,.input-group-sm>.form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.input-group-sm>.input-group-addon{height:30px;line-height:1.5}.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.form-horizontal .form-group-sm select.form-control,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn{height:30px;line-height:30px}textarea.input-sm,.form-horizontal .form-group-sm textarea.form-control,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,select[multiple].input-sm,.form-horizontal .form-group-sm select[multiple].form-control,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control,.input-group-lg>.form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.input-group-lg>.input-group-addon{height:46px;line-height:1.33}.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,.form-horizontal .form-group-lg select.form-control,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn{height:46px;line-height:46px}textarea.input-lg,.form-horizontal .form-group-lg textarea.form-control,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,select[multiple].input-lg,.form-horizontal .form-group-lg select[multiple].form-control,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback,.form-horizontal .form-group-lg .form-control+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.form-horizontal .form-group-sm .form-control+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:before{content:\" \";display:table}.form-horizontal .form-group:after{content:\" \";display:table;clear:both}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px}.btn-sm,.btn-xs{font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=\"submit\"].btn-block,input[type=\"reset\"].btn-block,input[type=\"button\"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=\"col-\"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon{white-space:nowrap}.input-group-addon,.input-group-btn{width:1%;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm,.form-horizontal .form-group-sm .input-group-addon.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.form-horizontal .form-group-lg .input-group-addon.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=\"radio\"],.input-group-addon input[type=\"checkbox\"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn,.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.4285714;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>a:focus,.pagination>li>span:hover,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:hover,.pagination>.active>a:focus,.pagination>.active>span,.pagination>.active>span:hover,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open,.modal{overflow:hidden}.modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0);-webkit-transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.4285714px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.4285714}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{content:\" \";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.clearfix:before,.clearfix:after{content:\" \";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496;-webkit-text-size-adjust:none}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-doctype,.hljs-pi,.lisp .hljs-string,.hljs-javadoc{color:#586e75}.hljs-keyword,.hljs-winutils,.method,.hljs-addition,.css .hljs-tag,.hljs-request,.hljs-status,.nginx .hljs-title{color:#859900}.hljs-number,.hljs-command,.hljs-string,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula,.hljs-regexp,.hljs-hexcolor,.hljs-link_url{color:#2aa198}.hljs-title,.hljs-localvars,.hljs-chunk,.hljs-decorator,.hljs-built_in,.hljs-identifier,.vhdl .hljs-literal,.hljs-id,.css .hljs-function{color:#268bd2}.hljs-attribute,.hljs-variable,.lisp .hljs-body,.smalltalk .hljs-number,.hljs-constant,.hljs-class .hljs-title,.hljs-parent,.hljs-type,.hljs-link_reference{color:#b58900}.hljs-preprocessor,.hljs-preprocessor .hljs-keyword,.hljs-pragma,.hljs-shebang,.hljs-symbol,.hljs-symbol .hljs-string,.diff .hljs-change,.hljs-special,.hljs-attr_selector,.hljs-subst,.hljs-cdata,.css .hljs-pseudo,.hljs-header{color:#cb4b16}.hljs-deletion,.hljs-important{color:#dc322f}.hljs-link_label{color:#6c71c4}.tex .hljs-formula{background:#073642}*,*:before,*:after{box-sizing:border-box}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}images{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd{font-size:1em}code,kbd,pre,samp{font-family:monospace,monospace}samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=\"button\"],input[type=\"reset\"],input[type=\"submit\"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=\"checkbox\"],input[type=\"radio\"]{box-sizing:border-box;padding:0}input[type=\"number\"]::-webkit-inner-spin-button,input[type=\"number\"]::-webkit-outer-spin-button{height:auto}input[type=\"search\"]{-webkit-appearance:textfield;box-sizing:content-box}input[type=\"search\"]::-webkit-search-cancel-button,input[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}.debug{background-color:#ffc0cb!important}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ir{background-color:transparent;border:0;overflow:hidden}.ir::before{content:'';display:block;height:150%;width:0}html{font-size:.875em;background:#fafafa;color:#373D49}html,body{font-family:Georgia,Cambria,serif;height:100%}body{font-size:1rem;font-weight:400;line-height:2rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}li{-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-left:1rem}li>ul,li>ol{margin-bottom:0}p{padding-top:.66001rem;-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-top:0}p,pre{margin-bottom:1.33999rem}pre{font-size:1rem;padding:.66001rem 9.5px 9.5px;line-height:2rem;background:-webkit-linear-gradient(top,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background:linear-gradient(to bottom,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background-size:100% 4rem;border-color:#D3DAEA}blockquote{margin:0}blockquote p{font-size:1rem;margin-bottom:.33999rem;font-style:italic;padding:.66001rem 1rem 1rem;border-left:3px solid #A0AABF}th,td{padding:12px}h1,h2,h3,h4,h5,h6{font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;-webkit-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;-moz-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-style:normal;font-weight:600;margin-top:0}h1{line-height:3rem;font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h2,h3{line-height:3rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}a{cursor:pointer;color:#35D7BB;text-decoration:none}a:hover,a:focus{border-bottom-color:#35D7BB;color:#dff9f4}img{height:auto;max-width:100%}.g{display:block}.g:after{clear:both;content:'';display:table}.g-b{float:left;margin:0;width:100%}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--center{display:block;float:none;margin:0 auto}.g-b--right{float:right}.g-b--1of1{width:100%}.g-b--1of2,.g-b--2of4,.g-b--3of6,.g-b--4of8,.g-b--5of10,.g-b--6of12{width:50%}.g-b--1of3,.g-b--2of6,.g-b--4of12{width:33.333%}.g-b--2of3,.g-b--4of6,.g-b--8of12{width:66.666%}.g-b--1of4,.g-b--2of8,.g-b--3of12{width:25%}.g-b--3of4,.g-b--6of8,.g-b--9of12{width:75%}.g-b--1of5,.g-b--2of10{width:20%}.g-b--2of5,.g-b--4of10{width:40%}.g-b--3of5,.g-b--6of10{width:60%}.g-b--4of5,.g-b--8of10{width:80%}.g-b--1of6,.g-b--2of12{width:16.666%}.g-b--5of6,.g-b--10of12{width:83.333%}.g-b--1of8{width:12.5%}.g-b--3of8{width:37.5%}.g-b--5of8{width:62.5%}.g-b--7of8{width:87.5%}.g-b--1of10{width:10%}.g-b--3of10{width:30%}.g-b--7of10{width:70%}.g-b--9of10{width:90%}.g-b--1of12{width:8.333%}.g-b--5of12{width:41.666%}.g-b--7of12{width:58.333%}.g-b--11of12{width:91.666%}.g-b--push--1of1{margin-left:100%}.g-b--push--1of2,.g-b--push--2of4,.g-b--push--3of6,.g-b--push--4of8,.g-b--push--5of10,.g-b--push--6of12{margin-left:50%}.g-b--push--1of3,.g-b--push--2of6,.g-b--push--4of12{margin-left:33.333%}.g-b--push--2of3,.g-b--push--4of6,.g-b--push--8of12{margin-left:66.666%}.g-b--push--1of4,.g-b--push--2of8,.g-b--push--3of12{margin-left:25%}.g-b--push--3of4,.g-b--push--6of8,.g-b--push--9of12{margin-left:75%}.g-b--push--1of5,.g-b--push--2of10{margin-left:20%}.g-b--push--2of5,.g-b--push--4of10{margin-left:40%}.g-b--push--3of5,.g-b--push--6of10{margin-left:60%}.g-b--push--4of5,.g-b--push--8of10{margin-left:80%}.g-b--push--1of6,.g-b--push--2of12{margin-left:16.666%}.g-b--push--5of6,.g-b--push--10of12{margin-left:83.333%}.g-b--push--1of8{margin-left:12.5%}.g-b--push--3of8{margin-left:37.5%}.g-b--push--5of8{margin-left:62.5%}.g-b--push--7of8{margin-left:87.5%}.g-b--push--1of10{margin-left:10%}.g-b--push--3of10{margin-left:30%}.g-b--push--7of10{margin-left:70%}.g-b--push--9of10{margin-left:90%}.g-b--push--1of12{margin-left:8.333%}.g-b--push--5of12{margin-left:41.666%}.g-b--push--7of12{margin-left:58.333%}.g-b--push--11of12{margin-left:91.666%}.g-b--pull--1of1{margin-right:100%}.g-b--pull--1of2,.g-b--pull--2of4,.g-b--pull--3of6,.g-b--pull--4of8,.g-b--pull--5of10,.g-b--pull--6of12{margin-right:50%}.g-b--pull--1of3,.g-b--pull--2of6,.g-b--pull--4of12{margin-right:33.333%}.g-b--pull--2of3,.g-b--pull--4of6,.g-b--pull--8of12{margin-right:66.666%}.g-b--pull--1of4,.g-b--pull--2of8,.g-b--pull--3of12{margin-right:25%}.g-b--pull--3of4,.g-b--pull--6of8,.g-b--pull--9of12{margin-right:75%}.g-b--pull--1of5,.g-b--pull--2of10{margin-right:20%}.g-b--pull--2of5,.g-b--pull--4of10{margin-right:40%}.g-b--pull--3of5,.g-b--pull--6of10{margin-right:60%}.g-b--pull--4of5,.g-b--pull--8of10{margin-right:80%}.g-b--pull--1of6,.g-b--pull--2of12{margin-right:16.666%}.g-b--pull--5of6,.g-b--pull--10of12{margin-right:83.333%}.g-b--pull--1of8{margin-right:12.5%}.g-b--pull--3of8{margin-right:37.5%}.g-b--pull--5of8{margin-right:62.5%}.g-b--pull--7of8{margin-right:87.5%}.g-b--pull--1of10{margin-right:10%}.g-b--pull--3of10{margin-right:30%}.g-b--pull--7of10{margin-right:70%}.g-b--pull--9of10{margin-right:90%}.g-b--pull--1of12{margin-right:8.333%}.g-b--pull--5of12{margin-right:41.666%}.g-b--pull--7of12{margin-right:58.333%}.g-b--pull--11of12{margin-right:91.666%}.splashscreen{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#373D49;z-index:22}.splashscreen-dillinger{width:260px;height:auto;display:block;margin:0 auto;padding-bottom:3rem}.splashscreen p{font-size:1.25rem;padding-top:.56251rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;text-align:center;max-width:500px;margin:0 auto;color:#FFF}.sp-center{position:relative;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);top:50%}.open-menu>.wrapper{overflow-x:hidden}.page{margin:0 auto;position:relative;top:0;left:0;width:100%;height:100%;z-index:2;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;background-color:#fff;padding-top:51px;will-change:left}.open-menu .page{left:270px}.title{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;padding-left:16px;padding-right:16px;margin-top:1rem}.split-preview .title{padding-left:0}.title-document{line-height:1rem;font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem;font-weight:400;font-family:\"Ubuntu Mono\",Monaco;color:#373D49;padding-left:16px;padding-right:16px;width:80%;min-width:300px;outline:0;border:none}.icon{display:block;margin:0 auto;width:36px;height:36px;border-radius:3px;text-align:center}.icon svg{display:inline-block;margin-left:auto;margin-right:auto}.icon-preview{background-color:#373D49;line-height:40px}.icon-preview svg{width:19px;height:12px}.icon-settings{background-color:#373D49;line-height:44px}.icon-settings svg{width:18px;height:18px}.icon-link{width:16px;height:16px;line-height:1;margin-right:24px;text-align:right}.navbar{background-color:#373D49;height:51px;width:100%;position:fixed;top:0;left:0;z-index:6;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;will-change:left}.navbar:after{content:\"\";display:table;clear:both}.open-menu .navbar{left:270px}.navbar-brand{float:left;margin:0 0 0 24px;padding:0;line-height:42px}.navbar-brand svg{width:85px;height:11px}.nav-left{float:left}.nav-right{float:right}.nav-sidebar{width:100%}.menu{list-style:none;margin:0;padding:0}.menu a{border:0;color:#A0AABF;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;outline:none;text-transform:uppercase}.menu a:hover{color:#35D7BB}.menu .menu-item{border:0;display:none;float:left;margin:0;position:relative}.menu .menu-item>a{display:block;font-size:12px;height:51px;letter-spacing:1px;line-height:51px;padding:0 24px}.menu .menu-item--settings,.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar,.menu .menu-item--link-unlink.in-sidebar,.menu .menu-item--documents.in-sidebar{display:block}.menu .menu-item--documents{padding-bottom:1rem}.menu .menu-item.open>a{background-color:#1D212A}.menu .menu-item-icon>a{height:auto;padding:0}.menu .menu-item-icon:hover>a{background-color:transparent}.menu .menu-link.open i{background-color:#1D212A}.menu .menu-link.open g{fill:#35D7BB}.menu .menu-link-preview,.menu .menu-link-settings{margin-top:8px;width:51px}.menu-sidebar{width:100%}.menu-sidebar .menu-item{float:none;margin-bottom:1px;width:100%}.menu-sidebar .menu-item.open>a{background-color:#373D49}.menu-sidebar .open .caret{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.menu-sidebar>.menu-item:hover .dropdown a,.menu-sidebar>.menu-item:hover .settings a{background-color:transparent}.menu-sidebar .menu-link{background-color:#373D49;font-weight:600}.menu-sidebar .menu-link:after{content:\"\";display:table;clear:both}.menu-sidebar .menu-link>span{float:left}.menu-sidebar .menu-link>.caret{float:right;text-align:right;top:22px}.menu-sidebar .dropdown,.menu-sidebar .settings{background-color:transparent;position:static;width:100%}.dropdown{position:absolute;right:0;top:51px;width:188px}.dropdown,.settings{display:none;background-color:#1D212A}.dropdown{padding:0}.dropdown,.settings,.sidebar-list{list-style:none;margin:0}.sidebar-list{padding:0}.dropdown li{margin:32px 0;padding:0 0 0 32px}.dropdown li,.settings li{line-height:1}.sidebar-list li{line-height:1;margin:32px 0;padding:0 0 0 32px}.dropdown a{color:#D0D6E2}.dropdown a,.settings a,.sidebar-list a{display:block;text-transform:none}.sidebar-list a{color:#D0D6E2}.dropdown a:after,.settings a:after,.sidebar-list a:after{content:\"\";display:table;clear:both}.dropdown .icon,.settings .icon,.sidebar-list .icon{float:right}.open .dropdown,.open .settings,.open .sidebar-list{display:block}.open .dropdown.collapse,.open .collapse.settings,.open .sidebar-list.collapse{display:none}.open .dropdown.collapse.in,.open .collapse.in.settings,.open .sidebar-list.collapse.in{display:block}.dropdown .unlinked .icon,.settings .unlinked .icon,.sidebar-list .unlinked .icon{opacity:.3}.dropdown.documents li,.documents.settings li,.sidebar-list.documents li{background-image:url(\"../img/icons/file.svg\");background-position:240px center;background-repeat:no-repeat;background-size:14px 16px;padding:3px 32px}.dropdown.documents li.octocat,.documents.settings li.octocat,.sidebar-list.documents li.octocat{background-image:url(\"../img/icons/octocat.svg\");background-position:234px center;background-size:24px 24px}.dropdown.documents li:last-child,.documents.settings li:last-child,.sidebar-list.documents li:last-child{margin-bottom:1rem}.dropdown.documents li.active a,.documents.settings li.active a,.sidebar-list.documents li.active a{color:#35D7BB}.settings{position:fixed;top:67px;right:16px;border-radius:3px;width:288px;background-color:#373D49;padding:16px;z-index:7}.show-settings .settings{display:block}.settings .has-checkbox{float:left}.settings a{font-size:1.25rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;-webkit-font-smoothing:antialiased;line-height:28px;color:#D0D6E2}.settings a:after{content:\"\";display:table;clear:both}.settings a:hover{color:#35D7BB}.settings li{border-bottom:1px solid #4F535B;margin:0;padding:16px 0}.settings li:last-child{border-bottom:none}.brand{border:none;display:block}.brand:hover g{fill:#35D7BB}.toggle{display:block;float:left;height:16px;padding:25px 16px 26px;width:40px}.toggle span:after,.toggle span:before{content:'';left:0;position:absolute;top:-6px}.toggle span:after{top:6px}.toggle span{display:block;position:relative}.toggle span,.toggle span:after,.toggle span:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#D3DAEA;height:2px;-webkit-transition:all .3s;transition:all .3s;width:20px}.open-menu .toggle span{background-color:transparent}.open-menu .toggle span:before{-webkit-transform:rotate(45deg)translate(3px,3px);-ms-transform:rotate(45deg)translate(3px,3px);transform:rotate(45deg)translate(3px,3px)}.open-menu .toggle span:after{-webkit-transform:rotate(-45deg)translate(5px,-6px);-ms-transform:rotate(-45deg)translate(5px,-6px);transform:rotate(-45deg)translate(5px,-6px)}.caret{display:inline-block;width:0;height:0;margin-left:6px;vertical-align:middle;position:relative;top:-1px;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.sidebar{overflow:auto;height:100%;padding-right:15px;padding-bottom:15px;width:285px}.sidebar-wrapper{-webkit-overflow-scrolling:touch;background-color:#2B2F36;left:0;height:100%;overflow-y:hidden;position:fixed;top:0;width:285px;z-index:1}.sidebar-branding{width:160px;padding:0;margin:16px auto}.header{border-bottom:1px solid #E8E8E8;position:relative}.words{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;z-index:5;position:absolute;right:16px;top:0}.words span{color:#000}.btn{text-align:center;display:inline-block;width:100%;text-transform:uppercase;font-weight:600;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:14px;text-shadow:0 1px 0 #1b8b77;padding:16px 24px;background-color:#35D7BB;border-radius:3px;margin:0 auto 16px;line-height:1;color:#fff;-webkit-transition:all .15s linear;transition:all .15s linear;-webkit-font-smoothing:antialiased}.btn--new,.btn--save{display:block;width:238px}.btn--new:hover,.btn--new:focus,.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 3px #24b59c;text-shadow:0 1px 0 #24b59c}.btn--save{background-color:#4A5261;text-shadow:0 1px 1px #1e2127}.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 5px #08090a;text-shadow:none}.btn--delete{display:block;width:238px;background-color:transparent;font-size:12px;text-shadow:none}.btn--delete:hover,.btn--delete:focus{color:#fff;border-bottom-color:transparent;text-shadow:0 1px 0 #08090a;opacity:.8}.btn--ok,.btn--close{border-top:0;background-color:#4A5261;text-shadow:0 1px 0 #08090a;margin:0}.btn--ok:hover,.btn--ok:focus,.btn--close:hover,.btn--close:focus{color:#fff;background-color:#292d36;text-shadow:none}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(55,61,73,.8);-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;-webkit-transition-timing-function:ease-out;transition-timing-function:ease-out;will-change:left,opacity,visibility;z-index:5;opacity:0;visibility:hidden}.show-settings .overlay{visibility:visible;opacity:1}.switch{float:right;line-height:1}.switch input{display:none}.switch small{display:inline-block;cursor:pointer;padding:0 24px 0 0;-webkit-transition:all ease .2s;transition:all ease .2s;background-color:#2B2F36;border-color:#2B2F36}.switch small,.switch small:before{border-radius:30px;box-shadow:inset 0 0 2px 0 #14171F}.switch small:before{display:block;content:'';width:28px;height:28px;background:#fff}.switch.checked small{padding-right:0;padding-left:24px;background-color:#35D7BB;box-shadow:none}.modal--dillinger.about .modal-dialog{font-size:1.25rem;max-width:500px}.modal--dillinger .modal-dialog{max-width:600px;width:auto;margin:5rem auto}.modal--dillinger .modal-content{background:#373D49;border-radius:3px;box-shadow:0 2px 5px 0 #2C3B59;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;padding:2rem}.modal--dillinger ul{list-style-type:disc;margin:1rem 0;padding:0 0 0 1rem}.modal--dillinger li{padding:0;margin:0}.modal--dillinger .modal-header{border:0;padding:0}.modal--dillinger .modal-body{padding:0}.modal--dillinger .modal-footer{border:0;padding:0}.modal--dillinger .close{color:#fff;opacity:1}.modal-backdrop{background-color:#373D49}.pagination--dillinger{padding:0!important;margin:1.5rem 0!important;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-align-content:stretch;-ms-flex-line-pack:stretch;align-content:stretch}.pagination--dillinger,.pagination--dillinger li{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.pagination--dillinger li{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.pagination--dillinger li:first-child>a,.pagination--dillinger li.disabled>a,.pagination--dillinger li.disabled>a:hover,.pagination--dillinger li.disabled>a:focus,.pagination--dillinger li>a{background-color:transparent;border-color:#4F535B;border-right-color:transparent}.pagination--dillinger li.active>a,.pagination--dillinger li.active>a:hover,.pagination--dillinger li.active>a:focus{border-color:#4A5261;background-color:#4A5261;color:#fff}.pagination--dillinger li>a{float:none;color:#fff;width:100%;display:block;text-align:center;margin:0;border-right-color:transparent;padding:6px}.pagination--dillinger li>a:hover,.pagination--dillinger li>a:focus{border-color:#35D7BB;background-color:#35D7BB;color:#fff}.pagination--dillinger li:last-child a{border-color:#4F535B}.pagination--dillinger li:first-child a{border-right-color:transparent}.diNotify{position:absolute;z-index:9999;left:0;right:0;top:0;margin:0 auto;max-width:400px;text-align:center;-webkit-transition:top .5s ease-in-out,opacity .5s ease-in-out;transition:top .5s ease-in-out,opacity .5s ease-in-out;visibility:hidden}.diNotify-body{-webkit-font-smoothing:antialiased;background-color:#35D7BB;background:#666E7F;border-radius:3px;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;overflow:hidden;padding:1rem 2rem .5rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-webkit-align-items:baseline;-ms-flex-align:baseline;align-items:baseline;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.diNotify-icon{display:block;width:16px;height:16px;line-height:16px;position:relative;top:3px}.diNotify-message{padding-left:1rem}.zen-wrapper{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;z-index:10;background-color:#FFF;opacity:0;-webkit-transition:opacity .25s ease-in-out;transition:opacity .25s ease-in-out}.zen-wrapper.on{opacity:1}.enter-zen-mode{background-image:url(\"../img/icons/enter-zen.svg\");right:.5rem;top:.5rem;display:none}.enter-zen-mode,.close-zen-mode{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;background-repeat:no-repeat;width:32px;height:32px;display:block;position:absolute}.close-zen-mode{background-image:url(\"../img/icons/exit-zen.svg\");right:1rem;top:1rem}.zen-page{position:relative;top:0;bottom:0;z-index:11;height:100%;width:100%}#zen{font-size:1.25rem;width:300px;height:80%;margin:0 auto;position:relative;top:10%}#zen:before,#zen:after{content:\"\";position:absolute;height:10%;width:100%;z-index:12;pointer-events:none}.split{overflow:scroll;padding:0!important}.split-editor{padding-left:0;padding-right:0;position:relative}.show-preview .split-editor{display:none}.split-preview{background-color:#fff;display:none;top:0;position:relative;z-index:4}.show-preview .split-preview{display:block}#editor{font-size:1rem;font-family:\"Ubuntu Mono\",Monaco;font-weight:400;line-height:2rem;width:100%;height:100%}#editor .ace_gutter{-webkit-font-smoothing:antialiased}#preview a{color:#A0AABF;text-decoration:underline}.sr-only{visibility:hidden;text-overflow:110%;overflow:hidden;top:-100px;position:absolute}.mnone{margin:0!important}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=\"radio\"],.form-inline .checkbox input[type=\"checkbox\"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}.form-horizontal .form-group-lg .control-label{padding-top:14.3px}.form-horizontal .form-group-sm .control-label{padding-top:6px}.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}@media screen and (min-width:27.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--m1of1{width:100%}.g-b--m1of2,.g-b--m2of4,.g-b--m3of6,.g-b--m4of8,.g-b--m5of10,.g-b--m6of12{width:50%}.g-b--m1of3,.g-b--m2of6,.g-b--m4of12{width:33.333%}.g-b--m2of3,.g-b--m4of6,.g-b--m8of12{width:66.666%}.g-b--m1of4,.g-b--m2of8,.g-b--m3of12{width:25%}.g-b--m3of4,.g-b--m6of8,.g-b--m9of12{width:75%}.g-b--m1of5,.g-b--m2of10{width:20%}.g-b--m2of5,.g-b--m4of10{width:40%}.g-b--m3of5,.g-b--m6of10{width:60%}.g-b--m4of5,.g-b--m8of10{width:80%}.g-b--m1of6,.g-b--m2of12{width:16.666%}.g-b--m5of6,.g-b--m10of12{width:83.333%}.g-b--m1of8{width:12.5%}.g-b--m3of8{width:37.5%}.g-b--m5of8{width:62.5%}.g-b--m7of8{width:87.5%}.g-b--m1of10{width:10%}.g-b--m3of10{width:30%}.g-b--m7of10{width:70%}.g-b--m9of10{width:90%}.g-b--m1of12{width:8.333%}.g-b--m5of12{width:41.666%}.g-b--m7of12{width:58.333%}.g-b--m11of12{width:91.666%}.g-b--push--m1of1{margin-left:100%}.g-b--push--m1of2,.g-b--push--m2of4,.g-b--push--m3of6,.g-b--push--m4of8,.g-b--push--m5of10,.g-b--push--m6of12{margin-left:50%}.g-b--push--m1of3,.g-b--push--m2of6,.g-b--push--m4of12{margin-left:33.333%}.g-b--push--m2of3,.g-b--push--m4of6,.g-b--push--m8of12{margin-left:66.666%}.g-b--push--m1of4,.g-b--push--m2of8,.g-b--push--m3of12{margin-left:25%}.g-b--push--m3of4,.g-b--push--m6of8,.g-b--push--m9of12{margin-left:75%}.g-b--push--m1of5,.g-b--push--m2of10{margin-left:20%}.g-b--push--m2of5,.g-b--push--m4of10{margin-left:40%}.g-b--push--m3of5,.g-b--push--m6of10{margin-left:60%}.g-b--push--m4of5,.g-b--push--m8of10{margin-left:80%}.g-b--push--m1of6,.g-b--push--m2of12{margin-left:16.666%}.g-b--push--m5of6,.g-b--push--m10of12{margin-left:83.333%}.g-b--push--m1of8{margin-left:12.5%}.g-b--push--m3of8{margin-left:37.5%}.g-b--push--m5of8{margin-left:62.5%}.g-b--push--m7of8{margin-left:87.5%}.g-b--push--m1of10{margin-left:10%}.g-b--push--m3of10{margin-left:30%}.g-b--push--m7of10{margin-left:70%}.g-b--push--m9of10{margin-left:90%}.g-b--push--m1of12{margin-left:8.333%}.g-b--push--m5of12{margin-left:41.666%}.g-b--push--m7of12{margin-left:58.333%}.g-b--push--m11of12{margin-left:91.666%}.g-b--pull--m1of1{margin-right:100%}.g-b--pull--m1of2,.g-b--pull--m2of4,.g-b--pull--m3of6,.g-b--pull--m4of8,.g-b--pull--m5of10,.g-b--pull--m6of12{margin-right:50%}.g-b--pull--m1of3,.g-b--pull--m2of6,.g-b--pull--m4of12{margin-right:33.333%}.g-b--pull--m2of3,.g-b--pull--m4of6,.g-b--pull--m8of12{margin-right:66.666%}.g-b--pull--m1of4,.g-b--pull--m2of8,.g-b--pull--m3of12{margin-right:25%}.g-b--pull--m3of4,.g-b--pull--m6of8,.g-b--pull--m9of12{margin-right:75%}.g-b--pull--m1of5,.g-b--pull--m2of10{margin-right:20%}.g-b--pull--m2of5,.g-b--pull--m4of10{margin-right:40%}.g-b--pull--m3of5,.g-b--pull--m6of10{margin-right:60%}.g-b--pull--m4of5,.g-b--pull--m8of10{margin-right:80%}.g-b--pull--m1of6,.g-b--pull--m2of12{margin-right:16.666%}.g-b--pull--m5of6,.g-b--pull--m10of12{margin-right:83.333%}.g-b--pull--m1of8{margin-right:12.5%}.g-b--pull--m3of8{margin-right:37.5%}.g-b--pull--m5of8{margin-right:62.5%}.g-b--pull--m7of8{margin-right:87.5%}.g-b--pull--m1of10{margin-right:10%}.g-b--pull--m3of10{margin-right:30%}.g-b--pull--m7of10{margin-right:70%}.g-b--pull--m9of10{margin-right:90%}.g-b--pull--m1of12{margin-right:8.333%}.g-b--pull--m5of12{margin-right:41.666%}.g-b--pull--m7of12{margin-right:58.333%}.g-b--pull--m11of12{margin-right:91.666%}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:400px}#editor{font-size:1rem}}@media screen and (min-width:46.25em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--t1of1{width:100%}.g-b--t1of2,.g-b--t2of4,.g-b--t3of6,.g-b--t4of8,.g-b--t5of10,.g-b--t6of12{width:50%}.g-b--t1of3,.g-b--t2of6,.g-b--t4of12{width:33.333%}.g-b--t2of3,.g-b--t4of6,.g-b--t8of12{width:66.666%}.g-b--t1of4,.g-b--t2of8,.g-b--t3of12{width:25%}.g-b--t3of4,.g-b--t6of8,.g-b--t9of12{width:75%}.g-b--t1of5,.g-b--t2of10{width:20%}.g-b--t2of5,.g-b--t4of10{width:40%}.g-b--t3of5,.g-b--t6of10{width:60%}.g-b--t4of5,.g-b--t8of10{width:80%}.g-b--t1of6,.g-b--t2of12{width:16.666%}.g-b--t5of6,.g-b--t10of12{width:83.333%}.g-b--t1of8{width:12.5%}.g-b--t3of8{width:37.5%}.g-b--t5of8{width:62.5%}.g-b--t7of8{width:87.5%}.g-b--t1of10{width:10%}.g-b--t3of10{width:30%}.g-b--t7of10{width:70%}.g-b--t9of10{width:90%}.g-b--t1of12{width:8.333%}.g-b--t5of12{width:41.666%}.g-b--t7of12{width:58.333%}.g-b--t11of12{width:91.666%}.g-b--push--t1of1{margin-left:100%}.g-b--push--t1of2,.g-b--push--t2of4,.g-b--push--t3of6,.g-b--push--t4of8,.g-b--push--t5of10,.g-b--push--t6of12{margin-left:50%}.g-b--push--t1of3,.g-b--push--t2of6,.g-b--push--t4of12{margin-left:33.333%}.g-b--push--t2of3,.g-b--push--t4of6,.g-b--push--t8of12{margin-left:66.666%}.g-b--push--t1of4,.g-b--push--t2of8,.g-b--push--t3of12{margin-left:25%}.g-b--push--t3of4,.g-b--push--t6of8,.g-b--push--t9of12{margin-left:75%}.g-b--push--t1of5,.g-b--push--t2of10{margin-left:20%}.g-b--push--t2of5,.g-b--push--t4of10{margin-left:40%}.g-b--push--t3of5,.g-b--push--t6of10{margin-left:60%}.g-b--push--t4of5,.g-b--push--t8of10{margin-left:80%}.g-b--push--t1of6,.g-b--push--t2of12{margin-left:16.666%}.g-b--push--t5of6,.g-b--push--t10of12{margin-left:83.333%}.g-b--push--t1of8{margin-left:12.5%}.g-b--push--t3of8{margin-left:37.5%}.g-b--push--t5of8{margin-left:62.5%}.g-b--push--t7of8{margin-left:87.5%}.g-b--push--t1of10{margin-left:10%}.g-b--push--t3of10{margin-left:30%}.g-b--push--t7of10{margin-left:70%}.g-b--push--t9of10{margin-left:90%}.g-b--push--t1of12{margin-left:8.333%}.g-b--push--t5of12{margin-left:41.666%}.g-b--push--t7of12{margin-left:58.333%}.g-b--push--t11of12{margin-left:91.666%}.g-b--pull--t1of1{margin-right:100%}.g-b--pull--t1of2,.g-b--pull--t2of4,.g-b--pull--t3of6,.g-b--pull--t4of8,.g-b--pull--t5of10,.g-b--pull--t6of12{margin-right:50%}.g-b--pull--t1of3,.g-b--pull--t2of6,.g-b--pull--t4of12{margin-right:33.333%}.g-b--pull--t2of3,.g-b--pull--t4of6,.g-b--pull--t8of12{margin-right:66.666%}.g-b--pull--t1of4,.g-b--pull--t2of8,.g-b--pull--t3of12{margin-right:25%}.g-b--pull--t3of4,.g-b--pull--t6of8,.g-b--pull--t9of12{margin-right:75%}.g-b--pull--t1of5,.g-b--pull--t2of10{margin-right:20%}.g-b--pull--t2of5,.g-b--pull--t4of10{margin-right:40%}.g-b--pull--t3of5,.g-b--pull--t6of10{margin-right:60%}.g-b--pull--t4of5,.g-b--pull--t8of10{margin-right:80%}.g-b--pull--t1of6,.g-b--pull--t2of12{margin-right:16.666%}.g-b--pull--t5of6,.g-b--pull--t10of12{margin-right:83.333%}.g-b--pull--t1of8{margin-right:12.5%}.g-b--pull--t3of8{margin-right:37.5%}.g-b--pull--t5of8{margin-right:62.5%}.g-b--pull--t7of8{margin-right:87.5%}.g-b--pull--t1of10{margin-right:10%}.g-b--pull--t3of10{margin-right:30%}.g-b--pull--t7of10{margin-right:70%}.g-b--pull--t9of10{margin-right:90%}.g-b--pull--t1of12{margin-right:8.333%}.g-b--pull--t5of12{margin-right:41.666%}.g-b--pull--t7of12{margin-right:58.333%}.g-b--pull--t11of12{margin-right:91.666%}.splashscreen-dillinger{width:500px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--save-to,.menu .menu-item--import-from{display:block}.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog{font-size:1.25rem}.enter-zen-mode{display:block}.close-zen-mode{right:3rem;top:3rem}#zen{font-size:1.25rem;width:500px}.split-editor{border-right:1px solid #E8E8E8;float:left;height:calc(100vh - 130px);-webkit-overflow-scrolling:touch;padding-right:16px;width:50%}.show-preview .split-editor{display:block}.split-preview{display:block;float:right;height:calc(100vh - 130px);-webkit-overflow-scrolling:touch;position:relative;top:0;width:50%}#editor{font-size:1rem}}@media screen and (min-width:62.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--d1of1{width:100%}.g-b--d1of2,.g-b--d2of4,.g-b--d3of6,.g-b--d4of8,.g-b--d5of10,.g-b--d6of12{width:50%}.g-b--d1of3,.g-b--d2of6,.g-b--d4of12{width:33.333%}.g-b--d2of3,.g-b--d4of6,.g-b--d8of12{width:66.666%}.g-b--d1of4,.g-b--d2of8,.g-b--d3of12{width:25%}.g-b--d3of4,.g-b--d6of8,.g-b--d9of12{width:75%}.g-b--d1of5,.g-b--d2of10{width:20%}.g-b--d2of5,.g-b--d4of10{width:40%}.g-b--d3of5,.g-b--d6of10{width:60%}.g-b--d4of5,.g-b--d8of10{width:80%}.g-b--d1of6,.g-b--d2of12{width:16.666%}.g-b--d5of6,.g-b--d10of12{width:83.333%}.g-b--d1of8{width:12.5%}.g-b--d3of8{width:37.5%}.g-b--d5of8{width:62.5%}.g-b--d7of8{width:87.5%}.g-b--d1of10{width:10%}.g-b--d3of10{width:30%}.g-b--d7of10{width:70%}.g-b--d9of10{width:90%}.g-b--d1of12{width:8.333%}.g-b--d5of12{width:41.666%}.g-b--d7of12{width:58.333%}.g-b--d11of12{width:91.666%}.g-b--push--d1of1{margin-left:100%}.g-b--push--d1of2,.g-b--push--d2of4,.g-b--push--d3of6,.g-b--push--d4of8,.g-b--push--d5of10,.g-b--push--d6of12{margin-left:50%}.g-b--push--d1of3,.g-b--push--d2of6,.g-b--push--d4of12{margin-left:33.333%}.g-b--push--d2of3,.g-b--push--d4of6,.g-b--push--d8of12{margin-left:66.666%}.g-b--push--d1of4,.g-b--push--d2of8,.g-b--push--d3of12{margin-left:25%}.g-b--push--d3of4,.g-b--push--d6of8,.g-b--push--d9of12{margin-left:75%}.g-b--push--d1of5,.g-b--push--d2of10{margin-left:20%}.g-b--push--d2of5,.g-b--push--d4of10{margin-left:40%}.g-b--push--d3of5,.g-b--push--d6of10{margin-left:60%}.g-b--push--d4of5,.g-b--push--d8of10{margin-left:80%}.g-b--push--d1of6,.g-b--push--d2of12{margin-left:16.666%}.g-b--push--d5of6,.g-b--push--d10of12{margin-left:83.333%}.g-b--push--d1of8{margin-left:12.5%}.g-b--push--d3of8{margin-left:37.5%}.g-b--push--d5of8{margin-left:62.5%}.g-b--push--d7of8{margin-left:87.5%}.g-b--push--d1of10{margin-left:10%}.g-b--push--d3of10{margin-left:30%}.g-b--push--d7of10{margin-left:70%}.g-b--push--d9of10{margin-left:90%}.g-b--push--d1of12{margin-left:8.333%}.g-b--push--d5of12{margin-left:41.666%}.g-b--push--d7of12{margin-left:58.333%}.g-b--push--d11of12{margin-left:91.666%}.g-b--pull--d1of1{margin-right:100%}.g-b--pull--d1of2,.g-b--pull--d2of4,.g-b--pull--d3of6,.g-b--pull--d4of8,.g-b--pull--d5of10,.g-b--pull--d6of12{margin-right:50%}.g-b--pull--d1of3,.g-b--pull--d2of6,.g-b--pull--d4of12{margin-right:33.333%}.g-b--pull--d2of3,.g-b--pull--d4of6,.g-b--pull--d8of12{margin-right:66.666%}.g-b--pull--d1of4,.g-b--pull--d2of8,.g-b--pull--d3of12{margin-right:25%}.g-b--pull--d3of4,.g-b--pull--d6of8,.g-b--pull--d9of12{margin-right:75%}.g-b--pull--d1of5,.g-b--pull--d2of10{margin-right:20%}.g-b--pull--d2of5,.g-b--pull--d4of10{margin-right:40%}.g-b--pull--d3of5,.g-b--pull--d6of10{margin-right:60%}.g-b--pull--d4of5,.g-b--pull--d8of10{margin-right:80%}.g-b--pull--d1of6,.g-b--pull--d2of12{margin-right:16.666%}.g-b--pull--d5of6,.g-b--pull--d10of12{margin-right:83.333%}.g-b--pull--d1of8{margin-right:12.5%}.g-b--pull--d3of8{margin-right:37.5%}.g-b--pull--d5of8{margin-right:62.5%}.g-b--pull--d7of8{margin-right:87.5%}.g-b--pull--d1of10{margin-right:10%}.g-b--pull--d3of10{margin-right:30%}.g-b--pull--d7of10{margin-right:70%}.g-b--pull--d9of10{margin-right:90%}.g-b--pull--d1of12{margin-right:8.333%}.g-b--pull--d5of12{margin-right:41.666%}.g-b--pull--d7of12{margin-right:58.333%}.g-b--pull--d11of12{margin-right:91.666%}.splashscreen-dillinger{width:700px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--export-as{display:block}.menu .menu-item--preview{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:700px}#editor{font-size:1rem}}@media screen and (min-width:87.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.splashscreen-dillinger{width:800px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#editor{font-size:1rem}}</style></head><body id=\"preview\">\n<p>When you encounter a bug, I encourage you to follow these steps to make it easier for me to troubleshoot.\n<ol>\n<li>Can you start a new instance of Happypanda and reproduce the bug?\n<ul>\n<li>If that’s not the case then skip the steps below and go to <strong>How to report</strong>\n<ol>\n<li>First close all instances of Happypanda.</li>\n<li>Open a command prompt *(terminal in <em>nix)</em> and navigate to where Happypanda is installed. <em>Eg.: <code>cd path/to/happypanda</code></em></li>\n<li>Now type the name of the main executable with a <code>-d</code> following, <em>eg.: <code>happypanda.exe -d</code> or <code>main.py -d</code> if you’re running from source.</em></li>\n<li>The program will now open and create a new file named <code>happypanda_debug.log</code></li>\n<li>Now you try to reproduce the error/bug</li>\n</ol>\n</li>\n</ul>\n</li>\n</ol>\n<h3><a id=\"How_to_report_12\"></a>How to report</h3>\n<p>If you completed the steps above, make sure to include the <code>happypanda_debug.log</code> file which was created\nand a description of how you reproduced the error/bug.</p>\n<ol>\n<li>Navigate to where you installed Happypanda with a file explorer and find <code>happypanda.log</code>. Send it to me with a description of the bug.</li>\n<li>You have 3 options of contacting me:\n<ul>\n<li>Go to the github repo <a href=\"https://github.com/Pewpews/happypanda/issues\">issue page</a> and create a new issue</li>\n<li>Enter the gitter chat <a href=\"https://gitter.im/Pewpews/happypanda\">here</a> and tell me about your issue</li>\n<li>If for some reason you don’t want anything to do with github, feel free to email me: <code>happypandabugs@gmail.com</code></li>\n</ul>\n</li>\n</ol>\n\n</body></html>\n\"\"\"\n\nSEARCH_TUTORIAL_TAGS =\\\n\t\"\"\"\n<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Untitled Document.md</title><style>@import 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.2.0/katex.min.css';code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}code,kbd{padding:2px 4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;margin:0 0 10px;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}fieldset{border:0;min-width:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=\"radio\"],input[type=\"checkbox\"]{margin:1px 0 0;line-height:normal}input[type=\"file\"]{display:block}input[type=\"range\"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=\"file\"]:focus,input[type=\"radio\"]:focus,input[type=\"checkbox\"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}output,.form-control{display:block;font-size:14px;line-height:1.4285714;color:#555}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=\"date\"],input[type=\"time\"],input[type=\"datetime-local\"],input[type=\"month\"]{line-height:34px;line-height:1.4285714 \\0}input[type=\"date\"].input-sm,.form-horizontal .form-group-sm input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-sm,.form-horizontal .form-group-sm input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-sm,.form-horizontal .form-group-sm input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-sm,.form-horizontal .form-group-sm input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"month\"].btn{line-height:30px}input[type=\"date\"].input-lg,.form-horizontal .form-group-lg input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-lg,.form-horizontal .form-group-lg input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-lg,.form-horizontal .form-group-lg input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-lg,.form-horizontal .form-group-lg input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"month\"].btn{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=\"radio\"],.radio-inline input[type=\"radio\"],.checkbox input[type=\"checkbox\"],.checkbox-inline input[type=\"checkbox\"]{position:absolute;margin-left:-20px;margin-top:4px \\9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=\"radio\"][disabled],input[type=\"radio\"].disabled,fieldset[disabled] input[type=\"radio\"],input[type=\"checkbox\"][disabled],input[type=\"checkbox\"].disabled,fieldset[disabled] input[type=\"checkbox\"],.radio-inline.disabled,fieldset[disabled] .radio-inline,.checkbox-inline.disabled,fieldset[disabled] .checkbox-inline,.radio.disabled label,fieldset[disabled] .radio label,.checkbox.disabled label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-horizontal .form-group-lg .form-control-static.form-control,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.form-control-static.input-sm,.form-horizontal .form-group-sm .form-control-static.form-control,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-left:0;padding-right:0}.input-sm,.form-horizontal .form-group-sm .form-control,.input-group-sm>.form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.input-group-sm>.input-group-addon{height:30px;line-height:1.5}.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.form-horizontal .form-group-sm select.form-control,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn{height:30px;line-height:30px}textarea.input-sm,.form-horizontal .form-group-sm textarea.form-control,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,select[multiple].input-sm,.form-horizontal .form-group-sm select[multiple].form-control,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control,.input-group-lg>.form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.input-group-lg>.input-group-addon{height:46px;line-height:1.33}.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,.form-horizontal .form-group-lg select.form-control,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn{height:46px;line-height:46px}textarea.input-lg,.form-horizontal .form-group-lg textarea.form-control,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,select[multiple].input-lg,.form-horizontal .form-group-lg select[multiple].form-control,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback,.form-horizontal .form-group-lg .form-control+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.form-horizontal .form-group-sm .form-control+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:before{content:\" \";display:table}.form-horizontal .form-group:after{content:\" \";display:table;clear:both}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px}.btn-sm,.btn-xs{font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=\"submit\"].btn-block,input[type=\"reset\"].btn-block,input[type=\"button\"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=\"col-\"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon{white-space:nowrap}.input-group-addon,.input-group-btn{width:1%;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm,.form-horizontal .form-group-sm .input-group-addon.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.form-horizontal .form-group-lg .input-group-addon.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=\"radio\"],.input-group-addon input[type=\"checkbox\"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn,.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.4285714;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>a:focus,.pagination>li>span:hover,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:hover,.pagination>.active>a:focus,.pagination>.active>span,.pagination>.active>span:hover,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open,.modal{overflow:hidden}.modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0);-webkit-transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.4285714px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.4285714}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{content:\" \";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.clearfix:before,.clearfix:after{content:\" \";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496;-webkit-text-size-adjust:none}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-doctype,.hljs-pi,.lisp .hljs-string,.hljs-javadoc{color:#586e75}.hljs-keyword,.hljs-winutils,.method,.hljs-addition,.css .hljs-tag,.hljs-request,.hljs-status,.nginx .hljs-title{color:#859900}.hljs-number,.hljs-command,.hljs-string,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula,.hljs-regexp,.hljs-hexcolor,.hljs-link_url{color:#2aa198}.hljs-title,.hljs-localvars,.hljs-chunk,.hljs-decorator,.hljs-built_in,.hljs-identifier,.vhdl .hljs-literal,.hljs-id,.css .hljs-function{color:#268bd2}.hljs-attribute,.hljs-variable,.lisp .hljs-body,.smalltalk .hljs-number,.hljs-constant,.hljs-class .hljs-title,.hljs-parent,.hljs-type,.hljs-link_reference{color:#b58900}.hljs-preprocessor,.hljs-preprocessor .hljs-keyword,.hljs-pragma,.hljs-shebang,.hljs-symbol,.hljs-symbol .hljs-string,.diff .hljs-change,.hljs-special,.hljs-attr_selector,.hljs-subst,.hljs-cdata,.css .hljs-pseudo,.hljs-header{color:#cb4b16}.hljs-deletion,.hljs-important{color:#dc322f}.hljs-link_label{color:#6c71c4}.tex .hljs-formula{background:#073642}*,*:before,*:after{box-sizing:border-box}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}images{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd{font-size:1em}code,kbd,pre,samp{font-family:monospace,monospace}samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=\"button\"],input[type=\"reset\"],input[type=\"submit\"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=\"checkbox\"],input[type=\"radio\"]{box-sizing:border-box;padding:0}input[type=\"number\"]::-webkit-inner-spin-button,input[type=\"number\"]::-webkit-outer-spin-button{height:auto}input[type=\"search\"]{-webkit-appearance:textfield;box-sizing:content-box}input[type=\"search\"]::-webkit-search-cancel-button,input[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}.debug{background-color:#ffc0cb!important}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ir{background-color:transparent;border:0;overflow:hidden}.ir::before{content:'';display:block;height:150%;width:0}html{font-size:.875em;background:#fafafa;color:#373D49}html,body{font-family:Georgia,Cambria,serif;height:100%}body{font-size:1rem;font-weight:400;line-height:2rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}li{-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-left:1rem}li>ul,li>ol{margin-bottom:0}p{padding-top:.66001rem;-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-top:0}p,pre{margin-bottom:1.33999rem}pre{font-size:1rem;padding:.66001rem 9.5px 9.5px;line-height:2rem;background:-webkit-linear-gradient(top,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background:linear-gradient(to bottom,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background-size:100% 4rem;border-color:#D3DAEA}blockquote{margin:0}blockquote p{font-size:1rem;margin-bottom:.33999rem;font-style:italic;padding:.66001rem 1rem 1rem;border-left:3px solid #A0AABF}th,td{padding:12px}h1,h2,h3,h4,h5,h6{font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;-webkit-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;-moz-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-style:normal;font-weight:600;margin-top:0}h1{line-height:3rem;font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h2,h3{line-height:3rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}a{cursor:pointer;color:#35D7BB;text-decoration:none}a:hover,a:focus{border-bottom-color:#35D7BB;color:#dff9f4}img{height:auto;max-width:100%}.g{display:block}.g:after{clear:both;content:'';display:table}.g-b{float:left;margin:0;width:100%}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--center{display:block;float:none;margin:0 auto}.g-b--right{float:right}.g-b--1of1{width:100%}.g-b--1of2,.g-b--2of4,.g-b--3of6,.g-b--4of8,.g-b--5of10,.g-b--6of12{width:50%}.g-b--1of3,.g-b--2of6,.g-b--4of12{width:33.333%}.g-b--2of3,.g-b--4of6,.g-b--8of12{width:66.666%}.g-b--1of4,.g-b--2of8,.g-b--3of12{width:25%}.g-b--3of4,.g-b--6of8,.g-b--9of12{width:75%}.g-b--1of5,.g-b--2of10{width:20%}.g-b--2of5,.g-b--4of10{width:40%}.g-b--3of5,.g-b--6of10{width:60%}.g-b--4of5,.g-b--8of10{width:80%}.g-b--1of6,.g-b--2of12{width:16.666%}.g-b--5of6,.g-b--10of12{width:83.333%}.g-b--1of8{width:12.5%}.g-b--3of8{width:37.5%}.g-b--5of8{width:62.5%}.g-b--7of8{width:87.5%}.g-b--1of10{width:10%}.g-b--3of10{width:30%}.g-b--7of10{width:70%}.g-b--9of10{width:90%}.g-b--1of12{width:8.333%}.g-b--5of12{width:41.666%}.g-b--7of12{width:58.333%}.g-b--11of12{width:91.666%}.g-b--push--1of1{margin-left:100%}.g-b--push--1of2,.g-b--push--2of4,.g-b--push--3of6,.g-b--push--4of8,.g-b--push--5of10,.g-b--push--6of12{margin-left:50%}.g-b--push--1of3,.g-b--push--2of6,.g-b--push--4of12{margin-left:33.333%}.g-b--push--2of3,.g-b--push--4of6,.g-b--push--8of12{margin-left:66.666%}.g-b--push--1of4,.g-b--push--2of8,.g-b--push--3of12{margin-left:25%}.g-b--push--3of4,.g-b--push--6of8,.g-b--push--9of12{margin-left:75%}.g-b--push--1of5,.g-b--push--2of10{margin-left:20%}.g-b--push--2of5,.g-b--push--4of10{margin-left:40%}.g-b--push--3of5,.g-b--push--6of10{margin-left:60%}.g-b--push--4of5,.g-b--push--8of10{margin-left:80%}.g-b--push--1of6,.g-b--push--2of12{margin-left:16.666%}.g-b--push--5of6,.g-b--push--10of12{margin-left:83.333%}.g-b--push--1of8{margin-left:12.5%}.g-b--push--3of8{margin-left:37.5%}.g-b--push--5of8{margin-left:62.5%}.g-b--push--7of8{margin-left:87.5%}.g-b--push--1of10{margin-left:10%}.g-b--push--3of10{margin-left:30%}.g-b--push--7of10{margin-left:70%}.g-b--push--9of10{margin-left:90%}.g-b--push--1of12{margin-left:8.333%}.g-b--push--5of12{margin-left:41.666%}.g-b--push--7of12{margin-left:58.333%}.g-b--push--11of12{margin-left:91.666%}.g-b--pull--1of1{margin-right:100%}.g-b--pull--1of2,.g-b--pull--2of4,.g-b--pull--3of6,.g-b--pull--4of8,.g-b--pull--5of10,.g-b--pull--6of12{margin-right:50%}.g-b--pull--1of3,.g-b--pull--2of6,.g-b--pull--4of12{margin-right:33.333%}.g-b--pull--2of3,.g-b--pull--4of6,.g-b--pull--8of12{margin-right:66.666%}.g-b--pull--1of4,.g-b--pull--2of8,.g-b--pull--3of12{margin-right:25%}.g-b--pull--3of4,.g-b--pull--6of8,.g-b--pull--9of12{margin-right:75%}.g-b--pull--1of5,.g-b--pull--2of10{margin-right:20%}.g-b--pull--2of5,.g-b--pull--4of10{margin-right:40%}.g-b--pull--3of5,.g-b--pull--6of10{margin-right:60%}.g-b--pull--4of5,.g-b--pull--8of10{margin-right:80%}.g-b--pull--1of6,.g-b--pull--2of12{margin-right:16.666%}.g-b--pull--5of6,.g-b--pull--10of12{margin-right:83.333%}.g-b--pull--1of8{margin-right:12.5%}.g-b--pull--3of8{margin-right:37.5%}.g-b--pull--5of8{margin-right:62.5%}.g-b--pull--7of8{margin-right:87.5%}.g-b--pull--1of10{margin-right:10%}.g-b--pull--3of10{margin-right:30%}.g-b--pull--7of10{margin-right:70%}.g-b--pull--9of10{margin-right:90%}.g-b--pull--1of12{margin-right:8.333%}.g-b--pull--5of12{margin-right:41.666%}.g-b--pull--7of12{margin-right:58.333%}.g-b--pull--11of12{margin-right:91.666%}.splashscreen{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#373D49;z-index:22}.splashscreen-dillinger{width:260px;height:auto;display:block;margin:0 auto;padding-bottom:3rem}.splashscreen p{font-size:1.25rem;padding-top:.56251rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;text-align:center;max-width:500px;margin:0 auto;color:#FFF}.sp-center{position:relative;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);top:50%}.open-menu>.wrapper{overflow-x:hidden}.page{margin:0 auto;position:relative;top:0;left:0;width:100%;height:100%;z-index:2;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;background-color:#fff;padding-top:51px;will-change:left}.open-menu .page{left:270px}.title{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;padding-left:16px;padding-right:16px;margin-top:1rem}.split-preview .title{padding-left:0}.title-document{line-height:1rem;font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem;font-weight:400;font-family:\"Ubuntu Mono\",Monaco;color:#373D49;padding-left:16px;padding-right:16px;width:80%;min-width:300px;outline:0;border:none}.icon{display:block;margin:0 auto;width:36px;height:36px;border-radius:3px;text-align:center}.icon svg{display:inline-block;margin-left:auto;margin-right:auto}.icon-preview{background-color:#373D49;line-height:40px}.icon-preview svg{width:19px;height:12px}.icon-settings{background-color:#373D49;line-height:44px}.icon-settings svg{width:18px;height:18px}.icon-link{width:16px;height:16px;line-height:1;margin-right:24px;text-align:right}.navbar{background-color:#373D49;height:51px;width:100%;position:fixed;top:0;left:0;z-index:6;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;will-change:left}.navbar:after{content:\"\";display:table;clear:both}.open-menu .navbar{left:270px}.navbar-brand{float:left;margin:0 0 0 24px;padding:0;line-height:42px}.navbar-brand svg{width:85px;height:11px}.nav-left{float:left}.nav-right{float:right}.nav-sidebar{width:100%}.menu{list-style:none;margin:0;padding:0}.menu a{border:0;color:#A0AABF;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;outline:none;text-transform:uppercase}.menu a:hover{color:#35D7BB}.menu .menu-item{border:0;display:none;float:left;margin:0;position:relative}.menu .menu-item>a{display:block;font-size:12px;height:51px;letter-spacing:1px;line-height:51px;padding:0 24px}.menu .menu-item--settings,.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar,.menu .menu-item--link-unlink.in-sidebar,.menu .menu-item--documents.in-sidebar{display:block}.menu .menu-item--documents{padding-bottom:1rem}.menu .menu-item.open>a{background-color:#1D212A}.menu .menu-item-icon>a{height:auto;padding:0}.menu .menu-item-icon:hover>a{background-color:transparent}.menu .menu-link.open i{background-color:#1D212A}.menu .menu-link.open g{fill:#35D7BB}.menu .menu-link-preview,.menu .menu-link-settings{margin-top:8px;width:51px}.menu-sidebar{width:100%}.menu-sidebar .menu-item{float:none;margin-bottom:1px;width:100%}.menu-sidebar .menu-item.open>a{background-color:#373D49}.menu-sidebar .open .caret{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.menu-sidebar>.menu-item:hover .dropdown a,.menu-sidebar>.menu-item:hover .settings a{background-color:transparent}.menu-sidebar .menu-link{background-color:#373D49;font-weight:600}.menu-sidebar .menu-link:after{content:\"\";display:table;clear:both}.menu-sidebar .menu-link>span{float:left}.menu-sidebar .menu-link>.caret{float:right;text-align:right;top:22px}.menu-sidebar .dropdown,.menu-sidebar .settings{background-color:transparent;position:static;width:100%}.dropdown{position:absolute;right:0;top:51px;width:188px}.dropdown,.settings{display:none;background-color:#1D212A}.dropdown{padding:0}.dropdown,.settings,.sidebar-list{list-style:none;margin:0}.sidebar-list{padding:0}.dropdown li{margin:32px 0;padding:0 0 0 32px}.dropdown li,.settings li{line-height:1}.sidebar-list li{line-height:1;margin:32px 0;padding:0 0 0 32px}.dropdown a{color:#D0D6E2}.dropdown a,.settings a,.sidebar-list a{display:block;text-transform:none}.sidebar-list a{color:#D0D6E2}.dropdown a:after,.settings a:after,.sidebar-list a:after{content:\"\";display:table;clear:both}.dropdown .icon,.settings .icon,.sidebar-list .icon{float:right}.open .dropdown,.open .settings,.open .sidebar-list{display:block}.open .dropdown.collapse,.open .collapse.settings,.open .sidebar-list.collapse{display:none}.open .dropdown.collapse.in,.open .collapse.in.settings,.open .sidebar-list.collapse.in{display:block}.dropdown .unlinked .icon,.settings .unlinked .icon,.sidebar-list .unlinked .icon{opacity:.3}.dropdown.documents li,.documents.settings li,.sidebar-list.documents li{background-image:url(\"../img/icons/file.svg\");background-position:240px center;background-repeat:no-repeat;background-size:14px 16px;padding:3px 32px}.dropdown.documents li.octocat,.documents.settings li.octocat,.sidebar-list.documents li.octocat{background-image:url(\"../img/icons/octocat.svg\");background-position:234px center;background-size:24px 24px}.dropdown.documents li:last-child,.documents.settings li:last-child,.sidebar-list.documents li:last-child{margin-bottom:1rem}.dropdown.documents li.active a,.documents.settings li.active a,.sidebar-list.documents li.active a{color:#35D7BB}.settings{position:fixed;top:67px;right:16px;border-radius:3px;width:288px;background-color:#373D49;padding:16px;z-index:7}.show-settings .settings{display:block}.settings .has-checkbox{float:left}.settings a{font-size:1.25rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;-webkit-font-smoothing:antialiased;line-height:28px;color:#D0D6E2}.settings a:after{content:\"\";display:table;clear:both}.settings a:hover{color:#35D7BB}.settings li{border-bottom:1px solid #4F535B;margin:0;padding:16px 0}.settings li:last-child{border-bottom:none}.brand{border:none;display:block}.brand:hover g{fill:#35D7BB}.toggle{display:block;float:left;height:16px;padding:25px 16px 26px;width:40px}.toggle span:after,.toggle span:before{content:'';left:0;position:absolute;top:-6px}.toggle span:after{top:6px}.toggle span{display:block;position:relative}.toggle span,.toggle span:after,.toggle span:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#D3DAEA;height:2px;-webkit-transition:all .3s;transition:all .3s;width:20px}.open-menu .toggle span{background-color:transparent}.open-menu .toggle span:before{-webkit-transform:rotate(45deg)translate(3px,3px);-ms-transform:rotate(45deg)translate(3px,3px);transform:rotate(45deg)translate(3px,3px)}.open-menu .toggle span:after{-webkit-transform:rotate(-45deg)translate(5px,-6px);-ms-transform:rotate(-45deg)translate(5px,-6px);transform:rotate(-45deg)translate(5px,-6px)}.caret{display:inline-block;width:0;height:0;margin-left:6px;vertical-align:middle;position:relative;top:-1px;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.sidebar{overflow:auto;height:100%;padding-right:15px;padding-bottom:15px;width:285px}.sidebar-wrapper{-webkit-overflow-scrolling:touch;background-color:#2B2F36;left:0;height:100%;overflow-y:hidden;position:fixed;top:0;width:285px;z-index:1}.sidebar-branding{width:160px;padding:0;margin:16px auto}.header{border-bottom:1px solid #E8E8E8;position:relative}.words{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;z-index:5;position:absolute;right:16px;top:0}.words span{color:#000}.btn{text-align:center;display:inline-block;width:100%;text-transform:uppercase;font-weight:600;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:14px;text-shadow:0 1px 0 #1b8b77;padding:16px 24px;background-color:#35D7BB;border-radius:3px;margin:0 auto 16px;line-height:1;color:#fff;-webkit-transition:all .15s linear;transition:all .15s linear;-webkit-font-smoothing:antialiased}.btn--new,.btn--save{display:block;width:238px}.btn--new:hover,.btn--new:focus,.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 3px #24b59c;text-shadow:0 1px 0 #24b59c}.btn--save{background-color:#4A5261;text-shadow:0 1px 1px #1e2127}.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 5px #08090a;text-shadow:none}.btn--delete{display:block;width:238px;background-color:transparent;font-size:12px;text-shadow:none}.btn--delete:hover,.btn--delete:focus{color:#fff;border-bottom-color:transparent;text-shadow:0 1px 0 #08090a;opacity:.8}.btn--ok,.btn--close{border-top:0;background-color:#4A5261;text-shadow:0 1px 0 #08090a;margin:0}.btn--ok:hover,.btn--ok:focus,.btn--close:hover,.btn--close:focus{color:#fff;background-color:#292d36;text-shadow:none}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(55,61,73,.8);-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;-webkit-transition-timing-function:ease-out;transition-timing-function:ease-out;will-change:left,opacity,visibility;z-index:5;opacity:0;visibility:hidden}.show-settings .overlay{visibility:visible;opacity:1}.switch{float:right;line-height:1}.switch input{display:none}.switch small{display:inline-block;cursor:pointer;padding:0 24px 0 0;-webkit-transition:all ease .2s;transition:all ease .2s;background-color:#2B2F36;border-color:#2B2F36}.switch small,.switch small:before{border-radius:30px;box-shadow:inset 0 0 2px 0 #14171F}.switch small:before{display:block;content:'';width:28px;height:28px;background:#fff}.switch.checked small{padding-right:0;padding-left:24px;background-color:#35D7BB;box-shadow:none}.modal--dillinger.about .modal-dialog{font-size:1.25rem;max-width:500px}.modal--dillinger .modal-dialog{max-width:600px;width:auto;margin:5rem auto}.modal--dillinger .modal-content{background:#373D49;border-radius:3px;box-shadow:0 2px 5px 0 #2C3B59;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;padding:2rem}.modal--dillinger ul{list-style-type:disc;margin:1rem 0;padding:0 0 0 1rem}.modal--dillinger li{padding:0;margin:0}.modal--dillinger .modal-header{border:0;padding:0}.modal--dillinger .modal-body{padding:0}.modal--dillinger .modal-footer{border:0;padding:0}.modal--dillinger .close{color:#fff;opacity:1}.modal-backdrop{background-color:#373D49}.pagination--dillinger{padding:0!important;margin:1.5rem 0!important;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-align-content:stretch;-ms-flex-line-pack:stretch;align-content:stretch}.pagination--dillinger,.pagination--dillinger li{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.pagination--dillinger li{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.pagination--dillinger li:first-child>a,.pagination--dillinger li.disabled>a,.pagination--dillinger li.disabled>a:hover,.pagination--dillinger li.disabled>a:focus,.pagination--dillinger li>a{background-color:transparent;border-color:#4F535B;border-right-color:transparent}.pagination--dillinger li.active>a,.pagination--dillinger li.active>a:hover,.pagination--dillinger li.active>a:focus{border-color:#4A5261;background-color:#4A5261;color:#fff}.pagination--dillinger li>a{float:none;color:#fff;width:100%;display:block;text-align:center;margin:0;border-right-color:transparent;padding:6px}.pagination--dillinger li>a:hover,.pagination--dillinger li>a:focus{border-color:#35D7BB;background-color:#35D7BB;color:#fff}.pagination--dillinger li:last-child a{border-color:#4F535B}.pagination--dillinger li:first-child a{border-right-color:transparent}.diNotify{position:absolute;z-index:9999;left:0;right:0;top:0;margin:0 auto;max-width:400px;text-align:center;-webkit-transition:top .5s ease-in-out,opacity .5s ease-in-out;transition:top .5s ease-in-out,opacity .5s ease-in-out;visibility:hidden}.diNotify-body{-webkit-font-smoothing:antialiased;background-color:#35D7BB;background:#666E7F;border-radius:3px;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;overflow:hidden;padding:1rem 2rem .5rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-webkit-align-items:baseline;-ms-flex-align:baseline;align-items:baseline;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.diNotify-icon{display:block;width:16px;height:16px;line-height:16px;position:relative;top:3px}.diNotify-message{padding-left:1rem}.zen-wrapper{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;z-index:10;background-color:#FFF;opacity:0;-webkit-transition:opacity .25s ease-in-out;transition:opacity .25s ease-in-out}.zen-wrapper.on{opacity:1}.enter-zen-mode{background-image:url(\"../img/icons/enter-zen.svg\");right:.5rem;top:.5rem;display:none}.enter-zen-mode,.close-zen-mode{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;background-repeat:no-repeat;width:32px;height:32px;display:block;position:absolute}.close-zen-mode{background-image:url(\"../img/icons/exit-zen.svg\");right:1rem;top:1rem}.zen-page{position:relative;top:0;bottom:0;z-index:11;height:100%;width:100%}#zen{font-size:1.25rem;width:300px;height:80%;margin:0 auto;position:relative;top:10%}#zen:before,#zen:after{content:\"\";position:absolute;height:10%;width:100%;z-index:12;pointer-events:none}.split{overflow:scroll;padding:0!important}.split-editor{padding-left:0;padding-right:0;position:relative}.show-preview .split-editor{display:none}.split-preview{background-color:#fff;display:none;top:0;position:relative;z-index:4}.show-preview .split-preview{display:block}#editor{font-size:1rem;font-family:\"Ubuntu Mono\",Monaco;font-weight:400;line-height:2rem;width:100%;height:100%}#editor .ace_gutter{-webkit-font-smoothing:antialiased}#preview a{color:#A0AABF;text-decoration:underline}.sr-only{visibility:hidden;text-overflow:110%;overflow:hidden;top:-100px;position:absolute}.mnone{margin:0!important}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=\"radio\"],.form-inline .checkbox input[type=\"checkbox\"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}.form-horizontal .form-group-lg .control-label{padding-top:14.3px}.form-horizontal .form-group-sm .control-label{padding-top:6px}.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}@media screen and (min-width:27.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--m1of1{width:100%}.g-b--m1of2,.g-b--m2of4,.g-b--m3of6,.g-b--m4of8,.g-b--m5of10,.g-b--m6of12{width:50%}.g-b--m1of3,.g-b--m2of6,.g-b--m4of12{width:33.333%}.g-b--m2of3,.g-b--m4of6,.g-b--m8of12{width:66.666%}.g-b--m1of4,.g-b--m2of8,.g-b--m3of12{width:25%}.g-b--m3of4,.g-b--m6of8,.g-b--m9of12{width:75%}.g-b--m1of5,.g-b--m2of10{width:20%}.g-b--m2of5,.g-b--m4of10{width:40%}.g-b--m3of5,.g-b--m6of10{width:60%}.g-b--m4of5,.g-b--m8of10{width:80%}.g-b--m1of6,.g-b--m2of12{width:16.666%}.g-b--m5of6,.g-b--m10of12{width:83.333%}.g-b--m1of8{width:12.5%}.g-b--m3of8{width:37.5%}.g-b--m5of8{width:62.5%}.g-b--m7of8{width:87.5%}.g-b--m1of10{width:10%}.g-b--m3of10{width:30%}.g-b--m7of10{width:70%}.g-b--m9of10{width:90%}.g-b--m1of12{width:8.333%}.g-b--m5of12{width:41.666%}.g-b--m7of12{width:58.333%}.g-b--m11of12{width:91.666%}.g-b--push--m1of1{margin-left:100%}.g-b--push--m1of2,.g-b--push--m2of4,.g-b--push--m3of6,.g-b--push--m4of8,.g-b--push--m5of10,.g-b--push--m6of12{margin-left:50%}.g-b--push--m1of3,.g-b--push--m2of6,.g-b--push--m4of12{margin-left:33.333%}.g-b--push--m2of3,.g-b--push--m4of6,.g-b--push--m8of12{margin-left:66.666%}.g-b--push--m1of4,.g-b--push--m2of8,.g-b--push--m3of12{margin-left:25%}.g-b--push--m3of4,.g-b--push--m6of8,.g-b--push--m9of12{margin-left:75%}.g-b--push--m1of5,.g-b--push--m2of10{margin-left:20%}.g-b--push--m2of5,.g-b--push--m4of10{margin-left:40%}.g-b--push--m3of5,.g-b--push--m6of10{margin-left:60%}.g-b--push--m4of5,.g-b--push--m8of10{margin-left:80%}.g-b--push--m1of6,.g-b--push--m2of12{margin-left:16.666%}.g-b--push--m5of6,.g-b--push--m10of12{margin-left:83.333%}.g-b--push--m1of8{margin-left:12.5%}.g-b--push--m3of8{margin-left:37.5%}.g-b--push--m5of8{margin-left:62.5%}.g-b--push--m7of8{margin-left:87.5%}.g-b--push--m1of10{margin-left:10%}.g-b--push--m3of10{margin-left:30%}.g-b--push--m7of10{margin-left:70%}.g-b--push--m9of10{margin-left:90%}.g-b--push--m1of12{margin-left:8.333%}.g-b--push--m5of12{margin-left:41.666%}.g-b--push--m7of12{margin-left:58.333%}.g-b--push--m11of12{margin-left:91.666%}.g-b--pull--m1of1{margin-right:100%}.g-b--pull--m1of2,.g-b--pull--m2of4,.g-b--pull--m3of6,.g-b--pull--m4of8,.g-b--pull--m5of10,.g-b--pull--m6of12{margin-right:50%}.g-b--pull--m1of3,.g-b--pull--m2of6,.g-b--pull--m4of12{margin-right:33.333%}.g-b--pull--m2of3,.g-b--pull--m4of6,.g-b--pull--m8of12{margin-right:66.666%}.g-b--pull--m1of4,.g-b--pull--m2of8,.g-b--pull--m3of12{margin-right:25%}.g-b--pull--m3of4,.g-b--pull--m6of8,.g-b--pull--m9of12{margin-right:75%}.g-b--pull--m1of5,.g-b--pull--m2of10{margin-right:20%}.g-b--pull--m2of5,.g-b--pull--m4of10{margin-right:40%}.g-b--pull--m3of5,.g-b--pull--m6of10{margin-right:60%}.g-b--pull--m4of5,.g-b--pull--m8of10{margin-right:80%}.g-b--pull--m1of6,.g-b--pull--m2of12{margin-right:16.666%}.g-b--pull--m5of6,.g-b--pull--m10of12{margin-right:83.333%}.g-b--pull--m1of8{margin-right:12.5%}.g-b--pull--m3of8{margin-right:37.5%}.g-b--pull--m5of8{margin-right:62.5%}.g-b--pull--m7of8{margin-right:87.5%}.g-b--pull--m1of10{margin-right:10%}.g-b--pull--m3of10{margin-right:30%}.g-b--pull--m7of10{margin-right:70%}.g-b--pull--m9of10{margin-right:90%}.g-b--pull--m1of12{margin-right:8.333%}.g-b--pull--m5of12{margin-right:41.666%}.g-b--pull--m7of12{margin-right:58.333%}.g-b--pull--m11of12{margin-right:91.666%}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:400px}#editor{font-size:1rem}}@media screen and (min-width:46.25em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--t1of1{width:100%}.g-b--t1of2,.g-b--t2of4,.g-b--t3of6,.g-b--t4of8,.g-b--t5of10,.g-b--t6of12{width:50%}.g-b--t1of3,.g-b--t2of6,.g-b--t4of12{width:33.333%}.g-b--t2of3,.g-b--t4of6,.g-b--t8of12{width:66.666%}.g-b--t1of4,.g-b--t2of8,.g-b--t3of12{width:25%}.g-b--t3of4,.g-b--t6of8,.g-b--t9of12{width:75%}.g-b--t1of5,.g-b--t2of10{width:20%}.g-b--t2of5,.g-b--t4of10{width:40%}.g-b--t3of5,.g-b--t6of10{width:60%}.g-b--t4of5,.g-b--t8of10{width:80%}.g-b--t1of6,.g-b--t2of12{width:16.666%}.g-b--t5of6,.g-b--t10of12{width:83.333%}.g-b--t1of8{width:12.5%}.g-b--t3of8{width:37.5%}.g-b--t5of8{width:62.5%}.g-b--t7of8{width:87.5%}.g-b--t1of10{width:10%}.g-b--t3of10{width:30%}.g-b--t7of10{width:70%}.g-b--t9of10{width:90%}.g-b--t1of12{width:8.333%}.g-b--t5of12{width:41.666%}.g-b--t7of12{width:58.333%}.g-b--t11of12{width:91.666%}.g-b--push--t1of1{margin-left:100%}.g-b--push--t1of2,.g-b--push--t2of4,.g-b--push--t3of6,.g-b--push--t4of8,.g-b--push--t5of10,.g-b--push--t6of12{margin-left:50%}.g-b--push--t1of3,.g-b--push--t2of6,.g-b--push--t4of12{margin-left:33.333%}.g-b--push--t2of3,.g-b--push--t4of6,.g-b--push--t8of12{margin-left:66.666%}.g-b--push--t1of4,.g-b--push--t2of8,.g-b--push--t3of12{margin-left:25%}.g-b--push--t3of4,.g-b--push--t6of8,.g-b--push--t9of12{margin-left:75%}.g-b--push--t1of5,.g-b--push--t2of10{margin-left:20%}.g-b--push--t2of5,.g-b--push--t4of10{margin-left:40%}.g-b--push--t3of5,.g-b--push--t6of10{margin-left:60%}.g-b--push--t4of5,.g-b--push--t8of10{margin-left:80%}.g-b--push--t1of6,.g-b--push--t2of12{margin-left:16.666%}.g-b--push--t5of6,.g-b--push--t10of12{margin-left:83.333%}.g-b--push--t1of8{margin-left:12.5%}.g-b--push--t3of8{margin-left:37.5%}.g-b--push--t5of8{margin-left:62.5%}.g-b--push--t7of8{margin-left:87.5%}.g-b--push--t1of10{margin-left:10%}.g-b--push--t3of10{margin-left:30%}.g-b--push--t7of10{margin-left:70%}.g-b--push--t9of10{margin-left:90%}.g-b--push--t1of12{margin-left:8.333%}.g-b--push--t5of12{margin-left:41.666%}.g-b--push--t7of12{margin-left:58.333%}.g-b--push--t11of12{margin-left:91.666%}.g-b--pull--t1of1{margin-right:100%}.g-b--pull--t1of2,.g-b--pull--t2of4,.g-b--pull--t3of6,.g-b--pull--t4of8,.g-b--pull--t5of10,.g-b--pull--t6of12{margin-right:50%}.g-b--pull--t1of3,.g-b--pull--t2of6,.g-b--pull--t4of12{margin-right:33.333%}.g-b--pull--t2of3,.g-b--pull--t4of6,.g-b--pull--t8of12{margin-right:66.666%}.g-b--pull--t1of4,.g-b--pull--t2of8,.g-b--pull--t3of12{margin-right:25%}.g-b--pull--t3of4,.g-b--pull--t6of8,.g-b--pull--t9of12{margin-right:75%}.g-b--pull--t1of5,.g-b--pull--t2of10{margin-right:20%}.g-b--pull--t2of5,.g-b--pull--t4of10{margin-right:40%}.g-b--pull--t3of5,.g-b--pull--t6of10{margin-right:60%}.g-b--pull--t4of5,.g-b--pull--t8of10{margin-right:80%}.g-b--pull--t1of6,.g-b--pull--t2of12{margin-right:16.666%}.g-b--pull--t5of6,.g-b--pull--t10of12{margin-right:83.333%}.g-b--pull--t1of8{margin-right:12.5%}.g-b--pull--t3of8{margin-right:37.5%}.g-b--pull--t5of8{margin-right:62.5%}.g-b--pull--t7of8{margin-right:87.5%}.g-b--pull--t1of10{margin-right:10%}.g-b--pull--t3of10{margin-right:30%}.g-b--pull--t7of10{margin-right:70%}.g-b--pull--t9of10{margin-right:90%}.g-b--pull--t1of12{margin-right:8.333%}.g-b--pull--t5of12{margin-right:41.666%}.g-b--pull--t7of12{margin-right:58.333%}.g-b--pull--t11of12{margin-right:91.666%}.splashscreen-dillinger{width:500px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--save-to,.menu .menu-item--import-from{display:block}.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog{font-size:1.25rem}.enter-zen-mode{display:block}.close-zen-mode{right:3rem;top:3rem}#zen{font-size:1.25rem;width:500px}.split-editor{border-right:1px solid #E8E8E8;float:left;height:calc(100vh - 130px);-webkit-overflow-scrolling:touch;padding-right:16px;width:50%}.show-preview .split-editor{display:block}.split-preview{display:block;float:right;height:calc(100vh - 130px);-webkit-overflow-scrolling:touch;position:relative;top:0;width:50%}#editor{font-size:1rem}}@media screen and (min-width:62.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--d1of1{width:100%}.g-b--d1of2,.g-b--d2of4,.g-b--d3of6,.g-b--d4of8,.g-b--d5of10,.g-b--d6of12{width:50%}.g-b--d1of3,.g-b--d2of6,.g-b--d4of12{width:33.333%}.g-b--d2of3,.g-b--d4of6,.g-b--d8of12{width:66.666%}.g-b--d1of4,.g-b--d2of8,.g-b--d3of12{width:25%}.g-b--d3of4,.g-b--d6of8,.g-b--d9of12{width:75%}.g-b--d1of5,.g-b--d2of10{width:20%}.g-b--d2of5,.g-b--d4of10{width:40%}.g-b--d3of5,.g-b--d6of10{width:60%}.g-b--d4of5,.g-b--d8of10{width:80%}.g-b--d1of6,.g-b--d2of12{width:16.666%}.g-b--d5of6,.g-b--d10of12{width:83.333%}.g-b--d1of8{width:12.5%}.g-b--d3of8{width:37.5%}.g-b--d5of8{width:62.5%}.g-b--d7of8{width:87.5%}.g-b--d1of10{width:10%}.g-b--d3of10{width:30%}.g-b--d7of10{width:70%}.g-b--d9of10{width:90%}.g-b--d1of12{width:8.333%}.g-b--d5of12{width:41.666%}.g-b--d7of12{width:58.333%}.g-b--d11of12{width:91.666%}.g-b--push--d1of1{margin-left:100%}.g-b--push--d1of2,.g-b--push--d2of4,.g-b--push--d3of6,.g-b--push--d4of8,.g-b--push--d5of10,.g-b--push--d6of12{margin-left:50%}.g-b--push--d1of3,.g-b--push--d2of6,.g-b--push--d4of12{margin-left:33.333%}.g-b--push--d2of3,.g-b--push--d4of6,.g-b--push--d8of12{margin-left:66.666%}.g-b--push--d1of4,.g-b--push--d2of8,.g-b--push--d3of12{margin-left:25%}.g-b--push--d3of4,.g-b--push--d6of8,.g-b--push--d9of12{margin-left:75%}.g-b--push--d1of5,.g-b--push--d2of10{margin-left:20%}.g-b--push--d2of5,.g-b--push--d4of10{margin-left:40%}.g-b--push--d3of5,.g-b--push--d6of10{margin-left:60%}.g-b--push--d4of5,.g-b--push--d8of10{margin-left:80%}.g-b--push--d1of6,.g-b--push--d2of12{margin-left:16.666%}.g-b--push--d5of6,.g-b--push--d10of12{margin-left:83.333%}.g-b--push--d1of8{margin-left:12.5%}.g-b--push--d3of8{margin-left:37.5%}.g-b--push--d5of8{margin-left:62.5%}.g-b--push--d7of8{margin-left:87.5%}.g-b--push--d1of10{margin-left:10%}.g-b--push--d3of10{margin-left:30%}.g-b--push--d7of10{margin-left:70%}.g-b--push--d9of10{margin-left:90%}.g-b--push--d1of12{margin-left:8.333%}.g-b--push--d5of12{margin-left:41.666%}.g-b--push--d7of12{margin-left:58.333%}.g-b--push--d11of12{margin-left:91.666%}.g-b--pull--d1of1{margin-right:100%}.g-b--pull--d1of2,.g-b--pull--d2of4,.g-b--pull--d3of6,.g-b--pull--d4of8,.g-b--pull--d5of10,.g-b--pull--d6of12{margin-right:50%}.g-b--pull--d1of3,.g-b--pull--d2of6,.g-b--pull--d4of12{margin-right:33.333%}.g-b--pull--d2of3,.g-b--pull--d4of6,.g-b--pull--d8of12{margin-right:66.666%}.g-b--pull--d1of4,.g-b--pull--d2of8,.g-b--pull--d3of12{margin-right:25%}.g-b--pull--d3of4,.g-b--pull--d6of8,.g-b--pull--d9of12{margin-right:75%}.g-b--pull--d1of5,.g-b--pull--d2of10{margin-right:20%}.g-b--pull--d2of5,.g-b--pull--d4of10{margin-right:40%}.g-b--pull--d3of5,.g-b--pull--d6of10{margin-right:60%}.g-b--pull--d4of5,.g-b--pull--d8of10{margin-right:80%}.g-b--pull--d1of6,.g-b--pull--d2of12{margin-right:16.666%}.g-b--pull--d5of6,.g-b--pull--d10of12{margin-right:83.333%}.g-b--pull--d1of8{margin-right:12.5%}.g-b--pull--d3of8{margin-right:37.5%}.g-b--pull--d5of8{margin-right:62.5%}.g-b--pull--d7of8{margin-right:87.5%}.g-b--pull--d1of10{margin-right:10%}.g-b--pull--d3of10{margin-right:30%}.g-b--pull--d7of10{margin-right:70%}.g-b--pull--d9of10{margin-right:90%}.g-b--pull--d1of12{margin-right:8.333%}.g-b--pull--d5of12{margin-right:41.666%}.g-b--pull--d7of12{margin-right:58.333%}.g-b--pull--d11of12{margin-right:91.666%}.splashscreen-dillinger{width:700px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--export-as{display:block}.menu .menu-item--preview{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:700px}#editor{font-size:1rem}}@media screen and (min-width:87.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.splashscreen-dillinger{width:800px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#editor{font-size:1rem}}</style></head><body id=\"preview\">\n<h4><a id=\"Constraints_0\"></a>Constraints</h4>\n<ul>\n<li><code>&quot; (quote)</code>, <code>(whitespace)</code>(unless in quotes), <code>, (comma)</code>, <code>: (semi-colon)</code>, <code>[ (starting bracket)</code> and <code>] (closing bracket)</code> are <strong>ignored</strong> and not included in the search</li>\n<li>terms and namespaces are separated by <code>(white space)</code> (if not in quotes) and/or a <code>, (comma)</code></li>\n<li>to include <em>whitespace</em> you must put the <em>term</em> in quotes: <code>&quot;this is a valid term&quot;</code></li>\n<li>to exclude a term, prefix the term with a <code>- (hyphen)</code>: <code>-&quot;i want to exclude this&quot;</code></li>\n</ul>\n<h4><a id=\"Searching_6\"></a>Searching</h4>\n<p><code>-&gt;</code> points where the terms will be searched</p>\n<ul>\n<li><code>term</code> =&gt; show only galleries with <code>term</code> in them <code>-&gt;</code> (title, artist, language, namespace &amp; tags)</li>\n<li><code>-term</code> =&gt; exclude all galleries with <code>term</code> in them <code>-&gt;</code> (title, artist, language, namespace &amp; tags)</li>\n<li><code>ns:term</code> =&gt; show only galleries where <code>term</code> is in the <code>ns</code> namespace in them <code>-&gt;</code> (namespace &amp; tags)</li>\n<li><code>-ns:term</code> =&gt; exclude all galleries where <code>term</code> is in the <code>ns</code> namespace in them <code>-&gt;</code> (namespace &amp; tags)</li>\n<li><code>ns:[term1, term2, ...]</code> =&gt; equivalent to <code>ns:term1</code>, <code>ns:term2</code></li>\n<li><code>-ns:[term1, term2, ...]</code> =&gt; equivalent to <code>-ns:term1</code>, <code>-ns:term2</code></li>\n</ul>\n<h4><a id=\"Protips__warnings_16\"></a>Protips &amp; warnings</h4>\n<ul>\n<li>When grouping tags under the same namespace in brackets, excluding tags (<code>-term</code>) and including tags (<code>term</code>) can both be used:\n<ul>\n<li><code>ns:[term, -term1, term2, ...]</code> -&gt; equivalent to <code>ns:term</code>, <code>-ns:term1</code>, <code>ns:term2</code></li>\n</ul>\n</li>\n<li><code>ns:-term</code> is <strong>NOT</strong> equivalent to <code>-ns:term</code></li>\n<li>You can enchance your search with regex. Regex can be used <em>anywhere</em> in your search terms\n<ul>\n<li>Enable <em>regex</em> in settings</li>\n</ul>\n</li>\n<li>Clicking on the search icon on the search bar will give you more options to search with</li>\n</ul>\n<h4><a id=\"Special_namespaced_tags_24\"></a>Special namespaced tags</h4>\n<p>They work just like normal namespaced tags, meaning that the <em>constraints</em> above also apply to them!</p>\n<ul>\n<li><strong>Reserved</strong> means that it shouldn’t be used on your galleries. It <strong>won’t</strong> be searched for in your gallery.</li>\n<li><strong>Operator</strong> means that less than <code>&lt;</code> and greater than <code>&gt;</code> are supported. They should be used just like the exclude operator <code>-</code>. E.g.: <code>ns:&lt;term</code> or <code>ns:&gt;[term1, term2]</code>.</li>\n<li><code>term</code> means… well, your term… what you want to search for… any kind of characters not in <strong>Constraints</strong> above</li>\n<li><code>integer</code> means that only numbers are allowed</li>\n<li><code>date</code> means a date format. Most (if not all) date formats are supported (try it out yourself)</li>\n</ul>\n<hr>\n<table>\n<thead>\n<tr>\n<th>Namespaced tag(s)</th>\n<th>Reserved</th>\n<th>Filtered galleries</th>\n<th>Operator</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>tag:none</code>, <code>tag:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> namespace &amp; tags set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>artist:none</code>, <code>artist:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> artist set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>status:none</code>, <code>status:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> status set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>language:none</code>, <code>language:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> language set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>type:none</code>, <code>type:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> type set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>path:none</code>, <code>path:null</code></td>\n<td>*</td>\n<td>Galleries that has been <strong>moved/deleted</strong> from the filesystem</td>\n<td></td>\n</tr>\n<tr>\n<td><code>descr:none</code>, <code>descr:null</code>, <code>description:none</code>, <code>description:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> description set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>&quot;pub date&quot;:none</code>, <code>&quot;pub date&quot;:null</code>, <code>pub_date:none</code>, <code>pub_date:null</code>, <code>publication:none</code>, <code>publication:null</code></td>\n<td>*</td>\n<td>Galleries with <strong>no</strong> publication date set</td>\n<td></td>\n</tr>\n<tr>\n<td><code>title:term</code></td>\n<td></td>\n<td>Galleries with <code>term</code> in their title <strong>OR</strong> has matching namespace &amp; tag</td>\n<td></td>\n</tr>\n<tr>\n<td><code>artist:term</code></td>\n<td></td>\n<td>Galleries with <code>term</code> in their artist <strong>OR</strong> has matching namespace &amp; tag</td>\n<td></td>\n</tr>\n<tr>\n<td><code>language:term</code>, <code>lang:term</code></td>\n<td></td>\n<td>Galleries with <code>term</code> in their language <strong>OR</strong> has matching namespace &amp; tag</td>\n<td></td>\n</tr>\n<tr>\n<td><code>type:term</code></td>\n<td></td>\n<td>Galleries with <code>term</code> in their type <strong>OR</strong> has matching namespace &amp; tag</td>\n<td></td>\n</tr>\n<tr>\n<td><code>status:term</code></td>\n<td></td>\n<td>Galleries with <code>term</code> in their status <strong>OR</strong> has matching namespace &amp; tag</td>\n<td></td>\n</tr>\n<tr>\n<td><code>descr:term</code>, <code>description:term</code></td>\n<td></td>\n<td>Galleries with <code>term</code> in their description <strong>OR</strong> has matching namespace &amp; tag</td>\n<td></td>\n</tr>\n<tr>\n<td><code>chapter:integer</code>, <code>chapters:integer</code></td>\n<td></td>\n<td>Galleries with <code>integer</code> chapters <strong>OR</strong> has matching namespace &amp; tag</td>\n<td>*</td>\n</tr>\n<tr>\n<td><code>read_count:integer</code>, <code>&quot;read count&quot;:integer</code>, <code>times_read:integer</code>, <code>&quot;times read&quot;:integer</code></td>\n<td></td>\n<td>Galleries read <code>integer</code> times <strong>OR</strong> has matching namespace &amp; tag</td>\n<td>*</td>\n</tr>\n<tr>\n<td><code>date_added:date</code>, <code>&quot;date added&quot;:date</code></td>\n<td></td>\n<td>Galleries added on <code>date</code> <strong>OR</strong> has matching namespace &amp; tag</td>\n<td>*</td>\n</tr>\n<tr>\n<td><code>pub_date:date</code>, <code>&quot;pub date&quot;:date</code>, <code>publication:date</code></td>\n<td></td>\n<td>Galleries published on <code>date</code> <strong>OR</strong> has matching namespace &amp; tag</td>\n<td>*</td>\n</tr>\n<tr>\n<td><code>last_read:date</code>, <code>&quot;last read&quot;:date</code></td>\n<td></td>\n<td>Galleries last read on <code>date</code> <strong>OR</strong> has matching namespace &amp; tag</td>\n<td>*</td>\n</tr>\n<tr>\n<td><code>rating:integer</code>, <code>stars:integer</code></td>\n<td></td>\n<td>Galleries that has been rated <code>integer</code> <strong>OR</strong> has matching namespace &amp; tag</td>\n<td>*</td>\n</tr>\n</tbody>\n</table>\n\n</body></html>\n\t\"\"\"\n\n\nKEYBOARD_SHORTCUTS_INFO =\\\n\t\"\"\"\n<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Untitled Document.md</title><style>@import 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.2.0/katex.min.css';code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}code,kbd{padding:2px 4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{display:block;margin:0 0 10px;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}fieldset{border:0;min-width:0}legend{display:block;width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=\"radio\"],input[type=\"checkbox\"]{margin:1px 0 0;line-height:normal}input[type=\"file\"]{display:block}input[type=\"range\"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=\"file\"]:focus,input[type=\"radio\"]:focus,input[type=\"checkbox\"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}output,.form-control{display:block;font-size:14px;line-height:1.4285714;color:#555}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=\"date\"],input[type=\"time\"],input[type=\"datetime-local\"],input[type=\"month\"]{line-height:34px;line-height:1.4285714 \\0}input[type=\"date\"].input-sm,.form-horizontal .form-group-sm input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].form-control,.input-group-sm>input[type=\"date\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-sm,.form-horizontal .form-group-sm input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].form-control,.input-group-sm>input[type=\"time\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-sm,.form-horizontal .form-group-sm input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].form-control,.input-group-sm>input[type=\"datetime-local\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-sm,.form-horizontal .form-group-sm input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].form-control,.input-group-sm>input[type=\"month\"].input-group-addon,.input-group-sm>.input-group-btn>input[type=\"month\"].btn{line-height:30px}input[type=\"date\"].input-lg,.form-horizontal .form-group-lg input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].form-control,.input-group-lg>input[type=\"date\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"date\"].btn,input[type=\"time\"].input-lg,.form-horizontal .form-group-lg input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].form-control,.input-group-lg>input[type=\"time\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"time\"].btn,input[type=\"datetime-local\"].input-lg,.form-horizontal .form-group-lg input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].form-control,.input-group-lg>input[type=\"datetime-local\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"datetime-local\"].btn,input[type=\"month\"].input-lg,.form-horizontal .form-group-lg input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].form-control,.input-group-lg>input[type=\"month\"].input-group-addon,.input-group-lg>.input-group-btn>input[type=\"month\"].btn{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=\"radio\"],.radio-inline input[type=\"radio\"],.checkbox input[type=\"checkbox\"],.checkbox-inline input[type=\"checkbox\"]{position:absolute;margin-left:-20px;margin-top:4px \\9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=\"radio\"][disabled],input[type=\"radio\"].disabled,fieldset[disabled] input[type=\"radio\"],input[type=\"checkbox\"][disabled],input[type=\"checkbox\"].disabled,fieldset[disabled] input[type=\"checkbox\"],.radio-inline.disabled,fieldset[disabled] .radio-inline,.checkbox-inline.disabled,fieldset[disabled] .checkbox-inline,.radio.disabled label,fieldset[disabled] .radio label,.checkbox.disabled label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-horizontal .form-group-lg .form-control-static.form-control,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.form-control-static.input-sm,.form-horizontal .form-group-sm .form-control-static.form-control,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-left:0;padding-right:0}.input-sm,.form-horizontal .form-group-sm .form-control,.input-group-sm>.form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.input-group-sm>.input-group-addon{height:30px;line-height:1.5}.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,.form-horizontal .form-group-sm select.form-control,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,.input-group-sm>.input-group-btn>select.btn{height:30px;line-height:30px}textarea.input-sm,.form-horizontal .form-group-sm textarea.form-control,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,.input-group-sm>.input-group-btn>textarea.btn,select[multiple].input-sm,.form-horizontal .form-group-sm select[multiple].form-control,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>.input-group-btn>select[multiple].btn{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control,.input-group-lg>.form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.input-group-lg>.input-group-addon{height:46px;line-height:1.33}.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,.form-horizontal .form-group-lg select.form-control,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,.input-group-lg>.input-group-btn>select.btn{height:46px;line-height:46px}textarea.input-lg,.form-horizontal .form-group-lg textarea.form-control,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,.input-group-lg>.input-group-btn>textarea.btn,select[multiple].input-lg,.form-horizontal .form-group-lg select[multiple].form-control,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>.input-group-btn>select[multiple].btn{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback,.form-horizontal .form-group-lg .form-control+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.form-horizontal .form-group-sm .form-control+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:before{content:\" \";display:table}.form-horizontal .form-group:after{content:\" \";display:table;clear:both}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled,.btn-default.disabled:hover,.btn-default.disabled:focus,.btn-default.disabled:active,.btn-default.disabled.active,.btn-default[disabled],.btn-default[disabled]:hover,.btn-default[disabled]:focus,.btn-default[disabled]:active,.btn-default[disabled].active,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default:hover,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled,.btn-primary.disabled:hover,.btn-primary.disabled:focus,.btn-primary.disabled:active,.btn-primary.disabled.active,.btn-primary[disabled],.btn-primary[disabled]:hover,.btn-primary[disabled]:focus,.btn-primary[disabled]:active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary:hover,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled,.btn-success.disabled:hover,.btn-success.disabled:focus,.btn-success.disabled:active,.btn-success.disabled.active,.btn-success[disabled],.btn-success[disabled]:hover,.btn-success[disabled]:focus,.btn-success[disabled]:active,.btn-success[disabled].active,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success:hover,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled,.btn-info.disabled:hover,.btn-info.disabled:focus,.btn-info.disabled:active,.btn-info.disabled.active,.btn-info[disabled],.btn-info[disabled]:hover,.btn-info[disabled]:focus,.btn-info[disabled]:active,.btn-info[disabled].active,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info:hover,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled,.btn-warning.disabled:hover,.btn-warning.disabled:focus,.btn-warning.disabled:active,.btn-warning.disabled.active,.btn-warning[disabled],.btn-warning[disabled]:hover,.btn-warning[disabled]:focus,.btn-warning[disabled]:active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning:hover,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled,.btn-danger.disabled:hover,.btn-danger.disabled:focus,.btn-danger.disabled:active,.btn-danger.disabled.active,.btn-danger[disabled],.btn-danger[disabled]:hover,.btn-danger[disabled]:focus,.btn-danger[disabled]:active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger:hover,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#428bca;font-weight:400;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:hover,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px}.btn-sm,.btn-xs{font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=\"submit\"].btn-block,input[type=\"reset\"].btn-block,input[type=\"button\"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=\"col-\"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon{white-space:nowrap}.input-group-addon,.input-group-btn{width:1%;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm,.form-horizontal .form-group-sm .input-group-addon.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.form-horizontal .form-group-lg .input-group-addon.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=\"radio\"],.input-group-addon input[type=\"checkbox\"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn,.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.4285714;text-decoration:none;color:#428bca;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>a:focus,.pagination>li>span:hover,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:hover,.pagination>.active>a:focus,.pagination>.active>span,.pagination>.active>span:hover,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open,.modal{overflow:hidden}.modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0);-webkit-transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5;min-height:16.4285714px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.4285714}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{content:\" \";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.clearfix:before,.clearfix:after{content:\" \";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.hljs{display:block;overflow-x:auto;padding:.5em;background:#002b36;color:#839496;-webkit-text-size-adjust:none}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-doctype,.hljs-pi,.lisp .hljs-string,.hljs-javadoc{color:#586e75}.hljs-keyword,.hljs-winutils,.method,.hljs-addition,.css .hljs-tag,.hljs-request,.hljs-status,.nginx .hljs-title{color:#859900}.hljs-number,.hljs-command,.hljs-string,.hljs-tag .hljs-value,.hljs-rules .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula,.hljs-regexp,.hljs-hexcolor,.hljs-link_url{color:#2aa198}.hljs-title,.hljs-localvars,.hljs-chunk,.hljs-decorator,.hljs-built_in,.hljs-identifier,.vhdl .hljs-literal,.hljs-id,.css .hljs-function{color:#268bd2}.hljs-attribute,.hljs-variable,.lisp .hljs-body,.smalltalk .hljs-number,.hljs-constant,.hljs-class .hljs-title,.hljs-parent,.hljs-type,.hljs-link_reference{color:#b58900}.hljs-preprocessor,.hljs-preprocessor .hljs-keyword,.hljs-pragma,.hljs-shebang,.hljs-symbol,.hljs-symbol .hljs-string,.diff .hljs-change,.hljs-special,.hljs-attr_selector,.hljs-subst,.hljs-cdata,.css .hljs-pseudo,.hljs-header{color:#cb4b16}.hljs-deletion,.hljs-important{color:#dc322f}.hljs-link_label{color:#6c71c4}.tex .hljs-formula{background:#073642}*,*:before,*:after{box-sizing:border-box}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}images{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd{font-size:1em}code,kbd,pre,samp{font-family:monospace,monospace}samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=\"button\"],input[type=\"reset\"],input[type=\"submit\"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=\"checkbox\"],input[type=\"radio\"]{box-sizing:border-box;padding:0}input[type=\"number\"]::-webkit-inner-spin-button,input[type=\"number\"]::-webkit-outer-spin-button{height:auto}input[type=\"search\"]{-webkit-appearance:textfield;box-sizing:content-box}input[type=\"search\"]::-webkit-search-cancel-button,input[type=\"search\"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}.debug{background-color:#ffc0cb!important}.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ir{background-color:transparent;border:0;overflow:hidden}.ir::before{content:'';display:block;height:150%;width:0}html{font-size:.875em;background:#fafafa;color:#373D49}html,body{font-family:Georgia,Cambria,serif;height:100%}body{font-size:1rem;font-weight:400;line-height:2rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}li{-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-left:1rem}li>ul,li>ol{margin-bottom:0}p{padding-top:.66001rem;-webkit-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;-moz-font-feature-settings:'kern' 1,'onum' 1,'liga' 1;font-feature-settings:'kern' 1,'onum' 1,'liga' 1;margin-top:0}p,pre{margin-bottom:1.33999rem}pre{font-size:1rem;padding:.66001rem 9.5px 9.5px;line-height:2rem;background:-webkit-linear-gradient(top,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background:linear-gradient(to bottom,#fff 0,#fff .75rem,#f5f7fa .75rem,#f5f7fa 2.75rem,#fff 2.75rem,#fff 4rem);background-size:100% 4rem;border-color:#D3DAEA}blockquote{margin:0}blockquote p{font-size:1rem;margin-bottom:.33999rem;font-style:italic;padding:.66001rem 1rem 1rem;border-left:3px solid #A0AABF}th,td{padding:12px}h1,h2,h3,h4,h5,h6{font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;-webkit-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;-moz-font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-feature-settings:'dlig' 1,'liga' 1,'lnum' 1,'kern' 1;font-style:normal;font-weight:600;margin-top:0}h1{line-height:3rem;font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h2,h3{line-height:3rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}a{cursor:pointer;color:#35D7BB;text-decoration:none}a:hover,a:focus{border-bottom-color:#35D7BB;color:#dff9f4}img{height:auto;max-width:100%}.g{display:block}.g:after{clear:both;content:'';display:table}.g-b{float:left;margin:0;width:100%}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--center{display:block;float:none;margin:0 auto}.g-b--right{float:right}.g-b--1of1{width:100%}.g-b--1of2,.g-b--2of4,.g-b--3of6,.g-b--4of8,.g-b--5of10,.g-b--6of12{width:50%}.g-b--1of3,.g-b--2of6,.g-b--4of12{width:33.333%}.g-b--2of3,.g-b--4of6,.g-b--8of12{width:66.666%}.g-b--1of4,.g-b--2of8,.g-b--3of12{width:25%}.g-b--3of4,.g-b--6of8,.g-b--9of12{width:75%}.g-b--1of5,.g-b--2of10{width:20%}.g-b--2of5,.g-b--4of10{width:40%}.g-b--3of5,.g-b--6of10{width:60%}.g-b--4of5,.g-b--8of10{width:80%}.g-b--1of6,.g-b--2of12{width:16.666%}.g-b--5of6,.g-b--10of12{width:83.333%}.g-b--1of8{width:12.5%}.g-b--3of8{width:37.5%}.g-b--5of8{width:62.5%}.g-b--7of8{width:87.5%}.g-b--1of10{width:10%}.g-b--3of10{width:30%}.g-b--7of10{width:70%}.g-b--9of10{width:90%}.g-b--1of12{width:8.333%}.g-b--5of12{width:41.666%}.g-b--7of12{width:58.333%}.g-b--11of12{width:91.666%}.g-b--push--1of1{margin-left:100%}.g-b--push--1of2,.g-b--push--2of4,.g-b--push--3of6,.g-b--push--4of8,.g-b--push--5of10,.g-b--push--6of12{margin-left:50%}.g-b--push--1of3,.g-b--push--2of6,.g-b--push--4of12{margin-left:33.333%}.g-b--push--2of3,.g-b--push--4of6,.g-b--push--8of12{margin-left:66.666%}.g-b--push--1of4,.g-b--push--2of8,.g-b--push--3of12{margin-left:25%}.g-b--push--3of4,.g-b--push--6of8,.g-b--push--9of12{margin-left:75%}.g-b--push--1of5,.g-b--push--2of10{margin-left:20%}.g-b--push--2of5,.g-b--push--4of10{margin-left:40%}.g-b--push--3of5,.g-b--push--6of10{margin-left:60%}.g-b--push--4of5,.g-b--push--8of10{margin-left:80%}.g-b--push--1of6,.g-b--push--2of12{margin-left:16.666%}.g-b--push--5of6,.g-b--push--10of12{margin-left:83.333%}.g-b--push--1of8{margin-left:12.5%}.g-b--push--3of8{margin-left:37.5%}.g-b--push--5of8{margin-left:62.5%}.g-b--push--7of8{margin-left:87.5%}.g-b--push--1of10{margin-left:10%}.g-b--push--3of10{margin-left:30%}.g-b--push--7of10{margin-left:70%}.g-b--push--9of10{margin-left:90%}.g-b--push--1of12{margin-left:8.333%}.g-b--push--5of12{margin-left:41.666%}.g-b--push--7of12{margin-left:58.333%}.g-b--push--11of12{margin-left:91.666%}.g-b--pull--1of1{margin-right:100%}.g-b--pull--1of2,.g-b--pull--2of4,.g-b--pull--3of6,.g-b--pull--4of8,.g-b--pull--5of10,.g-b--pull--6of12{margin-right:50%}.g-b--pull--1of3,.g-b--pull--2of6,.g-b--pull--4of12{margin-right:33.333%}.g-b--pull--2of3,.g-b--pull--4of6,.g-b--pull--8of12{margin-right:66.666%}.g-b--pull--1of4,.g-b--pull--2of8,.g-b--pull--3of12{margin-right:25%}.g-b--pull--3of4,.g-b--pull--6of8,.g-b--pull--9of12{margin-right:75%}.g-b--pull--1of5,.g-b--pull--2of10{margin-right:20%}.g-b--pull--2of5,.g-b--pull--4of10{margin-right:40%}.g-b--pull--3of5,.g-b--pull--6of10{margin-right:60%}.g-b--pull--4of5,.g-b--pull--8of10{margin-right:80%}.g-b--pull--1of6,.g-b--pull--2of12{margin-right:16.666%}.g-b--pull--5of6,.g-b--pull--10of12{margin-right:83.333%}.g-b--pull--1of8{margin-right:12.5%}.g-b--pull--3of8{margin-right:37.5%}.g-b--pull--5of8{margin-right:62.5%}.g-b--pull--7of8{margin-right:87.5%}.g-b--pull--1of10{margin-right:10%}.g-b--pull--3of10{margin-right:30%}.g-b--pull--7of10{margin-right:70%}.g-b--pull--9of10{margin-right:90%}.g-b--pull--1of12{margin-right:8.333%}.g-b--pull--5of12{margin-right:41.666%}.g-b--pull--7of12{margin-right:58.333%}.g-b--pull--11of12{margin-right:91.666%}.splashscreen{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#373D49;z-index:22}.splashscreen-dillinger{width:260px;height:auto;display:block;margin:0 auto;padding-bottom:3rem}.splashscreen p{font-size:1.25rem;padding-top:.56251rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;text-align:center;max-width:500px;margin:0 auto;color:#FFF}.sp-center{position:relative;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);top:50%}.open-menu>.wrapper{overflow-x:hidden}.page{margin:0 auto;position:relative;top:0;left:0;width:100%;height:100%;z-index:2;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;background-color:#fff;padding-top:51px;will-change:left}.open-menu .page{left:270px}.title{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;padding-left:16px;padding-right:16px;margin-top:1rem}.split-preview .title{padding-left:0}.title-document{line-height:1rem;font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem;font-weight:400;font-family:\"Ubuntu Mono\",Monaco;color:#373D49;padding-left:16px;padding-right:16px;width:80%;min-width:300px;outline:0;border:none}.icon{display:block;margin:0 auto;width:36px;height:36px;border-radius:3px;text-align:center}.icon svg{display:inline-block;margin-left:auto;margin-right:auto}.icon-preview{background-color:#373D49;line-height:40px}.icon-preview svg{width:19px;height:12px}.icon-settings{background-color:#373D49;line-height:44px}.icon-settings svg{width:18px;height:18px}.icon-link{width:16px;height:16px;line-height:1;margin-right:24px;text-align:right}.navbar{background-color:#373D49;height:51px;width:100%;position:fixed;top:0;left:0;z-index:6;-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;will-change:left}.navbar:after{content:\"\";display:table;clear:both}.open-menu .navbar{left:270px}.navbar-brand{float:left;margin:0 0 0 24px;padding:0;line-height:42px}.navbar-brand svg{width:85px;height:11px}.nav-left{float:left}.nav-right{float:right}.nav-sidebar{width:100%}.menu{list-style:none;margin:0;padding:0}.menu a{border:0;color:#A0AABF;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;outline:none;text-transform:uppercase}.menu a:hover{color:#35D7BB}.menu .menu-item{border:0;display:none;float:left;margin:0;position:relative}.menu .menu-item>a{display:block;font-size:12px;height:51px;letter-spacing:1px;line-height:51px;padding:0 24px}.menu .menu-item--settings,.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar,.menu .menu-item--link-unlink.in-sidebar,.menu .menu-item--documents.in-sidebar{display:block}.menu .menu-item--documents{padding-bottom:1rem}.menu .menu-item.open>a{background-color:#1D212A}.menu .menu-item-icon>a{height:auto;padding:0}.menu .menu-item-icon:hover>a{background-color:transparent}.menu .menu-link.open i{background-color:#1D212A}.menu .menu-link.open g{fill:#35D7BB}.menu .menu-link-preview,.menu .menu-link-settings{margin-top:8px;width:51px}.menu-sidebar{width:100%}.menu-sidebar .menu-item{float:none;margin-bottom:1px;width:100%}.menu-sidebar .menu-item.open>a{background-color:#373D49}.menu-sidebar .open .caret{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.menu-sidebar>.menu-item:hover .dropdown a,.menu-sidebar>.menu-item:hover .settings a{background-color:transparent}.menu-sidebar .menu-link{background-color:#373D49;font-weight:600}.menu-sidebar .menu-link:after{content:\"\";display:table;clear:both}.menu-sidebar .menu-link>span{float:left}.menu-sidebar .menu-link>.caret{float:right;text-align:right;top:22px}.menu-sidebar .dropdown,.menu-sidebar .settings{background-color:transparent;position:static;width:100%}.dropdown{position:absolute;right:0;top:51px;width:188px}.dropdown,.settings{display:none;background-color:#1D212A}.dropdown{padding:0}.dropdown,.settings,.sidebar-list{list-style:none;margin:0}.sidebar-list{padding:0}.dropdown li{margin:32px 0;padding:0 0 0 32px}.dropdown li,.settings li{line-height:1}.sidebar-list li{line-height:1;margin:32px 0;padding:0 0 0 32px}.dropdown a{color:#D0D6E2}.dropdown a,.settings a,.sidebar-list a{display:block;text-transform:none}.sidebar-list a{color:#D0D6E2}.dropdown a:after,.settings a:after,.sidebar-list a:after{content:\"\";display:table;clear:both}.dropdown .icon,.settings .icon,.sidebar-list .icon{float:right}.open .dropdown,.open .settings,.open .sidebar-list{display:block}.open .dropdown.collapse,.open .collapse.settings,.open .sidebar-list.collapse{display:none}.open .dropdown.collapse.in,.open .collapse.in.settings,.open .sidebar-list.collapse.in{display:block}.dropdown .unlinked .icon,.settings .unlinked .icon,.sidebar-list .unlinked .icon{opacity:.3}.dropdown.documents li,.documents.settings li,.sidebar-list.documents li{background-image:url(\"../img/icons/file.svg\");background-position:240px center;background-repeat:no-repeat;background-size:14px 16px;padding:3px 32px}.dropdown.documents li.octocat,.documents.settings li.octocat,.sidebar-list.documents li.octocat{background-image:url(\"../img/icons/octocat.svg\");background-position:234px center;background-size:24px 24px}.dropdown.documents li:last-child,.documents.settings li:last-child,.sidebar-list.documents li:last-child{margin-bottom:1rem}.dropdown.documents li.active a,.documents.settings li.active a,.sidebar-list.documents li.active a{color:#35D7BB}.settings{position:fixed;top:67px;right:16px;border-radius:3px;width:288px;background-color:#373D49;padding:16px;z-index:7}.show-settings .settings{display:block}.settings .has-checkbox{float:left}.settings a{font-size:1.25rem;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;-webkit-font-smoothing:antialiased;line-height:28px;color:#D0D6E2}.settings a:after{content:\"\";display:table;clear:both}.settings a:hover{color:#35D7BB}.settings li{border-bottom:1px solid #4F535B;margin:0;padding:16px 0}.settings li:last-child{border-bottom:none}.brand{border:none;display:block}.brand:hover g{fill:#35D7BB}.toggle{display:block;float:left;height:16px;padding:25px 16px 26px;width:40px}.toggle span:after,.toggle span:before{content:'';left:0;position:absolute;top:-6px}.toggle span:after{top:6px}.toggle span{display:block;position:relative}.toggle span,.toggle span:after,.toggle span:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#D3DAEA;height:2px;-webkit-transition:all .3s;transition:all .3s;width:20px}.open-menu .toggle span{background-color:transparent}.open-menu .toggle span:before{-webkit-transform:rotate(45deg)translate(3px,3px);-ms-transform:rotate(45deg)translate(3px,3px);transform:rotate(45deg)translate(3px,3px)}.open-menu .toggle span:after{-webkit-transform:rotate(-45deg)translate(5px,-6px);-ms-transform:rotate(-45deg)translate(5px,-6px);transform:rotate(-45deg)translate(5px,-6px)}.caret{display:inline-block;width:0;height:0;margin-left:6px;vertical-align:middle;position:relative;top:-1px;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.sidebar{overflow:auto;height:100%;padding-right:15px;padding-bottom:15px;width:285px}.sidebar-wrapper{-webkit-overflow-scrolling:touch;background-color:#2B2F36;left:0;height:100%;overflow-y:hidden;position:fixed;top:0;width:285px;z-index:1}.sidebar-branding{width:160px;padding:0;margin:16px auto}.header{border-bottom:1px solid #E8E8E8;position:relative}.words{line-height:1rem;font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem;font-weight:500;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;color:#A0AABF;letter-spacing:1px;text-transform:uppercase;z-index:5;position:absolute;right:16px;top:0}.words span{color:#000}.btn{text-align:center;display:inline-block;width:100%;text-transform:uppercase;font-weight:600;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-size:14px;text-shadow:0 1px 0 #1b8b77;padding:16px 24px;background-color:#35D7BB;border-radius:3px;margin:0 auto 16px;line-height:1;color:#fff;-webkit-transition:all .15s linear;transition:all .15s linear;-webkit-font-smoothing:antialiased}.btn--new,.btn--save{display:block;width:238px}.btn--new:hover,.btn--new:focus,.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 3px #24b59c;text-shadow:0 1px 0 #24b59c}.btn--save{background-color:#4A5261;text-shadow:0 1px 1px #1e2127}.btn--save:hover,.btn--save:focus{color:#fff;border-bottom-color:transparent;box-shadow:0 1px 5px #08090a;text-shadow:none}.btn--delete{display:block;width:238px;background-color:transparent;font-size:12px;text-shadow:none}.btn--delete:hover,.btn--delete:focus{color:#fff;border-bottom-color:transparent;text-shadow:0 1px 0 #08090a;opacity:.8}.btn--ok,.btn--close{border-top:0;background-color:#4A5261;text-shadow:0 1px 0 #08090a;margin:0}.btn--ok:hover,.btn--ok:focus,.btn--close:hover,.btn--close:focus{color:#fff;background-color:#292d36;text-shadow:none}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:rgba(55,61,73,.8);-webkit-transition:all .25s ease-in-out;transition:all .25s ease-in-out;-webkit-transition-timing-function:ease-out;transition-timing-function:ease-out;will-change:left,opacity,visibility;z-index:5;opacity:0;visibility:hidden}.show-settings .overlay{visibility:visible;opacity:1}.switch{float:right;line-height:1}.switch input{display:none}.switch small{display:inline-block;cursor:pointer;padding:0 24px 0 0;-webkit-transition:all ease .2s;transition:all ease .2s;background-color:#2B2F36;border-color:#2B2F36}.switch small,.switch small:before{border-radius:30px;box-shadow:inset 0 0 2px 0 #14171F}.switch small:before{display:block;content:'';width:28px;height:28px;background:#fff}.switch.checked small{padding-right:0;padding-left:24px;background-color:#35D7BB;box-shadow:none}.modal--dillinger.about .modal-dialog{font-size:1.25rem;max-width:500px}.modal--dillinger .modal-dialog{max-width:600px;width:auto;margin:5rem auto}.modal--dillinger .modal-content{background:#373D49;border-radius:3px;box-shadow:0 2px 5px 0 #2C3B59;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;padding:2rem}.modal--dillinger ul{list-style-type:disc;margin:1rem 0;padding:0 0 0 1rem}.modal--dillinger li{padding:0;margin:0}.modal--dillinger .modal-header{border:0;padding:0}.modal--dillinger .modal-body{padding:0}.modal--dillinger .modal-footer{border:0;padding:0}.modal--dillinger .close{color:#fff;opacity:1}.modal-backdrop{background-color:#373D49}.pagination--dillinger{padding:0!important;margin:1.5rem 0!important;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-align-content:stretch;-ms-flex-line-pack:stretch;align-content:stretch}.pagination--dillinger,.pagination--dillinger li{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.pagination--dillinger li{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.pagination--dillinger li:first-child>a,.pagination--dillinger li.disabled>a,.pagination--dillinger li.disabled>a:hover,.pagination--dillinger li.disabled>a:focus,.pagination--dillinger li>a{background-color:transparent;border-color:#4F535B;border-right-color:transparent}.pagination--dillinger li.active>a,.pagination--dillinger li.active>a:hover,.pagination--dillinger li.active>a:focus{border-color:#4A5261;background-color:#4A5261;color:#fff}.pagination--dillinger li>a{float:none;color:#fff;width:100%;display:block;text-align:center;margin:0;border-right-color:transparent;padding:6px}.pagination--dillinger li>a:hover,.pagination--dillinger li>a:focus{border-color:#35D7BB;background-color:#35D7BB;color:#fff}.pagination--dillinger li:last-child a{border-color:#4F535B}.pagination--dillinger li:first-child a{border-right-color:transparent}.diNotify{position:absolute;z-index:9999;left:0;right:0;top:0;margin:0 auto;max-width:400px;text-align:center;-webkit-transition:top .5s ease-in-out,opacity .5s ease-in-out;transition:top .5s ease-in-out,opacity .5s ease-in-out;visibility:hidden}.diNotify-body{-webkit-font-smoothing:antialiased;background-color:#35D7BB;background:#666E7F;border-radius:3px;color:#fff;font-family:\"Source Sans Pro\",\"Helvetica Neue\",Helvetica,Arial,sans-serif;font-weight:400;overflow:hidden;padding:1rem 2rem .5rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:baseline;-webkit-align-items:baseline;-ms-flex-align:baseline;align-items:baseline;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.diNotify-icon{display:block;width:16px;height:16px;line-height:16px;position:relative;top:3px}.diNotify-message{padding-left:1rem}.zen-wrapper{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;z-index:10;background-color:#FFF;opacity:0;-webkit-transition:opacity .25s ease-in-out;transition:opacity .25s ease-in-out}.zen-wrapper.on{opacity:1}.enter-zen-mode{background-image:url(\"../img/icons/enter-zen.svg\");right:.5rem;top:.313rem;display:none}.enter-zen-mode,.close-zen-mode{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;background-repeat:no-repeat;width:32px;height:32px;display:block;position:absolute}.close-zen-mode{background-image:url(\"../img/icons/exit-zen.svg\");right:1rem;top:1rem}.zen-page{position:relative;top:0;bottom:0;z-index:11;height:100%;width:100%}#zen{font-size:1.25rem;width:300px;height:80%;margin:0 auto;position:relative;top:10%}#zen:before,#zen:after{content:\"\";position:absolute;height:10%;width:100%;z-index:12;pointer-events:none}.split{overflow:scroll;padding:0!important}.split-editor{padding-left:0;padding-right:0;position:relative}.show-preview .split-editor{display:none}.split-preview{background-color:#fff;display:none;top:0;position:relative;z-index:4}.show-preview .split-preview{display:block}#editor{font-size:1rem;font-family:\"Ubuntu Mono\",Monaco;font-weight:400;line-height:2rem;width:100%;height:100%}#editor .ace_gutter{-webkit-font-smoothing:antialiased}.editor-header{width:50%;float:left;border-bottom:1px solid #E8E8E8;position:relative}.editor-header--first{border-right:1px solid #E8E8E8}.editor-header .title{display:inline-block}#preview{padding:10px}#preview a{color:#A0AABF;text-decoration:underline}.sr-only{visibility:hidden;text-overflow:110%;overflow:hidden;top:-100px;position:absolute}.mnone{margin:0!important}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=\"radio\"],.form-inline .checkbox input[type=\"checkbox\"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}.form-horizontal .form-group-lg .control-label{padding-top:14.3px}.form-horizontal .form-group-sm .control-label{padding-top:6px}.modal-dialog{width:600px;margin:30px auto}.modal-content{box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}@media screen and (min-width:27.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--m1of1{width:100%}.g-b--m1of2,.g-b--m2of4,.g-b--m3of6,.g-b--m4of8,.g-b--m5of10,.g-b--m6of12{width:50%}.g-b--m1of3,.g-b--m2of6,.g-b--m4of12{width:33.333%}.g-b--m2of3,.g-b--m4of6,.g-b--m8of12{width:66.666%}.g-b--m1of4,.g-b--m2of8,.g-b--m3of12{width:25%}.g-b--m3of4,.g-b--m6of8,.g-b--m9of12{width:75%}.g-b--m1of5,.g-b--m2of10{width:20%}.g-b--m2of5,.g-b--m4of10{width:40%}.g-b--m3of5,.g-b--m6of10{width:60%}.g-b--m4of5,.g-b--m8of10{width:80%}.g-b--m1of6,.g-b--m2of12{width:16.666%}.g-b--m5of6,.g-b--m10of12{width:83.333%}.g-b--m1of8{width:12.5%}.g-b--m3of8{width:37.5%}.g-b--m5of8{width:62.5%}.g-b--m7of8{width:87.5%}.g-b--m1of10{width:10%}.g-b--m3of10{width:30%}.g-b--m7of10{width:70%}.g-b--m9of10{width:90%}.g-b--m1of12{width:8.333%}.g-b--m5of12{width:41.666%}.g-b--m7of12{width:58.333%}.g-b--m11of12{width:91.666%}.g-b--push--m1of1{margin-left:100%}.g-b--push--m1of2,.g-b--push--m2of4,.g-b--push--m3of6,.g-b--push--m4of8,.g-b--push--m5of10,.g-b--push--m6of12{margin-left:50%}.g-b--push--m1of3,.g-b--push--m2of6,.g-b--push--m4of12{margin-left:33.333%}.g-b--push--m2of3,.g-b--push--m4of6,.g-b--push--m8of12{margin-left:66.666%}.g-b--push--m1of4,.g-b--push--m2of8,.g-b--push--m3of12{margin-left:25%}.g-b--push--m3of4,.g-b--push--m6of8,.g-b--push--m9of12{margin-left:75%}.g-b--push--m1of5,.g-b--push--m2of10{margin-left:20%}.g-b--push--m2of5,.g-b--push--m4of10{margin-left:40%}.g-b--push--m3of5,.g-b--push--m6of10{margin-left:60%}.g-b--push--m4of5,.g-b--push--m8of10{margin-left:80%}.g-b--push--m1of6,.g-b--push--m2of12{margin-left:16.666%}.g-b--push--m5of6,.g-b--push--m10of12{margin-left:83.333%}.g-b--push--m1of8{margin-left:12.5%}.g-b--push--m3of8{margin-left:37.5%}.g-b--push--m5of8{margin-left:62.5%}.g-b--push--m7of8{margin-left:87.5%}.g-b--push--m1of10{margin-left:10%}.g-b--push--m3of10{margin-left:30%}.g-b--push--m7of10{margin-left:70%}.g-b--push--m9of10{margin-left:90%}.g-b--push--m1of12{margin-left:8.333%}.g-b--push--m5of12{margin-left:41.666%}.g-b--push--m7of12{margin-left:58.333%}.g-b--push--m11of12{margin-left:91.666%}.g-b--pull--m1of1{margin-right:100%}.g-b--pull--m1of2,.g-b--pull--m2of4,.g-b--pull--m3of6,.g-b--pull--m4of8,.g-b--pull--m5of10,.g-b--pull--m6of12{margin-right:50%}.g-b--pull--m1of3,.g-b--pull--m2of6,.g-b--pull--m4of12{margin-right:33.333%}.g-b--pull--m2of3,.g-b--pull--m4of6,.g-b--pull--m8of12{margin-right:66.666%}.g-b--pull--m1of4,.g-b--pull--m2of8,.g-b--pull--m3of12{margin-right:25%}.g-b--pull--m3of4,.g-b--pull--m6of8,.g-b--pull--m9of12{margin-right:75%}.g-b--pull--m1of5,.g-b--pull--m2of10{margin-right:20%}.g-b--pull--m2of5,.g-b--pull--m4of10{margin-right:40%}.g-b--pull--m3of5,.g-b--pull--m6of10{margin-right:60%}.g-b--pull--m4of5,.g-b--pull--m8of10{margin-right:80%}.g-b--pull--m1of6,.g-b--pull--m2of12{margin-right:16.666%}.g-b--pull--m5of6,.g-b--pull--m10of12{margin-right:83.333%}.g-b--pull--m1of8{margin-right:12.5%}.g-b--pull--m3of8{margin-right:37.5%}.g-b--pull--m5of8{margin-right:62.5%}.g-b--pull--m7of8{margin-right:87.5%}.g-b--pull--m1of10{margin-right:10%}.g-b--pull--m3of10{margin-right:30%}.g-b--pull--m7of10{margin-right:70%}.g-b--pull--m9of10{margin-right:90%}.g-b--pull--m1of12{margin-right:8.333%}.g-b--pull--m5of12{margin-right:41.666%}.g-b--pull--m7of12{margin-right:58.333%}.g-b--pull--m11of12{margin-right:91.666%}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:400px}#editor{font-size:1rem}}@media screen and (min-width:46.25em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--t1of1{width:100%}.g-b--t1of2,.g-b--t2of4,.g-b--t3of6,.g-b--t4of8,.g-b--t5of10,.g-b--t6of12{width:50%}.g-b--t1of3,.g-b--t2of6,.g-b--t4of12{width:33.333%}.g-b--t2of3,.g-b--t4of6,.g-b--t8of12{width:66.666%}.g-b--t1of4,.g-b--t2of8,.g-b--t3of12{width:25%}.g-b--t3of4,.g-b--t6of8,.g-b--t9of12{width:75%}.g-b--t1of5,.g-b--t2of10{width:20%}.g-b--t2of5,.g-b--t4of10{width:40%}.g-b--t3of5,.g-b--t6of10{width:60%}.g-b--t4of5,.g-b--t8of10{width:80%}.g-b--t1of6,.g-b--t2of12{width:16.666%}.g-b--t5of6,.g-b--t10of12{width:83.333%}.g-b--t1of8{width:12.5%}.g-b--t3of8{width:37.5%}.g-b--t5of8{width:62.5%}.g-b--t7of8{width:87.5%}.g-b--t1of10{width:10%}.g-b--t3of10{width:30%}.g-b--t7of10{width:70%}.g-b--t9of10{width:90%}.g-b--t1of12{width:8.333%}.g-b--t5of12{width:41.666%}.g-b--t7of12{width:58.333%}.g-b--t11of12{width:91.666%}.g-b--push--t1of1{margin-left:100%}.g-b--push--t1of2,.g-b--push--t2of4,.g-b--push--t3of6,.g-b--push--t4of8,.g-b--push--t5of10,.g-b--push--t6of12{margin-left:50%}.g-b--push--t1of3,.g-b--push--t2of6,.g-b--push--t4of12{margin-left:33.333%}.g-b--push--t2of3,.g-b--push--t4of6,.g-b--push--t8of12{margin-left:66.666%}.g-b--push--t1of4,.g-b--push--t2of8,.g-b--push--t3of12{margin-left:25%}.g-b--push--t3of4,.g-b--push--t6of8,.g-b--push--t9of12{margin-left:75%}.g-b--push--t1of5,.g-b--push--t2of10{margin-left:20%}.g-b--push--t2of5,.g-b--push--t4of10{margin-left:40%}.g-b--push--t3of5,.g-b--push--t6of10{margin-left:60%}.g-b--push--t4of5,.g-b--push--t8of10{margin-left:80%}.g-b--push--t1of6,.g-b--push--t2of12{margin-left:16.666%}.g-b--push--t5of6,.g-b--push--t10of12{margin-left:83.333%}.g-b--push--t1of8{margin-left:12.5%}.g-b--push--t3of8{margin-left:37.5%}.g-b--push--t5of8{margin-left:62.5%}.g-b--push--t7of8{margin-left:87.5%}.g-b--push--t1of10{margin-left:10%}.g-b--push--t3of10{margin-left:30%}.g-b--push--t7of10{margin-left:70%}.g-b--push--t9of10{margin-left:90%}.g-b--push--t1of12{margin-left:8.333%}.g-b--push--t5of12{margin-left:41.666%}.g-b--push--t7of12{margin-left:58.333%}.g-b--push--t11of12{margin-left:91.666%}.g-b--pull--t1of1{margin-right:100%}.g-b--pull--t1of2,.g-b--pull--t2of4,.g-b--pull--t3of6,.g-b--pull--t4of8,.g-b--pull--t5of10,.g-b--pull--t6of12{margin-right:50%}.g-b--pull--t1of3,.g-b--pull--t2of6,.g-b--pull--t4of12{margin-right:33.333%}.g-b--pull--t2of3,.g-b--pull--t4of6,.g-b--pull--t8of12{margin-right:66.666%}.g-b--pull--t1of4,.g-b--pull--t2of8,.g-b--pull--t3of12{margin-right:25%}.g-b--pull--t3of4,.g-b--pull--t6of8,.g-b--pull--t9of12{margin-right:75%}.g-b--pull--t1of5,.g-b--pull--t2of10{margin-right:20%}.g-b--pull--t2of5,.g-b--pull--t4of10{margin-right:40%}.g-b--pull--t3of5,.g-b--pull--t6of10{margin-right:60%}.g-b--pull--t4of5,.g-b--pull--t8of10{margin-right:80%}.g-b--pull--t1of6,.g-b--pull--t2of12{margin-right:16.666%}.g-b--pull--t5of6,.g-b--pull--t10of12{margin-right:83.333%}.g-b--pull--t1of8{margin-right:12.5%}.g-b--pull--t3of8{margin-right:37.5%}.g-b--pull--t5of8{margin-right:62.5%}.g-b--pull--t7of8{margin-right:87.5%}.g-b--pull--t1of10{margin-right:10%}.g-b--pull--t3of10{margin-right:30%}.g-b--pull--t7of10{margin-right:70%}.g-b--pull--t9of10{margin-right:90%}.g-b--pull--t1of12{margin-right:8.333%}.g-b--pull--t5of12{margin-right:41.666%}.g-b--pull--t7of12{margin-right:58.333%}.g-b--pull--t11of12{margin-right:91.666%}.splashscreen-dillinger{width:500px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--save-to,.menu .menu-item--import-from{display:block}.menu .menu-item--preview,.menu .menu-item--save-to.in-sidebar,.menu .menu-item--import-from.in-sidebar{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog{font-size:1.25rem}.enter-zen-mode{display:block}.close-zen-mode{right:3rem;top:3rem}#zen{font-size:1.25rem;width:500px}.split-editor{border-right:1px solid #E8E8E8;float:left;height:calc(100vh - 172px);-webkit-overflow-scrolling:touch;padding-right:16px;width:50%}.show-preview .split-editor{display:block}.split-preview{display:block;float:right;height:calc(100vh - 172px);-webkit-overflow-scrolling:touch;position:relative;top:0;width:50%}#editor{font-size:1rem}}@media screen and (min-width:62.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.g{margin-left:-16px;margin-right:-16px}.g-b{padding-left:16px;padding-right:16px}.g-b--d1of1{width:100%}.g-b--d1of2,.g-b--d2of4,.g-b--d3of6,.g-b--d4of8,.g-b--d5of10,.g-b--d6of12{width:50%}.g-b--d1of3,.g-b--d2of6,.g-b--d4of12{width:33.333%}.g-b--d2of3,.g-b--d4of6,.g-b--d8of12{width:66.666%}.g-b--d1of4,.g-b--d2of8,.g-b--d3of12{width:25%}.g-b--d3of4,.g-b--d6of8,.g-b--d9of12{width:75%}.g-b--d1of5,.g-b--d2of10{width:20%}.g-b--d2of5,.g-b--d4of10{width:40%}.g-b--d3of5,.g-b--d6of10{width:60%}.g-b--d4of5,.g-b--d8of10{width:80%}.g-b--d1of6,.g-b--d2of12{width:16.666%}.g-b--d5of6,.g-b--d10of12{width:83.333%}.g-b--d1of8{width:12.5%}.g-b--d3of8{width:37.5%}.g-b--d5of8{width:62.5%}.g-b--d7of8{width:87.5%}.g-b--d1of10{width:10%}.g-b--d3of10{width:30%}.g-b--d7of10{width:70%}.g-b--d9of10{width:90%}.g-b--d1of12{width:8.333%}.g-b--d5of12{width:41.666%}.g-b--d7of12{width:58.333%}.g-b--d11of12{width:91.666%}.g-b--push--d1of1{margin-left:100%}.g-b--push--d1of2,.g-b--push--d2of4,.g-b--push--d3of6,.g-b--push--d4of8,.g-b--push--d5of10,.g-b--push--d6of12{margin-left:50%}.g-b--push--d1of3,.g-b--push--d2of6,.g-b--push--d4of12{margin-left:33.333%}.g-b--push--d2of3,.g-b--push--d4of6,.g-b--push--d8of12{margin-left:66.666%}.g-b--push--d1of4,.g-b--push--d2of8,.g-b--push--d3of12{margin-left:25%}.g-b--push--d3of4,.g-b--push--d6of8,.g-b--push--d9of12{margin-left:75%}.g-b--push--d1of5,.g-b--push--d2of10{margin-left:20%}.g-b--push--d2of5,.g-b--push--d4of10{margin-left:40%}.g-b--push--d3of5,.g-b--push--d6of10{margin-left:60%}.g-b--push--d4of5,.g-b--push--d8of10{margin-left:80%}.g-b--push--d1of6,.g-b--push--d2of12{margin-left:16.666%}.g-b--push--d5of6,.g-b--push--d10of12{margin-left:83.333%}.g-b--push--d1of8{margin-left:12.5%}.g-b--push--d3of8{margin-left:37.5%}.g-b--push--d5of8{margin-left:62.5%}.g-b--push--d7of8{margin-left:87.5%}.g-b--push--d1of10{margin-left:10%}.g-b--push--d3of10{margin-left:30%}.g-b--push--d7of10{margin-left:70%}.g-b--push--d9of10{margin-left:90%}.g-b--push--d1of12{margin-left:8.333%}.g-b--push--d5of12{margin-left:41.666%}.g-b--push--d7of12{margin-left:58.333%}.g-b--push--d11of12{margin-left:91.666%}.g-b--pull--d1of1{margin-right:100%}.g-b--pull--d1of2,.g-b--pull--d2of4,.g-b--pull--d3of6,.g-b--pull--d4of8,.g-b--pull--d5of10,.g-b--pull--d6of12{margin-right:50%}.g-b--pull--d1of3,.g-b--pull--d2of6,.g-b--pull--d4of12{margin-right:33.333%}.g-b--pull--d2of3,.g-b--pull--d4of6,.g-b--pull--d8of12{margin-right:66.666%}.g-b--pull--d1of4,.g-b--pull--d2of8,.g-b--pull--d3of12{margin-right:25%}.g-b--pull--d3of4,.g-b--pull--d6of8,.g-b--pull--d9of12{margin-right:75%}.g-b--pull--d1of5,.g-b--pull--d2of10{margin-right:20%}.g-b--pull--d2of5,.g-b--pull--d4of10{margin-right:40%}.g-b--pull--d3of5,.g-b--pull--d6of10{margin-right:60%}.g-b--pull--d4of5,.g-b--pull--d8of10{margin-right:80%}.g-b--pull--d1of6,.g-b--pull--d2of12{margin-right:16.666%}.g-b--pull--d5of6,.g-b--pull--d10of12{margin-right:83.333%}.g-b--pull--d1of8{margin-right:12.5%}.g-b--pull--d3of8{margin-right:37.5%}.g-b--pull--d5of8{margin-right:62.5%}.g-b--pull--d7of8{margin-right:87.5%}.g-b--pull--d1of10{margin-right:10%}.g-b--pull--d3of10{margin-right:30%}.g-b--pull--d7of10{margin-right:70%}.g-b--pull--d9of10{margin-right:90%}.g-b--pull--d1of12{margin-right:8.333%}.g-b--pull--d5of12{margin-right:41.666%}.g-b--pull--d7of12{margin-right:58.333%}.g-b--pull--d11of12{margin-right:91.666%}.splashscreen-dillinger{width:700px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{font-size:1.25rem;margin-bottom:.89999rem;padding-top:.10001rem}.menu .menu-item--export-as{display:block}.menu .menu-item--preview{display:none}.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#zen{width:700px}#editor{font-size:1rem}}@media screen and (min-width:87.5em){html{font-size:.875em}body{font-size:1rem}ul,ol{margin-bottom:.83999rem;padding-top:.16001rem}p{padding-top:.66001rem}p,pre{margin-bottom:1.33999rem}pre,blockquote p{font-size:1rem;padding-top:.66001rem}blockquote p{margin-bottom:.33999rem}h1{font-size:2.0571429rem;margin-bottom:.21999rem;padding-top:.78001rem}h2{font-size:1.953125rem;margin-bottom:.1835837rem;padding-top:.8164163rem}h3{font-size:1.6457143rem;margin-bottom:.07599rem;padding-top:.92401rem}h4{font-size:1.5625rem;margin-bottom:.546865rem;padding-top:.453135rem}h5{font-size:1.25rem;margin-bottom:-.56251rem;padding-top:.56251rem}h6{font-size:1rem;margin-bottom:-.65001rem;padding-top:.65001rem}.splashscreen-dillinger{width:800px}.splashscreen p{font-size:1.25rem;margin-bottom:1.43749rem;padding-top:.56251rem}.title{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.title-document{margin-bottom:.89999rem;padding-top:.10001rem}.title-document,.settings a{font-size:1.25rem}.words{font-size:.8rem;margin-bottom:.77999rem;padding-top:.22001rem}.modal--dillinger.about .modal-dialog,#zen{font-size:1.25rem}#editor{font-size:1rem}}@media screen and (max-width:46.1875em){.editor-header{display:none}.editor-header--first{display:block;width:100%}}</style></head><body id=\"preview\">\n<p>The not so obvious keyboard shortcuts:</p>\n<hr>\n<table>\n<thead>\n<tr>\n<th>Action</th>\n<th>Windows</th>\n<th>Mac</th>\n<th>KDE</th>\n<th>Gnome</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Quit</td>\n<td><code>Ctrl+Q</code></td>\n<td>same</td>\n<td>same</td>\n<td>same</td>\n</tr>\n<tr>\n<td>Focus search bar</td>\n<td><code>Ctrl+F</code></td>\n<td>same</td>\n<td>same</td>\n<td>same</td>\n</tr>\n<tr>\n<td>Next view</td>\n<td><code>Ctrl+Tab</code>, <code>Forward</code>, <code>Ctrl+F6</code></td>\n<td><code>Ctrl+}</code>, <code>Forward</code>, <code>Ctrl+Tab</code></td>\n<td><code>Ctrl+Tab</code>, <code>Forward</code>, <code>Ctrl+Comma</code></td>\n<td><code>Ctrl+Tab</code>, <code>Forward</code></td>\n</tr>\n<tr>\n<td>Previous view</td>\n<td><code>Ctrl+Shift+Tab</code>, <code>Back</code>, <code>Ctrl+Shift+F6</code></td>\n<td><code>Ctrl+{</code>, <code>Back</code>, <code>Ctrl+Shift+Tab</code></td>\n<td><code>Ctrl+Shift+Tab</code>, <code>Back</code>, <code>Ctrl+Period</code></td>\n<td><code>Ctrl+Shift+Tab</code>, <code>Back</code></td>\n</tr>\n<tr>\n<td>Next in search history</td>\n<td><code>Alt+Right</code>, <code>Shift+Backspace</code></td>\n<td><code>Ctrl+]</code></td>\n<td><code>Alt+Right</code></td>\n<td><code>Alt+Right</code></td>\n</tr>\n<tr>\n<td>Previous in search history</td>\n<td><code>Alt+Left</code>, <code>Backspace</code></td>\n<td><code>Ctrl+[</code></td>\n<td><code>Alt+Left</code></td>\n<td><code>Alt+Left</code></td>\n</tr>\n<tr>\n<td>Help</td>\n<td><code>F1</code></td>\n<td><code>Ctrl+?</code></td>\n<td><code>F1</code></td>\n<td><code>F1</code></td>\n</tr>\n<tr>\n<td>Toggle gallery menu</td>\n<td><code>Alt+G</code></td>\n<td>same</td>\n<td>same</td>\n<td>same</td>\n</tr>\n<tr>\n<td>Toggle view mode</td>\n<td><code>Alt+Space</code></td>\n<td>same</td>\n<td>same</td>\n<td>same</td>\n</tr>\n<tr>\n<td>Settings</td>\n<td><code>Ctrl+P</code></td>\n<td>same</td>\n<td>same</td>\n<td>same</td>\n</tr>\n</tbody>\n</table>\n\n</body></html>\n\t\"\"\"\n"
  },
  {
    "path": "version/asm_manager.py",
    "content": "\"\"\"asmhentai module.\"\"\"\nimport logging\nfrom pprint import pformat\n\nfrom app_constants import DOWNLOAD_TYPE_OTHER, VALID_GALLERY_CATEGORY\nfrom pewnet import (\n    DLManager as DLManagerObject,\n    Downloader as DownloaderObject,\n    HenItem,\n)\n\nlog = logging.getLogger(__name__)\n\"\"\":class:`logging.Logger`: Logger for module.\"\"\"\nlog_i = log.info\n\"\"\":meth:`logging.Logger.info`: Info logger func\"\"\"\nlog_d = log.debug\n\"\"\":meth:`logging.Logger.debug`: Debug logger func\"\"\"\nlog_w = log.warning\n\"\"\":meth:`logging.Logger.warning`: Warning logger func\"\"\"\nlog_e = log.error\n\"\"\":meth:`logging.Logger.error`: Error logger func\"\"\"\nlog_c = log.critical\n\"\"\":meth:`logging.Logger.critical`: Critical logger func\"\"\"\n\n\nclass AsmManager(DLManagerObject):\n    \"\"\"asmhentai manager.\n\n    Attributes:\n        url (str): Base url for manager.\n    \"\"\"\n\n    url = 'http://asmhentai.com/'\n\n    @staticmethod\n    def _find_tags(browser):\n        \"\"\"find tags from browser.\n\n        Args:\n            browser: Robobrowser instance.\n\n        Returns:\n            list: List of doujin/manga tags on the page.\n        \"\"\"\n        sibling_tags = browser.select('.tags h3')\n        tags = list(map(\n            lambda x: (\n                x.text.split(':')[0],\n                x.parent.select('span')\n            ),\n            sibling_tags\n        ))\n        res = []\n        for tag in tags:\n            for span_tag in tag[1]:\n                res.append('{}:{}'.format(tag[0], span_tag.text))\n        return res\n\n    def _get_metadata(self, g_url):\n        \"\"\"get metadata.\n\n        for key to fill see HenItem class.\n\n        Args:\n            g_url: Gallery url.\n\n        Returns:\n            dict: Metadata from gallery url.\n        \"\"\"\n        self.ensure_browser_on_url(url=g_url)\n        html_soup = self._browser\n        res = {}\n        res['title'] = html_soup.select('.info h1')[0].text\n        res['title_jpn'] = html_soup.select('.info h2')[0].text\n        res['filecount'] = html_soup.select('.pages')[0].text.split('Pages:')[1].strip()\n        res['tags'] = self._find_tags(browser=self._browser)\n        if any('Category:' in x for x in res['tags']):\n            res['category'] = [tag.split(':')[1] for tag in res['tags'] if 'Category:' in tag][0]\n        return res\n\n    def _get_server_id(self, link_parts):\n        \"\"\"get server id.\n\n        Args:\n            link_parts (tuple): Tuple of (gallery_id, url_basename)\n\n        Returns:\n            server_id (str): server id.\n        \"\"\"\n        gallery_id, url_basename = link_parts\n        url = 'http://asmhentai.com/gallery/{gallery_id}/{url_basename}/'.format(\n            gallery_id=gallery_id, url_basename=url_basename)\n        self._browser.open(url)\n        link_tags = self._browser.select('img.no_image')\n        # e.g.\n        # link_tag_src = '//images.asmhentai.com/001/12623/1.jpg'\n        link_tag_src = link_tags[0].get('src')\n        return link_tag_src.split('//images.asmhentai.com/')[1].split('/')[0]\n\n    @staticmethod\n    def _split_href_links_to_parts(links):\n        \"\"\"Split href links to parts.\n\n        Args:\n            links (list): List of hrefs.\n\n        Returns:\n            list of tuple contain url parts.\n        \"\"\"\n        return [(x.split('/')[2], x.split('/')[-2]) for x in links]\n\n    def _get_dl_urls(self, g_url):\n        \"\"\"get image urls from gallery url.\n\n        Args:\n            g_url: Gallery url.\n\n        Returns:\n            list: Image from gallery url.\n        \"\"\"\n        # ensure the url\n        self.ensure_browser_on_url(url=g_url)\n        links = self._browser.select('.preview_thumb a')\n        links = [x.get('href') for x in links]\n        # link = '/gallery/168260/22/'\n        links_parts = self._split_href_links_to_parts(links)\n        server_id = self._get_server_id(links_parts[0])\n        log_d('Server id: {}'.format(server_id))\n        imgs = list(map(\n            lambda x:\n            'http://images.asmhentai.com/{}/{}/{}.jpg'.format(server_id, x[0], x[1]),\n            links_parts\n        ))\n        return imgs\n\n    @staticmethod\n    def _set_ehen_metadata(h_item, dict_metadata):\n        \"\"\"set ehen metadata.\n\n        unlike set_metadata method, This will update metadata based on required metadata in\n        Ehen.apply_method.\n\n        It also use ehen keys if rather than the defined key in asm for better merging with\n        ehen data. (i.e. use 'Artist' instead of 'Artists').\n\n        Args:\n            h_item (hen_item.HenItem): Item.\n            dict_metadata (dict): Metadata source.\n\n        Returns:\n            Updated h_item\n        \"\"\"\n        # hardcoded asm to ehen dict\n        e2a_keys = {'Artists': 'Artist', 'Languages': 'Language', 'Characters': 'Character'}\n        new_data_tags = {}\n        for tag in dict_metadata['tags']:\n            namespace, tag_value = tag.split(':', 1)\n            if namespace in e2a_keys:\n                namespace = e2a_keys[namespace]\n            new_data_tags.setdefault(namespace, []).append(tag_value)\n        new_data = {\n            'title': {\n                'jpn': dict_metadata['title_jpn'],\n                'def': dict_metadata['title'],\n\n            },\n            'tags': new_data_tags,\n            'type': dict_metadata['category'],\n            'pub_date': ''  # asm manager don't parse publication date. it is not exist.\n        }\n        h_item.metadata.update(new_data)\n        return h_item\n\n    @staticmethod\n    def _set_metadata(h_item, dict_metadata):\n        \"\"\"set metadata on item from dict_metadata.\n\n        Args:\n            h_item (hen_item.HenItem): Item.\n            dict_metadata (dict): Metadata source.\n\n        Returns:\n            Updated h_item\n        \"\"\"\n        keys = ['title_jpn', 'title', 'filecount', \"tags\"]\n        for key in keys:\n            value = dict_metadata.get(key, None)\n            if value:\n                h_item.update_metadata(key=key, value=value)\n        # for hitem gallery value\n        catg_val = dict_metadata.get('category', None)\n        category_dict = {vcatg.lower(): vcatg for vcatg in VALID_GALLERY_CATEGORY}\n        category_value = category_dict.get(catg_val, catg_val)\n        if category_value and category_value in VALID_GALLERY_CATEGORY:\n            h_item.update_metadata(key='category', value=category_value)\n        elif category_value:\n            log_w('Unknown manga category:{}'.format(category_value))\n\n        return h_item\n\n    def from_gallery_url(self, g_url):\n        \"\"\"Find gallery download url and puts it in download queue.\n\n        Args:\n            g_url: Gallery url.\n\n        Returns:\n            Download item\n        \"\"\"\n        h_item = HenItem(self._browser.session)\n        h_item.download_type = DOWNLOAD_TYPE_OTHER\n        h_item.gallery_url = g_url\n        # ex/g.e\n        log_d(\"Opening {}\".format(g_url))\n        dict_metadata = self._get_metadata(g_url=g_url)\n        log_d('dict_metadata:\\n{}'.format(pformat(dict_metadata)))\n        h_item.thumb_url = 'http:' + self._browser.select('.cover img')[0].get('src')\n        h_item.fetch_thumb()\n\n        # name\n        h_item.gallery_name = dict_metadata['title']\n        # name is the name folder\n        h_item.name = dict_metadata['title']\n\n        # get dl link\n        log_d(\"Getting download URL!\")\n        h_item.download_url = self._get_dl_urls(g_url=g_url)\n\n        h_item = self._set_metadata(h_item=h_item, dict_metadata=dict_metadata)\n\n        old_metadata = h_item.metadata\n        h_item = self._set_ehen_metadata(h_item=h_item, dict_metadata=dict_metadata)\n        log_d('Old metadata\\n{}New metadata\\n{}'.format(\n            pformat(old_metadata),\n            pformat(h_item.metadata)\n        ))\n\n        DownloaderObject.add_to_queue(h_item, self._browser.session)\n        return h_item\n"
  },
  {
    "path": "version/color_line_edit.py",
    "content": "\"\"\"LineEdit for color input.\"\"\"\nimport sys\nimport logging\n\nfrom PyQt5 import QtWidgets\nfrom PyQt5.QtWidgets import (\n    QLineEdit,\n    QHBoxLayout,\n    QPushButton,\n    QWidget,\n    QColorDialog\n)\nfrom PyQt5.QtGui import (\n    QColor,\n    QRegularExpressionValidator,\n)\nfrom PyQt5.QtCore import (\n    QRegularExpression,\n)\nlog = logging.getLogger(__name__)\nlog_d = log.debug\n\n\nclass ColorLineEdit(QLineEdit):\n    \"\"\"custom line edit for color input.\n\n    Hex color regex taken from:\n        mkyong.com/regular-expressions/how-to-validate-hex-color-code-with-regular-expression/\n\n    Args:\n        hex_color (str): Default hex color.\n\n    Attributes:\n        default_color (str): Default color.\n        button (QPushButton): Button which reflect the input from user.\n        color_dialog (QColorDialog): Color dialog for this widget.\n    \"\"\"\n\n    hexcolor_regex = r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'\n    button_stylesheet_format = \\\n        'background-color: {}; border: 1px solid black; border-radius: 5px;'\n\n    def __init__(self, parent=None, hex_color=None):\n        \"\"\"init method.\"\"\"\n        super(ColorLineEdit, self).__init__(parent)\n        self.init_ui(hex_color=hex_color)\n\n    def init_ui(self, hex_color=None):\n        \"\"\".\"\"\"\n        self.setMaxLength(7)\n        self.setPlaceholderText('Hex colors. Eg.: #323232')\n        self.setMaximumWidth(200)\n        # attr\n        self.default_color = hex_color if hex_color is not None else '#fff'\n\n        self.button = QPushButton()\n        self.button.setMaximumWidth(200)\n        self.button.setStyleSheet(self.button_stylesheet_format.format(self.default_color))\n        self.color_dialog = QColorDialog()\n        self.button.clicked.connect(self.button_click)\n\n        regex = QRegularExpression(self.hexcolor_regex)\n        validator = QRegularExpressionValidator(regex, parent=self.validator())\n        self.setValidator(validator)\n\n        self.editingFinished.connect(self.update_button_color)\n\n    def button_click(self):\n        \"\"\"Function to run when button clicked.\n\n        Get the text from input, and use it as default arg for color selection dialog.\n        If dialog return valid result, update the button and text input.\n        \"\"\"\n        color_number = self.text()\n        current_color = QColor(color_number)\n        color_from_dialog = self.color_dialog.getColor(current_color)\n        if color_from_dialog.isValid():\n            color_name = color_from_dialog.name()\n            self.button.setStyleSheet(self.button_stylesheet_format.format(color_name))\n            self.setText(color_name)\n        else:\n            log_d('color is not valid')\n\n    def update_button_color(self):\n        \"\"\"Update button's color.\"\"\"\n        color_text = self.text()\n        self.button.setStyleSheet(self.button_stylesheet_format.format(color_text))\n\n\nif __name__ == '__main__':\n    app = QtWidgets.QApplication(sys.argv)\n    hbox_layout = QHBoxLayout()\n    line_edit = ColorLineEdit()\n    hbox_layout.addWidget(line_edit)\n    hbox_layout.addWidget(line_edit.button)\n\n    window = QWidget()\n    window.setLayout(hbox_layout)\n    window.show()\n\n    sys.exit(app.exec_())\n"
  },
  {
    "path": "version/database/__init__.py",
    "content": "\"\"\"\nThis file is part of Happypanda.\nHappypanda is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 2 of the License, or\nany later version.\nHappypanda is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\nYou should have received a copy of the GNU General Public License\nalong with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n\"\"\""
  },
  {
    "path": "version/database/db.py",
    "content": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport os, sqlite3, threading, queue\nimport logging, time, shutil\n\nfrom . import db_constants\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\ndef hashes_sql(cols=False):\n    col_list = [\n    'hash_id INTEGER PRIMARY KEY',\n    'hash BLOB',\n    'series_id INTEGER',\n    'chapter_id INTEGER',\n    'page INTEGER',\n    'FOREIGN KEY(series_id) REFERENCES series(series_id) ON DELETE CASCADE',\n    'FOREIGN KEY(chapter_id) REFERENCES chapters(chapter_id) ON DELETE CASCADE',\n    'UNIQUE(hash, series_id, chapter_id, page)'\n    ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS hashes({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef series_sql(cols=False):\n    col_list = [\n        'series_id INTEGER PRIMARY KEY',\n        'title TEXT',\n        'artist TEXT',\n        'profile BLOB',\n        'series_path BLOB',\n        'is_archive INTEGER',\n        'path_in_archive BLOB',\n        'info TEXT',\n        'fav INTEGER',\n        'type TEXT',\n        'link BLOB',\n        'language TEXT',\n        'rating INTEGER NOT NULL DEFAULT 0',\n        'status TEXT',\n        'pub_date TEXT',\n        'date_added TEXT',\n        'last_read TEXT',\n        'times_read INTEGER',\n        'exed INTEGER NOT NULL DEFAULT 0',\n        'db_v REAL',\n        'view INTEGER DEFAULT 1'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS series({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef chapters_sql(cols=False):\n    col_list = [\n        'chapter_id INTEGER PRIMARY KEY',\n        'series_id INTEGER',\n        \"chapter_title TEXT NOT NULL DEFAULT ''\",\n        'chapter_number INTEGER',\n        'chapter_path BLOB',\n        'pages INTEGER',\n        'in_archive INTEGER',\n        'FOREIGN KEY(series_id) REFERENCES series(series_id) ON DELETE CASCADE'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS chapters({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef namespaces_sql(cols=False):\n    col_list = [\n        'namespace_id INTEGER PRIMARY KEY',\n        'namespace TEXT NOT NULL UNIQUE'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS namespaces({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef tags_sql(cols=False):\n    col_list = [\n        'tag_id INTEGER PRIMARY KEY',\n        'tag TEXT NOT NULL UNIQUE'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS tags({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef tags_mappings_sql(cols=False):\n    col_list = [\n        'tags_mappings_id INTEGER PRIMARY KEY',\n        'namespace_id INTEGER',\n        'tag_id INTEGER',\n        'FOREIGN KEY(namespace_id) REFERENCES namespaces(namespace_id) ON DELETE CASCADE',\n        'FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE',\n        'UNIQUE(namespace_id, tag_id)'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS tags_mappings({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef series_tags_mappings_sql(cols=False):\n    col_list = [\n        'series_id INTEGER',\n        'tags_mappings_id INTEGER',\n        'FOREIGN KEY(series_id) REFERENCES series(series_id) ON DELETE CASCADE',\n        'FOREIGN KEY(tags_mappings_id) REFERENCES tags_mappings(tags_mappings_id) ON DELETE CASCADE',\n        'UNIQUE(series_id, tags_mappings_id)'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS series_tags_map({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef list_sql(cols=False):\n    col_list = [\n        'list_id INTEGER PRIMARY KEY',\n        \"list_name TEXT NOT NULL DEFAULT ''\",\n        'list_filter TEXT',\n        \"profile BLOB\",\n        \"type INTEGER DEFAULT 0\",\n        \"enforce INTEGER DEFAULT 0\",\n        \"regex INTEGER DEFAULT 0\",\n        \"l_case INTEGER DEFAULT 0\",\n        \"strict INTEGER DEFAULT 0\",\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS list({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\ndef series_list_map_sql(cols=False):\n    col_list = [\n        'list_id INTEGER NOT NULL',\n        'series_id INTEGER INTEGER NOT NULL',\n        'FOREIGN KEY(list_id) REFERENCES list(list_id) ON DELETE CASCADE',\n        'FOREIGN KEY(series_id) REFERENCES series(series_id) ON DELETE CASCADE',\n        'UNIQUE(list_id, series_id)'\n        ]\n\n    sql = \"CREATE TABLE IF NOT EXISTS series_list_map({});\".format(\",\".join(col_list))\n\n    if cols:\n        return sql, col_list\n    return sql\n\nSTRUCTURE_SCRIPT = series_sql()+chapters_sql()+namespaces_sql()+tags_sql()+tags_mappings_sql()+\\\n    series_tags_mappings_sql()+hashes_sql()+list_sql()+series_list_map_sql()\n\ndef global_db_convert(conn):\n    \"\"\"\n    Takes care of converting tables and columns.\n    Don't use this method directly. Use the add_db_revisions instead.\n    \"\"\"\n    log_i('Converting tables')\n    c = conn.cursor()\n    series, series_cols = series_sql(True)\n    chapters, chapters_cols = chapters_sql(True)\n    namespaces, namespaces_cols = namespaces_sql(True)\n    tags, tags_cols = tags_sql(True)\n    tags_mappings, tags_mappings_cols = tags_mappings_sql(True)\n    series_tags_mappings, series_tags_mappings_cols = series_tags_mappings_sql(True)\n    hashes, hashes_cols = hashes_sql(True)\n    _list, list_cols = list_sql(True)\n    series_list_map, series_list_map_cols = series_list_map_sql(True)\n    \n    t_d = {}\n    t_d['series'] = series_cols\n    t_d['chapters'] = chapters_cols\n    t_d['namespaces'] = namespaces_cols\n    t_d['tags'] = tags_cols\n    t_d['tags_mappings'] = tags_mappings_cols\n    t_d['series_tags_mappings'] = series_tags_mappings_cols\n    t_d['hashes'] = hashes_cols\n    t_d['list'] = list_cols\n    t_d['series_list_map'] = series_list_map_cols\n\n    log_d('Checking table structures')\n    c.executescript(STRUCTURE_SCRIPT)\n    conn.commit()\n\n    log_d('Checking columns')\n    for table in t_d:\n        for col in t_d[table]:\n            try:\n                c.execute('ALTER TABLE {} ADD COLUMN {}'.format(table, col))\n                log_d('Added new column: {}'.format(col))\n            except:\n                log_d('Skipped column: {}'.format(col))\n    conn.commit()\n    log_d('Commited DB changes')\n    return c\n\ndef add_db_revisions(old_db):\n    \"\"\"\n    Adds specific DB revisions items.\n    Note: pass a path to db\n    \"\"\"\n    log_i('Converting DB')\n    conn = sqlite3.connect(old_db, check_same_thread=False)\n    conn.row_factory = sqlite3.Row\n\n    log_i('Converting tables and columns')\n    c = global_db_convert(conn)\n\n    log_d('Updating DB version')\n    c.execute('UPDATE version SET version=? WHERE 1', (db_constants.CURRENT_DB_VERSION,))\n    conn.commit()\n    conn.close()\n    return\n\ndef create_db_path(db_path=db_constants.DB_PATH):\n    head = os.path.split(db_path)[0]\n    os.makedirs(head, exist_ok=True)\n    if not os.path.isfile(db_path):\n        with open(db_path, 'x') as f:\n            pass\n    return db_path\n\n\ndef check_db_version(conn):\n    \"Checks if DB version is allowed. Raises dialog if not\"\n    vs = \"SELECT version FROM version\"\n    c = conn.cursor()\n    c.execute(vs)\n    log_d('Checking DB Version')\n    db_vs = c.fetchone()\n    db_constants.REAL_DB_VERSION = db_vs[0]\n    if db_vs[0] not in db_constants.DB_VERSION:\n        msg = \"Incompatible database\"\n        log_c(msg)\n        log_d('Local database version: {}\\nProgram database version:{}'.format(db_vs[0],\n                                                                         db_constants.CURRENT_DB_VERSION))\n        #ErrorQueue.put(msg)\n        return False\n    return True\n    \n\ndef init_db(path=db_constants.DB_PATH):\n    \"\"\"Initialises the DB. Returns a sqlite3 connection,\n    which will be passed to the db thread.\n    \"\"\"\n\n    def db_layout(cursor):\n        c = cursor\n        # version\n        c.execute(\"\"\"\n        CREATE TABLE IF NOT EXISTS version(version REAL)\n        \"\"\")\n\n        c.execute(\"\"\"INSERT INTO version(version) VALUES(?)\"\"\", (db_constants.CURRENT_DB_VERSION,))\n        log_i(\"Constructing database layout\")\n        log_d(\"Database Layout:\\n\\t{}\".format(STRUCTURE_SCRIPT))\n        c.executescript(STRUCTURE_SCRIPT)\n\n    def new_db(p, new=False):\n        conn = sqlite3.connect(p, check_same_thread=False)\n        conn.row_factory = sqlite3.Row\n        if new:\n            c = conn.cursor()\n            db_layout(c)\n            conn.commit()\n        return conn\n\n    if os.path.isfile(path):\n        conn = new_db(path)\n        if path == db_constants.DB_PATH and not check_db_version(conn):\n            return None\n    else:\n        create_db_path()\n        conn = new_db(path, True)\n\n    conn.isolation_level = None\n    conn.execute(\"PRAGMA foreign_keys = on\")\n    return conn\n\nclass DBBase:\n    \"The base DB class. _DB_CONN should be set at runtime on startup\"\n    _DB_CONN = None\n    _AUTO_COMMIT = True\n    _STATE = {'active':False}\n\n    def __init__(self, **kwargs):\n        pass\n\n    @classmethod\n    def begin(cls):\n        \"Useful when modifying for a large amount of data\"\n        if not cls._STATE['active']:\n            cls._AUTO_COMMIT = False\n            cls.execute(cls, \"BEGIN TRANSACTION\")\n            cls._STATE['active'] = True\n        #print(\"STARTED DB OPTIMIZE\")\n\n    @classmethod\n    def end(cls):\n        \"Called to commit and end transaction\"\n        if cls._STATE['active']:\n            try:\n                cls.execute(cls, \"COMMIT\")\n            except sqlite3.OperationalError:\n                pass\n            cls._AUTO_COMMIT = True\n            cls._STATE['active'] = False\n        #print(\"ENDED DB OPTIMIZE\")\n\n    def execute(self, *args):\n        \"Same as cursor.execute\"\n        if not self._DB_CONN:\n            raise db_constants.NoDatabaseConnection\n        log_d('DB Query: {}'.format(args).encode(errors='ignore'))\n        if self._AUTO_COMMIT:\n            try:\n                with self._DB_CONN:\n                    return self._DB_CONN.execute(*args)\n            except sqlite3.InterfaceError:\n                    return self._DB_CONN.execute(*args)\n\n        else:\n            return self._DB_CONN.execute(*args)\n    \n    def executemany(self, *args):\n        \"Same as cursor.executemany\"\n        if not self._DB_CONN:\n            raise db_constants.NoDatabaseConnection\n        log_d('DB Query: {}'.format(args).encode(errors='ignore'))\n        if self._AUTO_COMMIT:\n            with self._DB_CONN:\n                return self._DB_CONN.executemany(*args)\n        else:\n            c = self._DB_CONN.executemany(*args)\n            return c\n\n    def commit(self):\n        self._DB_CONN.commit()\n\n    @classmethod\n    def analyze(cls):\n        cls._DB_CONN.execute('ANALYZE')\n\n    @classmethod\n    def close(cls):\n        cls._DB_CONN.close()\n\nif __name__ == '__main__':\n    raise RuntimeError(\"Unit tests not yet implemented\")\n    # unit tests here!"
  },
  {
    "path": "version/database/db_constants.py",
    "content": "﻿#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport os\n\nDB_NAME = 'happypanda.db'\nTHUMB_NAME = \"thumbnails\"\nif os.name == 'posix':\n\tDB_ROOT = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../db')\n\tTHUMBNAIL_PATH = os.path.join(DB_ROOT, THUMB_NAME)\n\tDB_PATH = os.path.join(DB_ROOT, DB_NAME)\nelse:\n\tDB_ROOT = \"db\"\n\tTHUMBNAIL_PATH = os.path.join(\"db\", THUMB_NAME)\n\tDB_PATH = os.path.join(DB_ROOT, DB_NAME)\n\nDB_VERSION = [0.26] # a list of accepted db versions. E.g. v3.5 will be backward compatible with v3.1 etc.\nCURRENT_DB_VERSION = DB_VERSION[0]\nREAL_DB_VERSION = DB_VERSION[len(DB_VERSION)-1]\nMETHOD_QUEUE = None\nMETHOD_RETURN = None\nDATABASE = None\n\nclass NoDatabaseConnection(Exception): pass\n"
  },
  {
    "path": "version/executors.py",
    "content": "﻿import logging, uuid, os\n\nfrom concurrent import futures\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtGui import QImage, QPainter, QBrush, QPen\n\nfrom database import db_constants\nimport utils\nimport app_constants\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\ndef _rounded_qimage(qimg, radius):\n\tr_image = QImage(qimg.width(), qimg.height(), QImage.Format_ARGB32)\n\tr_image.fill(Qt.transparent)\n\tp = QPainter()\n\tpen = QPen(Qt.darkGray)\n\tpen.setJoinStyle(Qt.RoundJoin)\n\tp.begin(r_image)\n\tp.setRenderHint(p.Antialiasing)\n\tp.setPen(Qt.NoPen)\n\tp.setBrush(QBrush(qimg))\n\tp.drawRoundedRect(0, 0, r_image.width(), r_image.height(), radius, radius)\n\tp.end()\n\treturn r_image\n\ndef _task_thumbnail(gallery_or_path, img=None, width=app_constants.THUMB_W_SIZE,\n\t\t\t\t\t\theight=app_constants.THUMB_H_SIZE):\n\t\"\"\"\n\t\"\"\"\n\tlog_i(\"Generating thumbnail\")\n\t# generate a cache dir if required\n\tif not os.path.isdir(db_constants.THUMBNAIL_PATH):\n\t\tos.mkdir(db_constants.THUMBNAIL_PATH)\n\n\ttry:\n\t\tif not img:\n\t\t\timg_path = utils.get_gallery_img(gallery_or_path)\n\t\telse:\n\t\t\timg_path = img\n\t\tif not img_path:\n\t\t\traise IndexError\n\t\tfor ext in utils.IMG_FILES:\n\t\t\tif img_path.lower().endswith(ext):\n\t\t\t\tsuff = ext # the image ext with dot\n\n\t\t# generate unique file name\n\t\tfile_name = str(uuid.uuid4()) + \".png\"\n\t\tnew_img_path = os.path.join(db_constants.THUMBNAIL_PATH, (file_name))\n\t\tif not os.path.isfile(img_path):\n\t\t\traise IndexError\n\n\t\t# Do the scaling\n\t\ttry:\n\t\t\tim_data = utils.PToQImageHelper(img_path)\n\t\t\timage = QImage(im_data['data'], im_data['im'].size[0], im_data['im'].size[1], im_data['format'])\n\t\t\tif im_data['colortable']:\n\t\t\t\timage.setColorTable(im_data['colortable'])\n\t\texcept ValueError:\n\t\t\timage = QImage()\n\t\t\timage.load(img_path)\n\t\tif image.isNull():\n\t\t\traise IndexError\n\t\tradius = 5\n\t\timage = image.scaled(width, height, Qt.KeepAspectRatio, Qt.SmoothTransformation)\n\t\tr_image = _rounded_qimage(image, radius)\n\t\tr_image.save(new_img_path, \"PNG\", quality=80)\n\texcept IndexError:\n\t\tnew_img_path = app_constants.NO_IMAGE_PATH\n\n\treturn new_img_path\n\ndef _task_load_thumbnail(ppath, thumb_size, on_method=None, **kwargs):\n\tif ppath:\n\t\timg = QImage(ppath)\n\t\tif not img.isNull():\n\t\t\tsize = img.size()\n\t\t\tif size.width() != thumb_size[0]:\n\t\t\t\t# TODO: use _task_thumbnail\n\t\t\t\timg = _rounded_qimage(img.scaled(thumb_size[0], thumb_size[1], Qt.KeepAspectRatio, Qt.SmoothTransformation), 5)\n\t\t\tif on_method:\n\t\t\t\ton_method(img, **kwargs)\n\t\t\treturn img\n\nclass Executors:\n\t_thumbnail_exec = futures.ThreadPoolExecutor(3)\n\t_profile_exec = futures.ThreadPoolExecutor(2)\n\t\n\t@classmethod\n\tdef generate_thumbnail(cls, gallery_or_path, img=None, width=app_constants.THUMB_W_SIZE,\n\t\t\t\t\t\theight=app_constants.THUMB_H_SIZE, on_method=None, blocking=False):\n\t\tlog_i(\"Generating thumbnail\")\n\t\tf = cls._thumbnail_exec.submit(_task_thumbnail, gallery_or_path, img=img, width=width, height=height)\n\t\tif on_method:\n\t\t\tf.add_done_callback(on_method)\n\t\tif blocking:\n\t\t\treturn f.result()\n\t\tif not on_method:\n\t\t\treturn f\n\n\t\tlog_d(\"Returning future\")\n\n\t@classmethod\n\tdef load_thumbnail(cls, ppath, thumb_size=app_constants.THUMB_DEFAULT, on_method=None, **kwargs):\n\t\t\"**kwargs will be passed to on_method\"\n\t\tf = cls._profile_exec.submit(_task_load_thumbnail, ppath, thumb_size, on_method, **kwargs)\n\t\treturn f\n\n"
  },
  {
    "path": "version/fetch.py",
    "content": "﻿#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport os, time, logging, uuid, random, queue, scandir\nimport re as regex\n\nfrom PyQt5.QtCore import QObject, pyqtSignal # need this for interaction with main thread\n\nfrom gallerydb import Gallery, GalleryDB, HashDB, execute\nimport app_constants\nimport pewnet\nimport settings\nimport utils\n\n\"\"\"This file contains functions to fetch gallery data\"\"\"\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass Fetch(QObject):\n    \"\"\"\n    A class containing methods to fetch gallery data.\n    Should be executed in a new thread.\n    Contains following methods:\n    local -> runs a local search in the given series_path\n    auto_web_metadata -> does a search online for the given galleries and returns their metdata\n    \"\"\"\n\n    # local signals\n    LOCAL_EMITTER = pyqtSignal(Gallery)\n    FINISHED = pyqtSignal(object)\n    DATA_COUNT = pyqtSignal(int)\n    PROGRESS = pyqtSignal(int)\n    SKIPPED = pyqtSignal(list)\n\n    # WEB signals\n    GALLERY_EMITTER = pyqtSignal(Gallery, object, object)\n    AUTO_METADATA_PROGRESS = pyqtSignal(str)\n    GALLERY_PICKER = pyqtSignal(object, list, queue.Queue)\n    GALLERY_PICKER_QUEUE = queue.Queue()\n    \n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.series_path = \"\"\n        self._data = []\n        self._curr_gallery = '' # for debugging purposes\n        self.skipped_paths = []\n\n        # web\n        self._default_ehen_url = app_constants.DEFAULT_EHEN_URL\n        self.galleries = []\n        self.galleries_in_queue = []\n        self.error_galleries = []\n        self._hen_list = []\n\n        #filter\n        self.galleries_from_db = []\n        self._refresh_filter_list()\n\n        #download\n        self._to_queue_container = False\n        self._galleries_queue = queue.Queue()\n\n    def _refresh_filter_list(self):\n            gallery_data = app_constants.GALLERY_DATA + app_constants.GALLERY_ADDITION_DATA\n            filter_list = []\n            for g in gallery_data:\n                filter_list.append(os.path.normcase(g.path))\n            self.galleries_from_db = sorted(filter_list)\n\n    def create_gallery(self, path, folder_name, do_chapters=True, archive=None):\n        is_archive = True if archive else False\n        temp_p = archive if is_archive else path\n        folder_name = folder_name or path if folder_name or path else os.path.split(archive)[1]\n        if utils.check_ignore_list(temp_p) and not GalleryDB.check_exists(temp_p, self.galleries_from_db, False):\n            log_i('Creating gallery: {}'.format(folder_name.encode('utf-8', 'ignore')))\n            new_gallery = Gallery()\n            images_paths = []\n            metafile = utils.GMetafile()\n            try:\n                con = scandir.scandir(temp_p) #all of content in the gallery folder\n                log_i('Gallery source is a directory')\n                chapters = sorted([sub.path for sub in con if sub.is_dir() or sub.name.endswith(utils.ARCHIVE_FILES)])\\\n                    if do_chapters else [] #subfolders\n                # if gallery has chapters divided into sub folders\n                numb_of_chapters = len(chapters)\n                if numb_of_chapters != 0:\n                    log_i('Gallery has {} chapters'.format(numb_of_chapters))\n                    for ch in chapters:\n                        chap = new_gallery.chapters.create_chapter()\n                        chap.title = utils.title_parser(ch)['title']\n                        chap.path = os.path.join(path, ch)\n                        chap.pages = len([x for x in scandir.scandir(chap.path) if x.name.endswith(utils.IMG_FILES)])\n                        metafile.update(utils.GMetafile(chap.path))\n\n                else: #else assume that all images are in gallery folder\n                    chap = new_gallery.chapters.create_chapter()\n                    chap.title = utils.title_parser(os.path.split(path)[1])['title']\n                    chap.path = path\n                    metafile.update(utils.GMetafile(chap.path))\n                    chap.pages = len(list(scandir.scandir(path)))\n                \n                parsed = utils.title_parser(folder_name)\n            except NotADirectoryError:\n                try:\n                    if is_archive or temp_p.endswith(utils.ARCHIVE_FILES):\n                        log_i('Gallery source is an archive')\n                        contents = utils.check_archive(temp_p)\n                        if contents:\n                            new_gallery.is_archive = 1\n                            new_gallery.path_in_archive = '' if not is_archive else path\n                            if folder_name.endswith('/'):\n                                folder_name = folder_name[:-1]\n                                fn = os.path.split(folder_name)\n                                folder_name = fn[1] or fn[2]\n                            folder_name = folder_name.replace('/','')\n                            if folder_name.endswith(utils.ARCHIVE_FILES):\n                                n = folder_name\n                                for ext in utils.ARCHIVE_FILES:\n                                    n = n.replace(ext, '')\n                                parsed = utils.title_parser(n)\n                            else:\n                                parsed = utils.title_parser(folder_name)\n                                                \n                            if do_chapters:\n                                archive_g = sorted(contents)\n                                if not archive_g:\n                                    log_w('No chapters found for {}'.format(temp_p.encode(errors='ignore')))\n                                    raise ValueError\n                                for g in archive_g:\n                                    chap = new_gallery.chapters.create_chapter()\n                                    chap.in_archive = 1\n                                    chap.title = parsed['title'] if not g else utils.title_parser(g.replace('/', ''))['title']\n                                    chap.path = g\n                                    metafile.update(utils.GMetafile(g, temp_p))\n                                    arch = utils.ArchiveFile(temp_p)\n                                    chap.pages = len([x for x in arch.dir_contents(g) if x.endswith(utils.IMG_FILES)])\n                                    arch.close()\n                            else:\n                                chap = new_gallery.chapters.create_chapter()\n                                chap.title = utils.title_parser(os.path.split(path)[1])['title']\n                                chap.in_archive = 1\n                                chap.path = path\n                                metafile.update(utils.GMetafile(path, temp_p))\n                                arch = utils.ArchiveFile(temp_p)\n                                chap.pages = len(arch.dir_contents(''))\n                                arch.close()\n                        else:\n                            raise ValueError\n                    else:\n                        raise ValueError\n                except ValueError:\n                    log_w('Skipped {} in local search'.format(path.encode(errors='ignore')))\n                    self.skipped_paths.append((temp_p, 'Empty archive',))\n                    return\n                except app_constants.CreateArchiveFail:\n                    log_w('Skipped {} in local search'.format(path.encode(errors='ignore')))\n                    self.skipped_paths.append((temp_p, 'Error creating archive',))\n                    return\n                except app_constants.TitleParsingError:\n                    log_w('Skipped {} in local search'.format(path.encode(errors='ignore')))\n                    self.skipped_paths.append((temp_p, 'Error while parsing folder/archive name',))\n                    return\n\n            new_gallery.title = parsed['title']\n            new_gallery.path = temp_p\n            new_gallery.artist = parsed['artist']\n            new_gallery.language = parsed['language']\n            new_gallery.info = \"\"\n            new_gallery.view = app_constants.ViewType.Addition\n            metafile.apply_gallery(new_gallery)\n\n            if app_constants.MOVE_IMPORTED_GALLERIES and not app_constants.OVERRIDE_MOVE_IMPORTED_IN_FETCH:\n                new_gallery.move_gallery()\n\n            self.LOCAL_EMITTER.emit(new_gallery)\n            self._data.append(new_gallery)\n            log_i('Gallery successful created: {}'.format(folder_name.encode('utf-8', 'ignore')))\n            return True\n        else:\n            log_i('Gallery already exists or ignored: {}'.format(folder_name.encode('utf-8', 'ignore')))\n            self.skipped_paths.append((temp_p, 'Already exists or ignored'))\n            return False\n\n    def local(self, s_path=None):\n        \"\"\"\n        Do a local search in the given series_path.\n        \"\"\"\n        self._data.clear()\n        if s_path:\n            self.series_path = s_path\n        try:\n            gallery_l = sorted([p.name for p in scandir.scandir(self.series_path)]) #list of folders in the \"Gallery\" folder\n            mixed = False\n        except TypeError:\n            gallery_l = self.series_path\n            mixed = True\n        if len(gallery_l) != 0: # if gallery path list is not empty\n            log_i('Gallery folder is not empty')\n            if len(self.galleries_from_db) != len(app_constants.GALLERY_DATA):\n                self._refresh_filter_list()\n            self.DATA_COUNT.emit(len(gallery_l)) #tell model how many items are going to be added\n            log_i('Received {} paths'.format(len(gallery_l)))\n            progress = 0\n\n            for folder_name in gallery_l: # folder_name = gallery folder title\n                self._curr_gallery = folder_name\n                if mixed:\n                    path = folder_name\n                    folder_name = os.path.split(path)[1]\n                else:\n                    path = os.path.join(self.series_path, folder_name)\n                if app_constants.SUBFOLDER_AS_GALLERY or app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY:\n                    if app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY:\n                        app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = False\n                    log_i(\"Treating each subfolder as gallery\")\n                    if os.path.isdir(path):\n                        gallery_folders, gallery_archives = utils.recursive_gallery_check(path)\n                        for gs in gallery_folders:\n                                self.create_gallery(gs, os.path.split(gs)[1], False)\n                        p_saving = {}\n                        for gs in gallery_archives:\n                                    \n                                self.create_gallery(gs[0], os.path.split(gs[0])[1], False, archive=gs[1])\n                    elif path.endswith(utils.ARCHIVE_FILES):\n                        for g in utils.check_archive(path):\n                            self.create_gallery(g, os.path.split(g)[1], False, archive=path)\n                else:\n                    try:\n                        if os.path.isdir(path):\n                            if not list(scandir.scandir(path)):\n                                raise ValueError\n                        elif not path.endswith(utils.ARCHIVE_FILES):\n                            raise NotADirectoryError\n\n                        log_i(\"Treating each subfolder as chapter\")\n                        self.create_gallery(path, folder_name, do_chapters=True)\n\n                    except ValueError:\n                        self.skipped_paths.append((path, 'Empty directory'))\n                        log_w('Directory is empty: {}'.format(path.encode(errors='ignore')))\n                    except NotADirectoryError:\n                        self.skipped_paths.append((path, 'Unsupported file'))\n                        log_w('Unsupported file: {}'.format(path.encode(errors='ignore')))\n\n                progress += 1 # update the progress bar\n                self.PROGRESS.emit(progress)\n        else: # if gallery folder is empty\n            log_e('Local search error: Invalid directory')\n            log_e('Gallery folder is empty')\n            app_constants.OVERRIDE_MOVE_IMPORTED_IN_FETCH = True # sanity check\n            self.FINISHED.emit(False)\n            # might want to include an error message\n        app_constants.OVERRIDE_MOVE_IMPORTED_IN_FETCH = False\n        # everything went well\n        log_i('Local search: OK')\n        log_i('Created {} items'.format(len(self._data)))\n        if self._to_queue_container:\n            for x in self._data:\n                self._galleries_queue.put(x)\n        else:\n            self.FINISHED.emit(self._data)\n        if self.skipped_paths:\n            self.SKIPPED.emit(self.skipped_paths)\n\n    def _return_gallery_metadata(self, gallery):\n        \"Emits galleries\"\n        assert isinstance(gallery, Gallery)\n        if gallery:\n            gallery.exed = 1\n            self.GALLERY_EMITTER.emit(gallery, None, False)\n            log_d('Success')\n\n    def fetch_metadata(self, gallery=None, hen=None, proc=False):\n        \"\"\"\n        Puts gallery in queue for metadata fetching. Applies received galleries and sends\n        them to gallery model.\n        Set proc to true if you want to process the queue immediately\n        \"\"\"\n        if gallery:\n            log_i(\"Fetching metadata for gallery: {}\".format(gallery.title.encode(errors='ignore')))\n            log_i(\"Adding to queue: {}\".format(gallery.title.encode(errors='ignore')))\n            if proc:\n                metadata = hen.add_to_queue(gallery.temp_url, True)\n            else:\n                metadata = hen.add_to_queue(gallery.temp_url)\n            self.galleries_in_queue.append(gallery)\n        else:\n            metadata = hen.add_to_queue(proc=True)\n\n        if metadata == 1: # Gallery is now put in queue\n            return None\n        # We received something from get_metadata\n        if not metadata: # metadata fetching failed\n            if gallery:\n                self.error_galleries.append((gallery, \"No metadata found for gallery\"))\n                log_i(\"An error occured while fetching metadata with gallery: {}\".format(\n                    gallery.title.encode(errors='ignore')))\n            return None\n        self.AUTO_METADATA_PROGRESS.emit(\"Applying metadata...\")\n\n        for x, g in enumerate(self.galleries_in_queue, 1):\n            try:\n                data = metadata[g.temp_url]\n            except KeyError:\n                self.AUTO_METADATA_PROGRESS.emit(\"No metadata found for gallery: {}\".format(g.title))\n                self.error_galleries.append((g, \"No metadata found for gallery\"))\n                log_w(\"No metadata found for gallery: {}\".format(g.title.encode(errors='ignore')))\n                continue\n            log_i('({}/{}) Applying metadata for gallery: {}'.format(x, len(self.galleries_in_queue),\n                                                            g.title.encode(errors='ignore')))\n            if app_constants.REPLACE_METADATA:\n                g = hen.apply_metadata(g, data, False)\n            else:\n                g = hen.apply_metadata(g, data)\n            self._return_gallery_metadata(g)\n            log_i('Successfully applied metadata to gallery: {}'.format(g.title.encode(errors='ignore')))\n        self.galleries_in_queue.clear()\n        self.AUTO_METADATA_PROGRESS.emit('Finished applying metadata')\n        log_i('Finished applying metadata')\n\n    def _auto_metadata_process(self, galleries, hen, valid_url, **kwargs):\n        hen.LAST_USED = time.time()\n        self.AUTO_METADATA_PROGRESS.emit(\"Checking gallery urls...\")\n\n        fetched_galleries = []\n        checked_pre_url_galleries = []\n        multiple_hit_galleries = []\n        for x, gallery in enumerate(galleries, 1):\n            custom_args = {} # send to hen class\n            log_i(\"Checking gallery url\")\n\n            # coming from GalleryDialog\n            if hasattr(gallery, \"_g_dialog_url\"):\n                if gallery._g_dialog_url:\n                    gallery.temp_url = gallery._g_dialog_url\n                    checked_pre_url_galleries.append(gallery)\n                    # to process even if this gallery is last and fails\n                    if x == len(galleries):\n                        self.fetch_metadata(hen=hen)\n                    continue\n\n            if gallery.link and app_constants.USE_GALLERY_LINK:\n                log_i(\"Using existing gallery url\")\n                check = self._website_checker(gallery.link)\n                if check == valid_url:\n                    # convert g.e-h to e-h\n                    gallery.link = pewnet.HenManager.gtoEh(gallery.link)\n                    gallery.temp_url = gallery.link\n                    checked_pre_url_galleries.append(gallery)\n                    if x == len(galleries):\n                        self.fetch_metadata(hen=hen)\n                    continue\n\n            self.AUTO_METADATA_PROGRESS.emit(\"({}/{}) Generating gallery hash: {}\".format(x, len(galleries), gallery.title))\n            log_i(\"Generating gallery hash: {}\".format(gallery.title.encode(errors='ignore')))\n            hash = None\n            try:\n                if not gallery.hashes:\n                    color_img = kwargs['color'] if 'color' in kwargs else False # used for similarity search on EH\n                    hash_dict = execute(HashDB.gen_gallery_hash, False, gallery, 0, 'mid', color_img)\n                    if color_img and 'color' in hash_dict:\n                        custom_args['color'] = hash_dict['color'] # will be path to filename\n                        hash = hash_dict['color']\n                    elif hash_dict:\n                        hash = hash_dict['mid']\n                else:\n                    hash = gallery.hashes[random.randint(0, len(gallery.hashes)-1)]\n            except app_constants.CreateArchiveFail:\n                pass\n            if not hash:\n                self.error_galleries.append((gallery, \"Could not generate hash\"))\n                log_e(\"Could not generate hash for gallery: {}\".format(gallery.title.encode(errors='ignore')))\n                if x == len(galleries):\n                    self.fetch_metadata(hen=hen)\n                continue\n            gallery.hash = hash\n\n            # dict -> hash:[list of title,url tuples] or None\n            self.AUTO_METADATA_PROGRESS.emit(\"({}/{}) Searching url for gallery: {}\".format(x, len(galleries), gallery.title))\n            found_url = hen.search(gallery.hash, **custom_args)\n            if found_url == 'error':\n                app_constants.GLOBAL_EHEN_LOCK = False\n                self.FINISHED.emit(True)\n                return\n            if not gallery.hash in found_url:\n                self.error_galleries.append((gallery, \"Could not find url for gallery\"))\n                self.AUTO_METADATA_PROGRESS.emit(\"Could not find url for gallery: {}\".format(gallery.title))\n                log_w('Could not find url for gallery: {}'.format(gallery.title.encode(errors='ignore')))\n                if x == len(galleries):\n                    self.fetch_metadata(hen=hen)\n                continue\n            title_url_list = found_url[gallery.hash]\n\n            if not len(title_url_list) > 1 or app_constants.ALWAYS_CHOOSE_FIRST_HIT:\n                title = title_url_list[0][0]\n                url = title_url_list[0][1]\n            else:\n                multiple_hit_galleries.append([gallery, title_url_list])\n                if x == len(galleries):\n                    self.fetch_metadata(hen=hen)\n                continue\n\n            if not gallery.link:\n                if isinstance(hen, (pewnet.EHen, pewnet.ExHen)):\n                    gallery.link = url\n                    self.GALLERY_EMITTER.emit(gallery, None, None)\n            gallery.temp_url = url\n            self.AUTO_METADATA_PROGRESS.emit(\"({}/{}) Adding to queue: {}\".format(\n                x, len(galleries), gallery.title))\n\n            self.fetch_metadata(gallery, hen, x == len(galleries))\n\n        if checked_pre_url_galleries:\n            for x, gallery in enumerate(checked_pre_url_galleries, 1):\n                self.AUTO_METADATA_PROGRESS.emit(\"({}/{}) Adding to queue: {}\".format(\n                    x, len(checked_pre_url_galleries), gallery.title))\n                self.fetch_metadata(gallery, hen, x == len(checked_pre_url_galleries))\n\n        if multiple_hit_galleries:\n            skip_all = False\n            multiple_hit_g_queue = []\n            for x, g_data in enumerate(multiple_hit_galleries, 1):\n                gallery = g_data[0]\n                log_w(\"Multiple galleries found for gallery: {}\".format(gallery.title.encode(errors='ignore')))\n                if skip_all:\n                    log_w(\"Skipping gallery\")\n                    continue\n                title_url_list = g_data[1]\n\n                self.AUTO_METADATA_PROGRESS.emit(\"Multiple galleries found for gallery: {}\".format(gallery.title))\n                app_constants.SYSTEM_TRAY.showMessage('Happypanda', 'Multiple galleries found for gallery:\\n{}'.format(gallery.title),\n                                    minimized=True)\n                self.GALLERY_PICKER.emit(gallery, title_url_list, self.GALLERY_PICKER_QUEUE)\n                user_choice = self.GALLERY_PICKER_QUEUE.get()\n\n                if user_choice == None:\n                    skip_all = True\n                if not user_choice:\n                    log_w(\"Skipping gallery\")\n                    continue\n\n                title = user_choice[0]\n                url = user_choice[1]\n\n                if not gallery.link:\n                    gallery.link = url\n                    if isinstance(hen, (pewnet.EHen, pewnet.ExHen)):\n                        self.GALLERY_EMITTER.emit(gallery, None, None)\n                gallery.temp_url = url\n                self.AUTO_METADATA_PROGRESS.emit(\"({}/{}) Adding to queue: {}\".format(\n                    x, len(multiple_hit_galleries), gallery.title))\n                multiple_hit_g_queue.append(gallery)\n\n            for x, g in enumerate(multiple_hit_g_queue, 1):\n                self.fetch_metadata(g, hen, x == len(multiple_hit_g_queue))\n\n\n    def _website_checker(self, url):\n        log_i(\"Checking if valid URL: {}\".format(url))\n        if not url:\n            return None\n        if 'g.e-hentai.org/g/' in url:\n            return 'ehen'\n        elif 'exhentai.org/g/' in url:\n            return 'exhen'\n        elif 'panda.chaika.moe/archive/' in url or 'panda.chaika.moe/gallery/' in url:\n            return 'chaikahen'\n        else:\n            log_e('Invalid URL')\n            return None\n\n    def auto_web_metadata(self):\n        \"\"\"\n        Auto fetches metadata for the provided list of galleries.\n        Appends or replaces metadata with the new fetched metadata.\n        \"\"\"\n        log_i('Initiating auto metadata fetcher')\n        self._hen_list = pewnet.hen_list_init()\n        if self.galleries and not app_constants.GLOBAL_EHEN_LOCK:\n            log_i('Auto metadata fetcher is now running')\n            app_constants.GLOBAL_EHEN_LOCK = True\n\n            def fetch_cancelled(rsn=''):\n                if rsn:\n                    self.AUTO_METADATA_PROGRESS.emit(\"Metadata fetching cancelled: {}\".format(rsn))\n                    app_constants.SYSTEM_TRAY.showMessage(\"Metadata\", \"Metadata fetching cancelled: {}\".format(rsn), minimized=True)\n                else:\n                    self.AUTO_METADATA_PROGRESS.emit(\"Metadata fetching cancelled!\")\n                    app_constants.SYSTEM_TRAY.showMessage(\"Metadata\", \"Metadata fetching cancelled!\", minimized=True)\n                app_constants.GLOBAL_EHEN_LOCK = False\n                self.FINISHED.emit(False)\n\n            if 'exhentai' in self._default_ehen_url:\n                try:\n                    exprops = settings.ExProperties()\n                    hen = pewnet.ExHen(exprops.cookies)\n                    if hen.check_login(exprops.cookies):\n                        valid_url = 'exhen'\n                        log_i(\"using exhen\")\n                    else:\n                        raise ValueError\n                except ValueError:\n                    hen = pewnet.EHen()\n                    valid_url = 'ehen'\n                    log_i(\"using ehen\")\n            else:\n                hen = pewnet.EHen()\n                valid_url = 'ehen'\n                log_i(\"Using Exhentai\")\n            try:\n                self._auto_metadata_process(self.galleries, hen, valid_url, color=True)\n            except app_constants.MetadataFetchFail as err:\n                fetch_cancelled(err)\n                return\n\n            if self.error_galleries:\n                if self._hen_list:\n                    log_i(\"Using fallback source\")\n                    self.AUTO_METADATA_PROGRESS.emit(\"Using fallback source\")\n                    for hen in self._hen_list:\n                        if not self.error_galleries:\n                            break\n                        galleries = [x[0] for x in self.error_galleries]\n                        self.error_galleries.clear()\n                        \n                        valid_url = \"\"\n\n                        if hen == pewnet.ChaikaHen:\n                            valid_url = \"chaikahen\"\n                            log_i(\"using chaika hen\")\n                        try:\n                            self._auto_metadata_process(galleries, hen(), valid_url)\n                        except app_constants.MetadataFetchFail as err:\n                            fetch_cancelled(err)\n                            return\n\n            if not self.error_galleries:\n                self.AUTO_METADATA_PROGRESS.emit('Successfully fetched metadata! Went through {} galleries successfully!'.format(len(self.galleries)))\n                app_constants.SYSTEM_TRAY.showMessage('Successfully fetched metadata', 'Went through {} galleries successfully!'.format(len(self.galleries)), minimized=True)\n                self.FINISHED.emit(True)\n            else:\n                self.AUTO_METADATA_PROGRESS.emit('Finished fetching metadata! Could not fetch metadata for {} galleries. Check happypanda.log for more details!'.format(len(self.error_galleries)))\n                app_constants.SYSTEM_TRAY.showMessage('Finished fetching metadata',\n                                            'Could not fetch metadata for {} galleries. Check happypanda.log for more details!'.format(len(self.error_galleries)),\n                                            minimized=True)\n                for tup in self.error_galleries:\n                    log_e(\"{}: {}\".format(tup[1], tup[0].title.encode(errors='ignore')))\n                self.FINISHED.emit(self.error_galleries)\n            log_i('Auto metadata fetcher is done')\n            app_constants.GLOBAL_EHEN_LOCK = False\n        else:\n            log_e('Auto metadata fetcher is already running')\n            self.AUTO_METADATA_PROGRESS.emit('Auto metadata fetcher is already running!')\n            self.FINISHED.emit(False)\n\n"
  },
  {
    "path": "version/gallery.py",
    "content": "﻿#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport threading\nimport logging\nimport os\nimport math\nimport functools\nimport random\nimport datetime\nimport pickle\nimport enum\nimport time\nimport re as regex\n\nfrom PyQt5.QtCore import (Qt, QAbstractListModel, QModelIndex, QVariant,\n                          QSize, QRect, QEvent, pyqtSignal, QThread,\n                          QTimer, QPointF, QSortFilterProxyModel,\n                          QAbstractTableModel, QItemSelectionModel,\n                          QPoint, QRectF, QDate, QDateTime, QObject,\n                          QEvent, QSizeF, QMimeData, QByteArray, QTime)\nfrom PyQt5.QtGui import (QPixmap, QBrush, QColor, QPainter, \n                         QPen, QTextDocument,\n                         QMouseEvent, QHelpEvent,\n                         QPixmapCache, QCursor, QPalette, QKeyEvent,\n                         QFont, QTextOption, QFontMetrics, QFontMetricsF,\n                         QTextLayout, QPainterPath, QScrollPrepareEvent,\n                         QWheelEvent, QPolygonF, QLinearGradient)\nfrom PyQt5.QtWidgets import (QListView, QFrame, QLabel,\n                             QStyledItemDelegate, QStyle,\n                             QMenu, QAction, QToolTip, QVBoxLayout,\n                             QSizePolicy, QTableWidget, QScrollArea,\n                             QHBoxLayout, QFormLayout, QDesktopWidget,\n                             QWidget, QHeaderView, QTableView, QApplication,\n                             QMessageBox, QActionGroup, QScroller, QStackedLayout)\n\nfrom executors import Executors\nimport gallerydb\nimport app_constants\nimport misc\nimport gallerydialog\nimport io_misc\nimport utils\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\n# attempt at implementing treemodel\n#class TreeNode:\n#\tdef __init__(self, parent, row):\n#\t\tself.parent = parent\n#\t\tself.row = row\n#\t\tself.subnodes = self._get_children()\n\n#\tdef _get_children(self):\n#\t\traise NotImplementedError()\n\n#class GalleryInfoModel(QAbstractItemModel):\n#\tdef __init__(self, parent=None):\n#\t\tsuper().__init__(parent)\n#\t\tself.root_nodes = self._get_root_nodes()\n\n#\tdef _get_root_nodes(self):\n#\t\traise NotImplementedError()\n\n#\tdef index(self, row, column, parent):\n#\t\tif not parent.isValid():\n#\t\t\treturn self.createIndex(row, column, self.root_nodes[row])\n#\t\tparent_node = parent.internalPointer()\n#\t\treturn self.createIndex(row, column, parent_node[row])\n\n#\tdef parent(self, index):\n#\t\tif not index.isValid():\n#\t\t\treturn QModelIndex()\n\n#\t\tnode = index.internalPointer()\n#\t\tif not node.parent:\n#\t\t\treturn QModelIndex()\n#\t\telse:\n#\t\t\treturn self.createIndex(node.parent.row, 0, node.parent)\n\n#\tdef reset(self):\n#\t\tself.root_nodes = self._get_root_nodes()\n#\t\tsuper().resetInternalData()\n\n#\tdef rowCount(self, parent = QModelIndex()):\n#\t\tif not parent.isValid():\n#\t\t\treturn len(self.root_nodes)\n#\t\tnode = parent.internalPointer()\n#\t\treturn len(node.subnodes)\nclass GallerySearch(QObject):\n    FINISHED = pyqtSignal()\n    def __init__(self, data):\n        super().__init__()\n        self._data = data\n        self.result = {}\n\n        # filtering\n        self.fav = False\n        self._gallery_list = None\n\n    def set_gallery_list(self, g_list):\n        self._gallery_list = g_list\n\n    def set_data(self, new_data):\n        self._data = new_data\n        self.result = {g.id: True for g in self._data}\n\n    def set_fav(self, new_fav):\n        self.fav = new_fav\n\n    def search(self, term, args):\n        term = ' '.join(term.split())\n        search_pieces = utils.get_terms(term)\n\n        self._filter(search_pieces, args)\n        self.FINISHED.emit()\n\n    def _filter(self, terms, args):\n        self.result.clear()\n        for gallery in self._data:\n            if self.fav:\n                if not gallery.fav:\n                    continue\n            if self._gallery_list:\n                if not gallery in self._gallery_list:\n                    continue\n            all_terms = {t: False for t in terms}\n            allow = False\n            if utils.all_opposite(terms):\n                self.result[gallery.id] = True\n                continue\n            \n            for t in terms:\n                if gallery.contains(t, args):\n                    all_terms[t] = True\n\n            if all(all_terms.values()):\n                allow = True\n\n            self.result[gallery.id] = allow\n\nclass SortFilterModel(QSortFilterProxyModel):\n    ROWCOUNT_CHANGE = pyqtSignal()\n    _DO_SEARCH = pyqtSignal(str, object)\n    _CHANGE_SEARCH_DATA = pyqtSignal(list)\n    _CHANGE_FAV = pyqtSignal(bool)\n    _SET_GALLERY_LIST = pyqtSignal(object)\n\n    HISTORY_SEARCH_TERM = pyqtSignal(str)\n    # Navigate terms\n    NEXT, PREV = range(2)\n    # Views\n    CAT_VIEW, FAV_VIEW = range(2)\n\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.parent_widget = parent\n        self._data = app_constants.GALLERY_DATA\n        self._search_ready = False\n        self.current_term = ''\n        self._history_count = 50\n        self._prev_term = ''\n        self.terms_history = []\n        self.current_term_history = -1\n        self.current_gallery_list = None\n        self.current_args = []\n        self.current_view = self.CAT_VIEW\n        self.setDynamicSortFilter(True)\n        self.setFilterCaseSensitivity(Qt.CaseInsensitive)\n        self.setSortLocaleAware(True)\n        self.setSortCaseSensitivity(Qt.CaseInsensitive)\n        self.enable_drag = False\n\n    def navigate_history(self, direction=PREV):\n        new_term = ''\n        if self.terms_history:\n            if direction == self.NEXT:\n                if self.current_term_history < len(self.terms_history) - 1:\n                    self.current_term_history += 1\n            elif direction == self.PREV:\n                if self.current_term_history > 0:\n                    self.current_term_history -= 1\n            new_term = self.terms_history[self.current_term_history]\n            if new_term != self.current_term:\n                self.init_search(new_term, history=False)\n        return new_term\n\n    def set_gallery_list(self, g_list=None):\n        self.current_gallery_list = g_list\n        self._SET_GALLERY_LIST.emit(g_list)\n        self.refresh()\n\n    def fav_view(self):\n        self._CHANGE_FAV.emit(True)\n        self.refresh()\n        self.current_view = self.FAV_VIEW\n\n    def catalog_view(self):\n        self._CHANGE_FAV.emit(False)\n        self.refresh()\n        self.current_view = self.CAT_VIEW\n\n    def setup_search(self):\n        if not self._search_ready:\n            self.gallery_search = GallerySearch(self.sourceModel()._data)\n            self.gallery_search.FINISHED.connect(self.invalidateFilter)\n            self.gallery_search.FINISHED.connect(lambda: self.ROWCOUNT_CHANGE.emit())\n            self.gallery_search.moveToThread(app_constants.GENERAL_THREAD)\n            self._DO_SEARCH.connect(self.gallery_search.search)\n            self._SET_GALLERY_LIST.connect(self.gallery_search.set_gallery_list)\n            self._CHANGE_SEARCH_DATA.connect(self.gallery_search.set_data)\n            self._CHANGE_FAV.connect(self.gallery_search.set_fav)\n            self.sourceModel().rowsInserted.connect(self.refresh)\n            self._search_ready = True\n\n    def refresh(self):\n        self._DO_SEARCH.emit(self.current_term, self.current_args)\n\n    def init_search(self, term, args=None, **kwargs):\n        \"\"\"\n        Receives a search term and initiates a search\n        args should be a list of Search enums\n        \"\"\"\n        if not args:\n            args = self.current_args\n        history = kwargs.pop('history', True)\n        if history:\n            if self._prev_term != term:\n                self._prev_term = term\n\n                # ny path\n                if self.current_term_history != len(self.terms_history) - 1:\n                    self.terms_history = self.terms_history[:self.current_term_history+1]\n\n                if len(self.terms_history) > self._history_count:\n                    self.terms_history = self.terms_history[-self._history_count:]\n                self.terms_history.append(term)\n\n\n                self.current_term_history = len(self.terms_history) - 1\n                if self.current_term_history < 0:\n                    self.current_term_history = 0\n\n        self.current_term = term\n        if not history:\n            self.HISTORY_SEARCH_TERM.emit(term)\n        self.current_args = args\n        self._DO_SEARCH.emit(term, args)\n\n    def filterAcceptsRow(self, source_row, parent_index):\n        if self.sourceModel():\n            index = self.sourceModel().index(source_row, 0, parent_index)\n            if index.isValid():\n                if self._search_ready:\n                    gallery = index.data(Qt.UserRole + 1)\n                    try:\n                        return self.gallery_search.result[gallery.id]\n                    except KeyError:\n                        pass\n                else:\n                    return True\n        return False\n    \n    def change_model(self, model):\n        self.setSourceModel(model)\n        self._data = self.sourceModel()._data\n        self._CHANGE_SEARCH_DATA.emit(self._data)\n        self.refresh()\n\n    def change_data(self, data):\n        self._CHANGE_SEARCH_DATA.emit(data)\n\n    def status_b_msg(self, msg):\n        self.sourceModel().status_b_msg(msg)\n\n    def canDropMimeData(self, data, action, row, coloumn, index):\n        return False\n        if not data.hasFormat(\"list/gallery\"):\n            return False\n        return True\n\n    def dropMimeData(self, data, action, row, coloumn, index):\n        if not self.canDropMimeData(data, action, row, coloumn, index):\n            return False\n        if action == Qt.IgnoreAction:\n            return True\n        \n        # if the drop occured on an item\n        if not index.isValid():\n            return False\n\n        g_list = pickle.loads(data.data(\"list/gallery\").data())\n        item_g = index.data(GalleryModel.GALLERY_ROLE)\n        # ignore false positive\n        for g in g_list:\n            if g.id == item_g.id:\n                return False\n\n        txt = 'galleries' if len(g_list) > 1 else 'gallery'\n        msg = QMessageBox(self.parent_widget)\n        msg.setText(\"Are you sure you want to merge the galleries into this gallery as chapter(s)?\".format(txt))\n        msg.setStandardButtons(msg.Yes | msg.No)\n        if msg.exec() == msg.No:\n            return False\n        \n        # TODO: finish this\n\n        return True\n\n    def mimeTypes(self):\n        return ['list/gallery'] + super().mimeTypes()\n\n    def mimeData(self, index_list):\n        data = QMimeData()\n        g_list = []\n        for idx in index_list:\n            g = idx.data(GalleryModel.GALLERY_ROLE)\n            if g != None:\n                g_list.append(g)\n        data.setData(\"list/gallery\", QByteArray(pickle.dumps(g_list)))\n        return data\n\n    def flags(self, index):\n        default_flags = super().flags(index)\n        \n        if self.enable_drag:\n            if (index.isValid()):\n                return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | default_flags\n            else:\n                return Qt.ItemIsDropEnabled | default_flags\n        return default_flags\n\n    def supportedDragActions(self):\n        return Qt.ActionMask\n\nclass StarRating():\n    # enum EditMode\n    Editable, ReadOnly = range(2)\n\n    PaintingScaleFactor = 18\n\n    def __init__(self, starCount=1, maxStarCount=5):\n        self._starCount = starCount\n        self._maxStarCount = maxStarCount\n\n        self.starPolygon = QPolygonF([QPointF(1.0, 0.5)])\n        for i in range(5):\n            self.starPolygon << QPointF(0.5 + 0.5 * math.cos(0.8 * i * math.pi),\n                                        0.5 + 0.5 * math.sin(0.8 * i * math.pi))\n\n        self.diamondPolygon = QPolygonF()\n        self.diamondPolygon << QPointF(0.4, 0.5) \\\n                            << QPointF(0.5, 0.4) \\\n                            << QPointF(0.6, 0.5) \\\n                            << QPointF(0.5, 0.6) \\\n                            << QPointF(0.4, 0.5)\n\n    def starCount(self):\n        return self._starCount\n\n    def maxStarCount(self):\n        return self._maxStarCount\n\n    def setStarCount(self, starCount):\n        self._starCount = starCount\n\n    def setMaxStarCount(self, maxStarCount):\n        self._maxStarCount = maxStarCount\n\n    def sizeHint(self):\n        return self.PaintingScaleFactor * QSize(self._maxStarCount, 1)\n\n    def paint(self, painter, rect, editMode=ReadOnly):\n        painter.save()\n\n        painter.setRenderHint(QPainter.Antialiasing, True)\n        painter.setPen(Qt.NoPen)\n\n        painter.setBrush(QBrush(QColor(0, 0, 0, 100)))\n        painter.drawRoundedRect(QRectF(rect), 2, 2)\n\n        painter.setBrush(QBrush(Qt.yellow))\n\n        scaleFactor = self.PaintingScaleFactor\n        yOffset = (rect.height() - scaleFactor) / 2\n        painter.translate(rect.x(), rect.y() + yOffset)\n        painter.scale(scaleFactor, scaleFactor)\n\n        for i in range(self._maxStarCount):\n            if i < self._starCount:\n                painter.drawPolygon(self.starPolygon, Qt.WindingFill)\n            elif editMode == StarRating.Editable:\n                painter.drawPolygon(self.diamondPolygon, Qt.WindingFill)\n\n            painter.translate(1.0, 0.0)\n\n        painter.restore()\n\nclass GalleryModel(QAbstractTableModel):\n    \"\"\"\n    Model for Model/View/Delegate framework\n    \"\"\"\n    GALLERY_ROLE = Qt.UserRole + 1\n    ARTIST_ROLE = Qt.UserRole + 2\n    FAV_ROLE = Qt.UserRole + 3\n    DATE_ADDED_ROLE = Qt.UserRole + 4\n    PUB_DATE_ROLE = Qt.UserRole + 5\n    TIMES_READ_ROLE = Qt.UserRole + 6\n    LAST_READ_ROLE = Qt.UserRole + 7\n    TIME_ROLE = Qt.UserRole + 8\n    RATING_ROLE = Qt.UserRole + 9\n    RATING_COUNT = Qt.UserRole + 10\n\n    ROWCOUNT_CHANGE = pyqtSignal()\n    STATUSBAR_MSG = pyqtSignal(str)\n    CUSTOM_STATUS_MSG = pyqtSignal(str)\n    ADDED_ROWS = pyqtSignal()\n    ADD_MORE = pyqtSignal()\n\n    REMOVING_ROWS = False\n\n    def __init__(self, data, parent=None):\n        super().__init__(parent)\n        self.dataChanged.connect(lambda: self.status_b_msg(\"Edited\"))\n        self.dataChanged.connect(lambda: self.ROWCOUNT_CHANGE.emit())\n        self.layoutChanged.connect(lambda: self.ROWCOUNT_CHANGE.emit())\n        self.CUSTOM_STATUS_MSG.connect(self.status_b_msg)\n        self._TITLE = app_constants.TITLE\n        self._ARTIST = app_constants.ARTIST\n        self._TAGS = app_constants.TAGS\n        self._TYPE = app_constants.TYPE\n        self._FAV = app_constants.FAV\n        self._CHAPTERS = app_constants.CHAPTERS\n        self._LANGUAGE = app_constants.LANGUAGE\n        self._LINK = app_constants.LINK\n        self._DESCR = app_constants.DESCR\n        self._DATE_ADDED = app_constants.DATE_ADDED\n        self._PUB_DATE = app_constants.PUB_DATE\n\n        self._data = data\n        self._data_count = 0 # number of items added to model\n        self._gallery_to_add = []\n        self._gallery_to_remove = []\n\n    def status_b_msg(self, msg):\n        self.STATUSBAR_MSG.emit(msg)\n\n    def data(self, index, role=Qt.DisplayRole):\n        if not index.isValid():\n            return QVariant()\n        if index.row() >= len(self._data) or \\\n            index.row() < 0:\n            return QVariant()\n\n        current_row = index.row() \n        current_gallery = self._data[current_row]\n        current_column = index.column()\n\n        def column_checker():\n            if current_column == self._TITLE:\n                title = current_gallery.title\n                return title\n            elif current_column == self._ARTIST:\n                artist = current_gallery.artist\n                return artist\n            elif current_column == self._TAGS:\n                tags = utils.tag_to_string(current_gallery.tags)\n                return tags\n            elif current_column == self._TYPE:\n                type = current_gallery.type\n                return type\n            elif current_column == self._FAV:\n                if current_gallery.fav == 1:\n                    return u'\\u2605'\n                else:\n                    return ''\n            elif current_column == self._CHAPTERS:\n                return len(current_gallery.chapters)\n            elif current_column == self._LANGUAGE:\n                return current_gallery.language\n            elif current_column == self._LINK:\n                return current_gallery.link\n            elif current_column == self._DESCR:\n                return current_gallery.info\n            elif current_column == self._DATE_ADDED:\n                g_dt = \"{}\".format(current_gallery.date_added)\n                qdate_g_dt = QDateTime.fromString(g_dt, \"yyyy-MM-dd HH:mm:ss\")\n                return qdate_g_dt\n            elif current_column == self._PUB_DATE:\n                g_pdt = \"{}\".format(current_gallery.pub_date)\n                qdate_g_pdt = QDateTime.fromString(g_pdt, \"yyyy-MM-dd HH:mm:ss\")\n                if qdate_g_pdt.isValid():\n                    return qdate_g_pdt\n                else:\n                    return 'No date set'\n\n        # TODO: name all these roles and put them in app_constants...\n\n        if role == Qt.DisplayRole:\n            return column_checker()\n        # for artist searching\n        if role == self.ARTIST_ROLE:\n            artist = current_gallery.artist\n            return artist\n\n        if role == Qt.DecorationRole:\n            pixmap = current_gallery.profile\n            return pixmap\n        \n        if role == Qt.BackgroundRole:\n            bg_color = QColor(242, 242, 242)\n            bg_brush = QBrush(bg_color)\n            return bg_color\n\n        if app_constants.GRID_TOOLTIP and role == Qt.ToolTipRole:\n            add_bold = []\n            add_tips = []\n            if app_constants.TOOLTIP_TITLE:\n                add_bold.append('<b>Title:</b>')\n                add_tips.append(current_gallery.title)\n            if app_constants.TOOLTIP_AUTHOR:\n                add_bold.append('<b>Author:</b>')\n                add_tips.append(current_gallery.artist)\n            if app_constants.TOOLTIP_CHAPTERS:\n                add_bold.append('<b>Chapters:</b>')\n                add_tips.append(len(current_gallery.chapters))\n            if app_constants.TOOLTIP_STATUS:\n                add_bold.append('<b>Status:</b>')\n                add_tips.append(current_gallery.status)\n            if app_constants.TOOLTIP_TYPE:\n                add_bold.append('<b>Type:</b>')\n                add_tips.append(current_gallery.type)\n            if app_constants.TOOLTIP_LANG:\n                add_bold.append('<b>Language:</b>')\n                add_tips.append(current_gallery.language)\n            if app_constants.TOOLTIP_DESCR:\n                add_bold.append('<b>Description:</b><br />')\n                add_tips.append(current_gallery.info)\n            if app_constants.TOOLTIP_TAGS:\n                add_bold.append('<b>Tags:</b>')\n                add_tips.append(utils.tag_to_string(current_gallery.tags))\n            if app_constants.TOOLTIP_LAST_READ:\n                add_bold.append('<b>Last read:</b>')\n                add_tips.append('{} ago'.format(utils.get_date_age(current_gallery.last_read)) if current_gallery.last_read else \"Never!\")\n            if app_constants.TOOLTIP_TIMES_READ:\n                add_bold.append('<b>Times read:</b>')\n                add_tips.append(current_gallery.times_read)\n            if app_constants.TOOLTIP_PUB_DATE:\n                add_bold.append('<b>Publication Date:</b>')\n                add_tips.append('{}'.format(current_gallery.pub_date).split(' ')[0])\n            if app_constants.TOOLTIP_DATE_ADDED:\n                add_bold.append('<b>Date added:</b>')\n                add_tips.append('{}'.format(current_gallery.date_added).split(' ')[0])\n\n            tooltip = \"\"\n            tips = list(zip(add_bold, add_tips))\n            for tip in tips:\n                tooltip += \"{} {}<br />\".format(tip[0], tip[1])\n            return tooltip\n\n        if role == self.GALLERY_ROLE:\n            return current_gallery\n\n        # favorite satus\n        if role == self.FAV_ROLE:\n            return current_gallery.fav\n\n        if role == self.DATE_ADDED_ROLE:\n            date_added = \"{}\".format(current_gallery.date_added)\n            qdate_added = QDateTime.fromString(date_added, \"yyyy-MM-dd HH:mm:ss\")\n            return qdate_added\n        \n        if role == self.PUB_DATE_ROLE:\n            if current_gallery.pub_date:\n                pub_date = \"{}\".format(current_gallery.pub_date)\n                qpub_date = QDateTime.fromString(pub_date, \"yyyy-MM-dd HH:mm:ss\")\n                return qpub_date\n\n        if role == self.TIMES_READ_ROLE:\n            return current_gallery.times_read\n\n        if role == self.LAST_READ_ROLE:\n            if current_gallery.last_read:\n                last_read = \"{}\".format(current_gallery.last_read)\n                qlast_read = QDateTime.fromString(last_read, \"yyyy-MM-dd HH:mm:ss\")\n                return qlast_read\n\n        if role == self.TIME_ROLE:\n            return current_gallery.qtime\n\n        if role == self.RATING_ROLE:\n            return StarRating(current_gallery.rating)\n\n        if role == self.RATING_COUNT:\n            return current_gallery.rating\n\n        return None\n\n    def rowCount(self, index=QModelIndex()):\n        if index.isValid():\n            return 0\n        return len(self._data)\n\n    def columnCount(self, parent=QModelIndex()):\n        return len(app_constants.COLUMNS)\n\n    def headerData(self, section, orientation, role=Qt.DisplayRole):\n        if role == Qt.TextAlignmentRole:\n            return Qt.AlignLeft\n        if role != Qt.DisplayRole:\n            return None\n        if orientation == Qt.Horizontal:\n            if section == self._TITLE:\n                return 'Title'\n            elif section == self._ARTIST:\n                return 'Author'\n            elif section == self._TAGS:\n                return 'Tags'\n            elif section == self._TYPE:\n                return 'Type'\n            elif section == self._FAV:\n                return u'\\u2605'\n            elif section == self._CHAPTERS:\n                return 'Chapters'\n            elif section == self._LANGUAGE:\n                return 'Language'\n            elif section == self._LINK:\n                return 'URL'\n            elif section == self._DESCR:\n                return 'Description'\n            elif section == self._DATE_ADDED:\n                return 'Date Added'\n            elif section == self._PUB_DATE:\n                return 'Published'\n        return section + 1\n\n\n    def insertRows(self, position, rows, index=QModelIndex()):\n        self._data_count += rows\n        if not self._gallery_to_add:\n            return False\n\n        self.beginInsertRows(QModelIndex(), position, position + rows - 1)\n        for r in range(rows):\n            self._data.insert(position, self._gallery_to_add.pop())\n        self.endInsertRows()\n        return True\n\n    def replaceRows(self, list_of_gallery, position, rows=1, index=QModelIndex()):\n        \"replaces gallery data to the data list WITHOUT adding to DB\"\n        for pos, gallery in enumerate(list_of_gallery):\n            del self._data[position + pos]\n            self._data.insert(position + pos, gallery)\n        self.dataChanged.emit(index, index, [Qt.UserRole + 1, Qt.DecorationRole])\n\n    def removeRows(self, position, rows, index=QModelIndex()):\n        self._data_count -= rows\n        self.beginRemoveRows(QModelIndex(), position, position + rows - 1)\n        for r in range(rows):\n            try:\n                self._data.remove(self._gallery_to_remove.pop())\n            except ValueError:\n                return False\n        self.endRemoveRows()\n        return True\n\nclass GridDelegate(QStyledItemDelegate):\n    \"A custom delegate for the model/view framework\"\n\n    POPUP = pyqtSignal()\n    CONTEXT_ON = False\n\n    def __init__(self, app_inst, parent):\n        super().__init__(parent)\n        QPixmapCache.setCacheLimit(app_constants.THUMBNAIL_CACHE_SIZE[0] * app_constants.THUMBNAIL_CACHE_SIZE[1])\n        self._painted_indexes = {}\n        self.view = parent\n        self.parent_widget = app_inst\n        self._paint_level = 0\n\n        #misc.FileIcon.refresh_default_icon()\n        self.file_icons = misc.FileIcon()\n        if app_constants.USE_EXTERNAL_VIEWER:\n            self.external_icon = self.file_icons.get_external_file_icon()\n        else:\n            self.external_icon = self.file_icons.get_default_file_icon()\n\n        self.font_size = app_constants.GALLERY_FONT[1]\n        self.font_name = 0 # app_constants.GALLERY_FONT[0]\n        if not self.font_name:\n            self.font_name = QWidget().font().family()\n        self.title_font = QFont()\n        self.title_font.setBold(True)\n        self.title_font.setFamily(self.font_name)\n        self.artist_font = QFont()\n        self.artist_font.setFamily(self.font_name)\n        if self.font_size is not 0:\n            self.title_font.setPixelSize(self.font_size)\n            self.artist_font.setPixelSize(self.font_size)\n        self.title_font_m = QFontMetrics(self.title_font)\n        self.artist_font_m = QFontMetrics(self.artist_font)\n        t_h = self.title_font_m.height()\n        a_h = self.artist_font_m.height()\n        self.text_label_h = a_h + t_h * 2\n        self.W = app_constants.THUMB_W_SIZE\n        self.H = app_constants.THUMB_H_SIZE + app_constants.GRIDBOX_LBL_H\n\n    def key(self, key):\n        \"Assigns an unique key to indexes\"\n        if key in self._painted_indexes:\n            return self._painted_indexes[key]\n        else:\n            k = str(key)\n            self._painted_indexes[key] = k\n            return k\n\n    def _increment_paint_level(self):\n        self._paint_level += 1\n        self.view.update()\n\n    def paint(self, painter, option, index):\n        assert isinstance(painter, QPainter)\n        rec = option.rect.getRect()\n        x = rec[0]\n        y = rec[1]\n        w = rec[2]\n        h = rec[3]\n        if self._paint_level:\n            #if app_constants.HIGH_QUALITY_THUMBS:\n            #\tpainter.setRenderHint(QPainter.SmoothPixmapTransform)\n            painter.setRenderHint(QPainter.Antialiasing)\n            gallery = index.data(Qt.UserRole + 1)\n            star_rating = index.data(GalleryModel.RATING_ROLE)\n            title = gallery.title\n            artist = gallery.artist\n            title_color = app_constants.GRID_VIEW_TITLE_COLOR\n            artist_color = app_constants.GRID_VIEW_ARTIST_COLOR\n            label_color = app_constants.GRID_VIEW_LABEL_COLOR\n            # Enable this to see the defining box\n            #painter.drawRect(option.rect)\n            # define font size\n            if 20 > len(title) > 15:\n                title_size = \"font-size:{}px;\".format(self.font_size)\n            elif 30 > len(title) > 20:\n                title_size = \"font-size:{}px;\".format(self.font_size - 1)\n            elif 40 > len(title) >= 30:\n                title_size = \"font-size:{}px;\".format(self.font_size - 2)\n            elif 50 > len(title) >= 40:\n                title_size = \"font-size:{}px;\".format(self.font_size - 3)\n            elif len(title) >= 50:\n                title_size = \"font-size:{}px;\".format(self.font_size - 4)\n            else:\n                title_size = \"font-size:{}px;\".format(self.font_size)\n\n            if 30 > len(artist) > 20:\n                artist_size = \"font-size:{}px;\".format(self.font_size)\n            elif 40 > len(artist) >= 30:\n                artist_size = \"font-size:{}px;\".format(self.font_size - 1)\n            elif len(artist) >= 40:\n                artist_size = \"font-size:{}px;\".format(self.font_size - 2)\n            else:\n                artist_size = \"font-size:{}px;\".format(self.font_size)\n\n            text_area = QTextDocument()\n            text_area.setDefaultFont(option.font)\n            text_area.setHtml(\"\"\"\n            <head>\n            <style>\n            #area\n            {{\n                display:flex;\n                width:{6}px;\n                height:{7}px\n            }}\n            #title {{\n            position:absolute;\n            color: {4};\n            font-weight:bold;\n            {0}\n            }}\n            #artist {{\n            position:absolute;\n            color: {5};\n            top:20px;\n            right:0;\n            {1}\n            }}\n            </style>\n            </head>\n            <body>\n            <div id=\"area\">\n            <center>\n            <div id=\"title\">{2}\n            </div>\n            <div id=\"artist\">{3}\n            </div>\n            </div>\n            </center>\n            </body>\n            \"\"\".format(title_size, artist_size, title, artist, title_color, artist_color,\n              130 + app_constants.SIZE_FACTOR, 1 + app_constants.SIZE_FACTOR))\n            text_area.setTextWidth(w)\n\n            #chapter_area = QTextDocument()\n            #chapter_area.setDefaultFont(option.font)\n            #chapter_area.setHtml(\"\"\"\n            #<font color=\"black\">{}</font>\n            #\"\"\".format(\"chapter\"))\n            #chapter_area.setTextWidth(w)\n            def center_img(width):\n                new_x = x\n                if width < w:\n                    diff = w - width\n                    offset = diff // 2\n                    new_x += offset\n                return new_x\n\n            def img_too_big(start_x):\n                txt_layout = misc.text_layout(\"Thumbnail regeneration needed!\", w, self.title_font, self.title_font_m)\n\n                clipping = QRectF(x, y + h // 4, w, app_constants.GRIDBOX_LBL_H - 10)\n                txt_layout.draw(painter, QPointF(x, y + h // 4),\n                      clip=clipping)\n\n            loaded_image = gallery.get_profile(app_constants.ProfileType.Default)\n            if loaded_image and self._paint_level > 0 and self.view.scroll_speed < 600:\n                # if we can't find a cached image\n                pix_cache = QPixmapCache.find(self.key(loaded_image.cacheKey()))\n                if isinstance(pix_cache, QPixmap):\n                    self.image = pix_cache\n                    img_x = center_img(self.image.width())\n                    if self.image.width() > w or self.image.height() > h:\n                        img_too_big(img_x)\n                    else:\n                        if self.image.height() < self.image.width(): #to keep aspect ratio\n                            painter.drawPixmap(QPoint(img_x,y),\n                                    self.image)\n                        else:\n                            painter.drawPixmap(QPoint(img_x,y),\n                                    self.image)\n                else:\n                    self.image = QPixmap.fromImage(loaded_image)\n                    img_x = center_img(self.image.width())\n                    QPixmapCache.insert(self.key(loaded_image.cacheKey()), self.image)\n                    if self.image.width() > w or self.image.height() > h:\n                        img_too_big(img_x)\n                    else:\n                        if self.image.height() < self.image.width(): #to keep aspect ratio\n                            painter.drawPixmap(QPoint(img_x,y),\n                                    self.image)\n                        else:\n                            painter.drawPixmap(QPoint(img_x,y),\n                                    self.image)\n            else:\n\n                painter.save()\n                painter.setPen(QColor(164,164,164,200))\n                if gallery.profile:\n                    thumb_text = \"Loading...\"\n                else:\n                    thumb_text = \"Thumbnail regeneration needed!\"\n                txt_layout = misc.text_layout(thumb_text, w, self.title_font, self.title_font_m)\n\n                clipping = QRectF(x, y + h // 4, w, app_constants.GRIDBOX_LBL_H - 10)\n                txt_layout.draw(painter, QPointF(x, y + h // 4),\n                      clip=clipping)\n                painter.restore()\n\n            # draw ribbon type\n            painter.save()\n            painter.setPen(Qt.NoPen)\n            if app_constants.DISPLAY_GALLERY_RIBBON:\n                type_ribbon_w = type_ribbon_l = w * 0.11\n                rib_top_1 = QPointF(x + w - type_ribbon_l - type_ribbon_w, y)\n                rib_top_2 = QPointF(x + w - type_ribbon_l, y)\n                rib_side_1 = QPointF(x + w, y + type_ribbon_l)\n                rib_side_2 = QPointF(x + w, y + type_ribbon_l + type_ribbon_w)\n                ribbon_polygon = QPolygonF([rib_top_1, rib_top_2, rib_side_1, rib_side_2])\n                ribbon_path = QPainterPath()\n                ribbon_path.setFillRule(Qt.WindingFill)\n                ribbon_path.addPolygon(ribbon_polygon)\n                ribbon_path.closeSubpath()\n                painter.setBrush(QBrush(QColor(self._ribbon_color(gallery.type))))\n                painter.drawPath(ribbon_path)\n\n            # draw if favourited\n            if gallery.fav == 1:\n                star_ribbon_w = w * 0.1\n                star_ribbon_l = w * 0.08\n                rib_top_1 = QPointF(x + star_ribbon_l, y)\n                rib_side_1 = QPointF(x, y + star_ribbon_l)\n                rib_top_2 = QPointF(x + star_ribbon_l + star_ribbon_w, y)\n                rib_side_2 = QPointF(x, y + star_ribbon_l + star_ribbon_w)\n                rib_star_mid_1 = QPointF((rib_top_1.x() + rib_side_1.x()) / 2, (rib_top_1.y() + rib_side_1.y()) / 2)\n                rib_star_factor = star_ribbon_l / 4\n                rib_star_p1_1 = rib_star_mid_1 + QPointF(rib_star_factor, -rib_star_factor)\n                rib_star_p1_2 = rib_star_p1_1 + QPointF(-rib_star_factor, -rib_star_factor)\n                rib_star_p1_3 = rib_star_mid_1 + QPointF(-rib_star_factor, rib_star_factor)\n                rib_star_p1_4 = rib_star_p1_3 + QPointF(-rib_star_factor, -rib_star_factor)\n\n                crown_1 = QPolygonF([rib_star_p1_1, rib_star_p1_2, rib_star_mid_1, rib_star_p1_4, rib_star_p1_3])\n                painter.setBrush(QBrush(QColor(255, 255, 0, 200)))\n                painter.drawPolygon(crown_1)\n\n                ribbon_polygon = QPolygonF([rib_top_1, rib_side_1, rib_side_2, rib_top_2])\n                ribbon_path = QPainterPath()\n                ribbon_path.setFillRule(Qt.WindingFill)\n                ribbon_path.addPolygon(ribbon_polygon)\n                ribbon_path.closeSubpath()\n                painter.drawPath(ribbon_path)\n                painter.setPen(QColor(255, 0, 0, 100))\n                painter.drawPolyline(rib_top_1, rib_star_p1_1, rib_star_p1_2, rib_star_mid_1, rib_star_p1_4, rib_star_p1_3, rib_side_1)\n                painter.drawLine(rib_top_1, rib_top_2)\n                painter.drawLine(rib_top_2, rib_side_2)\n                painter.drawLine(rib_side_1, rib_side_2)\n            painter.restore()\n\n            if self._paint_level > 0:\n                if app_constants._REFRESH_EXTERNAL_VIEWER:\n                    if app_constants.USE_EXTERNAL_VIEWER:\n                        self.external_icon = self.file_icons.get_external_file_icon()\n                    else:\n                        self.external_icon = self.file_icons.get_default_file_icon()\n            \n\n                type_w = painter.fontMetrics().width(gallery.file_type)\n                type_h = painter.fontMetrics().height()\n                type_p = QPoint(x + 4, y + app_constants.THUMB_H_SIZE - type_h - 5)\n                type_rect = QRect(type_p.x() - 2, type_p.y() - 1, type_w + 4, type_h + 1)\n                if app_constants.DISPLAY_GALLERY_TYPE:\n                    type_color = QColor(239, 0, 0, 200)\n                    if gallery.file_type == \"zip\":\n                        type_color = QColor(241, 0, 83, 200)\n                    elif gallery.file_type == \"cbz\":\n                        type_color = QColor(0, 139, 0, 200)\n                    elif gallery.file_type == \"rar\":\n                        type_color = QColor(30, 127, 150, 200)\n                    elif gallery.file_type == \"cbr\":\n                        type_color = QColor(210, 0, 13, 200)\n\n                    painter.save()\n                    painter.setPen(QPen(Qt.white))\n                    painter.fillRect(type_rect, type_color)\n                    painter.drawText(type_p.x(), type_p.y() + painter.fontMetrics().height() - 4, gallery.file_type)\n                    painter.restore()\n                \n\n                if app_constants.DISPLAY_RATING and gallery.rating:\n                    star_start_x = type_rect.x()+type_rect.width() if app_constants.DISPLAY_GALLERY_TYPE else x\n                    star_width = star_rating.sizeHint().width()\n                    star_start_x += ((x+w-star_start_x)-(star_width))/2\n                    star_rating.paint(painter,\n                        QRect(star_start_x, type_rect.y(), star_width, type_rect.height()))\n\n            if gallery.state == app_constants.GalleryState.New:\n                painter.save()\n                painter.setPen(Qt.NoPen)\n                gradient = QLinearGradient()\n                gradient.setStart(x, y + app_constants.THUMB_H_SIZE / 2)\n                gradient.setFinalStop(x, y + app_constants.THUMB_H_SIZE)\n                gradient.setColorAt(0, QColor(255, 255, 255, 0))\n                gradient.setColorAt(1, QColor(0, 255, 0, 150))\n                painter.setBrush(QBrush(gradient))\n                painter.drawRoundedRect(QRectF(x, y + app_constants.THUMB_H_SIZE / 2, w, app_constants.THUMB_H_SIZE / 2), 2, 2)\n                painter.restore()\n\n            def draw_text_label(lbl_h):\n                #draw the label for text\n                painter.save()\n                painter.translate(x, y + app_constants.THUMB_H_SIZE)\n                box_color = QBrush(QColor(label_color))#QColor(0,0,0,123))\n                painter.setBrush(box_color)\n                rect = QRect(0, 0, w, lbl_h) #x, y, width, height\n                painter.fillRect(rect, box_color)\n                painter.restore()\n                return rect\n\n            if option.state & QStyle.State_MouseOver or \\\n                option.state & QStyle.State_Selected:\n                title_layout = misc.text_layout(title, w, self.title_font, self.title_font_m)\n                artist_layout = misc.text_layout(artist, w, self.artist_font, self.artist_font_m)\n                t_h = title_layout.boundingRect().height()\n                a_h = artist_layout.boundingRect().height()\n\n                if app_constants.GALLERY_FONT_ELIDE:\n                    lbl_rect = draw_text_label(min(t_h + a_h + 3, app_constants.GRIDBOX_LBL_H))\n                else:\n                    lbl_rect = draw_text_label(app_constants.GRIDBOX_LBL_H)\n\n                clipping = QRectF(x, y + app_constants.THUMB_H_SIZE, w, app_constants.GRIDBOX_LBL_H - 10)\n                painter.setPen(QColor(title_color))\n                title_layout.draw(painter, QPointF(x, y + app_constants.THUMB_H_SIZE),\n                      clip=clipping)\n                painter.setPen(QColor(artist_color))\n                artist_layout.draw(painter, QPointF(x, y + app_constants.THUMB_H_SIZE + t_h),\n                       clip=clipping)\n                #painter.fillRect(option.rect, QColor)\n            else:\n                if app_constants.GALLERY_FONT_ELIDE:\n                    lbl_rect = draw_text_label(self.text_label_h)\n                else:\n                    lbl_rect = draw_text_label(app_constants.GRIDBOX_LBL_H)\n                # draw text\n                painter.save()\n                alignment = QTextOption(Qt.AlignCenter)\n                alignment.setUseDesignMetrics(True)\n                title_rect = QRectF(0,0,w, self.title_font_m.height())\n                artist_rect = QRectF(0,self.artist_font_m.height(),w,\n                         self.artist_font_m.height())\n                painter.translate(x, y + app_constants.THUMB_H_SIZE)\n                if app_constants.GALLERY_FONT_ELIDE:\n                    painter.setFont(self.title_font)\n                    painter.setPen(QColor(title_color))\n                    painter.drawText(title_rect,\n                             self.title_font_m.elidedText(title, Qt.ElideRight, w - 10),\n                             alignment)\n                \n                    painter.setPen(QColor(artist_color))\n                    painter.setFont(self.artist_font)\n                    alignment.setWrapMode(QTextOption.NoWrap)\n                    painter.drawText(artist_rect,\n                                self.title_font_m.elidedText(artist, Qt.ElideRight, w - 10),\n                                alignment)\n                else:\n                    text_area.setDefaultFont(QFont(self.font_name))\n                    text_area.drawContents(painter)\n                ##painter.resetTransform()\n                painter.restore()\n\n            if option.state & QStyle.State_Selected:\n                painter.save()\n                selected_rect = QRectF(x, y, w, lbl_rect.height() + app_constants.THUMB_H_SIZE)\n                painter.setPen(Qt.NoPen)\n                painter.setBrush(QBrush(QColor(164,164,164,120)))\n                painter.drawRoundedRect(selected_rect, 5, 5)\n                #painter.fillRect(selected_rect, QColor(164,164,164,120))\n                painter.restore()\n\n            def warning(txt):\n                painter.save()\n                selected_rect = QRectF(x, y, w, lbl_rect.height() + app_constants.THUMB_H_SIZE)\n                painter.setPen(Qt.NoPen)\n                painter.setBrush(QBrush(QColor(255,0,0,120)))\n                p_path = QPainterPath()\n                p_path.setFillRule(Qt.WindingFill)\n                p_path.addRoundedRect(selected_rect, 5,5)\n                p_path.addRect(x,y, 20, 20)\n                p_path.addRect(x + w - 20,y, 20, 20)\n                painter.drawPath(p_path.simplified())\n                painter.setPen(QColor(\"white\"))\n                txt_layout = misc.text_layout(txt, w, self.title_font, self.title_font_m)\n                txt_layout.draw(painter, QPointF(x, y + h * 0.3))\n                painter.restore()\n\n            if not gallery.id and self.view.view_type != app_constants.ViewType.Addition:\n                warning(\"This gallery does not exist anymore!\")\n            elif gallery.dead_link:\n                warning(\"Cannot find gallery source!\")\n\n\n            if app_constants.DEBUG or self.view.view_type == app_constants.ViewType.Duplicate:\n                painter.save()\n                painter.setPen(QPen(Qt.white))\n                id_txt = \"ID: {}\".format(gallery.id)\n                type_w = painter.fontMetrics().width(id_txt)\n                type_h = painter.fontMetrics().height()\n                type_p = QPoint(x + 4, y + 50 - type_h - 5)\n                type_rect = QRect(type_p.x() - 2, type_p.y() - 1, type_w + 4, type_h + 1)\n                painter.fillRect(type_rect, QColor(239, 0, 0, 200))\n                painter.drawText(type_p.x(), type_p.y() + painter.fontMetrics().height() - 4, id_txt)\n                painter.restore()\n\n            if option.state & QStyle.State_Selected:\n                painter.setPen(QPen(option.palette.highlightedText().color()))\n        else:\n            painter.fillRect(option.rect, QColor(164,164,164,100))\n            painter.setPen(QColor(164,164,164,200))\n            txt_layout = misc.text_layout(\"Fetching...\", w, self.title_font, self.title_font_m)\n\n            clipping = QRectF(x, y + h // 4, w, app_constants.GRIDBOX_LBL_H - 10)\n            txt_layout.draw(painter, QPointF(x, y + h // 4),\n                    clip=clipping)\n\n    def _ribbon_color(self, gallery_type):\n        if gallery_type:\n            gallery_type = gallery_type.lower()\n        if gallery_type == \"manga\":\n            return app_constants.GRID_VIEW_T_MANGA_COLOR\n        elif gallery_type == \"doujinshi\":\n            return app_constants.GRID_VIEW_T_DOUJIN_COLOR\n        elif \"artist cg\" in gallery_type:\n            return app_constants.GRID_VIEW_T_ARTIST_CG_COLOR\n        elif \"game cg\" in gallery_type:\n            return app_constants.GRID_VIEW_T_GAME_CG_COLOR\n        elif gallery_type == \"western\":\n            return app_constants.GRID_VIEW_T_WESTERN_COLOR\n        elif \"image\" in gallery_type:\n            return app_constants.GRID_VIEW_T_IMAGE_COLOR\n        elif gallery_type == \"non-h\":\n            return app_constants.GRID_VIEW_T_NON_H_COLOR\n        elif gallery_type == \"cosplay\":\n            return app_constants.GRID_VIEW_T_COSPLAY_COLOR\n        else:\n            return app_constants.GRID_VIEW_T_OTHER_COLOR\n\n    def sizeHint(self, option, index):\n        return QSize(self.W, self.H)\n\nclass MangaView(QListView):\n    \"\"\"\n    Grid View\n    \"\"\"\n\n    STATUS_BAR_MSG = pyqtSignal(str)\n\n    def __init__(self, model, v_type, filter_model=None, parent=None):\n        super().__init__(parent)\n        self.parent_widget = parent\n        self.view_type = v_type\n        self.setViewMode(self.IconMode)\n        self.setResizeMode(self.Adjust)\n        self.setWrapping(True)\n        # all items have the same size (perfomance)\n        self.setUniformItemSizes(True)\n        # improve scrolling\n        self.setAutoScroll(True)\n        self.setVerticalScrollMode(self.ScrollPerPixel)\n        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.setLayoutMode(self.Batched)\n        self.setMouseTracking(True)\n        self.setAcceptDrops(True)\n        self.setDragEnabled(True)\n        self.viewport().setAcceptDrops(True)\n        self.setDropIndicatorShown(True)\n        self.setDragDropMode(self.DragDrop)\n        self.sort_model = filter_model if filter_model else SortFilterModel(self)\n        self.manga_delegate = GridDelegate(parent, self)\n        self.setItemDelegate(self.manga_delegate)\n        self.setSpacing(app_constants.GRID_SPACING)\n        self.setFlow(QListView.LeftToRight)\n        self.setIconSize(QSize(self.manga_delegate.W, self.manga_delegate.H))\n        self.setSelectionBehavior(self.SelectItems)\n        self.setSelectionMode(self.ExtendedSelection)\n        self.gallery_model = model\n        self.sort_model.change_model(self.gallery_model)\n        self.sort_model.sort(0)\n        self.setModel(self.sort_model)\n        self.doubleClicked.connect(lambda idx: idx.data(Qt.UserRole + 1).chapters[0].open())\n        self.setViewportMargins(0,0,0,0)\n\n        self.gallery_window = misc.GalleryMetaWindow(parent if parent else self)\n        self.gallery_window.arrow_size = (10,10,)\n        self.clicked.connect(lambda idx: self.gallery_window.show_gallery(idx, self))\n\n        self.current_sort = app_constants.CURRENT_SORT\n        if self.view_type == app_constants.ViewType.Duplicate:\n            self.sort_model.setSortRole(GalleryModel.TIME_ROLE)\n        else:\n            self.sort(self.current_sort)\n        if app_constants.DEBUG:\n            def debug_print(a):\n                g = a.data(Qt.UserRole + 1)\n                try:\n                    print(g)\n                except:\n                    print(\"{}\".format(g).encode(errors='ignore'))\n                #log_d(gallerydb.HashDB.gen_gallery_hash(g, 0, 'mid')['mid'])\n\n            self.clicked.connect(debug_print)\n\n        self.k_scroller = QScroller.scroller(self)\n        self._scroll_speed_timer = QTimer(self)\n        self._scroll_speed_timer.timeout.connect(self._calculate_scroll_speed)\n        self._scroll_speed_timer.setInterval(500) # ms\n        self._old_scroll_value = 0\n        self._scroll_zero_once = True\n        self._scroll_speed = 0\n        self._scroll_speed_timer.start()\n\n    @property\n    def scroll_speed(self):\n        return self._scroll_speed\n\n    def _calculate_scroll_speed(self):\n        new_value = self.verticalScrollBar().value()\n        self._scroll_speed = abs(self._old_scroll_value - new_value)\n        self._old_scroll_value = new_value\n        \n        if self.verticalScrollBar().value() in (0, self.verticalScrollBar().maximum()):\n            self._scroll_zero_once = True\n\n        if self._scroll_zero_once:\n            self.update()\n            self._scroll_zero_once = False\n\n        # update view if not scrolling\n        if new_value < 400 and self._old_scroll_value > 400:\n            self.update()\n\n\n    def get_visible_indexes(self, column=0):\n        \"find all galleries in viewport\"\n        gridW = self.manga_delegate.W + app_constants.GRID_SPACING * 2\n        gridH = self.manga_delegate.H + app_constants.GRID_SPACING * 2\n        region = self.viewport().visibleRegion()\n        idx_found = []\n\n        def idx_is_visible(idx):\n            idx_rect = self.visualRect(idx)\n            return region.contains(idx_rect) or region.intersects(idx_rect)\n\n        #get first index\n        first_idx = self.indexAt(QPoint(gridW // 2, 0)) # to get indexes on the way out of view\n        if not first_idx.isValid():\n            first_idx = self.indexAt(QPoint(gridW // 2, gridH // 2))\n\n        if first_idx.isValid():\n            nxt_idx = first_idx\n            # now traverse items until index isn't visible\n            while(idx_is_visible(nxt_idx)):\n                idx_found.append(nxt_idx)\n                nxt_idx = nxt_idx.sibling(nxt_idx.row() + 1, column)\n            \n        return idx_found\n\n    def wheelEvent(self, event):\n        if self.gallery_window.isVisible():\n            self.gallery_window.hide_animation.start()\n        return super().wheelEvent(event)\n\n    def mouseMoveEvent(self, event):\n        self.gallery_window.mouseMoveEvent(event)\n        return super().mouseMoveEvent(event)\n\n    def keyPressEvent(self, event):\n        if event.key() == Qt.Key_Return:\n            s_idx = self.selectedIndexes()\n            if s_idx:\n                for idx in s_idx:\n                    self.doubleClicked.emit(idx)\n        elif event.modifiers() == Qt.ShiftModifier and event.key() == Qt.Key_Delete:\n            CommonView.remove_selected(self, True)\n        elif event.key() == Qt.Key_Delete:\n            CommonView.remove_selected(self)\n        return super().keyPressEvent(event)\n\n    def favorite(self, index):\n        assert isinstance(index, QModelIndex)\n        gallery = index.data(Qt.UserRole + 1)\n        if gallery.fav == 1:\n            gallery.fav = 0\n            #self.model().replaceRows([gallery], index.row(), 1, index)\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, gallery.id, {'fav':0})\n            self.gallery_model.CUSTOM_STATUS_MSG.emit(\"Unfavorited\")\n        else:\n            gallery.fav = 1\n            gallery.rating = 5\n            #self.model().replaceRows([gallery], index.row(), 1, index)\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, gallery.id, {'fav':1, 'rating':5})\n            self.gallery_model.CUSTOM_STATUS_MSG.emit(\"Favorited\")\n\n    def del_chapter(self, index, chap_numb):\n        gallery = index.data(Qt.UserRole + 1)\n        if len(gallery.chapters) < 2:\n            CommonView.remove_gallery(self, [index])\n        else:\n            msgbox = QMessageBox(self)\n            msgbox.setText('Are you sure you want to delete:')\n            msgbox.setIcon(msgbox.Question)\n            msgbox.setInformativeText('Chapter {} of {}'.format(chap_numb + 1,\n                                                          gallery.title))\n            msgbox.setStandardButtons(msgbox.Yes | msgbox.No)\n            if msgbox.exec() == msgbox.Yes:\n                gallery.chapters.pop(chap_numb, None)\n                self.gallery_model.replaceRows([gallery], index.row())\n                gallerydb.execute(gallerydb.ChapterDB.del_chapter, True, gallery.id, chap_numb)\n\n    def sort(self, name):\n        if not self.view_type == app_constants.ViewType.Duplicate:\n            if name == 'title':\n                self.sort_model.setSortRole(Qt.DisplayRole)\n                self.sort_model.sort(0, Qt.AscendingOrder)\n                self.current_sort = 'title'\n            elif name == 'artist':\n                self.sort_model.setSortRole(GalleryModel.ARTIST_ROLE)\n                self.sort_model.sort(0, Qt.AscendingOrder)\n                self.current_sort = 'artist'\n            elif name == 'date_added':\n                self.sort_model.setSortRole(GalleryModel.DATE_ADDED_ROLE)\n                self.sort_model.sort(0, Qt.DescendingOrder)\n                self.current_sort = 'date_added'\n            elif name == 'pub_date':\n                self.sort_model.setSortRole(GalleryModel.PUB_DATE_ROLE)\n                self.sort_model.sort(0, Qt.DescendingOrder)\n                self.current_sort = 'pub_date'\n            elif name == 'times_read':\n                self.sort_model.setSortRole(GalleryModel.TIMES_READ_ROLE)\n                self.sort_model.sort(0, Qt.DescendingOrder)\n                self.current_sort = 'times_read'\n            elif name == 'last_read':\n                self.sort_model.setSortRole(GalleryModel.LAST_READ_ROLE)\n                self.sort_model.sort(0, Qt.DescendingOrder)\n                self.current_sort = 'last_read'\n            elif name == 'rating':\n                self.sort_model.setSortRole(GalleryModel.RATING_COUNT)\n                self.sort_model.sort(0, Qt.DescendingOrder)\n                self.current_sort = 'rating'\n\n    def contextMenuEvent(self, event):\n        CommonView.contextMenuEvent(self, event)\n\n    def updateGeometries(self):\n        super().updateGeometries()\n        self.verticalScrollBar().setSingleStep(app_constants.SCROLL_SPEED)\n\nclass MangaTableView(QTableView):\n    STATUS_BAR_MSG = pyqtSignal(str)\n\n    def __init__(self, v_type, parent=None):\n        super().__init__(parent)\n        self.view_type = v_type\n\n        # options\n        self.parent_widget = parent\n        self.setAcceptDrops(True)\n        self.setDragEnabled(True)\n        self.viewport().setAcceptDrops(True)\n        self.setDropIndicatorShown(True)\n        self.setDragDropMode(self.DragDrop)\n        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n        self.setSelectionBehavior(self.SelectRows)\n        self.setSelectionMode(self.ExtendedSelection)\n        self.setShowGrid(True)\n        self.setSortingEnabled(True)\n        h_header = self.horizontalHeader()\n        h_header.setSortIndicatorShown(True)\n        v_header = self.verticalHeader()\n        v_header.sectionResizeMode(QHeaderView.Fixed)\n        v_header.setDefaultSectionSize(24)\n        v_header.hide()\n        palette = self.palette()\n        palette.setColor(palette.Highlight, QColor(88, 88, 88, 70))\n        palette.setColor(palette.HighlightedText, QColor('black'))\n        self.setPalette(palette)\n        self.setIconSize(QSize(0,0))\n        self.doubleClicked.connect(lambda idx: idx.data(Qt.UserRole + 1).chapters[0].open())\n        self.grabGesture(Qt.SwipeGesture)\n        self.k_scroller = QScroller.scroller(self)\n\n    # display tooltip only for elided text\n    #def viewportEvent(self, event):\n    #\tif event.type() == QEvent.ToolTip:\n    #\t\th_event = QHelpEvent(event)\n    #\t\tindex = self.indexAt(h_event.pos())\n    #\t\tif index.isValid():\n    #\t\t\tsize_hint = self.itemDelegate(index).sizeHint(self.viewOptions(),\n    #\t\t\t\t\t\t\t\t\t\t\t  index)\n    #\t\t\trect = QRect(0, 0, size_hint.width(), size_hint.height())\n    #\t\t\trect_visual = self.visualRect(index)\n    #\t\t\tif rect.width() <= rect_visual.width():\n    #\t\t\t\tQToolTip.hideText()\n    #\t\t\t\treturn True\n    #\treturn super().viewportEvent(event)\n\n    def keyPressEvent(self, event):\n        if event.key() == Qt.Key_Return:\n            s_idx = self.selectionModel().selectedRows()\n            if s_idx:\n                for idx in s_idx:\n                    self.doubleClicked.emit(idx)\n        elif event.modifiers() == Qt.ShiftModifier and event.key() == Qt.Key_Delete:\n            CommonView.remove_selected(self, True)\n        elif event.key() == Qt.Key_Delete:\n            CommonView.remove_selected(self)\n        return super().keyPressEvent(event)\n\n    def contextMenuEvent(self, event):\n        CommonView.contextMenuEvent(self, event)\n\nclass CommonView:\n    \"\"\"\n    Contains identical view implentations\n    \"\"\"\n\n    @staticmethod\n    def remove_selected(view_cls, source=False):\n        s_indexes = []\n        if isinstance(view_cls, QListView):\n            s_indexes = view_cls.selectedIndexes()\n        elif isinstance(view_cls, QTableView):\n            s_indexes = view_cls.selectionModel().selectedRows()\n\n        CommonView.remove_gallery(view_cls, s_indexes, source)\n\n    @staticmethod\n    def remove_gallery(view_cls, index_list, local=False):\n        #view_cls.sort_model.setDynamicSortFilter(False)\n        msgbox = QMessageBox(view_cls)\n        msgbox.setIcon(msgbox.Question)\n        msgbox.setStandardButtons(msgbox.Yes | msgbox.No)\n        if len(index_list) > 1:\n            if not local:\n                msg = 'Are you sure you want to remove {} selected galleries?'.format(len(index_list))\n            else:\n                msg = 'Are you sure you want to remove {} selected galleries and their files/directories?'.format(len(index_list))\n\n            msgbox.setText(msg)\n        else:\n            if not local:\n                msg = 'Are you sure you want to remove this gallery?'\n            else:\n                msg = 'Are you sure you want to remove this gallery and its file/directory?'\n            msgbox.setText(msg)\n\n        if msgbox.exec() == msgbox.Yes:\n            #view_cls.setUpdatesEnabled(False)\n            gallery_list = []\n            gallery_db_list = []\n            log_i('Removing {} galleries'.format(len(index_list)))\n            for index in index_list:\n                gallery = index.data(Qt.UserRole + 1)\n                gallery_list.append(gallery)\n                log_i('Attempt to remove: {} by {}'.format(gallery.title.encode(errors=\"ignore\"),\n                                            gallery.artist.encode(errors=\"ignore\")))\n                if gallery.id:\n                    gallery_db_list.append(gallery)\n            gallerydb.execute(gallerydb.GalleryDB.del_gallery, True, gallery_db_list, local=local, priority=0)\n\n            rows = len(gallery_list)\n            view_cls.gallery_model._gallery_to_remove.extend(gallery_list)\n            view_cls.gallery_model.removeRows(view_cls.gallery_model.rowCount() - rows, rows)\n            view_cls.sort_model.refresh()\n\n            #view_cls.STATUS_BAR_MSG.emit('Gallery removed!')\n            #view_cls.setUpdatesEnabled(True)\n        #view_cls.sort_model.setDynamicSortFilter(True)\n\n    @staticmethod\n    def find_index(view_cls, gallery_id, sort_model=False):\n        \"Finds and returns the index associated with the gallery id\"\n        index = None\n        model = view_cls.sort_model if sort_model else view_cls.gallery_model\n        rows = model.rowCount()\n        for r in range(rows):\n            indx = model.index(r, 0)\n            m_gallery = indx.data(Qt.UserRole + 1)\n            if m_gallery.id == gallery_id:\n                index = indx\n                break\n        return index\n\n    @staticmethod\n    def open_random_gallery(view_cls):\n        try:\n            g = random.randint(0, view_cls.sort_model.rowCount() - 1)\n        except ValueError:\n            return\n        indx = view_cls.sort_model.index(g, 1)\n        chap_numb = 0\n        if app_constants.OPEN_RANDOM_GALLERY_CHAPTERS:\n            gallery = indx.data(Qt.UserRole + 1)\n            b = len(gallery.chapters)\n            if b > 1:\n                chap_numb = random.randint(0, b - 1)\n\n        CommonView.scroll_to_index(view_cls, view_cls.sort_model.index(indx.row(), 0))\n        try:\n            indx.data(Qt.UserRole + 1).chapters[chap_numb].open()\n        except KeyError:\n            log.exception(\"Failed to open chapter\")\n            return;\n\n    @staticmethod\n    def scroll_to_index(view_cls, idx, select=True):\n        old_value = view_cls.verticalScrollBar().value()\n        view_cls.setAutoScroll(False)\n        view_cls.setUpdatesEnabled(False)\n        view_cls.verticalScrollBar().setValue(0)\n        idx_rect = view_cls.visualRect(idx)\n        view_cls.verticalScrollBar().setValue(old_value)\n        view_cls.setUpdatesEnabled(True)\n        rect = QRectF(idx_rect)\n        if app_constants.DEBUG:\n            print(\"Scrolling to index:\", rect.getRect())\n        view_cls.k_scroller.ensureVisible(rect, 0, 0)\n        if select:\n            view_cls.setCurrentIndex(idx)\n        view_cls.setAutoScroll(True)\n        view_cls.update()\n\n    @staticmethod\n    def contextMenuEvent(view_cls, event):\n        grid_view = False\n        table_view = False\n        if isinstance(view_cls, QListView):\n            grid_view = True\n        elif isinstance(view_cls, QTableView):\n            table_view = True\n\n        handled = False\n        index = view_cls.indexAt(event.pos())\n        index = view_cls.sort_model.mapToSource(index)\n\n        selected = False\n        if table_view:\n            s_indexes = view_cls.selectionModel().selectedRows()\n        else:\n            s_indexes = view_cls.selectedIndexes()\n        select_indexes = []\n        for idx in s_indexes:\n            if idx.isValid() and idx.column() == 0:\n                select_indexes.append(view_cls.sort_model.mapToSource(idx))\n        if len(select_indexes) > 1:\n            selected = True\n\n        if index.isValid():\n            if grid_view:\n                if view_cls.gallery_window.isVisible():\n                    view_cls.gallery_window.hide_animation.start()\n                view_cls.manga_delegate.CONTEXT_ON = True\n            if selected:\n                menu = misc.GalleryMenu(view_cls, index, view_cls.sort_model,\n                               view_cls.parent_widget, select_indexes)\n            else:\n                menu = misc.GalleryMenu(view_cls, index, view_cls.sort_model,\n                               view_cls.parent_widget)\n            menu.delete_galleries.connect(lambda s: CommonView.remove_gallery(view_cls, select_indexes, s))\n            menu.edit_gallery.connect(CommonView.spawn_dialog)\n            handled = True\n\n        if handled:\n            menu.exec_(event.globalPos())\n            if grid_view:\n                view_cls.manga_delegate.CONTEXT_ON = False\n            event.accept()\n            del menu\n        else:\n            event.ignore()\n\n    @staticmethod\n    def spawn_dialog(app_inst, gallery=None):\n        dialog = gallerydialog.GalleryDialog(app_inst, gallery)\n        dialog.show()\n\nclass MangaViews:\n\n    manga_views = []\n    \n    @enum.unique\n    class View(enum.Enum):\n        List = 1\n        Table = 2\n\n    def __init__(self, v_type, parent, allow_sidebarwidget=False):\n        self.allow_sidebarwidget = allow_sidebarwidget\n        self._delete_proxy_model = None\n\n        self.view_type = v_type\n\n        if v_type == app_constants.ViewType.Default:\n            model = GalleryModel(app_constants.GALLERY_DATA, parent)\n        elif v_type == app_constants.ViewType.Addition:\n            model = GalleryModel(app_constants.GALLERY_ADDITION_DATA, parent)\n        elif v_type == app_constants.ViewType.Duplicate:\n            model = GalleryModel([], parent)\n\n        #list view\n        self.list_view = MangaView(model, v_type, parent=parent)\n        self.list_view.sort_model.setup_search()\n        self.sort_model = self.list_view.sort_model\n        self.gallery_model = self.list_view.gallery_model\n        #table view\n        self.table_view = MangaTableView(v_type, parent)\n        self.table_view.gallery_model = self.gallery_model\n        self.table_view.sort_model = self.sort_model\n        self.table_view.setModel(self.sort_model)\n        self.table_view.setColumnWidth(app_constants.FAV, 20)\n        self.table_view.setColumnWidth(app_constants.ARTIST, 200)\n        self.table_view.setColumnWidth(app_constants.TITLE, 400)\n        self.table_view.setColumnWidth(app_constants.TAGS, 300)\n        self.table_view.setColumnWidth(app_constants.TYPE, 60)\n        self.table_view.setColumnWidth(app_constants.CHAPTERS, 60)\n        self.table_view.setColumnWidth(app_constants.LANGUAGE, 100)\n        self.table_view.setColumnWidth(app_constants.LINK, 400)\n\n        self.view_layout = QStackedLayout()\n        # init the chapter view variables\n        self.m_l_view_index = self.view_layout.addWidget(self.list_view)\n        self.m_t_view_index = self.view_layout.addWidget(self.table_view)\n\n        self.current_view = self.View.List\n        self.manga_views.append(self)\n\n        if v_type in (app_constants.ViewType.Default, app_constants.ViewType.Addition):\n            self.sort_model.enable_drag = True\n\n    def _delegate_delete(self):\n        if self._delete_proxy_model:\n            gs = [g for g in self.gallery_model._gallery_to_remove]\n            self._delete_proxy_model._gallery_to_remove = gs\n            self._delete_proxy_model.removeRows(self._delete_proxy_model.rowCount() - len(gs), len(gs))\n\n    def set_delete_proxy(self, other_model):\n        self._delete_proxy_model = other_model\n        self.gallery_model.rowsAboutToBeRemoved.connect(self._delegate_delete, Qt.DirectConnection)\n\n    def add_gallery(self, gallery, db=False, record_time=False):\n        if isinstance(gallery, (list, tuple)):\n            for g in gallery:\n                g.view = self.view_type\n                if self.view_type != app_constants.ViewType.Duplicate:\n                    g.state = app_constants.GalleryState.New\n                if db:\n                    gallerydb.execute(gallerydb.GalleryDB.add_gallery, True, g)\n                else:\n                    if not g.profile:\n                        Executors.generate_thumbnail(g, on_method=g.set_profile)\n            rows = len(gallery)\n            self.list_view.gallery_model._gallery_to_add.extend(gallery)\n            if record_time:\n                g.qtime = QTime.currentTime()\n        else:\n            gallery.view = self.view_type\n            if self.view_type != app_constants.ViewType.Duplicate:\n                gallery.state = app_constants.GalleryState.New\n            rows = 1\n            self.list_view.gallery_model._gallery_to_add.append(gallery)\n            if record_time:\n                g.qtime = QTime.currentTime()\n            if db:\n                gallerydb.execute(gallerydb.GalleryDB.add_gallery, True, gallery)\n            else:\n                if not gallery.profile:\n                    Executors.generate_thumbnail(gallery, on_method=gallery.set_profile)\n        self.list_view.gallery_model.insertRows(self.list_view.gallery_model.rowCount(), rows)\n        self.list_view.sort_model.refresh()\n        \n    def replace_gallery(self, list_of_gallery, db_optimize=True):\n        \"Replaces the view and DB with given list of gallery, at given position\"\n        assert isinstance(list_of_gallery, (list, gallerydb.Gallery)), \"Please pass a gallery to replace with\"\n        if isinstance(list_of_gallery, gallerydb.Gallery):\n            list_of_gallery = [list_of_gallery]\n        log_d('Replacing {} galleries'.format(len(list_of_gallery)))\n        if db_optimize:\n            gallerydb.execute(gallerydb.GalleryDB.begin, True)\n        for gallery in list_of_gallery:\n            kwdict = {'title':gallery.title,\n             'profile':gallery.profile,\n             'artist':gallery.artist,\n             'info':gallery.info,\n             'type':gallery.type,\n             'language':gallery.language,\n             'rating':gallery.rating,\n             'status':gallery.status,\n             'pub_date':gallery.pub_date,\n             'tags':gallery.tags,\n             'link':gallery.link,\n             'series_path':gallery.path,\n             'chapters':gallery.chapters,\n             'exed':gallery.exed}\n\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery,\n                             True, gallery.id, **kwdict)\n        if db_optimize:\n            gallerydb.execute(gallerydb.GalleryDB.end, True)\n\n    def changeTo(self, idx):\n        \"change view\"\n        self.view_layout.setCurrentIndex(idx)\n        if idx == self.m_l_view_index:\n            self.current_view = self.View.List\n        elif idx == self.m_t_view_index:\n            self.current_view = self.View.Table\n\n    def get_current_view(self):\n        if self.current_view == self.View.List:\n            return self.list_view\n        else:\n            return self.table_view\n\n    def fav_is_current(self):\n        if self.table_view.sort_model.current_view == \\\n            self.table_view.sort_model.CAT_VIEW:\n            return False\n        return True\n\n    def hide(self):\n        self.view_layout.currentWidget().hide()\n\n    def show(self):\n        self.view_layout.currentWidget().show()\n\nif __name__ == '__main__':\n    raise NotImplementedError(\"Unit testing not yet implemented\")\n"
  },
  {
    "path": "version/gallerydb.py",
    "content": "﻿#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport datetime\nimport os\nimport enum\nimport scandir\nimport threading\nimport logging\nimport queue\nimport io\nimport uuid\nimport functools\nimport re as regex\nfrom dateutil import parser as dateparser\n\nfrom PyQt5.QtCore import QObject, pyqtSignal, QTime\n\nfrom utils import (today, ArchiveFile, generate_img_hash, delete_path,\n                     ARCHIVE_FILES, get_gallery_img, IMG_FILES)\nfrom database import db_constants\nfrom database import db\nfrom database.db import DBBase\nfrom executors import Executors\n\nimport app_constants\nimport utils\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\n\nmethod_queue = queue.PriorityQueue()\nmethod_return = queue.Queue()\ndb_constants.METHOD_QUEUE = method_queue\ndb_constants.METHOD_RETURN = method_return\n\nclass PriorityObject:\n    def __init__(self, priority, data):\n        self.p = priority\n        self.data = data\n\n    def __lt__(self, other):\n        return self.p < other.p\n\ndef process_methods():\n    \"\"\"\n    Methods are objects.\n    Put a list in the method queue where first index is the\n    method. Named arguments are put in a dict.\n    \"\"\"\n    while True:\n        l = method_queue.get().data\n        log_d('Processing a method from queue...')\n        method = l.pop(0)\n        log_d(method)\n        args = []\n        kwargs = {}\n        get_args = 1\n        no_return = False\n        while get_args:\n            try:\n                a = l.pop(0)\n                if a == 'no return':\n                    no_return = True\n                    continue\n                if isinstance(a, dict):\n                    kwargs = a\n                else:\n                    args.append(a)\n            except IndexError:\n                get_args = 0\n        args = tuple(args)\n        if args and kwargs:\n            r = method(*args, **kwargs)\n        elif args:\n            r = method(*args)\n        elif kwargs:\n            r = method(**kwargs)\n        else:\n            r = method()\n        if not no_return:\n            method_return.put(r)\n        method_queue.task_done()\n\nmethod_queue_thread = threading.Thread(name='Method Queue Thread', target=process_methods,\n                                       daemon=True)\nmethod_queue_thread.start()\n\ndef execute(method, no_return, *args, **kwargs):\n    log_d('Added method to queue')\n    log_d('Method name: {}'.format(method.__name__))\n    arg_list = [method]\n    priority = kwargs.pop(\"priority\", 999)\n    if no_return:\n        arg_list.append('no return')\n    if args:\n        for x in args:\n            arg_list.append(x)\n    if kwargs:\n        arg_list.append(kwargs)\n    method_queue.put(PriorityObject(priority, arg_list))\n    if not no_return:\n        return method_return.get()\n\ndef chapter_map(row, chapter):\n    assert isinstance(chapter, Chapter)\n    chapter.title = row['chapter_title']\n    chapter.path = bytes.decode(row['chapter_path'])\n    chapter.in_archive = row['in_archive']\n    chapter.pages = row['pages']\n    return chapter\n\ndef gallery_map(row, gallery, chapters=True, tags=True, hashes=True):\n    gallery.title = row['title']\n    gallery.artist = row['artist']\n    gallery.profile = bytes.decode(row['profile'])\n    gallery.path = bytes.decode(row['series_path'])\n    gallery.is_archive = row['is_archive']\n    try:\n        gallery.path_in_archive = bytes.decode(row['path_in_archive'])\n    except TypeError:\n        pass\n    gallery.info = row['info']\n    gallery.language = row['language']\n    gallery.rating = row['rating']\n    gallery.status = row['status']\n    gallery.type = row['type']\n    gallery.fav = row['fav']\n\n    def convert_date(date_str):\n        #2015-10-25 21:44:38\n        if date_str and date_str != 'None':\n            return datetime.datetime.strptime(date_str, \"%Y-%m-%d %H:%M:%S\")\n\n    gallery.pub_date = convert_date(row['pub_date'])\n    gallery.last_read = convert_date(row['last_read'])\n    gallery.date_added = convert_date(row['date_added'])\n    gallery.times_read = row['times_read']\n    gallery._db_v = row['db_v']\n    gallery.exed = row['exed']\n    gallery.view = row['view']\n    try:\n        gallery.link = bytes.decode(row['link'])\n    except TypeError:\n        gallery.link = row['link']\n\n    if chapters:\n        gallery.chapters = ChapterDB.get_chapters_for_gallery(gallery.id)\n\n    if tags:\n        gallery.tags = TagDB.get_gallery_tags(gallery.id)\n    \n    if hashes:\n        gallery.hashes = HashDB.get_gallery_hashes(gallery.id)\n\n    gallery.set_defaults()\n    return gallery\n\ndef default_chap_exec(gallery_or_id, chap, only_values=False):\n    \"Pass a Gallery object or gallery id and a Chapter object\"\n    if isinstance(gallery_or_id, Gallery):\n        gid = gallery_or_id.id\n        in_archive = gallery_or_id.is_archive\n    else:\n        gid = gallery_or_id\n        in_archive = chap.in_archive\n\n    if only_values:\n        execute = (gid, chap.title, chap.number, str.encode(chap.path), chap.pages, in_archive)\n    else:\n        execute = (\"\"\"\n                INSERT INTO chapters(series_id, chapter_title, chapter_number, chapter_path, pages, in_archive)\n                VALUES(:series_id, :chapter_title, :chapter_number, :chapter_path, :pages, :in_archive)\"\"\",\n                {'series_id':gid,\n                'chapter_title':chap.title,\n                'chapter_number':chap.number,\n                'chapter_path':str.encode(chap.path),\n                'pages':chap.pages,\n                'in_archive':in_archive})\n    return execute\n\ndef default_exec(object):\n    object.set_defaults()\n    def check(obj):\n        if obj == \"None\":\n            return None\n        else:\n            return obj\n    executing = [\"\"\"INSERT INTO series(title, artist, profile, series_path, is_archive, path_in_archive,\n                    info, type, fav, language, rating, status, pub_date, date_added, last_read, link,\n                    times_read, db_v, exed, view)\n                VALUES(:title, :artist, :profile, :series_path, :is_archive, :path_in_archive, :info, :type, :fav, :language,\n                    :rating, :status, :pub_date, :date_added, :last_read, :link, :times_read, :db_v, :exed, :view)\"\"\",\n                {\n                'title':check(object.title),\n                'artist':check(object.artist),\n                'profile':str.encode(object.profile),\n                'series_path':str.encode(object.path),\n                'is_archive':check(object.is_archive),\n                'path_in_archive':str.encode(object.path_in_archive),\n                'info':check(object.info),\n                'fav':check(object.fav),\n                'type':check(object.type),\n                'language':check(object.language),\n                'rating':check(object.rating),\n                'status':check(object.status),\n                'pub_date':check(object.pub_date),\n                'date_added':check(object.date_added),\n                'last_read':check(object.last_read),\n                'link':str.encode(object.link),\n                'times_read':check(object.times_read),\n                'db_v':check(db_constants.REAL_DB_VERSION),\n                'exed':check(object.exed),\n                'view':check(object.view)\n                }]\n    return executing\n\nclass GalleryDB(DBBase):\n    \"\"\"\n    Provides the following s methods:\n        rebuild_thumb -> Rebuilds gallery thumbnail\n        rebuild_galleries -> Rebuilds the galleries in DB\n        modify_gallery -> Modifies gallery with given gallery id\n        get_all_gallery -> returns a list of all gallery (<Gallery> class) currently in DB\n        get_gallery_by_path -> Returns gallery with given path\n        get_gallery_by_id -> Returns gallery with given id\n        add_gallery -> adds gallery into db\n        set_gallery_title -> changes gallery title\n        gallery_count -> returns amount of gallery (can be used for indexing)\n        del_gallery -> deletes the gallery with the given id recursively\n        check_exists -> Checks if provided string exists\n        clear_thumb -> Deletes a thumbnail\n        clear_thumb_dir -> Dletes everything in the thumbnail directory\n    \"\"\"\n    def __init__(self):\n        raise Exception(\"GalleryDB should not be instantiated\")\n\n    @staticmethod\n    def rebuild_thumb(gallery):\n        \"Rebuilds gallery thumbnail\"\n        try:\n            log_i('Recreating thumb {}'.format(gallery.title.encode(errors='ignore')))\n            if gallery.profile:\n                GalleryDB.clear_thumb(gallery.profile)\n            gallery.profile = Executors.generate_thumbnail(gallery, blocking=True)\n            GalleryDB.modify_gallery(gallery.id,\n                profile=gallery.profile)\n        except:\n            log.exception(\"Failed rebuilding thumbnail\")\n            return False\n        return True\n\n    @staticmethod\n    def clear_thumb(path):\n        \"Deletes a thumbnail\"\n        try:\n            if os.path.samefile(path, app_constants.NO_IMAGE_PATH):\n                return\n        except FileNotFoundError:\n            pass\n\n        try:\n            os.unlink(path)\n        except FileNotFoundError:\n            pass\n        except:\n            log.exception('Failed to delete thumb {}'.format(os.path.split(path)[1].encode(errors='ignore')))\n\n    @staticmethod\n    def clear_thumb_dir():\n        \"Deletes everything in the thumbnail directory\"\n        if os.path.exists(db_constants.THUMBNAIL_PATH):\n            for thumbfile in scandir.scandir(db_constants.THUMBNAIL_PATH):\n                GalleryDB.clear_thumb(thumbfile.path)\n\n    @staticmethod\n    def rebuild_gallery(gallery, thumb=False):\n        \"Rebuilds the galleries in DB\"\n        try:\n            log_i('Rebuilding {}'.format(gallery.title.encode(errors='ignore')))\n            log_i(\"Rebuilding gallery {}\".format(gallery.id))\n            HashDB.del_gallery_hashes(gallery.id)\n            GalleryDB.modify_gallery(gallery.id,\n                title=gallery.title,\n                artist=gallery.artist,\n                info=gallery.info,\n                type=gallery.type,\n                fav=gallery.fav,\n                tags=gallery.tags,\n                language=gallery.language,\n                rating=gallery.rating,\n                status=gallery.status,\n                pub_date=gallery.pub_date,\n                link=gallery.link,\n                times_read=gallery.times_read,\n                last_read=gallery.last_read,\n                _db_v=db_constants.CURRENT_DB_VERSION,\n                exed=gallery.exed,\n                is_archive=gallery.is_archive,\n                path_in_archive=gallery.path_in_archive,\n                view=gallery.view)\n            if thumb:\n                GalleryDB.rebuild_thumb(gallery)\n        except:\n            log.exception('Failed rebuilding')\n            return False\n        return True\n\n    @classmethod\n    def modify_gallery(cls, series_id, title=None, profile=None, artist=None, info=None, type=None, fav=None,\n                   tags=None, language=None, rating=None, status=None, pub_date=None, link=None,\n                   times_read=None, last_read=None, series_path=None, chapters=None, _db_v=None,\n                   hashes=None, exed=None, is_archive=None, path_in_archive=None, view=None, date_added=None):\n        \"Modifies gallery with given gallery id\"\n        assert isinstance(series_id, int)\n        assert not isinstance(series_id, bool)\n        executing = []\n        if title != None:\n            assert isinstance(title, str)\n            executing.append([\"UPDATE series SET title=? WHERE series_id=?\", (title, series_id)])\n        if profile != None:\n            assert isinstance(profile, str)\n            executing.append([\"UPDATE series SET profile=? WHERE series_id=?\", (str.encode(profile), series_id)])\n        if artist != None:\n            assert isinstance(artist, str)\n            executing.append([\"UPDATE series SET artist=? WHERE series_id=?\", (artist, series_id)])\n        if info != None:\n            assert isinstance(info, str)\n            executing.append([\"UPDATE series SET info=? WHERE series_id=?\", (info, series_id)])\n        if type != None:\n            assert isinstance(type, str)\n            executing.append([\"UPDATE series SET type=? WHERE series_id=?\", (type, series_id)])\n        if fav != None:\n            assert isinstance(fav, int)\n            executing.append([\"UPDATE series SET fav=? WHERE series_id=?\", (fav, series_id)])\n        if language != None:\n            assert isinstance(language, str)\n            executing.append([\"UPDATE series SET language=? WHERE series_id=?\", (language, series_id)])\n        if rating != None:\n            assert isinstance(rating, int)\n            executing.append([\"UPDATE series SET rating=? WHERE series_id=?\", (rating, series_id)])\n        if status != None:\n            assert isinstance(status, str)\n            executing.append([\"UPDATE series SET status=? WHERE series_id=?\", (status, series_id)])\n        if pub_date != None:\n            executing.append([\"UPDATE series SET pub_date=? WHERE series_id=?\", (pub_date, series_id)])\n        if link != None:\n            executing.append([\"UPDATE series SET link=? WHERE series_id=?\", (link, series_id)])\n        if times_read != None:\n            executing.append([\"UPDATE series SET times_read=? WHERE series_id=?\", (times_read, series_id)])\n        if last_read != None:\n            executing.append([\"UPDATE series SET last_read=? WHERE series_id=?\", (last_read, series_id)])\n        if series_path != None:\n            executing.append([\"UPDATE series SET series_path=? WHERE series_id=?\", (str.encode(series_path), series_id)])\n        if _db_v != None:\n            executing.append([\"UPDATE series SET db_v=? WHERE series_id=?\", (_db_v, series_id)])\n        if exed != None:\n            executing.append([\"UPDATE series SET exed=? WHERE series_id=?\", (exed, series_id)])\n        if is_archive != None:\n            executing.append([\"UPDATE series SET is_archive=? WHERE series_id=?\", (is_archive, series_id)])\n        if path_in_archive != None:\n            executing.append([\"UPDATE series SET path_in_archive=? WHERE series_id=?\", (path_in_archive, series_id)])\n        if view != None:\n            executing.append([\"UPDATE series SET view=? WHERE series_id=?\", (view, series_id)])\n        if date_added != None:\n            executing.append([\"UPDATE series SET date_added=? WHERE series_id=?\", (date_added, series_id)])\n\n        if tags != None:\n            assert isinstance(tags, dict)\n            TagDB.modify_tags(series_id, tags)\n        if chapters != None:\n            assert isinstance(chapters, ChaptersContainer)\n            ChapterDB.update_chapter(chapters)\n\n        if hashes != None:\n            assert isinstance(hashes, Gallery)\n            HashDB.rebuild_gallery_hashes(hashes)\n\n        for query in executing:\n            cls.execute(cls, *query)\n\n    @classmethod\n    def get_all_gallery(cls, chapters=True, tags=True, hashes=True):\n        \"\"\"\n        Careful, might crash with very large libraries i think...\n        Returns a list of all galleries (<Gallery> class) currently in DB\n        \"\"\"\n        cursor = cls.execute(cls, 'SELECT * FROM series')\n        all_gallery = cursor.fetchall()\n        return GalleryDB.gen_galleries(all_gallery, chapters, tags, hashes)\n\n    @staticmethod\n    def gen_galleries(gallery_dict, chapters=True, tags=True, hashes=True):\n        \"\"\"\n        Map galleries fetched from DB\n        \"\"\"\n        gallery_list = []\n        for gallery_row in gallery_dict:\n            gallery = Gallery()\n            gallery.id = gallery_row['series_id']\n            gallery = gallery_map(gallery_row, gallery, chapters, tags, hashes)\n            if not os.path.exists(gallery.path):\n                gallery.dead_link = True\n            ListDB.query_gallery(gallery)\n            gallery_list.append(gallery)\n\n        return gallery_list\n\n    @classmethod\n    def get_gallery_by_path(cls, path):\n        \"Returns gallery with given path\"\n        assert isinstance(path, str), \"Provided path is invalid\"\n        cursor = cls.execute(cls, 'SELECT * FROM series WHERE series_path=?', (str.encode(path),))\n        row = cursor.fetchone()\n        try:\n            gallery = Gallery()\n            gallery.id = row['series_id']\n            gallery = gallery_map(row, gallery)\n            return gallery\n        except TypeError:\n            return None\n\n    @classmethod\n    def get_gallery_by_id(cls, id):\n        \"Returns gallery with given id\"\n        assert isinstance(id, int), \"Provided ID is invalid\"\n        cursor = cls.execute(cls, 'SELECT * FROM series WHERE series_id=?', (id,))\n        row = cursor.fetchone()\n        gallery = Gallery()\n        try:\n            gallery.id = row['series_id']\n            gallery = gallery_map(row, gallery)\n            return gallery\n        except TypeError:\n            return None\n\n    @classmethod\n    def add_gallery(cls, object, test_mode=False):\n        \"Receives an object of class gallery, and appends it to DB\"\n        \"Adds gallery of <Gallery> class into database\"\n        assert isinstance(object, Gallery), \"add_gallery method only accepts gallery items\"\n        log_i('Recevied gallery: {}'.format(object.path.encode(errors='ignore')))\n\n        #TODO: implement mass gallery adding!  User execute_many method for\n        #effeciency!\n\n        cursor = cls.execute(cls, *default_exec(object))\n        series_id = cursor.lastrowid\n        object.id = series_id\n        if not object.profile:\n            Executors.generate_thumbnail(object, on_method=object.set_profile)\n        if object.tags:\n            TagDB.add_tags(object)\n        ChapterDB.add_chapters(object)\n\n    @classmethod\n    def gallery_count(cls):\n        \"\"\"\n        Returns the amount of galleries in db.\n        \"\"\"\n        cursor = cls.execute(cls, \"SELECT count(*) AS 'size' FROM series\")\n        return cursor.fetchone()['size']\n\n    @classmethod\n    def del_gallery(cls, list_of_gallery, local=False):\n        \"Deletes all galleries in the list recursively.\"\n        assert isinstance(list_of_gallery, list), \"Please provide a valid list of galleries to delete\"\n        for gallery in list_of_gallery:\n            if local:\n                app_constants.TEMP_PATH_IGNORE.append(os.path.normcase(gallery.path))\n                if gallery.is_archive:\n                    s = delete_path(gallery.path)\n                else:\n                    paths = [x.path for x in gallery.chapters]\n                    [app_constants.TEMP_PATH_IGNORE.append(os.path.normcase(x)) for x in paths] # to avoid data race?\n                    for path in paths:\n                        s = delete_path(path)\n                        if not s:\n                            log_e('Failed to delete chapter {}:{}, {}'.format(chap,\n                                                            gallery.id, gallery.title.encode('utf-8', 'ignore')))\n                            continue\n                    s = delete_path(gallery.path)\n\n                if not s:\n                    log_e('Failed to delete gallery:{}, {}'.format(gallery.id,\n                                                      gallery.title.encode('utf-8', 'ignore')))\n                    continue\n\n            GalleryDB.clear_thumb(gallery.profile)\n            cls.execute(cls, 'DELETE FROM series WHERE series_id=?', (gallery.id,))\n            gallery.id = None\n            log_i('Successfully deleted: {}'.format(gallery.title.encode('utf-8', 'ignore')))\n            app_constants.NOTIF_BAR.add_text('Successfully deleted: {}'.format(gallery.title))\n\n    @staticmethod\n    def check_exists(name, galleries=None, filter=True):\n        \"\"\"\n        Checks if provided string exists in provided sorted\n        list based on path name.\n        Note: key will be normcased\n        \"\"\"\n        #pdb.set_trace()\n        if galleries is None:\n            galleries = app_constants.GALLERY_DATA + app_constants.GALLERY_ADDITION_DATA\n            filter = True\n\n        if filter:\n            filter_list = []\n            for gallery in galleries:\n                filter_list.append(os.path.normcase(gallery.path))\n            filter_list = sorted(filter_list)\n        else:\n            filter_list = galleries\n\n        def binary_search(key):\n            low = 0\n            high = len(filter_list) - 1\n            while high >= low:\n                mid = low + (high - low) // 2\n                if filter_list[mid] < key:\n                    low = mid + 1\n                elif filter_list[mid] > key:\n                    high = mid - 1\n                else:\n                    return True\n            return False\n\n        return binary_search(os.path.normcase(name))\n\nclass ChapterDB(DBBase):\n    \"\"\"\n    Provides the following database methods:\n        update_chapter -> Updates an existing chapter in DB\n        add_chapter -> adds chapter into db\n        add_chapter_raw -> links chapter to the given seires id, and adds into db\n        get_chapters_for_gallery -> returns a dict with chapters linked to the given series_id\n        get_chapter-> returns a dict with chapter matching the given chapter_number\n        get_chapter_id -> returns id of the chapter number\n        chapter_size -> returns amount of manga (can be used for indexing)\n        del_all_chapters <- Deletes all chapters with the given series_id\n        del_chapter <- Deletes chapter with the given number from gallery\n    \"\"\"\n\n    def __init__(self):\n        raise Exception(\"ChapterDB should not be instantiated\")\n\n    @classmethod\n    def update_chapter(cls, chapter_container, numbers=[]):\n        \"\"\"\n        Updates an existing chapter in DB.\n        Pass a gallery's ChapterContainer, specify number with a list of ints\n        leave empty to update all chapters.\n        \"\"\"\n        assert isinstance(chapter_container, ChaptersContainer) and isinstance(numbers, (list, tuple))\n        if numbers:\n            chapters = []\n            for n in numbers:\n                chapters.append(chapter_container[n])\n        else:\n            chapters = chapter_container.get_all_chapters()\n\n        executing = []\n        for chap in chapters:\n            new_path = chap.path\n            executing.append((chap.title, str.encode(new_path), chap.pages, chap.in_archive, chap.gallery.id, chap.number,))\n\n        cls.executemany(cls, \"UPDATE chapters SET chapter_title=?, chapter_path=?, pages=?, in_archive=? WHERE series_id=? AND chapter_number=?\",\n            executing)\n\n    @classmethod\n    def add_chapters(cls, gallery_object):\n        \"Adds chapters linked to gallery into database\"\n        assert isinstance(gallery_object, Gallery), \"Parent gallery need to be of class Gallery\"\n        series_id = gallery_object.id\n        executing = []\n        for chap in gallery_object.chapters:\n            executing.append(default_chap_exec(gallery_object, chap, True))\n        if not executing:\n            raise Exception\n        cls.executemany(cls, 'INSERT INTO chapters VALUES(NULL, ?, ?, ?, ?, ?, ?)', executing)\n\n    @classmethod\n    def add_chapters_raw(cls, series_id, chapters_container):\n        \"Adds chapter(s) to a gallery with the received series_id\"\n        assert isinstance(chapters_container, ChaptersContainer), \"chapters_container must be of class ChaptersContainer\"\n        executing = []\n        for chap in chapters_container:\n            if not ChapterDB.get_chapter(series_id, chap.number):\n                executing.append(default_chap_exec(series_id, chap, True))\n            else:\n                ChapterDB.update_chapter(chapters_container, [chap.number])\n\n        cls.executemany(cls, 'INSERT INTO chapters VALUES(NULL, ?, ?, ?, ?, ?, ?)', executing)\n\n\n    @classmethod\n    def get_chapters_for_gallery(cls, series_id):\n        \"\"\"\n        Returns a ChaptersContainer of chapters matching the received series_id\n        \"\"\"\n        assert isinstance(series_id, int), \"Please provide a valid gallery ID\"\n        cursor = cls.execute(cls, 'SELECT * FROM chapters WHERE series_id=?', (series_id,))\n        rows = cursor.fetchall()\n        chapters = ChaptersContainer()\n\n        for row in rows:\n            chap = chapters.create_chapter(row['chapter_number'])\n            chapter_map(row, chap)\n        return chapters\n\n\n    @classmethod\n    def get_chapter(cls, series_id, chap_numb):\n        \"\"\"Returns a ChaptersContainer of chapters matching the recieved chapter_number\n        return None for no match\n        \"\"\"\n        assert isinstance(chap_numb, int), \"Please provide a valid chapter number\"\n        cursor = cls.execute(cls, 'SELECT * FROM chapters WHERE series_id=? AND chapter_number=?', (series_id, chap_numb,))\n        try:\n            rows = cursor.fetchall()\n            chapters = ChaptersContainer()\n            for row in rows:\n                chap = chapters.create_chapter(row['chapter_number'])\n                chapter_map(row, chap)\n        except TypeError:\n            return None\n        return chapters\n\n    @classmethod\n    def get_chapter_id(cls, series_id, chapter_number):\n        \"Returns id of the chapter number\"\n        assert isinstance(series_id, int) and isinstance(chapter_number, int),\\\n            \"Passed args must be of int not {} and {}\".format(type(series_id), type(chapter_number))\n        cursor = cls.execute(cls, 'SELECT chapter_id FROM chapters WHERE series_id=? AND chapter_number=?',\n                        (series_id, chapter_number,))\n        try:\n            row = cursor.fetchone()\n            chp_id = row['chapter_id']\n            return chp_id\n        except KeyError:\n            return None\n        except TypeError:\n            return None\n\n    @staticmethod\n    def chapter_size(gallery_id):\n        \"\"\"Returns the amount of chapters for the given\n        gallery id\n        \"\"\"\n        pass\n\n    @classmethod\n    def del_all_chapters(cls, series_id):\n        \"Deletes all chapters with the given series_id\"\n        assert isinstance(series_id, int), \"Please provide a valid gallery ID\"\n        cls.execute(cls, 'DELETE FROM chapters WHERE series_id=?', (series_id,))\n\n    @classmethod\n    def del_chapter(cls, series_id, chap_number):\n        \"Deletes chapter with the given number from gallery\"\n        assert isinstance(series_id, int), \"Please provide a valid gallery ID\"\n        assert isinstance(chap_number, int), \"Please provide a valid chapter number\"\n        cls.execute(cls, 'DELETE FROM chapters WHERE series_id=? AND chapter_number=?',\n                (series_id, chap_number,))\n\nclass TagDB(DBBase):\n    \"\"\"\n    Tags are returned in a dict where {\"namespace\":[\"tag1\",\"tag2\"]}\n    The namespace \"default\" will be used for tags without namespaces.\n\n    Provides the following methods:\n    del_tags <- Deletes the tags with corresponding tag_ids from DB\n    del_gallery_tags_mapping <- Deletes the tags and gallery mappings with corresponding series_ids from DB\n    get_gallery_tags -> Returns all tags and namespaces found for the given series_id;\n    get_tag_gallery -> Returns all galleries with the given tag\n    get_ns_tags -> \"Returns a dict with namespace as key and list of tags as value\"\n    get_ns_tags_to_gallery -> Returns all galleries linked to the namespace tags. Receives a dict like this: {\"namespace\":[\"tag1\",\"tag2\"]}\n    get_tags_from_namespace -> Returns all galleries linked to the namespace\n    add_tags <- Adds the given dict_of_tags to the given series_id\n    modify_tags <- Modifies the given tags\n    get_all_tags -> Returns all tags in database\n    get_all_ns -> Returns all namespaces in database\n    \"\"\"\n\n    def __init__(self):\n        raise Exception(\"TagsDB should not be instantiated\")\n\n    @staticmethod\n    def del_tags(list_of_tags_id):\n        \"Deletes the tags with corresponding tag_ids from DB\"\n        pass\n\n    @classmethod\n    def del_gallery_mapping(cls, series_id):\n        \"Deletes the tags and gallery mappings with corresponding series_ids from DB\"\n        assert isinstance(series_id, int), \"Please provide a valid gallery id\"\n\n        # delete all mappings related to the given series_id\n        cls.execute(cls, 'DELETE FROM series_tags_map WHERE series_id=?', [series_id])\n\n    @classmethod\n    def get_gallery_tags(cls, series_id):\n        \"Returns all tags and namespaces found for the given series_id\"\n        if not isinstance(series_id, int):\n            return {}\n        cursor = cls.execute(cls, 'SELECT tags_mappings_id FROM series_tags_map WHERE series_id=?',\n                (series_id,))\n        tags = {}\n        result = cursor.fetchall()\n        for tag_map_row in result: # iterate all tag_mappings_ids\n            try:\n                if not tag_map_row:\n                    continue\n                # get tag and namespace\n                c = cls.execute(cls, 'SELECT namespace_id, tag_id FROM tags_mappings WHERE tags_mappings_id=?',\n                  (tag_map_row['tags_mappings_id'],))\n                for row in c.fetchall(): # iterate all rows\n                    # get namespace\n                    c = cls.execute(cls, 'SELECT namespace FROM namespaces WHERE namespace_id=?',\n                        (row['namespace_id'],))\n                    try:\n                        namespace = c.fetchone()['namespace']\n                    except TypeError:\n                        continue\n\n                    # get tag\n                    c = cls.execute(cls, 'SELECT tag FROM tags WHERE tag_id=?', (row['tag_id'],))\n                    try:\n                        tag = c.fetchone()['tag']\n                    except TypeError:\n                        continue\n\n                    # add them to dict\n                    if not namespace in tags:\n                        tags[namespace] = [tag]\n                    else:\n                        # namespace already exists in dict\n                        tags[namespace].append(tag)\n            except IndexError:\n                continue\n        return tags\n\n    @classmethod\n    def add_tags(cls, object):\n        \"Adds the given dict_of_tags to the given series_id\"\n        assert isinstance(object, Gallery), \"Please provide a valid gallery of class gallery\"\n\n        series_id = object.id\n        dict_of_tags = object.tags\n\n        def look_exists(tag_or_ns, what):\n            \"\"\"check if tag or namespace already exists in base\n            returns id, else returns None\"\"\"\n            c = cls.execute(cls, 'SELECT {}_id FROM {}s WHERE {} = ?'.format(what, what, what),\n                (tag_or_ns,))\n            try: # exists\n                return c.fetchone()['{}_id'.format(what)]\n            except TypeError: # doesnt exist\n                return None\n            except IndexError:\n                return None\n\n        tags_mappings_id_list = []\n        # first let's add the tags and namespaces to db\n        for namespace in dict_of_tags:\n            tags_list = dict_of_tags[namespace]\n            # don't add if it already exists\n            try:\n                namespace_id = look_exists(namespace, \"namespace\")\n                if not namespace_id:\n                    raise ValueError\n            except ValueError:\n                c = cls.execute(cls, 'INSERT INTO namespaces(namespace) VALUES(?)', (namespace,))\n                namespace_id = c.lastrowid\n\n            tags_id_list = []\n            for tag in tags_list:\n                try:\n                    tag_id = look_exists(tag, \"tag\")\n                    if not tag_id:\n                        raise ValueError\n                except ValueError:\n                    c = cls.execute(cls, 'INSERT INTO tags(tag) VALUES(?)', (tag,))\n                    tag_id = c.lastrowid\n\n                tags_id_list.append(tag_id)\n\n\n            def look_exist_tag_map(tag_id):\n                \"Checks DB if the tag_id already exists with the namespace_id, returns id else None\"\n                c = cls.execute(cls, 'SELECT tags_mappings_id FROM tags_mappings WHERE namespace_id=? AND tag_id=?',\n                    (namespace_id, tag_id,))\n                try: # exists\n                    return c.fetchone()['tags_mappings_id']\n                except TypeError: # doesnt exist\n                    return None\n                except IndexError:\n                    return None\n\n            # time to map the tags to the namespace now\n            for tag_id in tags_id_list:\n                # First check if tags mappings exists\n                try:\n                    t_map_id = look_exist_tag_map(tag_id)\n                    if t_map_id:\n                        tags_mappings_id_list.append(t_map_id)\n                    else:\n                        raise TypeError\n                except TypeError:\n                    c = cls.execute(cls, 'INSERT INTO tags_mappings(namespace_id, tag_id) VALUES(?, ?)',\n                     (namespace_id, tag_id,))\n                    # add the tags_mappings_id to our list\n                    tags_mappings_id_list.append(c.lastrowid)\n\n        # Lastly we map the series_id to the tags_mappings\n        executing = []\n        for tags_map in tags_mappings_id_list:\n            executing.append((series_id, tags_map,))\n            #cls.execute(cls, 'INSERT INTO series_tags_map(series_id, tags_mappings_id)\n            #VALUES(?, ?)', (series_id, tags_map,))\n        cls.executemany(cls, 'INSERT OR IGNORE INTO series_tags_map(series_id, tags_mappings_id) VALUES(?, ?)', executing)\n\n    @staticmethod\n    def modify_tags(series_id, dict_of_tags):\n        \"Modifies the given tags\"\n\n        # We first delete all mappings\n        TagDB.del_gallery_mapping(series_id)\n\n        # Now we add the new tags to DB\n        weak_gallery = Gallery()\n        weak_gallery.id = series_id\n        weak_gallery.tags = dict_of_tags\n\n        TagDB.add_tags(weak_gallery)\n\n\n    @staticmethod\n    def get_tag_gallery(tag):\n        \"Returns all galleries with the given tag\"\n        pass\n\n    @classmethod\n    def get_ns_tags(cls):\n        \"Returns a dict of all tags with namespace as key and list of tags as value\"\n        cursor = cls.execute(cls, 'SELECT namespace_id, tag_id FROM tags_mappings')\n        ns_tags = {}\n        ns_id_history = {} # to avoid unesseccary DB fetching\n        for t in cursor.fetchall():\n            try:\n                # get namespace\n                if not t['namespace_id'] in ns_id_history:\n                    c = cls.execute(cls, 'SELECT namespace FROM namespaces WHERE namespace_id=?', (t['namespace_id'],))\n                    ns = c.fetchone()['namespace']\n                    ns_id_history[t['namespace_id']] = ns\n                else:\n                    ns = ns_id_history[t['namespace_id']]\n                # get tag\n                c = cls.execute(cls, 'SELECT tag FROM tags WHERE tag_id=?', (t['tag_id'],))\n                tag = c.fetchone()['tag']\n                # put in dict\n                if ns in ns_tags:\n                    ns_tags[ns].append(tag)\n                else:\n                    ns_tags[ns] = [tag]\n            except:\n                continue\n        return ns_tags\n\n    @staticmethod\n    def get_tags_from_namespace(namespace):\n        \"Returns a dict with namespace as key and list of tags as value\"\n        pass\n\n    @staticmethod\n    def get_ns_tags_to_gallery(ns_tags):\n        \"\"\"\n        Returns all galleries linked to the namespace tags.\n        Receives a dict like this: {\"namespace\":[\"tag1\",\"tag2\"]}\n        \"\"\"\n        pass\n\n    @classmethod\n    def get_all_tags(cls):\n        \"\"\"\n        Returns all tags in database in a list\n        \"\"\"\n        cursor = cls.execute(cls, 'SELECT tag FROM tags')\n        tags = [t['tag'] for t in cursor.fetchall()]\n        return tags\n\n    @classmethod\n    def get_all_ns(cls):\n        \"\"\"\n        Returns all namespaces in database in a list\n        \"\"\"\n        cursor = cls.execute(cls, 'SELECT namespace FROM namespaces')\n        ns = [n['namespace'] for n in cursor.fetchall()]\n        return ns\n\nclass ListDB(DBBase):\n    \"\"\"\n    \"\"\"\n\n\n    @classmethod\n    def init_lists(cls):\n        \"Creates and returns lists fetched from DB\"\n        lists = []\n        c = cls.execute(cls, 'SELECT * FROM list')\n        list_rows = c.fetchall()\n        for l_row in list_rows:\n            l = GalleryList(l_row['list_name'], filter=l_row['list_filter'], id=l_row['list_id'])\n            if l_row['type'] == GalleryList.COLLECTION:\n                l.type = GalleryList.COLLECTION\n            elif l_row['type'] == GalleryList.REGULAR:\n                l.type = GalleryList.REGULAR\n            profile = l_row['profile']\n            if profile:\n                l.profile = bytes.decode(profile)\n            l.enforce = bool(l_row['enforce'])\n            l.regex = bool(l_row['regex'])\n            l.case = bool(l_row['l_case'])\n            l.strict = bool(l_row['strict'])\n            lists.append(l)\n            app_constants.GALLERY_LISTS.add(l)\n\n        return lists\n\n    @classmethod\n    def query_gallery(cls, gallery):\n        \"Maps gallery to the correct lists\"\n\n        c = cls.execute(cls, 'SELECT list_id FROM series_list_map WHERE series_id=?', (gallery.id,))\n        list_rows = [x['list_id'] for x in c.fetchall()]\n        for l in app_constants.GALLERY_LISTS:\n            if l._id in list_rows:\n                l.add_gallery(gallery, False, _check_filter=False)\n\n    @classmethod\n    def modify_list(cls, gallery_list):\n        assert isinstance(gallery_list, GalleryList)\n        if gallery_list._id:\n            cls.execute(cls,\n               \"\"\"UPDATE list SET list_name=?, list_filter=?, profile=?,\n               type=?, enforce=?, regex=?, l_case=?, strict=? WHERE list_id=?\"\"\",\n                   (gallery_list.name, gallery_list.filter, str.encode(gallery_list.profile),\n                    gallery_list.type, int(gallery_list.enforce), int(gallery_list.regex), int(gallery_list.case),\n                   int(gallery_list.strict), gallery_list._id))\n\n    @classmethod\n    def add_list(cls, gallery_list):\n        \"Adds a list of GalleryList class to DB\"\n        assert isinstance(gallery_list, GalleryList)\n        if gallery_list._id:\n            ListDB.modify_list(gallery_list)\n        else:\n            c = cls.execute(cls, \"\"\"INSERT INTO list(list_name, list_filter, profile, type,\n                            enforce, regex, l_case, strict) VALUES(?, ?, ?, ?, ?, ?, ?, ?)\"\"\",\n                   (gallery_list.name, gallery_list.filter, str.encode(gallery_list.profile), gallery_list.type,\n                        int(gallery_list.enforce), int(gallery_list.regex), int(gallery_list.case), int(gallery_list.strict)))\n            gallery_list._id = c.lastrowid\n\n        ListDB.add_gallery_to_list(gallery_list.galleries(), gallery_list)\n\n    @classmethod\n    def _g_id_or_list(cls, gallery_or_id_or_list):\n        \"Returns gallery ids\"\n        if isinstance(gallery_or_id_or_list, (Gallery, int)):\n            gallery_or_id_or_list = [gallery_or_id_or_list]\n\n        if isinstance(gallery_or_id_or_list, list):\n            if gallery_or_id_or_list:\n                if isinstance(gallery_or_id_or_list[0], Gallery):\n                    gallery_or_id_or_list = [g.id for g in gallery_or_id_or_list]\n        return gallery_or_id_or_list\n\n    @classmethod\n    def add_gallery_to_list(cls, gallery_or_id_or_list, gallery_list):\n        assert isinstance(gallery_list, GalleryList)\n        \"Maps provided gallery or list of galleries or gallery id to list\"\n        g_ids = ListDB._g_id_or_list(gallery_or_id_or_list)\n\n        values = [(gallery_list._id, x) for x in g_ids]\n        cls.executemany(cls, 'INSERT OR IGNORE INTO series_list_map(list_id, series_id) VALUES(?, ?)', values)\n\n    @classmethod\n    def remove_list(cls, gallery_list):\n        \"Deletes list from DB\"\n        assert isinstance(gallery_list, GalleryList)\n        if gallery_list._id:\n            cls.execute(cls, 'DELETE FROM list WHERE list_id=?', (gallery_list._id,))\n        try:\n            app_constants.GALLERY_LISTS.remove(gallery_list)\n        except KeyError:\n            pass\n\n    @classmethod\n    def remove_gallery_from_list(cls, gallery_or_id_or_list, gallery_list):\n        assert isinstance(gallery_list, GalleryList)\n        \"Removes provided gallery or list of galleries or gallery id from list\"\n        if gallery_list._id:\n            g_ids = ListDB._g_id_or_list(gallery_or_id_or_list)\n\n            values = [(gallery_list._id, x) for x in g_ids]\n            cls.executemany(cls, 'DELETE FROM series_list_map WHERE list_id=? AND series_id=?', values)\n\nclass HashDB(DBBase):\n    \"\"\"\n    Contains the following methods:\n\n    find_gallery -> returns galleries which matches the given list of hashes\n    get_gallery_hashes -> returns all hashes with the given gallery id in a list\n    get_gallery_hash -> returns hash of chapter specified. If page is specified, returns hash of chapter page\n    gen_gallery_hashes <- generates hashes for gallery's chapters and inserts them to db\n    rebuild_gallery_hashes <- inserts hashes into DB only if it doesnt already exist\n    \"\"\"\n\n    @classmethod\n    def find_gallery(cls, hashes):\n        assert isinstance(hashes, list)\n        gallery_ids = {}\n        hash_status = []\n        for hash in hashes:\n            r = cls.execute(cls, 'SELECT series_id FROM hashes WHERE hash=?', (hash,))\n            try:\n                g_ids = r.fetchall()\n                for r in g_ids:\n                    g_id = r['series_id']\n                    if g_id not in gallery_ids:\n                        gallery_ids[g_id] = 1\n                    else:\n                        gallery_ids[g_id] = gallery_ids[g_id] + 1\n                if g_ids:\n                    hash_status.append(True)\n                else:\n                    hash_status.append(False)\n            except KeyError:\n                hash_status.append(False)\n            except TypeError:\n                hash_status.append(False)\n\n        if all(hash_status):\n            # the one with most matching hashes\n            g_id = None\n            h_match_count = 0\n            for g in gallery_ids:\n                if gallery_ids[g] > h_match_count:\n                    h_match_count = gallery_ids[h]\n                    g_id = g\n            if g_id:\n                weak_gallery = Gallery()\n                weak_gallery.id = g_id\n                return weak_gallery\n\n        return None\n    \n    @classmethod\n    def get_gallery_hashes(cls, gallery_id):\n        \"Returns all hashes with the given gallery id in a list\"\n        cursor = cls.execute(cls, 'SELECT hash FROM hashes WHERE series_id=?',\n                (gallery_id,))\n        hashes = []\n        try:\n            for row in cursor.fetchall():\n                hashes.append(row['hash'])\n        except IndexError:\n            return []\n        return hashes\n\n    @classmethod\n    def get_gallery_hash(cls, gallery_id, chapter, page=None):\n        \"\"\"\n        returns hash of chapter. If page is specified, returns hash of chapter page\n        \"\"\"\n        assert isinstance(gallery_id, int)\n        assert isinstance(chapter, int)\n        if page:\n            assert isinstance(page, int)\n        chap_id = ChapterDB.get_chapter_id(gallery_id, chapter)\n        if not chap_id:\n            return None\n        if page:\n            exceuting = [\"SELECT hash FROM hashes WHERE series_id=? AND chapter_id=? AND page=?\",\n                     (gallery_id, chap_id, page)]\n        else:\n            exceuting = [\"SELECT hash FROM hashes WHERE series_id=? AND chapter_id=?\",\n                     (gallery_id, chap_id)]\n        hashes = []\n        c = cls.execute(cls, *exceuting)\n        for h in c.fetchall():\n            try:\n                hashes.append(h['hash'])\n            except KeyError:\n                pass\n        return hashes\n\n    @classmethod\n    def gen_gallery_hash(cls, gallery, chapter, page=None, color_img=False, _name=None):\n        \"\"\"\n        Generate hash for a specific chapter.\n        Set page to only generate specific page\n        page: 'mid' or number or list of numbers\n        color_img: if true then a hash to colored img will be returned if possible\n        Returns dict with chapter number or 'mid' as key and hash as value\n        \"\"\"\n        assert isinstance(gallery, Gallery)\n        assert isinstance(chapter, int)\n        if page != None:\n            assert isinstance(page, (int, str, list))\n        skip_gen = False\n        if gallery.id:\n            chap_id = ChapterDB.get_chapter_id(gallery.id, chapter)\n            \n            c = cls.execute(cls, 'SELECT hash, page FROM hashes WHERE series_id=? AND chapter_id=?',\n                   (gallery.id, chap_id,))\n            hashes = {}\n            for r in c.fetchall():\n                try:\n                    if r['hash'] and r['page'] != None:\n                        hashes[r['page']] = r['hash']\n                except TypeError:\n                    pass\n            if isinstance(page, (int, list)):\n                if isinstance(page, int):\n                    _page = [page]\n                else:\n                    _page = page\n                h = {}\n                t = False\n                for p in _page:\n                    if p in hashes:\n                        h[p] = hashes[p]\n                    else:\n                        t = True\n                if not t:\n                    skip_gen = True\n                    hashes = h\n\n            elif gallery.chapters[chapter].pages == len(hashes.keys()):\n                skip_gen = True\n                if page == \"mid\":\n                    try:\n                        hashes = {'mid':hashes[len(hashes) // 2]}\n                    except KeyError:\n                        skip_gen = False\n\n\n        if not skip_gen or color_img:\n\n            def look_exists(page):\n                \"\"\"check if hash already exists in database\n                returns hash, else returns None\"\"\"\n                c = cls.execute(cls, 'SELECT hash FROM hashes WHERE page=? AND chapter_id=?',\n                       (page, chap_id,))\n                try: # exists\n                    return c.fetchone()['hash']\n                except TypeError: # doesnt exist\n                    return None\n                except IndexError:\n                    return None\n\n            if gallery.dead_link:\n                log_e(\"Could not generate hash of dead gallery: {}\".format(gallery.title.encode(errors='ignore')))\n                return {}\n\n            try:\n                chap = gallery.chapters[chapter]\n            except KeyError:\n                utils.make_chapters(gallery)\n                try:\n                    chap = gallery.chapters[chapter]\n                except KeyError:\n                    return {}\n                \n            executing = []\n            try:\n                if gallery.is_archive:\n                    raise NotADirectoryError\n                imgs = sorted([x.path for x in scandir.scandir(chap.path) if x.path.endswith(utils.IMG_FILES)])\n                pages = {}\n                for n, i in enumerate(imgs):\n                    pages[n] = i\n\n                if page != None:\n                    pages = {}\n                    if color_img:\n                        # if first img is colored, then return filepath of that\n                        if not utils.image_greyscale(imgs[0]):\n                            return {'color':imgs[0]}\n                    if page == 'mid':\n                        imgs = imgs[len(imgs) // 2]\n                        pages[len(imgs) // 2] = imgs\n                    elif isinstance(page, list):\n                        try:\n                            for p in page:\n                                pages[p] = imgs[p]\n                        except IndexError:\n                            raise app_constants.InternalPagesMismatch\n                    else:\n                        imgs = imgs[page]\n                        pages = {page:imgs}\n\n                hashes = {}\n                if gallery.id != None:\n                    for p in pages:\n                        h = look_exists(p)\n                        if not h:\n                            with open(pages[p], 'rb') as f:\n                                h = generate_img_hash(f)\n                            executing.append((h, gallery.id, chap_id, p,))\n                        hashes[p] = h\n                else:\n                    for i in pages:\n                        with open(pages[i], 'rb') as f:\n                            hashes[i] = generate_img_hash(f)\n\n            except NotADirectoryError:\n                temp_dir = os.path.join(app_constants.temp_dir, str(uuid.uuid4()))\n                is_archive = gallery.is_archive\n                try:\n                    if is_archive:\n                        zip = ArchiveFile(gallery.path)\n                    else:\n                        zip = ArchiveFile(chap.path)\n                except app_constants.CreateArchiveFail:\n                    log_e('Could not generate hash: CreateZipFail')\n                    return {}\n\n                pages = {}\n                if page != None:\n                    p = 0\n                    con = sorted(zip.dir_contents(chap.path))\n                    if color_img:\n                        # if first img is colored, then return hash of that\n                        f_bytes = io.BytesIO(zip.open(con[0], False))\n                        if not utils.image_greyscale(f_bytes):\n                            return {'color':zip.extract(con[0])}\n                        f_bytes.close()\n                    if page == 'mid':\n                        p = len(con) // 2\n                        img = con[p]\n                        pages = {p:zip.open(img, True)}\n                    elif isinstance(page, list):\n                        for x in page:\n                            pages[x] = zip.open(con[x], True)\n                    else:\n                        p = page\n                        img = con[p]\n                        pages = {p:zip.open(img, True)}\n\n\n                else:\n                    imgs = sorted(zip.dir_contents(chap.path))\n                    for n, img in enumerate(imgs):\n                        pages[n] = zip.open(img, True)\n                zip.close()\n\n                hashes = {}\n                if gallery.id != None:\n                    for p in pages:\n                        h = look_exists(p)\n                        if not h:\n                            h = generate_img_hash(pages[p])\n                            executing.append((h, gallery.id, chap_id, p,))\n                        hashes[p] = h\n                else:\n                    for i in pages:\n                        hashes[i] = generate_img_hash(pages[i])\n\n            if executing:\n                cls.executemany(cls, 'INSERT INTO hashes(hash, series_id, chapter_id, page) VALUES(?, ?, ?, ?)',\n                       executing)\n\n\n        if page == 'mid':\n            r_hash = {'mid':list(hashes.values())[0]}\n        else:\n            r_hash = hashes\n\n        if _name != None:\n            try:\n                r_hash[_name] = r_hash[page]\n            except KeyError:\n                pass\n        return r_hash\n\n    @classmethod\n    def gen_gallery_hashes(cls, gallery):\n        \"Generates hashes for gallery's first chapter and inserts them to DB\"\n        return HashDB.gen_gallery_hash(gallery, 0)\n\n    @staticmethod\n    def rebuild_gallery_hashes(gallery):\n        \"Inserts hashes into DB only if it doesnt already exist\"\n        assert isinstance(gallery, Gallery)\n        hashes = HashDB.get_gallery_hashes(gallery.id)\n\n        if not hashes:\n            hashes = HashDB.gen_gallery_hashes(gallery)\n        return hashes\n\n    @classmethod\n    def del_gallery_hashes(cls, gallery_id):\n        \"Deletes all hashes linked to the given gallery id\"\n        cls.execute(cls, 'DELETE FROM hashes WHERE series_id=?', (gallery_id,))\n\nclass GalleryList:\n    \"\"\"\n    Provides access to lists..\n    methods:\n    - add_gallery <- adds a gallery of Gallery class to list\n    - remove_gallery <- removes galleries matching the provided gallery id\n    - clear <- removes all galleries from the list\n    - galleries -> returns a list with all galleries in list\n    - scan <- scans for galleries matching the listfilter and adds them to gallery\n    \"\"\"\n    # types\n    REGULAR, COLLECTION = range(2)\n\n    def __init__(self, name, list_of_galleries=[], filter=None, id=None, _db=True):\n        self._id = id # shouldnt ever be touched\n        self.name = name\n        self.profile = ''\n        self.type = self.REGULAR\n        self.filter = filter\n        self.enforce = False\n        self.regex = False\n        self.case = False\n        self.strict = False\n        self._galleries = set()\n        self._ids_chache = []\n        self._scanning = False\n        self.add_gallery(list_of_galleries, _db)\n\n    def add_gallery(self, gallery_or_list_of, _db=True, _check_filter=True):\n        \"add_gallery <- adds a gallery of Gallery class to list\"\n        assert isinstance(gallery_or_list_of, (Gallery, list))\n        if isinstance(gallery_or_list_of, Gallery):\n            gallery_or_list_of = [gallery_or_list_of]\n        if _check_filter and self.filter and self.enforce:\n            execute(self.scan, True, gallery_or_list_of)\n            return\n        new_galleries = []\n        for gallery in gallery_or_list_of:\n            self._galleries.add(gallery)\n            if not utils.b_search(self._ids_chache, gallery.id):\n                new_galleries.append(gallery)\n                self._ids_chache.append(gallery.id)\n                # uses timsort algorithm so it's ok\n                self._ids_chache.sort()\n        if _db:\n            execute(ListDB.add_gallery_to_list, True, new_galleries, self)\n\n    def remove_gallery(self, gallery_id_or_list_of):\n        \"remove_gallery <- removes galleries matching the provided gallery id\"\n        if isinstance(gallery_id_or_list_of, int):\n            gallery_id_or_list_of = [gallery_id_or_list_of]\n        g_ids = gallery_id_or_list_of\n        g_ids_to_delete = []\n        g_to_delete = []\n        for g in self._galleries:\n            if g.id in g_ids:\n                g_to_delete.append(g)\n                try:\n                    self._ids_chache.remove(g.id)\n                except ValueError:\n                    pass\n                g_ids_to_delete.append(g.id)\n        for g in g_to_delete:\n            self._galleries.remove(g)\n        execute(ListDB.remove_gallery_from_list, True, g_ids_to_delete, self)\n\n    def clear(self):\n        \"removes all galleries from the list\"\n        if self._galleries:\n            execute(ListDB.remove_gallery_from_list, True, list(self._galleries), self)\n        self._galleries.clear()\n        self._ids_chache.clear()\n\n    def galleries(self):\n        \"returns a list with all galleries in list\"\n        return list(self._galleries)\n\n    def __contains__(self, g):\n        return utils.b_search(self._ids_chache, g.id)\n\n    def add_to_db(self):\n        app_constants.GALLERY_LISTS.add(self)\n        execute(ListDB.add_list, True, self)\n\n    def scan(self, galleries=None):\n        if self.filter and not self._scanning:\n            self._scanning = True\n            if isinstance(galleries, Gallery):\n                galleries = [galleries]\n            if not galleries:\n                galleries = app_constants.GALLERY_DATA\n            new_galleries = []\n            filter_term = ' '.join(self.filter.split())\n            args = []\n            if self.regex:\n                args.append(app_constants.Search.Regex)\n            if self.case:\n                args.append(app_constants.Search.Case)\n            if self.strict:\n                args.append(app_constants.Search.Strict)\n            search_pieces = utils.get_terms(filter_term)\n\n            def _search_g(gallery):\n                all_terms = {t: False for t in search_pieces}\n\n                for t in search_pieces:\n                    if gallery.contains(t, args):\n                        all_terms[t] = True\n\n                if all(all_terms.values()):\n                    return True\n                return False\n\n            for gallery in galleries:\n                if _search_g(gallery):\n                    new_galleries.append(gallery)\n\n            if self.enforce:\n                g_to_remove = []\n                for g in self.galleries():\n                    if not _search_g(g):\n                        g_to_remove.append(g.id)\n                if g_to_remove:\n                    self.remove_gallery(g_to_remove)\n            self.add_gallery(new_galleries, _check_filter=False)\n            self._scanning = False\n\n    def __lt__(self, other):\n        return self.name < other.name\n\nclass Gallery:\n    \"\"\"\n    Base class for a gallery.\n    Available data:\n    id -> Not to be editied. Do not touch.\n    title <- [list of titles] or str\n    profile <- path to thumbnail\n    path <- path to gallery\n    artist <- str\n    chapters <- {<number>:<path>}\n    chapter_size <- int of number of chapters\n    info <- str\n    fav <- int (1 for true 0 for false)\n    rating <- float\n    type <- str (Manga? Doujin? Other?)\n    language <- str\n    status <- \"unknown\", \"completed\" or \"ongoing\"\n    tags <- list of str\n    pub_date <- date\n    date_added <- date, will be defaulted to today if not specified\n    last_read <- timestamp (e.g. time.time())\n    times_read <- an integer telling us how many times the gallery has been opened\n    hashes <- a list of hashes of the gallery's chapters\n    exed <- indicator on if gallery metadata has been fetched\n    valid <- a bool indicating the validity of the gallery\n\n    Takes ownership of ChaptersContainer\n    \"\"\"\n\n    def __init__(self):\n\n        self.id = None # Will be defaulted.\n        self.title = \"\"\n        self.profile = \"\"\n        self._path = \"\"\n        self.path_in_archive = \"\"\n        self.is_archive = 0\n        self.artist = \"\"\n        self._chapters = ChaptersContainer(self)\n        self.info = \"\"\n        self.fav = 0\n        self.rating = 0\n        self.type = \"\"\n        self.link = \"\"\n        self.language = \"\"\n        self.status = \"\"\n        self.tags = {}\n        self.pub_date = None\n        self.date_added = datetime.datetime.now().replace(microsecond=0)\n        self.last_read = None\n        self.times_read = 0\n        self.valid = False\n        self._db_v = None\n        self.hashes = []\n        self.exed = 0\n        self.file_type = \"folder\"\n        self.view = app_constants.ViewType.Default # default view\n\n        self._grid_visible = False\n        self._list_view_selected = False\n        self._profile_qimage = {}\n        self._profile_load_status = {}\n        self.dead_link = False\n        self.state = app_constants.GalleryState.Default\n        self.qtime = QTime() # used by views to record addition\n\n    @property\n    def path(self):\n        return self._path\n\n    @path.setter\n    def path(self, n_p):\n        self._path = n_p\n        _, ext = os.path.splitext(n_p)\n        if ext:\n            self.file_type = ext[1:].lower() # remove dot\n\n    def set_defaults(self):\n        if not self.type:\n            self.type = app_constants.G_DEF_TYPE.capitalize()\n        if not self.language:\n            self.language = app_constants.G_DEF_LANGUAGE.capitalize()\n        if not self.status:\n            self.status = app_constants.G_DEF_STATUS.capitalize()\n\n    def reset_profile(self):\n        self._profile_load_status.clear()\n        self._profile_qimage.clear()\n\n    def _profile_loaded(self, img, ptype=None, method=None):\n        self._profile_load_status[ptype] = img\n        if method and img:\n            method(self, img)\n\n    def get_profile(self, ptype, on_method=None):\n        psize = app_constants.THUMB_DEFAULT\n        if ptype == app_constants.ProfileType.Small:\n            psize = app_constants.THUMB_SMALL\n\n        if ptype in self._profile_qimage:\n            f = self._profile_qimage[ptype]\n            if not f.done():\n                return\n            if f.result():\n                return f.result()\n        img = self._profile_load_status.get(ptype)\n        if not img:\n            self._profile_qimage[ptype] = Executors.load_thumbnail(self.profile, psize,\n                on_method=self._profile_loaded,\n                ptype=ptype, method=on_method)\n\n        return img\n\n    def set_profile(self, future):\n        \"set with profile with future object\"\n        self.profile = future.result()\n        if self.id != None:\n            execute(GalleryDB.modify_gallery, True, self.id, profile=self.profile, priority=0)\n\n    @property\n    def chapters(self):\n        return self._chapters\n\n    @chapters.setter\n    def chapters(self, chp_cont):\n        assert isinstance(chp_cont, ChaptersContainer)\n        chp_cont.set_parent(self)\n        self._chapters = chp_cont\n\n    def merge(galleries):\n        \"Merge galleries into this galleries, adding them as chapters\"\n        pass\n\n    def gen_hashes(self):\n        \"Generate hashes while inserting them into DB\"\n        if not self.hashes:\n            hash = HashDB.gen_gallery_hashes(self)\n            if hash:\n                self.hashes = hash\n                return True\n            else:\n                return False\n        else:\n            return True\n\n    def validate(self):\n        \"Validates gallery, returns status\"\n        # TODO: Extend this\n        validity = []\n        status = False\n\n        #if not self.hashes:\n        #\tHashDB.gen_gallery_hashes(self)\n        #\tself.hashes = HashDB.get_gallery_hashes(self.id)\n\n        if all(validity):\n            status = True\n            self.valid = True\n        return status\n\n    def invalidities(self):\n        \"\"\"\n        Checks all attributes for invalidities.\n        Returns list of string with invalid attribute names\n        \"\"\"\n        return []\n\n    def _keyword_search(self, ns, tag, args=[]):\n        term = ''\n        lt, gt = range(2)\n        def _search(term):\n            if app_constants.Search.Regex in args:\n                if utils.regex_search(tag, term, args):\n                    return True\n            else:\n                if app_constants.DEBUG:\n                    log_d(\"{} {}\".format(tag, term))\n                if utils.search_term(tag, term, args):\n                    return True\n            return False\n\n        def _operator_parse(tag):\n            o = None\n            if tag:\n                if tag[0] == '<':\n                    o = lt\n                    tag = tag[1:]\n                elif tag[0] == '>':\n                    o = gt\n                    tag = tag[1:]\n            return tag, o\n\n        def _operator_supported(attr, date=False):\n            try:\n                o_tag, o = _operator_parse(tag)\n                if date:\n                    o_tag = dateparser.parse(o_tag, dayfirst=True)\n                    if o_tag:\n                        o_tag = o_tag.date()\n                else:\n                    o_tag = int(o_tag)\n                if o != None:\n                    if o == gt:\n                        return o_tag < attr\n                    elif o == lt:\n                        return o_tag > attr\n                else:\n                    return o_tag == attr\n            except ValueError:\n                return False\n\n        if ns == 'Title':\n            term = self.title\n        elif ns in ['Language', 'Lang']:\n            term = self.language\n        elif ns == 'Type':\n            term = self.type\n        elif ns == 'Status':\n            term = self.status\n        elif ns == 'Artist':\n            term = self.artist\n        elif ns == 'Url':\n            term = self.link\n        elif ns in ['Descr', 'Description']:\n            term = self.info\n        elif ns in ['Chapter', 'Chapters']:\n            return _operator_supported(self.chapters.count())\n        elif ns in ['Read_count', 'Read count', 'Times_read', 'Times read']:\n            return _operator_supported(self.times_read)\n        elif ns in ['Rating', 'Stars']:\n            return _operator_supported(self.rating)\n        elif ns in ['Date_added', 'Date added']:\n            return _operator_supported(self.date_added.date(), True)\n        elif ns in ['Pub_date', 'Publication', 'Pub date']:\n            if self.pub_date:\n                return _operator_supported(self.pub_date.date(), True)\n            return False\n        elif ns in ['Last_read', 'Last read']:\n            if self.last_read:\n                return _operator_supported(self.last_read.date(), True)\n            return False\n        return _search(term)\n\n    def __contains__(self, key):\n        assert isinstance(key, Chapter), \"Can only check for chapters in gallery\"\n        return self.chapters.__contains__(key)\n\n\n    def contains(self, key, args=[]):\n        \"Check if gallery contains keyword\"\n        is_exclude = False if key[0] == '-' else True\n        key = key[1:] if not is_exclude else key\n        default = False if is_exclude else True\n        if key:\n            # check in title/artist/language\n            found = False\n            if not ':' in key:\n                for g_attr in [self.title, self.artist, self.language]:\n                    if not g_attr:\n                        continue\n                    if app_constants.Search.Regex in args:\n                        if utils.regex_search(key, g_attr, args=args):\n                            found = True\n                            break\n                    else:\n                        if utils.search_term(key, g_attr, args=args):\n                            found = True\n                            break\n\n            # check in tag\n            if not found:\n                tags = key.split(':')\n                ns = tag = ''\n                # only namespace is lowered and capitalized for now\n                if len(tags) > 1:\n                    ns = tags[0].lower().capitalize()\n                    tag = tags[1]\n                else:\n                    tag = tags[0]\n\n                # very special keywords\n                if ns:\n                    key_word = ['none', 'null']\n                    if ns == 'Tag' and tag in key_word:\n                        if not self.tags or len(self.tags) == 1 and 'default' in self.tags and not self.tags['default']:\n                            return is_exclude\n                    elif ns == 'Artist' and tag in key_word:\n                        if not self.artist:\n                            return is_exclude\n                    elif ns == 'Status' and tag in key_word:\n                        if not self.status or self.status == 'Unknown':\n                            return is_exclude\n                    elif ns == 'Language' and tag in key_word:\n                        if not self.language:\n                            return is_exclude\n                    elif ns == 'Url' and tag in key_word:\n                        if not self.link:\n                            return is_exclude\n                    elif ns in ('Descr', 'Description') and tag in key_word:\n                        if not self.info or self.info == 'No description..':\n                            return is_exclude\n                    elif ns == 'Type' and tag in key_word:\n                        if not self.type:\n                            return is_exclude\n                    elif ns in ('Publication', 'Pub_date', 'Pub date') and tag in key_word:\n                        if not self.pub_date:\n                            return is_exclude\n                    elif ns == 'Path' and tag in key_word:\n                        if self.dead_link:\n                            return is_exclude\n\n                if app_constants.Search.Regex in args:\n                    if ns:\n                        if self._keyword_search(ns, tag, args = args):\n                            return is_exclude\n\n                        for x in self.tags:\n                            if utils.regex_search(ns, x):\n                                for t in self.tags[x]:\n                                    if utils.regex_search(tag, t, True, args=args):\n                                        return is_exclude\n                    else:\n                        for x in self.tags:\n                            for t in self.tags[x]:\n                                if utils.regex_search(tag, t, True, args=args):\n                                    return is_exclude\n                else:\n                    if ns:\n                        if self._keyword_search(ns, tag, args=args):\n                            return is_exclude\n\n                        if ns in self.tags:\n                            for t in self.tags[ns]:\n                                if utils.search_term(tag, t, True, args=args):\n                                    return is_exclude\n                    else:\n                        for x in self.tags:\n                            for t in self.tags[x]:\n                                if utils.search_term(tag, t, True, args=args):\n                                    return is_exclude\n            else:\n                return is_exclude\n        return default\n\n    def move_gallery(self, new_path=''):\n        log_i(\"Moving gallery...\")\n        log_d(\"Old gallery path: {}\".format(self.path))\n        old_head, old_tail = os.path.split(self.path)\n        try:\n            self.path = utils.move_files(self.path, new_path)\n        except PermissionError:\n            log.exception(\"Failed to move gallery\")\n            app_constants.NOTIF_BAR.add_text(\"Permission Error: Failed to move gallery ({})\".format(self.title))\n            return\n        new_head, new_tail = os.path.split(self.path)\n        for chap in self.chapters:\n            if not chap.in_archive:\n                head, tail = os.path.split(chap.path)\n                log_d(\"old chapter path: {}\".format(chap.path))\n                if os.path.exists(os.path.join(self.path, tail)):\n                    chap.path = os.path.join(self.path, tail)\n                    continue\n                if os.path.join(old_head, old_tail) == os.path.join(head, tail):\n                    chap.path = self.path\n                    continue\n\n                if self.is_archive:\n                    utils.move_files(chap.path, os.path.join(new_head, tail))\n                else:\n                    utils.move_files(chap.path, os.path.join(self.path, tail))\n\n    def __lt__(self, other):\n        return self.id < other.id\n\n    def __str__(self):\n        s = \"\"\n        for x in sorted(self.__dict__):\n            s += \"{:>20}: {:>15}\\n\".format(x, str(self.__dict__[x]))\n        return s\n\nclass Chapter:\n    \"\"\"\n    Base class for a chapter\n    Contains following attributes:\n    parent -> The ChapterContainer it belongs in\n    gallery -> The Gallery it belongs to\n    title -> title of chapter\n    path -> path to chapter\n    number -> chapter number\n    pages -> chapter pages\n    in_archive -> 1 if the chapter path is in an archive else 0\n    \"\"\"\n    def __init__(self, parent, gallery, number=0, path='', pages=0, in_archive=0, title=''):\n        self.parent = parent\n        self.gallery = gallery\n        self.title = title\n        self.path = path\n        self.number = number\n        self.pages = pages\n        self.in_archive = in_archive\n\n    def __lt__(self, other):\n        return self.number < other.number\n\n    def __str__(self):\n        s = \"\"\"\n        Chapter: {}\n        Title: {}\n        Path: {}\n        Pages: {}\n        in_archive: {}\n        \"\"\".format(self.number, self.title, self.path, self.pages, self.in_archive)\n        return s\n\n    @property\n    def next_chapter(self):\n        try:\n            return self.parent[self.number + 1]\n        except KeyError:\n            return None\n\n    @property\n    def previous_chapter(self):\n        try:\n            return self.parent[self.number - 1]\n        except KeyError:\n            return None\n\n    def open(self, stat_msg=True):\n        if stat_msg:\n            txt = \"Opening chapter {} of {}\".format(self.number + 1, self.gallery.title)\n            app_constants.STAT_MSG_METHOD(txt)\n            app_constants.NOTIF_BAR.add_text(txt)\n        if self.in_archive:\n            if self.gallery.is_archive:\n                execute(utils.open_chapter, True, self.path, self.gallery.path)\n            else:\n                execute(utils.open_chapter, True, '', self.path)\n        else:\n            execute(utils.open_chapter, True, self.path)\n        self.gallery.times_read += 1\n        self.gallery.last_read = datetime.datetime.now().replace(microsecond=0)\n        execute(GalleryDB.modify_gallery, True, self.gallery.id, times_read=self.gallery.times_read,\n                               last_read=self.gallery.last_read)\n\nclass ChaptersContainer:\n    \"\"\"\n    A container for chapters.\n    Acts like a list/dict of chapters.\n\n    Iterable returns a ordered list of chapters\n    Sets to gallery.chapters\n    \"\"\"\n    def __init__(self, gallery=None):\n        self.parent = None\n        self._data = {}\n\n        if gallery:\n            gallery.chapters = self\n\n    def set_parent(self, gallery):\n        assert isinstance(gallery, (Gallery, None))\n        self.parent = gallery\n        for n in self._data:\n            chap = self._data[n]\n            chap.gallery = gallery\n\n    def add_chapter(self, chp, overwrite=True, db=False):\n        \"Add a chapter of Chapter class to this container\"\n        assert isinstance(chp, Chapter), \"Chapter must be an instantiated Chapter class\"\n        \n        if not overwrite:\n            try:\n                _ = self._data[chp.number]\n                raise app_constants.ChapterExists\n            except KeyError:\n                pass\n        chp.gallery = self.parent\n        chp.parent = self\n        self[chp.number] = chp\n        \n\n        if db:\n            # TODO: implement this\n            pass\n\n    def create_chapter(self, number=None):\n        \"\"\"\n        Creates Chapter class with the next chapter number or passed number arg and adds to container\n        The chapter will be returned\n        \"\"\"\n        if number:\n            chp = Chapter(self, self.parent, number=number)\n            self[number] = chp\n        else:\n            next_number = 0\n            for n in list(self._data.keys()):\n                if n > next_number:\n                    next_number = n\n                else:\n                    next_number += 1\n            chp = Chapter(self, self.parent, number=next_number)\n            self[next_number] = chp\n        return chp\n\n    def update_chapter_pages(self, number):\n        \"Returns status on success\"\n        if self.parent.dead_link:\n            return False\n        chap = self[number]\n        if chap.in_archive:\n            _archive = utils.ArchiveFile(chap.gallery.path)\n            chap.pages = len([x for x in _archive.dir_contents(chap.path) if x.endswith(IMG_FILES)])\n            _archive.close()\n        else:\n            chap.pages = len([x for x in scandir.scandir(chap.path) if x.path.endswith(IMG_FILES)])\n\n        execute(ChapterDB.update_chapter, True, self, [chap.number])\n        return True\n\n    def pages(self):\n        p = 0\n        for c in self:\n            p += c.pages\n        return p\n\n    def get_chapter(self, number):\n        return self[number]\n\n    def get_all_chapters(self):\n        return list(self._data.values())\n\n    def count(self):\n        return len(self)\n\n    def pop(self, key, default=None):\n        return self._data.pop(key, default)\n\n    def __len__(self):\n        return len(self._data)\n\n    def __getitem__(self, key):\n        return self._data[key]\n\n    def __setitem__(self, key, value):\n        assert isinstance(key, int), \"Key must be a chapter number\"\n        assert isinstance(value, Chapter), \"Value must be an instantiated Chapter class\"\n        \n        if value.gallery != self.parent:\n            raise app_constants.ChapterWrongParentGallery\n        self._data[key] = value\n\n    def __delitem__(self, key):\n        del self._data[key]\n\n    def __iter__(self):\n        return iter([self[c] for c in sorted(self._data.keys())])\n\n    def __bool__(self):\n        return bool(self._data)\n\n    def __str__(self):\n        s = \"\"\n        for c in self:\n            s += '\\n' + '{}'.format(c)\n        if not s:\n            return '{}'\n        return s\n\n    def __contains__(self, key):\n        if key.gallery == self.parent and key in [self.data[c] for c in self._data]:\n            return True\n        return False\n\n\nclass AdminDB(QObject):\n    DONE = pyqtSignal(bool)\n    PROGRESS = pyqtSignal(int)\n    DATA_COUNT = pyqtSignal(int)\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n    def from_v021_to_v022(self, old_db_path=db_constants.DB_PATH):\n        log_i(\"Started rebuilding database\")\n        if DBBase._DB_CONN:\n            DBBase._DB_CONN.close()\n        DBBase._DB_CONN = db.init_db(old_db_path)\n        db_galleries = execute(GalleryDB.get_all_gallery, False, False, True, True)\n        galleries = []\n        for g in db_galleries:\n            if not os.path.exists(g.path):\n                log_i(\"Gallery doesn't exist anymore: {}\".format(g.title.encode(errors=\"ignore\")))\n            else:\n                galleries.append(g)\n\n        n_galleries = []\n        # get all chapters\n        log_i(\"Getting chapters...\")\n        chap_rows = DBBase().execute(\"SELECT * FROM chapters\").fetchall()\n        data_count = len(chap_rows) * 2\n        self.DATA_COUNT.emit(data_count)\n        for n, chap_row in enumerate(chap_rows, -1):\n            log_d('Next chapter row')\n            for gallery in galleries:\n                if gallery.id == chap_row['series_id']:\n                    log_d('Found gallery for chapter row')\n                    chaps = ChaptersContainer(gallery)\n                    chap = chaps.create_chapter(chap_row['chapter_number'])\n                    c_path = bytes.decode(chap_row['chapter_path'])\n                    if c_path:\n                        try:\n                            t = utils.title_parser(os.path.split(c_path)[1])['title']\n                        except IndexError:\n                            t = c_path\n                    else:\n                        t = ''\n                    chap.title = t\n                    chap.path = c_path\n                    chap.in_archive = chap_row['in_archive']\n                    if gallery.is_archive:\n                        zip = utils.ArchiveFile(gallery.path)\n                        chap.pages = len(zip.dir_contents(chap.path))\n                        zip.close()\n                    else:\n                        chap.pages = len(list(scandir.scandir(gallery.path)))\n                    n_galleries.append(gallery)\n                    galleries.remove(gallery)\n                    break\n            self.PROGRESS.emit(n)\n        log_d(\"G: {} C:{}\".format(len(n_galleries), data_count - 1))\n        log_i(\"Database magic...\")\n        if os.path.exists(db_constants.THUMBNAIL_PATH):\n            for root, dirs, files in scandir.walk(db_constants.THUMBNAIL_PATH, topdown=False):\n                for name in files:\n                    os.remove(os.path.join(root, name))\n                for name in dirs:\n                    os.rmdir(os.path.join(root, name))\n\n        head = os.path.split(old_db_path)[0]\n        DBBase._DB_CONN.close()\n        t_db_path = os.path.join(head, 'temp.db')\n        conn = db.init_db(t_db_path)\n        DBBase._DB_CONN = conn\n        for n, g in enumerate(n_galleries, len(chap_rows) - 1):\n            log_d('Adding new gallery')\n            GalleryDB.add_gallery(g)\n            self.PROGRESS.emit(n)\n\n        conn.commit()\n        conn.close()\n\n        log_i(\"Cleaning up...\")\n        if os.path.exists(old_db_path):\n            utils.backup_database(old_db_path)\n            os.remove(old_db_path)\n        if os.path.exists(db_constants.DB_PATH):\n            os.remove(db_constants.DB_PATH)\n\n        os.rename(t_db_path, db_constants.DB_PATH)\n        self.PROGRESS.emit(data_count)\n        log_i(\"Finished rebuilding database\")\n        self.DONE.emit(True)\n        return True\n\n    def rebuild_database(self):\n        \"Rebuilds database\"\n        log_i(\"Initiating database rebuild\")\n        utils.backup_database()\n        log_i(\"Getting galleries...\")\n        galleries = GalleryDB.get_all_gallery()\n        self.DATA_COUNT.emit(len(galleries))\n        db.DBBase._DB_CONN.close()\n        log_i(\"Removing old database...\")\n        log_i(\"Creating new database...\")\n        temp_db = os.path.join(db_constants.DB_ROOT, \"happypanda_temp.db\")\n        if os.path.exists(temp_db):\n            os.remove(temp_db)\n        db.DBBase._DB_CONN = db.init_db(temp_db)\n        DBBase.begin()\n        log_i(\"Adding galleries...\")\n        GalleryDB.clear_thumb_dir()\n        for n, g in enumerate(galleries):\n            if not os.path.exists(g.path):\n                log_i(\"Gallery doesn't exist anymore: {}\".format(g.title.encode(errors=\"ignore\")))\n            else:\n                GalleryDB.add_gallery(g)\n            self.PROGRESS.emit(n)\n        DBBase.end()\n        DBBase._DB_CONN.close()\n        os.remove(db_constants.DB_PATH)\n        os.rename(temp_db, db_constants.DB_PATH)\n        db.DBBase._DB_CONN = db.init_db(db_constants.DB_PATH)\n        self.PROGRESS.emit(len(galleries))\n        log_i(\"Succesfully rebuilt database\")\n        self.DONE.emit(True)\n        return True\n\n    def rebuild_galleries(self):\n        galleries = execute(GalleryDB.get_all_gallery, False)\n        if galleries:\n            self.DATA_COUNT.emit(len(galleries))\n            log_i('Rebuilding galleries')\n            for n, g in enumerate(galleries, 1):\n                execute(GalleryDB.rebuild_gallery, False, g)\n                self.PROGRESS.emit(n)\n        self.DONE.emit(True)\n\n    def rebuild_thumbs(self, clear_first):\n        if clear_first:\n            log_i(\"Clearing thumbanils dir..\")\n            GalleryDB.clear_thumb_dir()\n\n        gs = []\n        gs.extend(app_constants.GALLERY_DATA)\n        gs.extend(app_constants.GALLERY_ADDITION_DATA)\n        self.DATA_COUNT.emit(len(app_constants.GALLERY_DATA))\n        log_i('Regenerating thumbnails')\n        for n, g in enumerate(gs, 1):\n            execute(GalleryDB.rebuild_thumb, False, g)\n            g.reset_profile()\n            self.PROGRESS.emit(n)\n        self.DONE.emit(True)\n\nclass DatabaseStartup(QObject):\n    \"\"\"\n    Fetches and emits database records\n    START: emitted when fetching from DB occurs\n    DONE: emitted when the initial fetching from DB finishes\n    \"\"\"\n    START = pyqtSignal()\n    DONE = pyqtSignal()\n    PROGRESS = pyqtSignal(str)\n    _DB = DBBase()\n\n\n    def __init__(self):\n        super().__init__()\n        ListDB.init_lists()\n        self._fetch_count = 500\n        self._offset = 0\n        self._fetching = False\n        self.count = 0\n        self._finished = False\n        self._loaded_galleries = []\n\n    def startup(self, manga_views):\n        self.START.emit()\n        self._fetching = True\n        self.count = GalleryDB.gallery_count()\n        remaining = self.count\n        while remaining > 0:\n            self.PROGRESS.emit(\"Loading galleries: {}\".format(remaining))\n            rec_to_fetch = min(remaining, self._fetch_count)\n            self.fetch_galleries(self._offset, rec_to_fetch, manga_views)\n            self._offset += rec_to_fetch\n            remaining = self.count - self._offset\n        [v.list_view.manga_delegate._increment_paint_level() for v in manga_views]\n        self.PROGRESS.emit(\"Loading chapters...\")\n        self.fetch_chapters()\n        self.PROGRESS.emit(\"Loading tags...\")\n        self.fetch_tags()\n        [v.list_view.manga_delegate._increment_paint_level() for v in manga_views]\n        self.PROGRESS.emit(\"Loading hashes...\")\n        self.fetch_hashes()\n        self._fetching = False\n        self.DONE.emit()\n\n    def fetch_galleries(self, f, t, manga_views):\n        c = execute(self._DB.execute, False, 'SELECT * FROM series LIMIT {}, {}'.format(f, t))\n        if c:\n            new_data = c.fetchall()\n            gallery_list = execute(GalleryDB.gen_galleries, False, new_data, {\"chapters\":False, \"tags\":False, \"hashes\":False})\n            #self._current_data.extend(gallery_list)\n            if gallery_list:\n                self._loaded_galleries.extend(gallery_list)\n                for view in manga_views:\n                    view_galleries = [g for g in gallery_list if g.view == view.view_type]\n                    view.gallery_model._gallery_to_add = view_galleries\n                    view.gallery_model.insertRows(view.gallery_model.rowCount(), len(view_galleries))\n\n    def fetch_chapters(self):\n        for g in self._loaded_galleries:\n            g.chapters = execute(ChapterDB.get_chapters_for_gallery, False, g.id)\n\n    def fetch_tags(self):\n        for g in self._loaded_galleries:\n            g.tags = execute(TagDB.get_gallery_tags, False, g.id)\n\n    def fetch_hashes(self):\n        for g in self._loaded_galleries:\n            g.hashes = execute(HashDB.get_gallery_hashes, False, g.id)\n\n\nif __name__ == '__main__':\n    #unit testing here\n    pass\n"
  },
  {
    "path": "version/gallerydialog.py",
    "content": "import queue, os, threading, random, logging, time, scandir\nfrom datetime import datetime\n\nfrom PyQt5.QtWidgets import (QWidget, QVBoxLayout, QDesktopWidget, QGroupBox,\n                             QHBoxLayout, QFormLayout, QLabel, QLineEdit,\n                             QPushButton, QProgressBar, QTextEdit, QComboBox,\n                             QDateEdit, QFileDialog, QMessageBox, QScrollArea,\n                             QCheckBox, QSizePolicy, QSpinBox)\nfrom PyQt5.QtCore import (pyqtSignal, Qt, QPoint, QDate, QThread, QTimer)\n\nimport app_constants\nimport utils\nimport gallerydb\nimport fetch\nimport misc\nimport database\nimport settings\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass GalleryDialog(QWidget):\n    \"\"\"\n    A window for adding/modifying gallery.\n    Pass a list of QModelIndexes to edit their data\n    or pass a path to preset path\n    \"\"\"\n\n    def __init__(self, parent, arg=None):\n        super().__init__(parent, Qt.Dialog)\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        self.setAutoFillBackground(True)\n        self.parent_widget = parent\n        m_l = QVBoxLayout()\n        self.main_layout = QVBoxLayout()\n        dummy = QWidget(self)\n        scroll_area = QScrollArea(self)\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setFrameStyle(scroll_area.StyledPanel)\n        dummy.setLayout(self.main_layout)\n        scroll_area.setWidget(dummy)\n        m_l.addWidget(scroll_area, 3)\n\n        final_buttons = QHBoxLayout()\n        final_buttons.setAlignment(Qt.AlignRight)\n        m_l.addLayout(final_buttons)\n        self.done = QPushButton(\"Done\")\n        self.done.setDefault(True)\n        cancel = QPushButton(\"Cancel\")\n        final_buttons.addWidget(cancel)\n        final_buttons.addWidget(self.done)\n        self._multiple_galleries = False\n        self._edit_galleries = []\n\n        def new_gallery():\n            self.setWindowTitle('Add a new gallery')\n            self.newUI()\n            self.commonUI()\n            self.done.clicked.connect(self.accept)\n            cancel.clicked.connect(self.reject)\n\n        if arg:\n            if isinstance(arg, (list, gallerydb.Gallery)):\n                if isinstance(arg, gallerydb.Gallery):\n                    self.setWindowTitle('Edit gallery')\n                    self._edit_galleries.append(arg)\n                else:\n                    self.setWindowTitle('Edit {} galleries'.format(len(arg)))\n                    self._multiple_galleries = True\n                    self._edit_galleries.extend(arg)\n                self.commonUI()\n                self.setGallery(arg)\n                self.done.clicked.connect(self.accept_edit)\n                cancel.clicked.connect(self.reject_edit)\n            elif isinstance(arg, str):\n                new_gallery()\n                self.choose_dir(arg)\n        else:\n            new_gallery()\n\n        log_d('GalleryDialog: Create UI: successful')\n        self.setLayout(m_l)\n        if self._multiple_galleries:\n            self.resize(500, 480)\n        else:\n            self.resize(500, 600)\n        frect = self.frameGeometry()\n        frect.moveCenter(QDesktopWidget().availableGeometry().center())\n        self.move(frect.topLeft())\n        self._fetch_inst = fetch.Fetch()\n        self._fetch_thread = QThread(self)\n        self._fetch_thread.setObjectName(\"GalleryDialog metadata thread\")\n        self._fetch_inst.moveToThread(self._fetch_thread)\n        self._fetch_thread.started.connect(self._fetch_inst.auto_web_metadata)\n\n    def commonUI(self):\n        if not self._multiple_galleries:\n            f_web = QGroupBox(\"Metadata from the Web\")\n            f_web.setCheckable(False)\n            self.main_layout.addWidget(f_web)\n            web_main_layout = QVBoxLayout()\n            web_info = misc.ClickedLabel(\"Which gallery URLs are supported? (hover)\", parent=self)\n            web_info.setToolTip(app_constants.SUPPORTED_METADATA_URLS)\n            web_info.setToolTipDuration(999999999)\n            web_main_layout.addWidget(web_info)\n            web_layout = QHBoxLayout()\n            web_main_layout.addLayout(web_layout)\n            f_web.setLayout(web_main_layout)\n            def basic_web(name):\n                return QLabel(name), QLineEdit(), QPushButton(\"Get metadata\"), QProgressBar()\n\n            url_lbl, self.url_edit, url_btn, url_prog = basic_web(\"URL:\")\n            url_btn.clicked.connect(lambda: self.web_metadata(self.url_edit.text(), url_btn,\n                                                url_prog))\n            url_prog.setTextVisible(False)\n            url_prog.setMinimum(0)\n            url_prog.setMaximum(0)\n            web_layout.addWidget(url_lbl, 0, Qt.AlignLeft)\n            web_layout.addWidget(self.url_edit, 0)\n            web_layout.addWidget(url_btn, 0, Qt.AlignRight)\n            web_layout.addWidget(url_prog, 0, Qt.AlignRight)\n            self.url_edit.setPlaceholderText(\"Insert supported gallery URLs or just press the button!\")\n            url_prog.hide()\n\n        f_gallery = QGroupBox(\"Gallery Info\")\n        f_gallery.setCheckable(False)\n        self.main_layout.addWidget(f_gallery)\n        gallery_layout = QFormLayout()\n        f_gallery.setLayout(gallery_layout)\n\n        def checkbox_layout(widget):\n            if self._multiple_galleries:\n                l = QHBoxLayout()\n                l.addWidget(widget.g_check)\n                widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n                l.addWidget(widget)\n                return l\n            else:\n                widget.g_check.setChecked(True)\n                widget.g_check.hide()\n                return widget\n\n        def add_check(widget):\n            widget.g_check = QCheckBox(self)\n            return widget\n\n        self.title_edit = add_check(QLineEdit())\n        self.author_edit = add_check(QLineEdit())\n        author_completer = misc.GCompleter(self, False, True, False)\n        author_completer.setCaseSensitivity(Qt.CaseInsensitive)\n        self.author_edit.setCompleter(author_completer)\n        self.descr_edit = add_check(QTextEdit())\n        self.descr_edit.setAcceptRichText(True)\n        self.lang_box = add_check(QComboBox())\n        self.lang_box.addItems(app_constants.G_LANGUAGES)\n        self.lang_box.addItems(app_constants.G_CUSTOM_LANGUAGES)\n        self.rating_box = add_check(QSpinBox())\n        self.rating_box.setMaximum(5)\n        self.rating_box.setMinimum(0)\n        self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 0)\n        tags_l = QVBoxLayout()\n        tag_info = misc.ClickedLabel(\"How do i write namespace & tags? (hover)\", parent=self)\n        tag_info.setToolTip(\"Ways to write tags:\\n\\nNormal tags:\\ntag1, tag2, tag3\\n\\n\"+\n                      \"Namespaced tags:\\nns1:tag1, ns1:tag2\\n\\nNamespaced tags with one or more\"+\n                      \" tags under same namespace:\\nns1:[tag1, tag2, tag3], ns2:[tag1, tag2]\\n\\n\"+\n                      \"Those three ways of writing namespace & tags can be combined freely.\\n\"+\n                      \"Tags are seperated by a comma, NOT whitespace.\\nNamespaces will be capitalized while tags\"+\n                      \" will be lowercased.\")\n        tag_info.setToolTipDuration(99999999)\n        tags_l.addWidget(tag_info)\n        self.tags_edit = add_check(misc.CompleterTextEdit())\n        self.tags_edit.setCompleter(misc.GCompleter(self, False, False))\n        self.tags_append = QCheckBox(\"Append tags\", self)\n        self.tags_append.setChecked(False)\n        if not self._multiple_galleries:\n            self.tags_append.hide()\n        if self._multiple_galleries:\n            self.tags_append.setChecked(app_constants.APPEND_TAGS_GALLERIES)\n            tags_ml = QVBoxLayout()\n            tags_ml.addWidget(self.tags_append)\n            tags_ml.addLayout(checkbox_layout(self.tags_edit), 5)\n            tags_l.addLayout(tags_ml, 3)\n        else:\n            tags_l.addWidget(checkbox_layout(self.tags_edit), 5)\n        self.tags_edit.setPlaceholderText(\"Press Tab to autocomplete (Ctrl + E to show popup)\")\n        self.type_box = add_check(QComboBox())\n        self.type_box.addItems(app_constants.G_TYPES)\n        self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0)\n        #self.type_box.currentIndexChanged[int].connect(self.doujin_show)\n        #self.doujin_parent = QLineEdit()\n        #self.doujin_parent.setVisible(False)\n        self.status_box = add_check(QComboBox())\n        self.status_box.addItems(app_constants.G_STATUS)\n        self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0)\n        self.pub_edit = add_check(QDateEdit())\n        self.pub_edit.setCalendarPopup(True)\n        self.pub_edit.setDate(QDate.currentDate())\n        self.path_lbl = misc.ClickedLabel(\"\")\n        self.path_lbl.setWordWrap(True)\n        self.path_lbl.clicked.connect(lambda a: utils.open_path(a, a) if a else None)\n\n        link_layout = QHBoxLayout()\n        self.link_lbl = add_check(QLabel(\"\"))\n        self.link_lbl.setWordWrap(True)\n        self.link_edit = QLineEdit()\n        link_layout.addWidget(self.link_edit)\n        if self._multiple_galleries:\n            link_layout.addLayout(checkbox_layout(self.link_lbl))\n        else:\n            link_layout.addWidget(checkbox_layout(self.link_lbl))\n        self.link_edit.hide()\n        self.link_btn = QPushButton(\"Modify\")\n        self.link_btn.setFixedWidth(50)\n        self.link_btn2 = QPushButton(\"Set\")\n        self.link_btn2.setFixedWidth(40)\n        self.link_btn.clicked.connect(self.link_modify)\n        self.link_btn2.clicked.connect(self.link_set)\n        link_layout.addWidget(self.link_btn)\n        link_layout.addWidget(self.link_btn2)\n        self.link_btn2.hide()\n        \n        rating_ = checkbox_layout(self.rating_box)\n        lang_ = checkbox_layout(self.lang_box)\n        if self._multiple_galleries:\n            rating_.insertWidget(0, QLabel(\"Rating:\"))\n            lang_.addLayout(rating_)\n            lang_l = lang_\n        else:\n            lang_l = QHBoxLayout()\n            lang_l.addWidget(lang_)\n            lang_l.addWidget(QLabel(\"Rating:\"), 0, Qt.AlignRight)\n            lang_l.addWidget(rating_)\n\n\n        gallery_layout.addRow(\"Title:\", checkbox_layout(self.title_edit))\n        gallery_layout.addRow(\"Author:\", checkbox_layout(self.author_edit))\n        gallery_layout.addRow(\"Description:\", checkbox_layout(self.descr_edit))\n        gallery_layout.addRow(\"Language:\", lang_l)\n        gallery_layout.addRow(\"Tags:\", tags_l)\n        gallery_layout.addRow(\"Type:\", checkbox_layout(self.type_box))\n        gallery_layout.addRow(\"Status:\", checkbox_layout(self.status_box))\n        gallery_layout.addRow(\"Publication Date:\", checkbox_layout(self.pub_edit))\n        gallery_layout.addRow(\"Path:\", self.path_lbl)\n        gallery_layout.addRow(\"URL:\", link_layout)\n\n        self.title_edit.setFocus()\n\n    def resizeEvent(self, event):\n        self.tags_edit.setFixedHeight(event.size().height()//8)\n        self.descr_edit.setFixedHeight(event.size().height()//12.5)\n        return super().resizeEvent(event)\n\n    def _find_combobox_match(self, combobox, key, default):\n        f_index = combobox.findText(key, Qt.MatchFixedString)\n        if f_index != -1:\n            combobox.setCurrentIndex(f_index)\n            return True\n        else:\n            combobox.setCurrentIndex(default)\n            return False\n\n    def setGallery(self, gallery):\n        \"To be used for when editing a gallery\"\n        if isinstance(gallery, gallerydb.Gallery):\n            self.gallery = gallery\n\n            if not self._multiple_galleries:\n                self.url_edit.setText(gallery.link)\n\n            self.title_edit.setText(gallery.title)\n            self.author_edit.setText(gallery.artist)\n            self.descr_edit.setText(gallery.info)\n            self.rating_box.setValue(gallery.rating)\n\n            self.tags_edit.setText(utils.tag_to_string(gallery.tags))\n\n\n            if not self._find_combobox_match(self.lang_box, gallery.language, 1):\n                self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 1)\n            if not self._find_combobox_match(self.type_box, gallery.type, 0):\n                self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0)\n            if not self._find_combobox_match(self.status_box, gallery.status, 0):\n                self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0)\n\n            gallery_pub_date = \"{}\".format(gallery.pub_date).split(' ')\n            try:\n                self.gallery_time = datetime.strptime(gallery_pub_date[1], '%H:%M:%S').time()\n            except IndexError:\n                pass\n            qdate_pub_date = QDate.fromString(gallery_pub_date[0], \"yyyy-MM-dd\")\n            self.pub_edit.setDate(qdate_pub_date)\n\n            self.link_lbl.setText(gallery.link)\n            self.path_lbl.setText(gallery.path)\n\n        elif isinstance(gallery, list):\n            g = gallery[0]\n            if all(map(lambda x: x.title == g.title, gallery)):\n                self.title_edit.setText(g.title)\n                self.title_edit.g_check.setChecked(True)\n            if all(map(lambda x: x.artist == g.artist, gallery)):\n                self.author_edit.setText(g.artist)\n                self.author_edit.g_check.setChecked(True)\n            if all(map(lambda x: x.info == g.info, gallery)):\n                self.descr_edit.setText(g.info)\n                self.descr_edit.g_check.setChecked(True)\n            if all(map(lambda x: x.tags == g.tags, gallery)):\n                self.tags_edit.setText(utils.tag_to_string(g.tags))\n                self.tags_edit.g_check.setChecked(True)\n            if all(map(lambda x: x.language == g.language, gallery)):\n                if not self._find_combobox_match(self.lang_box, g.language, 1):\n                    self._find_combobox_match(self.lang_box, app_constants.G_DEF_LANGUAGE, 1)\n                self.lang_box.g_check.setChecked(True)\n            if all(map(lambda x: x.rating == g.rating, gallery)):\n                self.rating_box.setValue(g.rating)\n                self.rating_box.g_check.setChecked(True)\n            if all(map(lambda x: x.type == g.type, gallery)):\n                if not self._find_combobox_match(self.type_box, g.type, 0):\n                    self._find_combobox_match(self.type_box, app_constants.G_DEF_TYPE, 0)\n                self.type_box.g_check.setChecked(True)\n            if all(map(lambda x: x.status == g.status, gallery)):\n                if not self._find_combobox_match(self.status_box, g.status, 0):\n                    self._find_combobox_match(self.status_box, app_constants.G_DEF_STATUS, 0)\n                self.status_box.g_check.setChecked(True)\n            if all(map(lambda x: x.pub_date == g.pub_date, gallery)):\n                gallery_pub_date = \"{}\".format(g.pub_date).split(' ')\n                try:\n                    self.gallery_time = datetime.strptime(gallery_pub_date[1], '%H:%M:%S').time()\n                except IndexError:\n                    pass\n                qdate_pub_date = QDate.fromString(gallery_pub_date[0], \"yyyy-MM-dd\")\n                self.pub_edit.setDate(qdate_pub_date)\n                self.pub_edit.g_check.setChecked(True)\n            if all(map(lambda x: x.link == g.link, gallery)):\n                self.link_lbl.setText(g.link)\n                self.link_lbl.g_check.setChecked(True)\n\n    def newUI(self):\n\n        f_local = QGroupBox(\"Directory/Archive\")\n        f_local.setCheckable(False)\n        self.main_layout.addWidget(f_local)\n        local_layout = QHBoxLayout()\n        f_local.setLayout(local_layout)\n\n        choose_folder = QPushButton(\"From Directory\")\n        choose_folder.clicked.connect(lambda: self.choose_dir('f'))\n        local_layout.addWidget(choose_folder)\n\n        choose_archive = QPushButton(\"From Archive\")\n        choose_archive.clicked.connect(lambda: self.choose_dir('a'))\n        local_layout.addWidget(choose_archive)\n\n        self.file_exists_lbl = QLabel()\n        local_layout.addWidget(self.file_exists_lbl)\n        self.file_exists_lbl.hide()\n\n    def choose_dir(self, mode):\n        \"\"\"\n        Pass which mode to open the folder explorer in:\n        'f': directory\n        'a': files\n        Or pass a predefined path\n        \"\"\"\n        self.done.show()\n        self.file_exists_lbl.hide()\n        if mode == 'a':\n            name = QFileDialog.getOpenFileName(self, 'Choose archive',\n                                              filter=utils.FILE_FILTER)\n            name = name[0]\n        elif mode == 'f':\n            name = QFileDialog.getExistingDirectory(self, 'Choose folder')\n        elif mode:\n            if os.path.exists(mode):\n                name = mode\n            else:\n                return None\n        if not name:\n            return\n        head, tail = os.path.split(name)\n        name = os.path.join(head, tail)\n        parsed = utils.title_parser(tail)\n        self.title_edit.setText(parsed['title'])\n        self.author_edit.setText(parsed['artist'])\n        self.path_lbl.setText(name)\n        if not parsed['language']:\n            parsed['language'] = app_constants.G_DEF_LANGUAGE\n        l_i = self.lang_box.findText(parsed['language'])\n        if l_i != -1:\n            self.lang_box.setCurrentIndex(l_i)\n        if gallerydb.GalleryDB.check_exists(name):\n            self.file_exists_lbl.setText('<font color=\"red\">Gallery already exists.</font>')\n            self.file_exists_lbl.show()\n        # check galleries\n        gs = 1\n        if name.endswith(utils.ARCHIVE_FILES):\n            gs = len(utils.check_archive(name))\n        elif os.path.isdir(name):\n            g_dirs, g_archs = utils.recursive_gallery_check(name)\n            gs = len(g_dirs) + len(g_archs)\n        if gs == 0:\n            self.file_exists_lbl.setText('<font color=\"red\">Invalid gallery source.</font>')\n            self.file_exists_lbl.show()\n            self.done.hide()\n        if app_constants.SUBFOLDER_AS_GALLERY:\n            if gs > 1:\n                self.file_exists_lbl.setText('<font color=\"red\">More than one galleries detected in source! Use other methods to add.</font>')\n                self.file_exists_lbl.show()\n                self.done.hide()\n\n    def check(self):\n        if not self._multiple_galleries:\n            if len(self.title_edit.text()) is 0:\n                self.title_edit.setFocus()\n                self.title_edit.setStyleSheet(\"border-style:outset;border-width:2px;border-color:red;\")\n                return False\n            elif len(self.author_edit.text()) is 0:\n                self.author_edit.setText(\"Unknown\")\n\n            if len(self.path_lbl.text()) == 0 or self.path_lbl.text() == 'No path specified':\n                self.path_lbl.setStyleSheet(\"color:red\")\n                self.path_lbl.setText('No path specified')\n                return False\n\n        return True\n\n    def reject(self):\n        if self.check():\n            msgbox = QMessageBox()\n            msgbox.setText(\"<font color='red'><b>Noo oniichan! You were about to add a new gallery.</b></font>\")\n            msgbox.setInformativeText(\"Do you really want to discard?\")\n            msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)\n            msgbox.setDefaultButton(QMessageBox.No)\n            if msgbox.exec() == QMessageBox.Yes:\n                self.close()\n        else:\n            self.close()\n\n    def web_metadata(self, url, btn_widget, pgr_widget):\n        if not self.path_lbl.text():\n            return\n        self.link_lbl.setText(url)\n        btn_widget.hide()\n        pgr_widget.show()\n\n        def status(stat):\n            def do_hide():\n                try:\n                    pgr_widget.hide()\n                    btn_widget.show()\n                except RuntimeError:\n                    pass\n\n            if stat:\n                do_hide()\n            else:\n                danger = \"\"\"QProgressBar::chunk {\n                    background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0,stop: 0 #FF0350,stop: 0.4999 #FF0020,stop: 0.5 #FF0019,stop: 1 #FF0000 );\n                    border-bottom-right-radius: 5px;\n                    border-bottom-left-radius: 5px;\n                    border: .px solid black;}\"\"\"\n                pgr_widget.setStyleSheet(danger)\n                QTimer.singleShot(3000, do_hide)\n\n        def gallery_picker(gallery, title_url_list, q):\n            self.parent_widget._web_metadata_picker(gallery, title_url_list, q, self)\n\n        try:\n            dummy_gallery = self.make_gallery(self.gallery, False)\n        except AttributeError:\n            dummy_gallery = self.make_gallery(gallerydb.Gallery(), False, True)\n        if not dummy_gallery:\n            status(False)\n            return None\n\n        dummy_gallery._g_dialog_url = url\n        self._fetch_inst.galleries = [dummy_gallery]\n        self._disconnect()\n        self._fetch_inst.GALLERY_PICKER.connect(gallery_picker)\n        self._fetch_inst.GALLERY_EMITTER.connect(self.set_web_metadata)\n        self._fetch_inst.FINISHED.connect(status)\n        self._fetch_thread.start()\n            \n    def set_web_metadata(self, metadata):\n        assert isinstance(metadata, gallerydb.Gallery)\n        self.link_lbl.setText(metadata.link)\n        self.title_edit.setText(metadata.title)\n        self.author_edit.setText(metadata.artist)\n        tags = \"\"\n        lang = ['English', 'Japanese']\n        self._find_combobox_match(self.lang_box, metadata.language, 2)\n        self.tags_edit.setText(utils.tag_to_string(metadata.tags))\n        pub_string = \"{}\".format(metadata.pub_date)\n        pub_date = QDate.fromString(pub_string.split()[0], \"yyyy-MM-dd\")\n        self.pub_edit.setDate(pub_date)\n        self._find_combobox_match(self.type_box, metadata.type, 0)\n\n    def make_gallery(self, new_gallery, add_to_model=True, new=False):\n        def is_checked(widget):\n            return widget.g_check.isChecked()\n        if self.check():\n            if is_checked(self.title_edit):\n                new_gallery.title = self.title_edit.text()\n                log_d('Adding gallery title')\n            if is_checked(self.author_edit):\n                new_gallery.artist = self.author_edit.text()\n                log_d('Adding gallery artist')\n            if not self._multiple_galleries:\n                new_gallery.path = self.path_lbl.text()\n                log_d('Adding gallery path')\n            if is_checked(self.descr_edit):\n                new_gallery.info = self.descr_edit.toPlainText()\n                log_d('Adding gallery descr')\n            if is_checked(self.type_box):\n                new_gallery.type = self.type_box.currentText()\n                log_d('Adding gallery type')\n            if is_checked(self.lang_box):\n                new_gallery.language = self.lang_box.currentText()\n                log_d('Adding gallery lang')\n            if is_checked(self.rating_box):\n                new_gallery.rating = self.rating_box.value()\n                log_d('Adding gallery rating')\n            if is_checked(self.status_box):\n                new_gallery.status = self.status_box.currentText()\n                log_d('Adding gallery status')\n            if is_checked(self.tags_edit):\n                if self.tags_append.isChecked():\n                    new_gallery.tags = utils.tag_to_dict(utils.tag_to_string(new_gallery.tags)+\",\"+ self.tags_edit.toPlainText())\n                else:\n                    new_gallery.tags = utils.tag_to_dict(self.tags_edit.toPlainText())\n                log_d('Adding gallery: tagging to dict')\n            if is_checked(self.pub_edit):\n                qpub_d = self.pub_edit.date().toString(\"ddMMyyyy\")\n                dpub_d = datetime.strptime(qpub_d, \"%d%m%Y\").date()\n                try:\n                    d_t = self.gallery_time\n                except AttributeError:\n                    d_t = datetime.now().time().replace(microsecond=0)\n                dpub_d = datetime.combine(dpub_d, d_t)\n                new_gallery.pub_date = dpub_d\n                log_d('Adding gallery pub date')\n            if is_checked(self.link_lbl):\n                new_gallery.link = self.link_lbl.text()\n                log_d('Adding gallery link')\n\n            if new:\n                if not new_gallery.chapters:\n                    log_d('Starting chapters')\n                    thread = threading.Thread(target=utils.make_chapters, args=(new_gallery,))\n                    thread.start()\n                    thread.join()\n                    log_d('Finished chapters')\n                    if new and app_constants.MOVE_IMPORTED_GALLERIES:\n                        app_constants.OVERRIDE_MONITOR = True\n                        new_gallery.move_gallery()\n                if add_to_model:\n                    self.parent_widget.default_manga_view.add_gallery(new_gallery, True)\n                    log_i('Sent gallery to model')\n            else:\n                if add_to_model:\n                    self.parent_widget.default_manga_view.replace_gallery([new_gallery], False)\n            return new_gallery\n\n\n    def link_set(self):\n        t = self.link_edit.text()\n        self.link_edit.hide()\n        self.link_lbl.show()\n        self.link_lbl.setText(t)\n        self.link_btn2.hide()\n        self.link_btn.show() \n\n    def link_modify(self):\n        t = self.link_lbl.text()\n        self.link_lbl.hide()\n        self.link_edit.show()\n        self.link_edit.setText(t)\n        self.link_btn.hide()\n        self.link_btn2.show()\n\n    def _disconnect(self):\n        try:\n            self._fetch_inst.GALLERY_PICKER.disconnect()\n            self._fetch_inst.GALLERY_EMITTER.disconnect()\n            self._fetch_inst.FINISHED.disconnect()\n        except TypeError:\n            pass\n\n    def delayed_close(self):\n        if self._fetch_thread.isRunning():\n            self._fetch_thread.finished.connect(self.close)\n            self.hide()\n        else:\n            self.close()\n\n    def accept(self):\n        self.make_gallery(gallerydb.Gallery(), new=True)\n        self.delayed_close()\n\n    def accept_edit(self):\n        gallerydb.execute(database.db.DBBase.begin, True)\n        app_constants.APPEND_TAGS_GALLERIES = self.tags_append.isChecked()\n        settings.set(app_constants.APPEND_TAGS_GALLERIES, 'Application', 'append tags to gallery')\n        for g in self._edit_galleries:\n            self.make_gallery(g)\n        self.delayed_close()\n        gallerydb.execute(database.db.DBBase.end, True)\n\n    def reject_edit(self):\n        self.delayed_close()\n\n\n"
  },
  {
    "path": "version/hplugins.py",
    "content": "import logging\nimport os\nimport uuid\nimport threading\nimport sys\n\nfrom PyQt5.QtCore import pyqtWrapperType\n\nlog = logging.getLogger(__name__)\nlog_i = lambda a: None\nlog_d = lambda a: None\nlog_w = lambda a: None\nlog_e = lambda a: None\nlog_c = lambda a: None\n\nclass PluginError(ValueError):\n\tpass\n\nclass PluginIDError(PluginError):\n\tpass\n\nclass PluginNameError(PluginIDError):\n\tpass\n\nclass PluginMethodError(PluginError):\n\tpass\n\nclass Plugins:\n\t\"\"\n\t_connections = []\n\t_plugins = {}\n\t_pluginsbyids = {}\n\thooks = {}\n\n\n\tdef register(self, plugin):\n\t\tassert isinstance(plugin, HPluginMeta)\n\t\tself.hooks[plugin.ID] = {}\n\t\tself._plugins[plugin.NAME] = plugin() # TODO: name conflicts?\n\t\tself._pluginsbyids[plugin.ID] = self._plugins[plugin.NAME]\n\n\tdef _connectHooks(self):\n\t\tfor plugin_name, pluginid, h_name, handler in self._connections:\n\t\t\tlog_i(\"{}:{} connection to {}:{}\".format(plugin_name, handler, pluginid, h_name))\n\t\t\tprint(self.hooks)\n\t\t\ttry:\n\t\t\t\tp = self.hooks[pluginid]\n\t\t\texcept KeyError:\n\t\t\t\tlog_e(\"Could not find plugin with plugin id: {}\".format(pluginid))\n\t\t\t\treturn\n\t\t\ttry:\n\t\t\t\th = p[h_name]\n\t\t\texcept KeyError:\n\t\t\t\tlog_e(\"Could not find pluginhook with name: {}\".format(h_name))\n\t\t\t\treturn\n\t\t\n\t\t\th.addHandler(handler, (plugin_name, pluginid))\n\t\treturn True\n\n\tdef __getattr__(self, key):\n\t\ttry:\n\t\t\treturn self._plugins[key]\n\t\texcept KeyError:\n\t\t\traise PluginNameError(key)\n\nregistered = Plugins()\n\nclass HPluginMeta(pyqtWrapperType):\n\n\tdef __init__(cls, name, bases, dct):\n\t\tif not name.endswith(\"HPlugin\"):\n\t\t\tlog_e(\"Main plugin class should end with name HPlugin\")\n\t\t\treturn\n\n\t\tif not hasattr(cls, \"ID\"):\n\t\t\tlog_e(\"ID attribute is missing\")\n\t\t\treturn\n\t\tcls.ID = cls.ID.replace('-', '')\n\t\tif not hasattr(cls, \"NAME\"):\n\t\t\tlog_e(\"NAME attribute is missing\")\n\t\t\treturn\n\t\tif not hasattr(cls, \"VERSION\"):\n\t\t\tlog_e(\"VERSION attribute is missing\")\n\t\t\treturn\n\t\tif not hasattr(cls, \"AUTHOR\"):\n\t\t\tlog_e(\"AUTHOR attribute is missing\")\n\t\t\treturn\n\t\tif not hasattr(cls, \"DESCRIPTION\"):\n\t\t\tlog_e(\"DESCRIPTION attribute is missing\")\n\t\t\treturn\n\n\t\ttry:\n\t\t\tval = uuid.UUID(cls.ID, version=4)\n\t\t\tassert val.hex == cls.ID\n\t\texcept ValueError:\n\t\t\tlog_e(\"Invalid plugin id. UUID4 is required.\")\n\t\t\treturn\n\t\texcept AssertionError:\n\t\t\tlog_e(\"Invalid plugin id. A valid UUID4 is required.\")\n\t\t\treturn\n\n\t\tif not isinstance(cls.NAME, str):\n\t\t\tlog_e(\"Plugin name should be a string\")\n\t\t\treturn\n\t\tif not isinstance(cls.VERSION, tuple):\n\t\t\tlog_e(\"Plugin version should be a tuple with 3 integers\")\n\t\t\treturn\n\t\tif not isinstance(cls.AUTHOR, str):\n\t\t\tlog_e(\"Plugin author should be a string\")\n\t\t\treturn\n\t\tif not isinstance(cls.DESCRIPTION, str):\n\t\t\tlog_e(\"Plugin description should be a string\")\n\t\t\treturn\n\n\t\tsuper().__init__(name, bases, dct)\n\n\t\tsetattr(cls, \"connectPlugin\", cls.connectPlugin)\n\t\tsetattr(cls, \"newHook\", cls.createHook)\n\t\tsetattr(cls, \"connectHook\", cls.connectHook)\n\t\tsetattr(cls, \"__getattr__\", cls.__getattr__)\n\n\t\tregistered.register(cls)\n\n\tdef connectPlugin(cls, pluginid, plugin_name):\n\t\t\"\"\"\n\t\tConnect to other plugins\n\t\tParams:\n\t\t\tpluginid: PluginID of the plugin you want to connect to\n\t\t\tplugin_name: Name you want to referrer the other plugin as\n\n\t\tOther methods of other plugins can be used as such: self.plugin_name.method()\n\t\t\"\"\"\n\n\t\tclass OtherHPlugin:\n\n\t\t\tdef __init__(self, pluginid):\n\t\t\t\tself._id = pluginid.replace('-', '')\n\t\n\t\t\tdef __getattr__(self, key):\n\t\t\t\ttry:\n\t\t\t\t\tplugin = registered._pluginsbyids[self._id]\n\t\t\t\t\t\n\t\t\t\t\tpluginmethod = getattr(plugin, key, None)\n\t\t\t\t\tif pluginmethod:\n\t\t\t\t\t\treturn pluginmethod \n\t\t\t\t\telse:\n\t\t\t\t\t\traise PluginMethodError(key)\n\t\t\t\texcept KeyError:\n\t\t\t\t\traise PluginIDError(self._id)\n\n\t\tsetattr(cls, plugin_name, OtherHPlugin(pluginid))\n\n\tdef connectHook(self, pluginid, hook_name, handler):\n\t\t\"\"\"\n\t\tConnect to other plugins' hooks\n\t\tParams:\n\t\t\tpluginid: PluginID of the plugin that has the hook you want to connect to\n\t\t\thook_name: Exact name of the hook you want to connect to\n\t\t\thandler: Your custom method that should be executed when the other plugin uses its hook.\n\t\t\"\"\"\n\n\t\tassert isinstance(pluginid, str) and isinstance(hook_name, str) and callable(handler), \"\"\n\t\tregistered._connections.append((self.NAME, pluginid.replace('-', ''), hook_name, handler))\n\n\tdef createHook(self, hook_name):\n\t\t\"\"\"\n\t\tCreate hooks that other plugins can extend\n\t\tParams:\n\t\t\thook_name: Name of the hook you want to create.\n\t\t\n\t\tHook will be used as such: self.hook_name()\n\t\t\"\"\"\n\n\t\tassert isinstance(hook_name, str), \"\"\n\n\t\tclass Hook:\n\t\t\t_handlers = set()\n\t\t\tdef addHandler(self, handler, pluginfo):\n\t\t\t\tself._handlers.add((handler, pluginfo))\n\n\t\t\tdef __call__(self, *args, **kwargs):\n\t\t\t\thandler_returns = []\n\t\t\t\tfor handlers, pluginfo in self._handlers:\n\t\t\t\t\ttry:\n\t\t\t\t\t\thandler_returns.append(handlers(*args, **kwargs))\n\t\t\t\t\texcept Exception as e:\n\t\t\t\t\t\traise PluginError(\"{}:{}\".format(pluginfo[0], pluginfo[1]))\n\t\t\t\treturn handler_returns\n\n\t\th = Hook()\n\t\tregistered.hooks[self.ID][hook_name] = h\n\n\tdef __getattr__(self, key):\n\t\ttry:\n\t\t\treturn registered.hooks[self.ID][key]\n\t\texcept KeyError:\n\t\t\treturn PluginMethodError(key)\n\n#def startConnectionLoop():\n#\tdef autoConnectHooks():\n#\t\trun = True\n#\t\twhile run:\n#\t\t\trun = registered._connectHooks()\n#\tauto_t = threading.Thread(target=autoConnectHooks)\n#\tauto_t.start()"
  },
  {
    "path": "version/io_misc.py",
    "content": "﻿import logging, os, json, datetime, random, re, queue\n\nfrom watchdog.events import FileSystemEventHandler, DirDeletedEvent\nfrom watchdog.observers import Observer\nfrom threading import Timer\n\nfrom PyQt5.QtCore import (Qt, QObject, pyqtSignal, QTimer, QSize, QThread)\nfrom PyQt5.QtGui import (QPixmap, QIcon, QColor, QTextOption, QKeySequence)\nfrom PyQt5.QtWidgets import (QWidget, QHBoxLayout, QVBoxLayout,\n                             QLabel, QFrame, QPushButton, QMessageBox,\n                             QFileDialog, QScrollArea, QLineEdit,\n                             QFormLayout, QGroupBox, QSizePolicy,\n                             QTableWidget, QTableWidgetItem, QPlainTextEdit,\n                             QShortcut, QMenu, qApp)\n\nimport app_constants\nimport misc\nimport gallerydb\nimport utils\nimport pewnet\nimport settings\nimport fetch\nfrom asm_manager import AsmManager\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass GalleryDownloaderUrlExtracter(QWidget):\n\n    url_emit = pyqtSignal(str)\n\n    def __init__(self, parent=None):\n        super().__init__(parent, flags=Qt.Window|Qt.WindowStaysOnTopHint)\n        self.main_layout = QVBoxLayout(self)\n        self.text_area = QPlainTextEdit(self)\n        self.text_area.setPlaceholderText(\"URLs are seperated by a newline\")\n        self.main_layout.addWidget(self.text_area)\n        self.text_area.setWordWrapMode(QTextOption.NoWrap)\n        add_to_queue = QPushButton('Add to queue')\n        add_to_queue.adjustSize()\n        add_to_queue.setFixedWidth(add_to_queue.width())\n        add_to_queue.clicked.connect(self.add_to_queue)\n        self.main_layout.addWidget(add_to_queue, 0, Qt.AlignRight)\n        self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))\n        self.show()\n\n    def add_to_queue(self):\n        txt = self.text_area.document().toPlainText()\n        urls = txt.split('\\n')\n        for u in urls:\n            if u:\n                self.url_emit.emit(u)\n        self.close()\n\nclass GalleryDownloaderItem(QObject):\n    \"\"\"\n    HenItem wrapper\n    \"\"\"\n    d_item_ready = pyqtSignal(object)\n    def __init__(self, hitem):\n        super().__init__()\n        assert isinstance(hitem, pewnet.HenItem)\n        self.d_item_ready.connect(self.done)\n        self.item = hitem\n        url = self.item.gallery_url\n\n        self.profile_item = QTableWidgetItem(self.item.name)\n        self.profile_item.setData(Qt.UserRole+1, hitem)\n        self.profile_item.setToolTip(url)\n        def set_profile(item):\n            self.profile_item.setIcon(QIcon(item.thumb))\n        self.item.thumb_rdy.connect(set_profile)\n\n        # status\n        self.status_item = QTableWidgetItem('In queue...')\n        self.status_item.setToolTip(url)\n        def set_finished(item):\n            self.status_item.setText('Finished!')\n            self.d_item_ready.emit(self)\n        self.item.file_rdy.connect(set_finished)\n\n        # other\n        self.cost_item = QTableWidgetItem(self.item.cost)\n        self.cost_item.setToolTip(url)\n        self.size_item = QTableWidgetItem(self.item.size)\n        self.size_item.setToolTip(url)\n        _type = 'Unknown'\n        if hitem.download_type == app_constants.DOWNLOAD_TYPE_ARCHIVE:\n            _type = 'Archive'\n        if hitem.download_type == app_constants.DOWNLOAD_TYPE_OTHER:\n            _type = 'Other'\n        if hitem.download_type == app_constants.DOWNLOAD_TYPE_TORRENT:\n            _type = 'Torrent'\n        self.type_item = QTableWidgetItem(_type)\n        self.type_item.setToolTip(url)\n\n        self.status_timer = QTimer()\n        self.status_timer.timeout.connect(self.check_progress)\n        self.status_timer.start(500)\n\n    def check_progress(self):\n        if self.item.current_state == self.item.DOWNLOADING:\n            btomb = 1048576\n            self.status_item.setText(\"{0:.2f}/{1:.2f} MB\".format(self.item.current_size/btomb,\n                                                              self.item.total_size/btomb))\n            self.size_item.setText(\"{0:.2f} MB\".format(self.item.total_size/btomb))\n        elif self.item.current_state == self.item.CANCELLED:\n            self.status_item.setText(\"Cancelled!\")\n            self.status_timer.stop()\n\n    def done(self):\n        self.status_timer.stop()\n        if self.item.download_type == app_constants.DOWNLOAD_TYPE_TORRENT:\n            self.status_item.setText(\"Sent to torrent client!\")\n        else:\n            self.status_item.setText(\"Creating gallery...\")\n\nclass GalleryDownloaderList(QTableWidget):\n    \"\"\"\n    \"\"\"\n    init_fetch_instance = pyqtSignal(list)\n    def __init__(self, app_inst, parent=None):\n        super().__init__(parent)\n        self.app_inst = app_inst\n        self.setColumnCount(5)\n        self.setIconSize(QSize(50, 100))\n        self.setAlternatingRowColors(True)\n        self.setEditTriggers(self.NoEditTriggers)\n        self.setFocusPolicy(Qt.NoFocus)\n        v_header = self.verticalHeader()\n        v_header.setSectionResizeMode(v_header.Fixed)\n        v_header.setDefaultSectionSize(100)\n        v_header.hide()\n        self.setDragEnabled(False)\n        self.setShowGrid(True)\n        self.setSelectionBehavior(self.SelectRows)\n        self.setSelectionMode(self.SingleSelection)\n        self.setSortingEnabled(True)\n        palette = self.palette()\n        palette.setColor(palette.Highlight, QColor(88, 88, 88, 70))\n        palette.setColor(palette.HighlightedText, QColor('black'))\n        self.setPalette(palette)\n        self.setHorizontalHeaderLabels(\n            [' ', 'Status', 'Size', 'Cost', 'Type'])\n        self.horizontalHeader().setStretchLastSection(True)\n        self.horizontalHeader().setSectionResizeMode(0, self.horizontalHeader().Stretch)\n        self.horizontalHeader().setSectionResizeMode(1, self.horizontalHeader().ResizeToContents)\n        self.horizontalHeader().setSectionResizeMode(2, self.horizontalHeader().ResizeToContents)\n        self.horizontalHeader().setSectionResizeMode(3, self.horizontalHeader().ResizeToContents)\n        self.horizontalHeader().setSectionResizeMode(4, self.horizontalHeader().ResizeToContents)\n\n        self._finish_checker = QTimer(self)\n        self._finish_checker.timeout.connect(self._gallery_to_model)\n        self._finish_checker.start(2000)\n        self._download_items = {}\n        self.fetch_instance = fetch.Fetch()\n        self.fetch_instance._to_queue_container = True\n        self.fetch_instance.moveToThread(app_constants.GENERAL_THREAD)\n        self.init_fetch_instance.connect(self.fetch_instance.local)\n\n        def open_item(idx):\n            hitem = self._get_hitem(idx)\n            if hitem.current_state == hitem.DOWNLOADING:\n                hitem.open(True)\n        self.doubleClicked.connect(open_item)\n\n    def add_entry(self, hitem):\n        assert isinstance(hitem, pewnet.HenItem)\n        g_item = GalleryDownloaderItem(hitem)\n        if not hitem.download_type == app_constants.DOWNLOAD_TYPE_TORRENT:\n            g_item.d_item_ready.connect(self._init_gallery)\n\n        self.insertRow(0)\n        self.setSortingEnabled(False)\n        self.setItem(0, 0, g_item.profile_item)\n        self.setItem(0, 1, g_item.status_item)\n        self.setItem(0, 2, g_item.size_item)\n        self.setItem(0, 3, g_item.cost_item)\n        self.setItem(0, 4, g_item.type_item)\n        self.setSortingEnabled(True)\n\n    def _get_hitem(self, idx):\n        r = idx.row()\n        return self.item(r, 0).data(Qt.UserRole+1)\n\n    def contextMenuEvent(self, event):\n        idx = self.indexAt(event.pos())\n        if idx.isValid():\n            hitem = self._get_hitem(idx)\n            clipboard = qApp.clipboard()\n            menu = QMenu()\n            if hitem.current_state == hitem.DOWNLOADING:\n                menu.addAction(\"Cancel\", hitem.cancel)\n            if hitem.current_state == hitem.FINISHED:\n                menu.addAction(\"Open\", hitem.open)\n                menu.addAction(\"Show in folder\", lambda: hitem.open(True))\n            menu.addAction(\"Copy path\", lambda: clipboard.setText(hitem.file))\n            menu.addAction(\"Copy gallery URL\", lambda: clipboard.setText(hitem.gallery_url))\n            menu.addAction(\"Copy download URL\", lambda: clipboard.setText(hitem.download_url))\n            if not hitem.current_state == hitem.DOWNLOADING:\n                menu.addAction(\"Remove\", lambda: self.removeRow(idx.row()))\n            menu.exec_(event.globalPos())\n            event.accept()\n            del menu\n        else:\n            event.ignore()\n\n    def _init_gallery(self, download_item):\n        \"\"\"Init gallery.\n\n        Args:\n            download_item(:class:`.gallery_downloader_item_obj.GalleryDownloaderItemObject`):\n            Downloaded item.\n        \"\"\"\n        assert isinstance(download_item, GalleryDownloaderItem)\n        # NOTE: try to use ehen's apply_metadata first\n        # manager have to edit item.metadata to match this method\n        file = download_item.item.file\n        app_constants.TEMP_PATH_IGNORE.append(os.path.normcase(file))\n        self._download_items[file] = download_item\n        self._download_items[utils.move_files(file, only_path=True)] = download_item  # better safe than sorry\n        if download_item.item.download_type == app_constants.DOWNLOAD_TYPE_OTHER:\n            pass # do stuff here?\n\n        self.init_fetch_instance.emit([file])\n\n    def _gallery_to_model(self):\n        try:\n            gallery = self.fetch_instance._galleries_queue.get_nowait()\n        except queue.Empty:\n            return\n        \n        log_i(\"Adding downloaded gallery to library\")\n        try:\n            d_item = self._download_items[gallery.path]\n            gallery.link = d_item.item.gallery_url\n            if d_item.item.metadata:\n                gallery = pewnet.EHen.apply_metadata(gallery, d_item.item.metadata)\n            if app_constants.DOWNLOAD_GALLERY_TO_LIB:\n                self.app_inst.default_manga_view.add_gallery(gallery, True)\n                d_item.status_item.setText('Added to library!')\n                log_i(\"Added downloaded gallery to library\")\n            else:\n                self.app_inst.addition_tab.view.add_gallery(gallery, True)\n                d_item.status_item.setText('Added to inbox!')\n                log_i(\"Added downloaded gallery to inbox\")\n        except KeyError:\n            d_item.status_item.setText('Gallery could not be added!')\n            log_i(\"Could not add downloaded gallery to library\")\n\n    def clear_list(self):\n        for r in range(self.rowCount()-1, -1, -1):\n            status = self.item(r, 1)\n            if '!' in status.text():\n                self.removeRow(r)\n\nclass GalleryDownloader(QWidget):\n    \"\"\"\n    A gallery downloader window\n    \"\"\"\n    def __init__(self, parent):\n        super().__init__(None,\n                   )#Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowMinMaxButtonsHint)\n        self.setAttribute(Qt.WA_DeleteOnClose, False)\n        main_layout = QVBoxLayout(self)\n        self.parent_widget = parent\n        self.url_inserter = QLineEdit()\n        self.url_inserter.setPlaceholderText(\"Hover to see supported URLs\")\n        self.url_inserter.setToolTip(app_constants.SUPPORTED_DOWNLOAD_URLS)\n        self.url_inserter.setToolTipDuration(999999999)\n        self.url_inserter.returnPressed.connect(self.add_download_entry)\n        main_layout.addWidget(self.url_inserter)\n        self.info_lbl = QLabel(self)\n        self.info_lbl.setAlignment(Qt.AlignCenter)\n        main_layout.addWidget(self.info_lbl)\n        self.info_lbl.hide()\n        buttons_layout = QHBoxLayout()\n        url_window_btn = QPushButton('Batch URLs')\n        url_window_btn.adjustSize()\n        url_window_btn.setFixedWidth(url_window_btn.width())\n        self._urls_queue = []\n        def batch_url_win():\n            self._batch_url = GalleryDownloaderUrlExtracter()\n            self._batch_url.url_emit.connect(lambda u: self._urls_queue.append(u))\n            self._batch_url.url_emit.connect(lambda u: self.info_lbl.setText(\"<font color='green'>Adding URLs to queue...</font>\") if u else None)\n        url_window_btn.clicked.connect(batch_url_win)\n        clear_all_btn = QPushButton('Clear List')\n        clear_all_btn.adjustSize()\n        clear_all_btn.setFixedWidth(clear_all_btn.width())\n        buttons_layout.addWidget(url_window_btn, 0, Qt.AlignLeft)\n        buttons_layout.addWidget(clear_all_btn, 0, Qt.AlignRight)\n        main_layout.addLayout(buttons_layout)\n        self.download_list = GalleryDownloaderList(parent, self)\n        clear_all_btn.clicked.connect(self.download_list.clear_list)\n        download_list_scroll = QScrollArea(self)\n        download_list_scroll.setBackgroundRole(self.palette().Base)\n        download_list_scroll.setWidgetResizable(True)\n        download_list_scroll.setWidget(self.download_list)\n        main_layout.addWidget(download_list_scroll, 1)\n        self.resize(480,600)\n        self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))\n\n        self._url_checker = QTimer(self)\n        self._url_checker.timeout.connect(lambda: self.add_download_entry(extractor=True))\n        self._url_checker.start(500)\n\n    def add_download_entry(self, url=None, extractor=False):\n        if extractor:\n            try:\n                url = self._urls_queue.pop(0)\n            except IndexError:\n                return\n        self.info_lbl.hide()\n        h_item = None\n        try:\n            if not url:\n                url = self.url_inserter.text()\n                if not url:\n                    return\n                self.url_inserter.clear()\n            url = url.lower()\n            \n            log_i('Adding download entry: {}'.format(url))\n            manager = self.website_validator(url)\n            if isinstance(manager, pewnet.HenManager):\n                url = pewnet.HenManager.gtoEh(url)\n            h_item = manager.from_gallery_url(url)\n        except app_constants.WrongURL:\n            self.info_lbl.setText(\"<font color='red'>Failed to add:\\n{}</font>\".format(url))\n            self.info_lbl.show()\n            return\n        except app_constants.NeedLogin:\n            self.info_lbl.setText(\"<font color='red'>Login is required to download:\\n{}</font>\".format(url))\n            self.info_lbl.show()\n            return\n        except app_constants.HTMLParsing:\n            self.info_lbl.setText(\"<font color='red'>HTML parsing error:\\n{}</font>\".format(url))\n            self.info_lbl.show()\n            return\n        except app_constants.WrongLogin:\n            self.info_lbl.setText(\"<font color='red'>Wrong login info to download:\\n{}</font>\".format(url))\n            self.info_lbl.show()\n            return\n        except app_constants.GNotAvailable:\n            self.info_lbl.setText(\"<font color='red'>Gallery has been removed:\\n{}</font>\".format(url))\n            self.info_lbl.show()\n            return\n        if h_item:\n            log_i('Successfully added to download entry: {}'.format(h_item.gallery_name if h_item.gallery_name else 'an item'))\n            self.download_list.add_entry(h_item)\n\n    def website_validator(self, url):\n        match_prefix = \"^(http\\:\\/\\/|https\\:\\/\\/)?(www\\.)?([^\\.]?)\" # http:// or https:// + www.\n        match_base = \"(.*\\.)+\" # base. Replace with domain\n        match_tld = \"[a-zA-Z0-9][a-zA-Z0-9\\-]*\" # com\n        end = \"/?$\"\n\n        # ATTENTION: the prefix will automatically get prepended to the pattern string! Don't try to match it.\n\n        def regex_validate(r):\n            if re.fullmatch(match_prefix+r+end, url):\n                return True\n            return False\n\n        if regex_validate(\"((g\\.e-hentai)\\.org\\/g\\/[0-9]+\\/[a-z0-9]+)\"):\n            manager = pewnet.HenManager()\n        elif regex_validate(\"((?<!g\\.)(e-hentai)\\.org\\/g\\/[0-9]+\\/[a-z0-9]+)\"):\n            manager = pewnet.HenManager()\n        elif regex_validate(\"((exhentai)\\.org\\/g\\/[0-9]+\\/[a-z0-9]+)\"):\n            exprops = settings.ExProperties()\n            if pewnet.ExHen().check_login(exprops.cookies):\n                manager = pewnet.ExHenManager()\n            else:\n                raise app_constants.NeedLogin()\n        elif regex_validate(\"(panda\\.chaika\\.moe\\/(archive|gallery)\\/[0-9]+)\"):\n            manager = pewnet.ChaikaManager()\n        elif regex_validate(\"(asmhentai\\.com\\/g\\/[0-9]+)\"):\n            manager = AsmManager()\n        else:\n            raise app_constants.WrongURL\n\n        return manager\n\n    def show(self):\n        if self.isVisible():\n            self.activateWindow()\n        else:\n            super().show()\n\n    def closeEvent(self, QCloseEvent):\n        self.hide()\n\nclass GalleryPopup(misc.BasePopup):\n    \"\"\"\n    Pass a tuple with text and a list of galleries\n    gallery profiles won't be scaled if scale is set to false\n    \"\"\"\n    gallery_doubleclicked = pyqtSignal(gallerydb.Gallery)\n    def __init__(self, tup_gallery, parent = None, menu = None):\n        super().__init__(parent)\n        self.setMaximumWidth(16777215)\n        assert isinstance(tup_gallery, tuple), \"Incorrect type received, expected tuple\"\n        assert isinstance(tup_gallery[0], str) and isinstance(tup_gallery[1], list)\n        main_layout = QVBoxLayout()\n        # todo make it scroll\n        scroll_area = QScrollArea()\n        dummy = QWidget()\n        self.gallery_layout = misc.FlowLayout(dummy)\n        scroll_area.setWidgetResizable(True)\n        scroll_area.setMaximumHeight(400)\n        scroll_area.setMidLineWidth(620)\n        scroll_area.setBackgroundRole(scroll_area.palette().Shadow)\n        scroll_area.setFrameStyle(scroll_area.NoFrame)\n        scroll_area.setWidget(dummy)\n        text = tup_gallery[0]\n        galleries = tup_gallery[1]\n        main_layout.addWidget(scroll_area, 3)\n        for g in galleries:\n            gall_w = misc.GalleryShowcaseWidget(parent=self, menu=menu())\n            gall_w.set_gallery(g, (170//1.40, 170))\n            gall_w.double_clicked.connect(self.gallery_doubleclicked.emit)\n            self.gallery_layout.addWidget(gall_w)\n\n        text_lbl =  QLabel(text)\n        text_lbl.setAlignment(Qt.AlignCenter)\n        main_layout.addWidget(text_lbl)\n        main_layout.addLayout(self.buttons_layout)\n        self.main_widget.setLayout(main_layout)\n        self.setMaximumHeight(500)\n        self.setMaximumWidth(620)\n        self.resize(620, 500)\n        self.show()\n\n    def get_all_items(self):\n        n = self.gallery_layout.rowCount()\n        items = []\n        for x in range(n):\n            item = self.gallery_layout.itemAt(x)\n            items.append(item.widget())\n        return items\n\nclass ModifiedPopup(misc.BasePopup):\n    def __init__(self, path, gallery_id, parent=None):\n        super().__init__(parent)\n        main_layout = QVBoxLayout()\n        main_layout.addWidget(QLabel(\"Modified:\\npath: {}\\nID:{}\".format(path, gallery_id)))\n        self.main_widget.setLayout(main_layout)\n        self.show()\n\n#class CreatedPopup(misc.BasePopup):\n#\tADD_SIGNAL = pyqtSignal(str)\n#\tdef __init__(self, path, parent=None):\n#\t\tsuper().__init__(parent)\n#\t\tdef commit():\n#\t\t\tself.ADD_SIGNAL.emit(path)\n#\t\t\tself.close()\n#\t\tmain_layout = QVBoxLayout()\n#\t\tinner_layout = QHBoxLayout()\n#\t\tname = os.path.split(path)[1]\n#\t\tcover = QLabel()\n#\t\timg = QPixmap(utils.get_gallery_img(path))\n#\t\tif img:\n#\t\t\tcover.setPixmap(img.scaled(350, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation))\n#\t\tinfo_lbl = QLabel('New gallery detected!\\n\\n{}\\n\\nDo you want to add it?'.format(name))\n#\t\tinfo_lbl.setWordWrap(True)\n#\t\tinfo_lbl.setAlignment(Qt.AlignCenter)\n#\t\tinner_layout.addWidget(cover)\n#\t\tinner_layout.addWidget(info_lbl)\n#\t\tmain_layout.addLayout(inner_layout)\n#\t\tmain_layout.addLayout(self.generic_buttons)\n#\t\tself.main_widget.setLayout(main_layout)\n#\t\tself.yes_button.clicked.connect(commit)\n#\t\tself.no_button.clicked.connect(self.close)\n#\t\tself.adjustSize()\n#\t\tself.show()\n\nclass MovedPopup(misc.BasePopup):\n    UPDATE_SIGNAL = pyqtSignal(object)\n    def __init__(self, new_path, gallery, parent=None):\n        super().__init__(parent)\n        def commit():\n            g = utils.update_gallery_path(new_path, gallery)\n            self.UPDATE_SIGNAL.emit(g)\n            self.close()\n        def cancel():\n            gallery.dead_link = True\n            self.close()\n        main_layout = QVBoxLayout()\n        inner_layout = QHBoxLayout()\n        title = QLabel(gallery.title)\n        title.setWordWrap(True)\n        title.setAlignment(Qt.AlignCenter)\n        title.adjustSize()\n        cover = QLabel()\n        img = QPixmap(gallery.profile)\n        cover.setPixmap(img)\n        text = QLabel(\"The path to this gallery has been renamed\\n\"+\n                \"\\n{}\\n\".format(os.path.basename(gallery.path))+u'\\u2192'+\"\\n{}\".format(os.path.basename(new_path)))\n        \n        text.setWordWrap(True)\n        text.setAlignment(Qt.AlignCenter)\n        button_layout = QHBoxLayout()\n        update_btn = QPushButton('Update')\n        update_btn.clicked.connect(commit)\n        close_btn = QPushButton('Close')\n        close_btn.clicked.connect(cancel)\n        button_layout.addWidget(update_btn)\n        button_layout.addWidget(close_btn)\n\n        inner_layout.addWidget(cover)\n        inner_layout.addWidget(text)\n        main_layout.addWidget(title)\n        main_layout.addLayout(inner_layout)\n        main_layout.addLayout(button_layout)\n        self.main_widget.setLayout(main_layout)\n\n        self.show()\n\nclass DeletedPopup(misc.BasePopup):\n    REMOVE_SIGNAL = pyqtSignal(object)\n    UPDATE_SIGNAL = pyqtSignal(object)\n    def __init__(self, path, gallery, parent=None):\n        super().__init__(parent)\n        gallery.dead_link = True\n        def commit():\n            msgbox = QMessageBox(self)\n            msgbox.setIcon(QMessageBox.Question)\n            msgbox.setWindowTitle('Type of gallery source')\n            msgbox.setInformativeText('What type of gallery source is it?')\n            dir = msgbox.addButton('Directory', QMessageBox.YesRole)\n            archive = msgbox.addButton('Archive', QMessageBox.NoRole)\n            msgbox.exec()\n            new_path = ''\n            if msgbox.clickedButton() == dir:\n                new_path = QFileDialog.getExistingDirectory(self, 'Choose directory')\n            elif msgbox.clickedButton() == archive:\n                new_path = QFileDialog.getOpenFileName(self, 'Choose archive',\n                                           filter=utils.FILE_FILTER)\n                new_path = new_path[0]\n            else: return None\n            if new_path:\n                g = utils.update_gallery_path(new_path, gallery)\n                self.UPDATE_SIGNAL.emit(g)\n                self.close()\n\n        def remove_commit():\n            self.REMOVE_SIGNAL.emit(gallery)\n            self.close()\n\n        main_layout = QVBoxLayout()\n        inner_layout = QHBoxLayout()\n        cover = QLabel()\n        img = QPixmap(gallery.profile)\n        cover.setPixmap(img)\n        title_lbl = QLabel(gallery.title)\n        title_lbl.setAlignment(Qt.AlignCenter)\n        info_lbl = QLabel(\"The path to this gallery has been removed\\n\"+\n                    \"What do you want to do?\")\n        #info_lbl.setWordWrap(True)\n        path_lbl = QLabel(path)\n        path_lbl.setWordWrap(True)\n        info_lbl.setAlignment(Qt.AlignCenter)\n        inner_layout.addWidget(cover)\n        inner_layout.addWidget(info_lbl)\n        main_layout.addLayout(inner_layout)\n        main_layout.addWidget(path_lbl)\n        close_btn = QPushButton('Close')\n        close_btn.clicked.connect(self.close)\n        update_btn = QPushButton('Update path...')\n        update_btn.clicked.connect(commit)\n        remove_btn = QPushButton('Remove')\n        remove_btn.clicked.connect(remove_commit)\n        buttons_layout = QHBoxLayout()\n        buttons_layout.addWidget(remove_btn)\n        buttons_layout.addWidget(update_btn)\n        buttons_layout.addWidget(close_btn)\n        main_layout.addWidget(title_lbl)\n        main_layout.addLayout(buttons_layout)\n        self.main_widget.setLayout(main_layout)\n        self.adjustSize()\n        self.show()\n\nclass GalleryHandler(FileSystemEventHandler, QObject):\n    CREATE_SIGNAL = pyqtSignal(str)\n    MODIFIED_SIGNAL = pyqtSignal(str, int)\n    DELETED_SIGNAL = pyqtSignal(str, object)\n    MOVED_SIGNAL = pyqtSignal(str, object)\n\n    def __init__(self):\n        super().__init__()\n        #self.g_queue = []\n\n    def file_filter(self, event):\n        if os.path.normcase(event.src_path) in app_constants.TEMP_PATH_IGNORE:\n            app_constants.TEMP_PATH_IGNORE.remove(os.path.normcase(event.src_path))\n            return False\n        # TODO: use utils.check_ignore_list?\n        _, ext = os.path.splitext(event.src_path)\n        if event.is_directory or ext in utils.ARCHIVE_FILES:\n            if event.is_directory and \"Folder\" in app_constants.IGNORE_EXTS:\n                return False\n            if ext[1:].upper() in app_constants.IGNORE_EXTS:\n                return False\n            return True\n        return False\n\n    #def process_queue(self, type):\n    #\tif self.g_queue:\n    #\t\tif type == 'create':\n    #\t\t\tself.CREATE_SIGNAL.emit(self.g_queue)\n\n    #\tself.g_queue = []\n\n    def on_created(self, event):\n        if not app_constants.OVERRIDE_MONITOR:\n            if self.file_filter(event):\n                gs = 0\n                if event.src_path.endswith(utils.ARCHIVE_FILES):\n                    gs = len(utils.check_archive(event.src_path))\n                elif event.is_directory:\n                    g_dirs, g_archs = utils.recursive_gallery_check(event.src_path)\n                    gs = len(g_dirs) + len(g_archs)\n                if gs:\n                    self.CREATE_SIGNAL.emit(event.src_path)\n        else:\n            app_constants.OVERRIDE_MONITOR = False\n\n    def on_deleted(self, event):\n        if not app_constants.OVERRIDE_MONITOR:\n            if self.file_filter(event):\n                path = event.src_path\n                gallery = gallerydb.GalleryDB.get_gallery_by_path(path)\n                if gallery:\n                    self.DELETED_SIGNAL.emit(path, gallery)\n        else:\n            app_constants.OVERRIDE_MONITOR = False\n\n    def on_modified(self, event):\n        pass\n\n    def on_moved(self, event):\n        if not app_constants.OVERRIDE_MONITOR:\n            if self.file_filter(event):\n                old_path = event.src_path\n                gallery = gallerydb.GalleryDB.get_gallery_by_path(old_path)\n                if gallery:\n                    self.MOVED_SIGNAL.emit(event.dest_path, gallery)\n        else:\n            app_constants.OVERRIDE_MONITOR = False\n\nclass Watchers:\n    def __init__(self):\n\n        self.gallery_handler = GalleryHandler()\n        self.watchers = []\n        for path in app_constants.MONITOR_PATHS:\n            gallery_observer = Observer()\n\n            try:\n                gallery_observer.schedule(self.gallery_handler, path, True)\n                gallery_observer.start()\n                self.watchers.append(gallery_observer)\n            except:\n                log.exception('Could not monitor: {}'.format(path.encode(errors='ignore')))\n    \n    def stop_all(self):\n        for watcher in self.watchers:\n            watcher.stop()\n\nclass GalleryImpExpData:\n\n    hash_pages_count = 4\n\n    def __init__(self, format=1):\n        self.type = format\n        if format == 0:\n            self.structure = \"\"\n        else:\n            self.structure = {}\n\n    @classmethod\n    def get_pages(self, pages):\n        \"Returns pages to generate hashes from\"\n        p = []\n        if pages < self.hash_pages_count+1:\n            for x in range(pages):\n                p.append(x)\n        else:\n            x = 0\n            i = pages//self.hash_pages_count\n            for t in range(self.hash_pages_count):\n                x += i\n                p.append(x-1)\n        return p\n\n    def add_data(self, name, data):\n        if self.type == 0:\n            pass\n        else:\n            self.structure[name] = data\n\n    def save(self, file_path):\n        file_name = os.path.join(file_path,\n                           'happypanda-{}.hpdb'.format(\n                              datetime.datetime.now().strftime(\"%Y-%m-%d %H-%M-%S\")))\n        with open(file_name, 'w', encoding='utf-8') as fp:\n            json.dump(self.structure, fp, indent=4)\n\n    def find_pair(self, found_pairs):\n        identifier = self.structure.get('identifier')\n        found = None\n        if identifier:\n            for g in app_constants.GALLERY_DATA:\n                if not g in found_pairs and g.chapters[0].pages == identifier['pages']:\n                    pages = self.get_pages(g.chapters[0].pages)\n                    hashes = gallerydb.HashDB.gen_gallery_hash(g, 0, pages)\n                    for p in hashes:\n                        if hashes[p] != identifier[str(p)]:\n                            break\n                    else:\n                        found = g\n                        g.title = self.structure['title']\n                        g.artist = self.structure['artist']\n                        if self.structure['pub_date'] and self.structure['pub_date'] != 'None':\n                            g.pub_date = datetime.datetime.strptime(\n                                self.structure['pub_date'], \"%Y-%m-%d %H:%M:%S\")\n                        g.date_added = datetime.datetime.strptime(\n                                self.structure['date_added'], \"%Y-%m-%d %H:%M:%S\")\n                        g.type = self.structure['type']\n                        g.status = self.structure['status']\n                        if self.structure['last_read'] and self.structure['last_read'] != 'None':\n                            g.last_read = datetime.datetime.strptime(\n                                self.structure['last_read'], \"%Y-%m-%d %H:%M:%S\")\n                        g.times_read += self.structure['times_read']\n                        g._db_v = self.structure['db_v']\n                        g.language = self.structure['language']\n                        g.link = self.structure['link']\n                        g.view = self.structure['view']\n                        g.rating = self.structure['rating']\n                        for ns in self.structure['tags']:\n                            if not ns in g.tags:\n                                g.tags[ns] = []\n                            for tag in self.structure['tags'][ns]:\n                                if not tag in g.tags[ns]:\n                                    g.tags[ns].append(tag)\n                        g.exed = self.structure['exed']\n                        g.info = self.structure['info']\n                        g.fav = self.structure['fav']\n                        gallerydb.GalleryDB.modify_gallery(\n                            g.id,\n                            g.title,\n                            artist=g.artist,\n                            info=g.info,\n                            type=g.type,\n                            fav=g.fav,\n                            tags=g.tags,\n                            language=g.language,\n                            status=g.status,\n                            pub_date=g.pub_date,\n                            date_added=g.date_added,\n                            link=g.link,\n                            times_read=g.times_read,\n                            _db_v=g._db_v,\n                            exed=g.exed,\n                            rating=g.rating,\n                            view=g.view,\n                            last_read=g.last_read\n                            )\n\n                if found:\n                    break\n        else:\n            log_w(\"Identifier key not found!\")\n        return found\n\nclass ListImpData:\n    pass\n\nclass ImportExport(QObject):\n    finished = pyqtSignal()\n    imported_g = pyqtSignal(str)\n    progress = pyqtSignal(int)\n    amount = pyqtSignal(int)\n\n    def __init__(self):\n        super().__init__()\n    \n    def import_data(self, path):\n        with open(path, 'r', encoding='utf-8') as fp:\n            data = json.load(fp)\n            pairs_found = []\n            galleries = data[\"galleries\"] if \"galleries\" in data else data\n            data_count = len(galleries)\n            self.amount.emit(data_count)\n            for prog, g_id in enumerate(galleries, 1):\n                g_data = GalleryImpExpData()\n                g_data.structure.update(galleries[g_id])\n                g = g_data.find_pair(pairs_found)\n                if g:\n                    pairs_found.append(g)\n                else:\n                    log_w(\"Could not find pair for id: {}\".format(g_id))\n                self.imported_g.emit(\n                    \"Importing database file... ({}/{} imported)\".format(len(pairs_found), data_count))\n                self.progress.emit(prog)\n\n            if \"lists\" in data:\n                data_count = len(data[\"lists\"])\n                self.amount.emit(data_count)\n                for prog, l_id in enumerate(data['lists'], 1):\n                    list_data = data['lists'][l_id]\n\n                    g_list = None\n                    for g_l in app_constants.GALLERY_LISTS:\n                        if g_l.name == list_data[\"name\"]:\n                            g_list = g_l\n                            break\n                    if not g_list:\n                        g_list = gallerydb.GalleryList(list_data[\"name\"])\n                        g_list.add_to_db()\n                       \n                    g_list.type = list_data[\"type\"]\n                    g_list.filter = list_data[\"filter\"]\n                    g_list.enforce = list_data[\"enforce\"]\n                    g_list.regex = list_data[\"regex\"]\n                    g_list.case = list_data[\"case\"]\n                    g_list.strict = list_data[\"strict\"]\n\n                    # ineffiecent, go through gallery data once\n                    for g in app_constants.GALLERY_DATA:\n                        pages = GalleryImpExpData.get_pages(g.chapters[0].pages)\n                        hashes = gallerydb.HashDB.gen_gallery_hash(g, 0, pages)\n                        p_hashes = {}\n                        for p in hashes:\n                            p_hashes[str(p)] = hashes[p]\n                        for g_id in list_data[\"galleries\"]:\n                            if p_hashes == list_data[\"galleries\"][g_id][\"identifier\"]:\n                                g_list.add_gallery(g, _check_filter=False)\n                                break\n                    \n                    self.imported_g.emit(\"Importing gallery lists\")\n                    self.progress.emit(prog)\n            self.finished.emit()\n\n    def export_data(self, gallery=None):\n        galleries = []\n        export_lists = False\n        if gallery:\n            galleries.append(gallery)\n        else:\n            galleries = app_constants.GALLERY_DATA\n            exports_lists = True\n\n        amount = len(galleries)\n        log_i(\"Exporting {} galleries\".format(amount))\n        data = GalleryImpExpData(app_constants.EXPORT_FORMAT)\n        gallery_data = {}\n        data.add_data(\"galleries\", gallery_data)\n        self.amount.emit(amount)\n        for prog, g in enumerate(galleries, 1):\n            log_d(\"Exporting {} out of {} galleries\".format(prog, amount))\n            g_data = {}\n            g_data['title'] = g.title\n            g_data['artist'] = g.artist\n            g_data['info'] = g.info\n            g_data['fav'] = g.fav\n            g_data['type'] = g.type\n            g_data['link'] = g.link\n            g_data['rating'] = g.rating\n            g_data['view'] = g.view\n            g_data['language'] = g.language\n            g_data['status'] = g.status\n            g_data['pub_date'] = \"{}\".format(g.pub_date)\n            g_data['last_read'] = \"{}\".format(g.last_read)\n            g_data['date_added'] = \"{}\".format(g.date_added)\n            g_data['times_read'] = g.times_read\n            g_data['exed'] = g.exed\n            g_data['db_v'] = g._db_v\n            g_data['tags'] = g.tags.copy()\n            g_data['identifier'] = {'pages':g.chapters[0].pages}\n            pages = data.get_pages(g.chapters[0].pages)\n            try:\n                h_list = gallerydb.HashDB.gen_gallery_hash(g, 0, pages)\n            except app_constants.InternalPagesMismatch:\n                if g.chapters.update_chapter_pages(0):\n                    pages = data.get_pages(g.chapters[0].pages)\n                    h_list = gallerydb.HashDB.gen_gallery_hash(g, 0, pages)\n                else:\n                    h_list = {}\n            if not h_list:\n                log_e(\"Failed to export gallery: {}\".format(g.title.encode(errors='ignore')))\n                continue\n            for n in pages:\n                g_data['identifier'][n] = h_list[n]\n\n            gallery_data[str(g.id)] = g_data\n            self.progress.emit(prog)\n\n        lists = {}\n        for l in app_constants.GALLERY_LISTS:\n            lists[str(l._id)] = l_data = {}\n            l_data[\"name\"] = l.name\n            l_data[\"type\"] = l.type\n            l_data[\"filter\"] = l.filter\n            l_data[\"enforce\"] = l.enforce\n            l_data[\"regex\"] = l.regex\n            l_data[\"case\"] = l.case\n            l_data[\"strict\"] = l.strict\n            l_galleries = l_data[\"galleries\"] = {}\n            for g in l._galleries:\n                l_galleries[str(g.id)] = l_gallery = {}\n                pages = data.get_pages(g.chapters[0].pages)\n                try:\n                    h_list = gallerydb.HashDB.gen_gallery_hash(g, 0, pages)\n                except app_constants.InternalPagesMismatch:\n                    if g.chapters.update_chapter_pages(0):\n                        pages = data.get_pages(g.chapters[0].pages)\n                        h_list = gallerydb.HashDB.gen_gallery_hash(g, 0, pages)\n                    else:\n                        h_list = {}\n                if not h_list:\n                    log_e(\"Failed to export gallery: {}\".format(g.title.encode(errors='ignore')))\n                    continue\n                l_gallery['identifier'] = {}\n                for n in pages:\n                    l_gallery['identifier'][n] = h_list[n]\n\n        if lists:\n            data.add_data(\"lists\", lists)\n\n        log_i(\"Finished exporting galleries!\")\n        data.save(app_constants.EXPORT_PATH)\n        self.finished.emit()\n\n"
  },
  {
    "path": "version/main.py",
    "content": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport sys, logging, logging.handlers, os, argparse, platform, scandir\nimport traceback\n\nfrom PyQt5.QtWidgets import QApplication\nfrom PyQt5.QtCore import QFile, Qt\nfrom PyQt5.QtGui import QFontDatabase\n\nfrom database import db, db_constants\nimport app\nimport app_constants\nimport gallerydb\nimport utils\n\n#IMPORTANT STUFF\ndef start(test=False):\n    app_constants.APP_RESTART_CODE = -123456789\n\n    if os.name == 'posix':\n        main_path = os.path.dirname(os.path.realpath(__file__))\n        log_path = os.path.join(main_path, 'happypanda.log')\n        debug_log_path = os.path.join(main_path, 'happypanda_debug.log')\n    else:\n        log_path = 'happypanda.log'\n        debug_log_path = 'happypanda_debug.log'\n    if os.path.exists('cacert.pem'):\n        os.environ[\"REQUESTS_CA_BUNDLE\"] = os.path.join(os.getcwd(), \"cacert.pem\")\n\n    parser = argparse.ArgumentParser(prog='Happypanda',\n                                  description='A manga/doujinshi manager with tagging support')\n    parser.add_argument('-d', '--debug', action='store_true',\n                     help='happypanda_debug_log.log will be created in main directory')\n    parser.add_argument('-v', '--version', action='version',\n                     version='Happypanda v{}'.format(app_constants.vs))\n    parser.add_argument('-e', '--exceptions', action='store_true',\n                     help='Disable custom excepthook')\n    parser.add_argument('-x', '--dev', action='store_true',\n                     help='Development Switch')\n\n    args = parser.parse_args()\n    log_handlers = []\n    log_level = logging.INFO\n    if args.dev:\n        log_handlers.append(logging.StreamHandler())\n    if args.debug:\n        print(\"happypanda_debug.log created at {}\".format(os.getcwd()))\n        # create log\n        try:\n            with open(debug_log_path, 'x') as f:\n                pass\n        except FileExistsError:\n            pass\n\n        log_handlers.append(logging.FileHandler(debug_log_path, 'w', 'utf-8'))\n        log_level = logging.DEBUG\n        app_constants.DEBUG = True\n    else:\n        try:\n            with open(log_path, 'x') as f:\n                pass\n        except FileExistsError: pass\n        log_handlers.append(logging.handlers.RotatingFileHandler(\n            log_path, maxBytes=1000000*10, encoding='utf-8', backupCount=2))\n\n    # Fix for logging not working\n    # clear the handlers first before adding these custom handler\n    # http://stackoverflow.com/a/15167862\n    logging.getLogger('').handlers = []\n    logging.basicConfig(level=log_level,\n                    format='%(asctime)-8s %(levelname)-6s %(name)-6s %(message)s',\n                    datefmt='%d-%m %H:%M',\n                    handlers=tuple(log_handlers))\n\n    log = logging.getLogger(__name__)\n    log_i = log.info\n    log_d = log.debug\n    log_w = log.warning\n    log_e = log.error\n    log_c = log.critical\n\n    if not args.exceptions:\n        def uncaught_exceptions(ex_type, ex, tb):\n            log_c(''.join(traceback.format_tb(tb)))\n            log_c('{}: {}'.format(ex_type, ex))\n            traceback.print_exception(ex_type, ex, tb)\n\n        sys.excepthook = uncaught_exceptions\n\n    if app_constants.FORCE_HIGH_DPI_SUPPORT:\n        log_i(\"Enabling high DPI display support\")\n        os.environ.putenv(\"QT_DEVICE_PIXEL_RATIO\", \"auto\")\n\n    effects = [Qt.UI_AnimateCombo, Qt.UI_FadeMenu, Qt.UI_AnimateMenu,\n            Qt.UI_AnimateTooltip, Qt.UI_FadeTooltip]\n    for effect in effects:\n        QApplication.setEffectEnabled(effect)\n\n    application = QApplication(sys.argv)\n    application.setOrganizationName('Pewpews')\n    application.setOrganizationDomain('https://github.com/Pewpews/happypanda')\n    application.setApplicationName('Happypanda')\n    application.setApplicationDisplayName('Happypanda')\n    application.setApplicationVersion('v{}'.format(app_constants.vs))\n    application.setAttribute(Qt.AA_UseHighDpiPixmaps)\n    application.font().setStyleStrategy(application.font().PreferAntialias)\n\n    log_i('Starting Happypanda...'.format(app_constants.vs))\n    if args.debug:\n        log_i('Running in debug mode'.format(app_constants.vs))\n        import pprint\n        sys.displayhook = pprint.pprint\n    app_constants.load_icons()\n    log_i('Happypanda Version {}'.format(app_constants.vs))\n    log_i('OS: {} {}\\n'.format(platform.system(), platform.release()))\n    conn = None\n    try:\n        conn = db.init_db()\n        log_d('Init DB Conn: OK')\n        log_i(\"DB Version: {}\".format(db_constants.REAL_DB_VERSION))\n    except:\n        log_c('Invalid database')\n        log.exception('Database connection failed!')\n        from PyQt5.QtGui import QIcon\n        from PyQt5.QtWidgets import QMessageBox\n        msg_box = QMessageBox()\n        msg_box.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))\n        msg_box.setText('Invalid database')\n        msg_box.setInformativeText(\"Do you want to create a new database?\")\n        msg_box.setIcon(QMessageBox.Critical)\n        msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)\n        msg_box.setDefaultButton(QMessageBox.Yes)\n        if msg_box.exec() == QMessageBox.Yes:\n            pass\n        else:\n            application.exit()\n            log_d('Normal Exit App: OK')\n            sys.exit()\n\n    def start_main_window(conn):\n        db.DBBase._DB_CONN = conn\n        #if args.test:\n        #\timport threading, time\n        #\tser_list = []\n        #\tfor x in range(5000):\n        #\t\ts = gallerydb.gallery()\n        #\t\ts.profile = app_constants.NO_IMAGE_PATH\n        #\t\ts.title = 'Test {}'.format(x)\n        #\t\ts.artist = 'Author {}'.format(x)\n        #\t\ts.path = app_constants.static_dir\n        #\t\ts.type = 'Test'\n        #\t\ts.language = 'English'\n        #\t\ts.info = 'I am number {}'.format(x)\n        #\t\tser_list.append(s)\n\n        #\tdone = False\n        #\tthread_list = []\n        #\ti = 0\n        #\twhile not done:\n        #\t\ttry:\n        #\t\t\tif threading.active_count() > 5000:\n            #\t\t\t\tthread_list = []\n        #\t\t\t\tdone = True\n        #\t\t\telse:\n        #\t\t\t\tthread_list.append(\n        #\t\t\t\t\tthreading.Thread(target=gallerydb.galleryDB.add_gallery,\n        #\t\t\t\t\t  args=(ser_list[i],)))\n        #\t\t\t\tthread_list[i].start()\n        #\t\t\t\ti += 1\n        #\t\t\t\tprint(i)\n        #\t\t\t\tprint('Threads running: {}'.format(threading.activeCount()))\n        #\t\texcept IndexError:\n        #\t\t\tdone = True\n\n        WINDOW = app.AppWindow(args.exceptions)\n\n        # styling\n        d_style = app_constants.default_stylesheet_path\n        u_style =  app_constants.user_stylesheet_path\n\n        if len(u_style) is not 0:\n            try:\n                style_file = QFile(u_style)\n                log_i('Select userstyle: OK')\n            except:\n                style_file = QFile(d_style)\n                log_i('Select defaultstyle: OK')\n        else:\n            style_file = QFile(d_style)\n            log_i('Select defaultstyle: OK')\n\n        style_file.open(QFile.ReadOnly)\n        style = str(style_file.readAll(), 'utf-8')\n        application.setStyleSheet(style)\n        try:\n            os.mkdir(app_constants.temp_dir)\n        except FileExistsError:\n            try:\n                for root, dirs, files in scandir.walk('temp', topdown=False):\n                    for name in files:\n                        os.remove(os.path.join(root, name))\n                    for name in dirs:\n                        os.rmdir(os.path.join(root, name))\n            except:\n                log.exception(\"Empty temp: FAIL\")\n        log_d('Create temp: OK')\n\n        if test:\n            return application, WINDOW\n\n        return application.exec_()\n\n    def db_upgrade():\n        log_d('Database connection failed')\n        from PyQt5.QtGui import QIcon\n        from PyQt5.QtWidgets import QMessageBox\n\n        msg_box = QMessageBox()\n        msg_box.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))\n        msg_box.setText('Incompatible database!')\n        msg_box.setInformativeText(\"Do you want to upgrade to newest version?\" +\n                             \" It shouldn't take more than a second. Don't start a new instance!\")\n        msg_box.setIcon(QMessageBox.Critical)\n        msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)\n        msg_box.setDefaultButton(QMessageBox.Yes)\n        if msg_box.exec() == QMessageBox.Yes:\n            utils.backup_database()\n            import threading\n            db_p = db_constants.DB_PATH\n            db.add_db_revisions(db_p)\n            conn = db.init_db()\n            return start_main_window(conn)\n        else:\n            application.exit()\n            log_d('Normal Exit App: OK')\n            return 0\n\n    if conn:\n        return start_main_window(conn)\n    else:\n        return db_upgrade()\n\nif __name__ == '__main__':\n    current_exit_code = 0\n    while current_exit_code == app_constants.APP_RESTART_CODE:\n        current_exit_code = start()\n    sys.exit(current_exit_code)\n"
  },
  {
    "path": "version/misc.py",
    "content": "﻿#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\nimport os\nimport threading\nimport queue\nimport time\nimport logging\nimport math\nimport random\nimport functools\nimport scandir\nfrom datetime import datetime\n\nfrom PyQt5.QtCore import (Qt, QDate, QPoint, pyqtSignal, QThread,\n                          QTimer, QObject, QSize, QRect, QFileInfo,\n                          QMargins, QPropertyAnimation, QRectF,\n                          QTimeLine, QMargins, QPropertyAnimation, QByteArray,\n                          QPointF, QSizeF, QProcess)\nfrom PyQt5.QtGui import (QTextCursor, QIcon, QMouseEvent, QFont,\n                         QPixmapCache, QPalette, QPainter, QBrush,\n                         QColor, QPen, QPixmap, QMovie, QPaintEvent, QFontMetrics,\n                         QPolygonF, QRegion, QCursor, QTextOption, QTextLayout,\n                         QPalette)\nfrom PyQt5.QtWidgets import (QWidget, QProgressBar, QLabel,\n                             QVBoxLayout, QHBoxLayout,\n                             QDialog, QGridLayout, QLineEdit,\n                             QFormLayout, QPushButton, QTextEdit,\n                             QComboBox, QDateEdit, QGroupBox,\n                             QDesktopWidget, QMessageBox, QFileDialog,\n                             QCompleter, QListWidgetItem,\n                             QListWidget, QApplication, QSizePolicy,\n                             QCheckBox, QFrame, QListView,\n                             QAbstractItemView, QTreeView, QSpinBox,\n                             QAction, QStackedLayout, QTabWidget,\n                             QGridLayout, QScrollArea, QLayout, QButtonGroup,\n                             QRadioButton, QFileIconProvider, QFontDialog,\n                             QColorDialog, QScrollArea, QSystemTrayIcon,\n                             QMenu, QGraphicsBlurEffect, QActionGroup,\n                             QCommonStyle, QApplication, QTableWidget,\n                             QTableWidgetItem, QTableView, QSplitter,\n                             QSplitterHandle, QStyledItemDelegate, QStyleOption)\n\nfrom utils import (tag_to_string, tag_to_dict, title_parser, ARCHIVE_FILES,\n                     ArchiveFile, IMG_FILES)\nfrom executors import Executors\nimport utils\nimport app_constants\nimport gallerydb\nimport fetch\nimport settings\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\ndef text_layout(text, width, font, font_metrics, alignment=Qt.AlignCenter):\n    \"Lays out wrapped text\"\n    text_option = QTextOption(alignment)\n    text_option.setUseDesignMetrics(True)\n    text_option.setWrapMode(QTextOption.WordWrap)\n    layout = QTextLayout(text, font)\n    layout.setTextOption(text_option)\n    leading = font_metrics.leading()\n    height = 0\n    layout.setCacheEnabled(True)\n    layout.beginLayout()\n    while True:\n        line = layout.createLine()\n        if not line.isValid():\n            break\n        line.setLineWidth(width)\n        height += leading\n        line.setPosition(QPointF(0, height))\n        height += line.height()\n    layout.endLayout()\n    return layout\n\ndef centerWidget(widget, parent_widget=None):\n    if parent_widget:\n        r = parent_widget.rect()\n    else:\n        r = QDesktopWidget().availableGeometry()\n\n    widget.setGeometry(QCommonStyle.alignedRect(Qt.LeftToRight,\n            Qt.AlignCenter,\n            widget.size(),\n            r))\n\ndef clearLayout(layout):\n    if layout != None:\n        while layout.count():\n            child = layout.takeAt(0)\n            if child.widget() is not None:\n                child.widget().deleteLater()\n            elif child.layout() is not None:\n                clearLayout(child.layout())\n\ndef create_animation(parent, prop):\n    p_array = QByteArray().append(prop)\n    return QPropertyAnimation(parent, p_array)\n\nclass ArrowHandle(QWidget):\n    \"Arrow Handle\"\n    IN, OUT = range(2)\n    CLICKED = pyqtSignal(int)\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.parent_widget = parent\n        self.current_arrow = self.IN\n        self.arrow_height = 20\n        self.setFixedWidth(10)\n        self.setCursor(Qt.PointingHandCursor)\n\n    def paintEvent(self, event):\n        rect = self.rect()\n        x, y, w, h = rect.getRect()\n        painter = QPainter(self)\n        painter.setPen(QColor(\"white\"))\n        painter.setBrush(QBrush(QColor(0,0,0,100)))\n        painter.fillRect(rect, QColor(0,0,0,100))\n\n        arrow_points = []\n\n        # for horizontal\n        if self.current_arrow == self.IN:\n            arrow_1 = QPointF(x + w, h / 2 - self.arrow_height / 2)\n            middle_point = QPointF(x, h / 2)\n            arrow_2 = QPointF(x + w, h / 2 + self.arrow_height / 2)\n        else:\n            arrow_1 = QPointF(x, h / 2 - self.arrow_height / 2)\n            middle_point = QPointF(x + w, h / 2)\n            arrow_2 = QPointF(x, h / 2 + self.arrow_height / 2)\n\n        arrow_points.append(arrow_1)\n        arrow_points.append(middle_point)\n        arrow_points.append(arrow_2)\n        painter.drawPolygon(QPolygonF(arrow_points))\n\n    def click(self):\n        if self.current_arrow == self.IN:\n            self.current_arrow = self.OUT\n            self.CLICKED.emit(1)\n        else:\n            self.current_arrow = self.IN\n            self.CLICKED.emit(0)\n        self.update()\n\n    def mousePressEvent(self, event):\n        if event.button() == Qt.LeftButton:\n            self.click()\n        return super().mousePressEvent(event)\n\nclass Line(QFrame):\n    \"'v' for vertical line or 'h' for horizontail line, color is hex string\"\n    def __init__(self, orentiation, parent=None):\n        super().__init__(parent)\n        self.setFrameStyle(self.StyledPanel)\n        if orentiation == 'v':\n            self.setFrameShape(self.VLine)\n        else:\n            self.setFrameShape(self.HLine)\n        self.setFrameShadow(self.Sunken)\n\nclass CompleterPopupView(QListView):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n    def _setup(self):\n        self.fade_animation = create_animation(self, 'windowOpacity')\n        self.fade_animation.setDuration(200)\n        self.fade_animation.setStartValue(0.0)\n        self.fade_animation.setEndValue(1.0)\n        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        self.setFrameStyle(self.StyledPanel)\n\n    def showEvent(self, event):\n        self.setWindowOpacity(0)\n        self.fade_animation.start()\n        super().showEvent(event)\n\nclass ElidedLabel(QLabel):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n    def paintEvent(self, event):\n        painter = QPainter(self)\n        metrics = QFontMetrics(self.font())\n        elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())\n        painter.drawText(self.rect(), self.alignment(), elided)\n\nclass BaseMoveWidget(QWidget):\n    def __init__(self, parent=None, **kwargs):\n        move_listener = kwargs.pop('move_listener', True)\n        super().__init__(parent, **kwargs)\n        self.parent_widget = parent\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        if parent and move_listener:\n            try:\n                parent.move_listener.connect(self.update_move)\n            except AttributeError:\n                pass\n\n    def update_move(self, new_size=None):\n        if new_size:\n            self.move(new_size)\n            return\n        if self.parent_widget:\n            self.move(self.parent_widget.window().frameGeometry().center() - \\\n                self.window().rect().center())\n\n\nclass SortMenu(QMenu):\n    new_sort = pyqtSignal(str)\n    def __init__(self, app_inst, parent=None, toolbutton=None):\n        super().__init__(parent)\n        self.parent_widget = app_inst\n        self.toolbutton = toolbutton\n        self.sort_actions = QActionGroup(self, exclusive=True)\n        asc_desc_act = QAction(\"Asc/Desc\", self)\n        asc_desc_act.triggered.connect(self.asc_desc)\n        s_title = self.sort_actions.addAction(QAction(\"Title\", self.sort_actions, checkable=True))\n        s_title.triggered.connect(functools.partial(self.new_sort.emit, 'title'))\n        s_artist = self.sort_actions.addAction(QAction(\"Author\", self.sort_actions, checkable=True))\n        s_artist.triggered.connect(functools.partial(self.new_sort.emit, 'artist'))\n        s_date = self.sort_actions.addAction(QAction(\"Date Added\", self.sort_actions, checkable=True))\n        s_date.triggered.connect(functools.partial(self.new_sort.emit, 'date_added'))\n        s_pub_d = self.sort_actions.addAction(QAction(\"Date Published\", self.sort_actions, checkable=True))\n        s_pub_d.triggered.connect(functools.partial(self.new_sort.emit, 'pub_date'))\n        s_times_read = self.sort_actions.addAction(QAction(\"Read Count\", self.sort_actions, checkable=True))\n        s_times_read.triggered.connect(functools.partial(self.new_sort.emit, 'times_read'))\n        s_last_read = self.sort_actions.addAction(QAction(\"Last Read\", self.sort_actions, checkable=True))\n        s_last_read.triggered.connect(functools.partial(self.new_sort.emit, 'last_read'))\n        s_rating = self.sort_actions.addAction(QAction(\"Rating\", self.sort_actions, checkable=True))\n        s_rating.triggered.connect(functools.partial(self.new_sort.emit, 'rating'))\n\n        self.addAction(asc_desc_act)\n        self.addSeparator()\n        self.addAction(s_artist)\n        self.addAction(s_date)\n        self.addAction(s_pub_d)\n        self.addAction(s_last_read)\n        self.addAction(s_title)\n        self.addAction(s_rating)\n        self.addAction(s_times_read)\n\n        self.set_current_sort()\n\n    def set_toolbutton_text(self):\n        act = self.sort_actions.checkedAction()\n        if self.toolbutton:\n            self.toolbutton.setText(act.text())\n\n    def set_current_sort(self):\n        def check_key(act, key):\n            if self.parent_widget.current_manga_view.list_view.current_sort == key:\n                act.setChecked(True)\n\n        for act in self.sort_actions.actions():\n            if act.text() == 'Title':\n                check_key(act, 'title')\n            elif act.text() == 'Artist':\n                check_key(act, 'artist')\n            elif act.text() == 'Date Added':\n                check_key(act, 'date_added')\n            elif act.text() == 'Date Published':\n                check_key(act, 'pub_date')\n            elif act.text() == 'Read Count':\n                check_key(act, 'times_read')\n            elif act.text() == 'Last Read':\n                check_key(act, 'last_read')\n            elif act.text() == 'Rating':\n                check_key(act, 'rating')\n\n    def asc_desc(self):\n        if self.parent_widget.current_manga_view.sort_model.sortOrder() == Qt.AscendingOrder:\n            if self.toolbutton:\n                self.toolbutton.setIcon(app_constants.SORT_ICON_DESC)\n            self.parent_widget.current_manga_view.sort_model.sort(0, Qt.DescendingOrder)\n        else:\n            if self.toolbutton:\n                self.toolbutton.setIcon(app_constants.SORT_ICON_ASC)\n            self.parent_widget.current_manga_view.sort_model.sort(0, Qt.AscendingOrder)\n\n    def showEvent(self, event):\n        self.set_current_sort()\n        super().showEvent(event)\n\nclass ToolbarButton(QPushButton):\n    select = pyqtSignal(object)\n    close_tab = pyqtSignal(object)\n    def __init__(self, parent=None, txt=''):\n        super().__init__(parent)\n        self.setText(txt)\n        self._selected = False\n        self.clicked.connect(lambda: self.select.emit(self))\n        self._enable_contextmenu = True\n\n    @property\n    def selected(self):\n        return self._selected\n\n    @selected.setter\n    def selected(self, b):\n        self._selected = b\n\n    def contextMenuEvent(self, event):\n        if self._enable_contextmenu:\n            m = QMenu(self)\n            m.addAction(\"Close Tab\").triggered.connect(lambda: self.close_tab.emit(self))\n            m.exec_(event.globalPos())\n            event.accept()\n        else:\n            event.ignore()\n\nclass TransparentWidget(BaseMoveWidget):\n    def __init__(self, parent = None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.setAttribute(Qt.WA_TranslucentBackground)\n\nclass ArrowWindow(TransparentWidget):\n    LEFT, RIGHT, TOP, BOTTOM = range(4)\n\n    def __init__(self, parent):\n        super().__init__(parent, flags=Qt.Window | Qt.FramelessWindowHint, move_listener=False)\n        self.setAttribute(Qt.WA_ShowWithoutActivating)\n        self.resize(550,300)\n        self.direction = self.LEFT\n        self._arrow_size = QSizeF(30, 30)\n        self.content_margin = 0\n\n    @property\n    def arrow_size(self):\n        return self._arrow_size\n\n    @arrow_size.setter\n    def arrow_size(self, w_h_tuple):\n        \"a tuple of width and height\"\n        if not isinstance(w_h_tuple, (tuple, list)) or len(w_h_tuple) != 2:\n            return\n\n        if self.direction in (self.LEFT, self.RIGHT):\n            s = QSizeF(w_h_tuple[1], w_h_tuple[0])\n        else:\n            s = QSizeF(w_h_tuple[0], w_h_tuple[1])\n\n        self._arrow_size = s\n        self.update()\n\n\n    def paintEvent(self, event):\n        assert isinstance(event, QPaintEvent)\n\n        opt = QStyleOption()\n        opt.initFrom(self)\n\n        painter = QPainter(self)\n        painter.setRenderHint(painter.Antialiasing)\n\n        size = self.size()\n        if self.direction in (self.LEFT, self.RIGHT):\n            actual_size = QSizeF(size.width() - self.arrow_size.width(), size.height())\n        else:\n            actual_size = QSizeF(size.width(), size.height() - self.arrow_size.height())\n\n        starting_point = QPointF(0, 0)\n        if self.direction == self.LEFT:\n            starting_point = QPointF(self.arrow_size.width(), 0)\n        elif self.direction == self.TOP:\n            starting_point = QPointF(0, self.arrow_size.height())\n\n        #painter.save()\n        #painter.translate(starting_point)\n        self.style().drawPrimitive(QCommonStyle.PE_Widget, opt, painter, self)\n        #painter.restore()\n        painter.setBrush(QBrush(painter.pen().color()))\n\n        # draw background\n        background_rect = QRectF(starting_point, actual_size)\n        #painter.drawRoundedRect(background_rect, 5, 5)\n\n        # calculate the arrow\n        arrow_points = []\n        if self.direction == self.LEFT:\n            middle_point = QPointF(0, actual_size.height() / 2)\n            arrow_1 = QPointF(self.arrow_size.width(), middle_point.y() - self.arrow_size.height() / 2)\n            arrow_2 = QPointF(self.arrow_size.width(), middle_point.y() + self.arrow_size.height() / 2)\n            arrow_points.append(arrow_1)\n            arrow_points.append(middle_point)\n            arrow_points.append(arrow_2)\n        elif self.direction == self.RIGHT:\n            middle_point = QPointF(actual_size.width() + self.arrow_size.width(), actual_size.height() / 2)\n            arrow_1 = QPointF(actual_size.width(), middle_point.y() + self.arrow_size.height() / 2)\n            arrow_2 = QPointF(actual_size.width(), middle_point.y() - self.arrow_size.height() / 2)\n            arrow_points.append(arrow_1)\n            arrow_points.append(middle_point)\n            arrow_points.append(arrow_2)\n        elif self.direction == self.TOP:\n            middle_point = QPointF(actual_size.width() / 2, 0)\n            arrow_1 = QPointF(actual_size.width() / 2 + self.arrow_size.width() / 2, self.arrow_size.height())\n            arrow_2 = QPointF(actual_size.width() / 2 - self.arrow_size.width() / 2, self.arrow_size.height())\n            arrow_points.append(arrow_1)\n            arrow_points.append(middle_point)\n            arrow_points.append(arrow_2)\n        elif self.direction == self.BOTTOM:\n            middle_point = QPointF(actual_size.width() / 2, actual_size.height() + self.arrow_size.height())\n            arrow_1 = QPointF(actual_size.width() / 2 - self.arrow_size.width() / 2, actual_size.height())\n            arrow_2 = QPointF(actual_size.width() / 2 + self.arrow_size.width() / 2, actual_size.height())\n            arrow_points.append(arrow_1)\n            arrow_points.append(middle_point)\n            arrow_points.append(arrow_2)\n\n        # draw it!\n        painter.drawPolygon(QPolygonF(arrow_points))\n\nclass GalleryMetaWindow(ArrowWindow):\n\n    def __init__(self, parent):\n        super().__init__(parent)\n        # gallery data stuff\n\n        self.content_margin = 10\n        self.current_gallery = None\n        self.g_widget = self.GalleryLayout(self, parent)\n        self.hide_timer = QTimer()\n        self.hide_timer.timeout.connect(self.delayed_hide)\n        self.hide_timer.setSingleShot(True)\n        self.hide_animation = create_animation(self, 'windowOpacity')\n        self.hide_animation.setDuration(250)\n        self.hide_animation.setStartValue(1.0)\n        self.hide_animation.setEndValue(0.0)\n        self.hide_animation.finished.connect(self.hide)\n        self.show_animation = create_animation(self, 'windowOpacity')\n        self.show_animation.setDuration(350)\n        self.show_animation.setStartValue(0.0)\n        self.show_animation.setEndValue(1.0)\n        self.setFocusPolicy(Qt.NoFocus)\n        self.setAttribute(Qt.WA_ShowWithoutActivating)\n\n    def show(self):\n        if not self.hide_animation.Running:\n            self.setWindowOpacity(0)\n            super().show()\n            self.show_animation.start()\n        else:\n            self.hide_animation.stop()\n            super().show()\n            self.show_animation.setStartValue(self.windowOpacity())\n            self.show_animation.start()\n\n    def focusOutEvent(self, event):\n        self.delayed_hide()\n        return super().focusOutEvent(event)\n\n    def _mouse_in_gallery(self):\n        mouse_p = QCursor.pos()\n        h = self.idx_top_l.x() <= mouse_p.x() <= self.idx_top_r.x()\n        v = self.idx_top_l.y() <= mouse_p.y() <= self.idx_btm_l.y()\n        if h and v:\n            return True\n        return False\n\n    def mouseMoveEvent(self, event):\n        if self.isVisible():\n            if not self._mouse_in_gallery():\n                if not self.hide_timer.isActive():\n                    self.hide_timer.start(300)\n        return super().mouseMoveEvent(event)\n\n    def delayed_hide(self):\n        if not self.underMouse() and not self._mouse_in_gallery():\n            self.hide_animation.start()\n\n    def show_gallery(self, index, view):\n        self.resize(app_constants.POPUP_WIDTH, app_constants.POPUP_HEIGHT)\n        self.view = view\n        desktop_w = QDesktopWidget().width()\n        desktop_h = QDesktopWidget().height()\n        \n        margin_offset = 20 # should be higher than gallery_touch_offset\n        gallery_touch_offset = 10 # How far away the window is from touching gallery\n\n        index_rect = view.visualRect(index)\n        self.idx_top_l = index_top_left = view.mapToGlobal(index_rect.topLeft())\n        self.idx_top_r = index_top_right = view.mapToGlobal(index_rect.topRight())\n        self.idx_btm_l = index_btm_left = view.mapToGlobal(index_rect.bottomLeft())\n        index_btm_right = view.mapToGlobal(index_rect.bottomRight())\n\n        if app_constants.DEBUG:\n            for idx in (index_top_left, index_top_right, index_btm_left, index_btm_right):\n                print(idx.x(), idx.y())\n\n        # adjust placement\n\n        def check_left():\n            middle = (index_top_left.y() + index_btm_left.y()) / 2 # middle of gallery left side\n            left = (index_top_left.x() - self.width() - margin_offset) > 0 # if the width can be there\n            top = (middle - (self.height() / 2) - margin_offset) > 0 # if the top half of window can be there\n            btm = (middle + (self.height() / 2) + margin_offset) < desktop_h # same as above, just for the bottom\n            if left and top and btm:\n                self.direction = self.RIGHT\n                x = index_top_left.x() - gallery_touch_offset - self.width()\n                y = middle - (self.height() / 2)\n                appear_point = QPoint(int(x), int(y))\n                self.move(appear_point)\n                return True\n            return False\n\n        def check_right():\n            middle = (index_top_right.y() + index_btm_right.y()) / 2 # middle of gallery right side\n            right = (index_top_right.x() + self.width() + margin_offset) < desktop_w # if the width can be there\n            top = (middle - (self.height() / 2) - margin_offset) > 0 # if the top half of window can be there\n            btm = (middle + (self.height() / 2) + margin_offset) < desktop_h # same as above, just for the bottom\n\n            if right and top and btm:\n                self.direction = self.LEFT\n                x = index_top_right.x() + gallery_touch_offset\n                y = middle - (self.height() / 2)\n                appear_point = QPoint(int(x), int(y))\n                self.move(appear_point)\n                return True\n            return False\n\n        def check_top():\n            middle = (index_top_left.x() + index_top_right.x()) / 2 # middle of gallery top side\n            top = (index_top_right.y() - self.height() - margin_offset) > 0 # if the height can be there\n            left = (middle - (self.width() / 2) - margin_offset) > 0 # if the left half of window can be there\n            right = (middle + (self.width() / 2) + margin_offset) < desktop_w # same as above, just for the right\n\n            if top and left and right:\n                self.direction = self.BOTTOM\n                x = middle - (self.width() / 2)\n                y = index_top_left.y() - gallery_touch_offset - self.height()\n                appear_point = QPoint(int(x), int(y))\n                self.move(appear_point)\n                return True\n            return False\n\n        def check_bottom(override=False):\n            middle = (index_btm_left.x() + index_btm_right.x()) / 2 # middle of gallery bottom side\n            btm = (index_btm_right.y() + self.height() + margin_offset) < desktop_h # if the height can be there\n            left = (middle - (self.width() / 2) - margin_offset) > 0 # if the left half of window can be there\n            right = (middle + (self.width() / 2) + margin_offset) < desktop_w # same as above, just for the right\n\n            if (btm and left and right) or override:\n                self.direction = self.TOP\n                x = middle - (self.width() / 2)\n                y = index_btm_left.y() + gallery_touch_offset\n                appear_point = QPoint(int(x), int(y))\n                self.move(appear_point)\n                return True\n            return False\n\n        for pos in (check_bottom, check_right, check_left, check_top):\n            if pos():\n                break\n        else: # default pos is bottom\n            check_bottom(True)\n\n        self._set_gallery(index.data(Qt.UserRole + 1))\n        self.show()\n\n    def closeEvent(self, ev):\n        ev.ignore()\n        self.delayed_hide()\n\n    def _set_gallery(self, gallery):\n        self.current_gallery = gallery\n        self.g_widget.apply_gallery(gallery)\n        self.g_widget.resize(self.width() - self.content_margin,\n                                     self.height() - self.content_margin)\n        if self.direction == self.LEFT:\n            start_point = QPoint(self.arrow_size.width(), 0)\n        elif self.direction == self.TOP:\n            start_point = QPoint(0, self.arrow_size.height())\n        else:\n            start_point = QPoint(0, 0)\n        # title\n        #title_region = QRegion(0, 0, self.g_title_lbl.width(),\n        #self.g_title_lbl.height())\n        self.g_widget.move(start_point)\n\n    class GalleryLayout(QFrame):\n        class ChapterList(QTableWidget):\n            def __init__(self, parent):\n                super().__init__(parent)\n                self.setColumnCount(3)\n                self.setEditTriggers(self.NoEditTriggers)\n                self.setFocusPolicy(Qt.NoFocus)\n                self.verticalHeader().setSectionResizeMode(self.verticalHeader().ResizeToContents)\n                self.horizontalHeader().setSectionResizeMode(0, self.horizontalHeader().ResizeToContents)\n                self.horizontalHeader().setSectionResizeMode(1, self.horizontalHeader().Stretch)\n                self.horizontalHeader().setSectionResizeMode(2, self.horizontalHeader().ResizeToContents)\n                self.horizontalHeader().hide()\n                self.verticalHeader().hide()\n                self.setSelectionMode(self.SingleSelection)\n                self.setSelectionBehavior(self.SelectRows)\n                self.setShowGrid(False)\n                self.viewport().setBackgroundRole(self.palette().Dark)\n                palette = self.viewport().palette()\n                palette.setColor(palette.Highlight, QColor(88, 88, 88, 70))\n                palette.setColor(palette.HighlightedText, QColor('black'))\n                self.viewport().setPalette(palette)\n                self.setWordWrap(False)\n                self.setTextElideMode(Qt.ElideRight)\n                self.doubleClicked.connect(lambda idx: self._get_chap(idx).open())\n\n            def set_chapters(self, chapter_container):\n                for r in range(self.rowCount()):\n                    self.removeRow(0)\n                def t_item(txt=''):\n                    t = QTableWidgetItem(txt)\n                    t.setBackground(QBrush(QColor('#585858')))\n                    return t\n\n                for chap in chapter_container:\n                    c_row = self.rowCount() + 1\n                    self.setRowCount(c_row)\n                    c_row -= 1\n                    n = t_item()\n                    n.setData(Qt.DisplayRole, chap.number + 1)\n                    n.setData(Qt.UserRole + 1, chap)\n                    self.setItem(c_row, 0, n)\n                    title = chap.title\n                    if not title:\n                        title = chap.gallery.title\n                    t = t_item(title)\n                    self.setItem(c_row, 1, t)\n                    p = t_item(str(chap.pages))\n                    self.setItem(c_row, 2, p)\n                self.sortItems(0)\n\n            def _get_chap(self, idx):\n                r = idx.row()\n                t = self.item(r, 0)\n                return t.data(Qt.UserRole + 1)\n\n            def contextMenuEvent(self, event):\n                idx = self.indexAt(event.pos())\n                if idx.isValid():\n                    chap = self._get_chap(idx)\n                    menu = QMenu(self)\n                    open = menu.addAction('Open', lambda: chap.open())\n                    def open_source():\n                        text = 'Opening archive...' if chap.in_archive else 'Opening folder...'\n                        app_constants.STAT_MSG_METHOD(text)\n                        path = chap.gallery.path if chap.in_archive else chap.path\n                        utils.open_path(path)\n                    t = \"Open archive\" if chap.in_archive else \"Open folder\"\n                    open_path = menu.addAction(t, open_source)\n                    menu.exec_(event.globalPos())\n                    event.accept()\n                    del menu\n                else:\n                    event.ignore()\n\n        def __init__(self, parent, appwindow):\n            super().__init__(parent)\n            self.setFocusPolicy(Qt.NoFocus)\n            self.appwindow = appwindow\n            self.setStyleSheet('color:white;')\n            main_layout = QHBoxLayout(self)\n            self.stacked_l = stacked_l = QStackedLayout()\n            general_info = QWidget(self)\n            chapter_info = QWidget(self)\n            chapter_layout = QVBoxLayout(chapter_info)\n            self.general_index = stacked_l.addWidget(general_info)\n            self.chap_index = stacked_l.addWidget(chapter_info)\n            self.chapter_list = self.ChapterList(self)\n            back_btn = TagText('Back')\n            back_btn.clicked.connect(lambda: stacked_l.setCurrentIndex(self.general_index))\n            chapter_layout.addWidget(back_btn, 0, Qt.AlignCenter)\n            chapter_layout.addWidget(self.chapter_list)\n            self.left_layout = QFormLayout()\n            self.main_left_layout = QVBoxLayout(general_info)\n            self.main_left_layout.addLayout(self.left_layout)\n            self.right_layout = QFormLayout()\n            main_layout.addLayout(stacked_l, 1)\n            main_layout.addWidget(Line('v'))\n            main_layout.addLayout(self.right_layout)\n            def get_label(txt):\n                lbl = QLabel(txt)\n                lbl.setWordWrap(True)\n                return lbl\n            self.g_title_lbl = get_label('')\n            self.g_title_lbl.setStyleSheet('color:white;font-weight:bold;')\n            self.left_layout.addRow(self.g_title_lbl)\n            self.g_artist_lbl = ClickedLabel()\n            self.g_artist_lbl.setWordWrap(True)\n            self.g_artist_lbl.clicked.connect(lambda a: appwindow.search('artist:\"{}\"'.format(a)))\n            self.g_artist_lbl.setStyleSheet('color:#bdc3c7;')\n            self.g_artist_lbl.setToolTip(\"Click to see more from this artist\")\n            self.left_layout.addRow(self.g_artist_lbl)\n            for lbl in (self.g_title_lbl, self.g_artist_lbl):\n                lbl.setAlignment(Qt.AlignCenter)\n            self.left_layout.addRow(Line('h'))\n\n            first_layout = QHBoxLayout()\n            self.g_type_lbl = ClickedLabel()\n            self.g_type_lbl.setStyleSheet('text-decoration: underline')\n            self.g_type_lbl.clicked.connect(lambda a: appwindow.search('type:\"{}\"'.format(a)))\n            self.g_lang_lbl = ClickedLabel()\n            self.g_lang_lbl.setStyleSheet('text-decoration: underline')\n            self.g_lang_lbl.clicked.connect(lambda a: appwindow.search('language:\"{}\"'.format(a)))\n            self.g_chapters_lbl = TagText('Chapters')\n            self.g_chapters_lbl.clicked.connect(lambda: stacked_l.setCurrentIndex(self.chap_index))\n            self.g_chap_count_lbl = QLabel()\n            self.right_layout.addRow(self.g_type_lbl)\n            self.right_layout.addRow(self.g_lang_lbl)\n            self.right_layout.addRow(self.g_chap_count_lbl)\n            #first_layout.addWidget(self.g_lang_lbl, 0, Qt.AlignLeft)\n            first_layout.addWidget(self.g_chapters_lbl, 0, Qt.AlignCenter)\n            #first_layout.addWidget(self.g_type_lbl, 0, Qt.AlignRight)\n            self.left_layout.addRow(first_layout)\n\n            self.g_status_lbl = QLabel()\n            self.g_d_added_lbl = QLabel()\n            self.g_pub_lbl = QLabel()\n            self.g_last_read_lbl = QLabel()\n            self.g_read_count_lbl = QLabel()\n            self.g_pages_total_lbl = QLabel()\n            self.right_layout.addRow(self.g_read_count_lbl)\n            self.right_layout.addRow('Pages:', self.g_pages_total_lbl)\n            self.right_layout.addRow('Status:', self.g_status_lbl)\n            self.right_layout.addRow('Added:', self.g_d_added_lbl)\n            self.right_layout.addRow('Published:', self.g_pub_lbl)\n            self.right_layout.addRow('Last read:', self.g_last_read_lbl)\n\n            self.g_info_lbl = get_label('')\n            self.left_layout.addRow(self.g_info_lbl)\n\n            self.g_url_lbl = ClickedLabel()\n            self.g_url_lbl.clicked.connect(lambda: utils.open_web_link(self.g_url_lbl.text()))\n            self.g_url_lbl.setWordWrap(True)\n            self.left_layout.addRow('URL:', self.g_url_lbl)\n            #self.left_layout.addRow(Line('h'))\n\n            self.tags_scroll = QScrollArea(self)\n            self.tags_widget = QWidget(self.tags_scroll)\n            self.tags_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n            self.tags_layout = QFormLayout(self.tags_widget)\n            self.tags_layout.setSizeConstraint(self.tags_layout.SetMaximumSize)\n            self.tags_scroll.setWidget(self.tags_widget)\n            self.tags_scroll.setWidgetResizable(True)\n            self.tags_scroll.setFrameShape(QFrame.NoFrame)\n            self.main_left_layout.addWidget(self.tags_scroll)\n\n\n        def has_tags(self, tags):\n            t_len = len(tags)\n            if not t_len:\n                return False\n            if t_len == 1:\n                if 'default' in tags:\n                    if not tags['default']:\n                        return False\n            return True\n\n        def apply_gallery(self, gallery):\n            self.stacked_l.setCurrentIndex(self.general_index)\n            self.chapter_list.set_chapters(gallery.chapters)\n            self.g_title_lbl.setText(gallery.title)\n            self.g_artist_lbl.setText(gallery.artist)\n            self.g_lang_lbl.setText(gallery.language)\n            chap_txt = \"chapters\" if gallery.chapters.count() > 1 else \"chapter\"\n            self.g_chap_count_lbl.setText('{} {}'.format(gallery.chapters.count(), chap_txt))\n            self.g_type_lbl.setText(gallery.type)\n            pages = gallery.chapters.pages()\n            self.g_pages_total_lbl.setText('{}'.format(pages))\n            self.g_status_lbl.setText(gallery.status)\n            self.g_d_added_lbl.setText(gallery.date_added.strftime('%d %b %Y'))\n            if gallery.pub_date:\n                self.g_pub_lbl.setText(gallery.pub_date.strftime('%d %b %Y'))\n            else:\n                self.g_pub_lbl.setText('Unknown')\n            last_read_txt = '{} ago'.format(utils.get_date_age(gallery.last_read)) if gallery.last_read else \"Unknown\"\n            self.g_last_read_lbl.setText(last_read_txt)\n            self.g_read_count_lbl.setText('Read {} times'.format(gallery.times_read))\n            self.g_info_lbl.setText(gallery.info)\n            if gallery.link:\n                self.g_url_lbl.setText(gallery.link)\n                self.g_url_lbl.show()\n            else:\n                self.g_url_lbl.hide()\n\n            \n            clearLayout(self.tags_layout)\n            if self.has_tags(gallery.tags):\n                ns_layout = QFormLayout()\n                self.tags_layout.addRow(ns_layout)\n                for namespace in sorted(gallery.tags):\n                    tags_lbls = FlowLayout()\n                    if namespace == 'default':\n                        self.tags_layout.insertRow(0, tags_lbls)\n                    else:\n                        self.tags_layout.addRow(namespace, tags_lbls)\n\n                    for n, tag in enumerate(sorted(gallery.tags[namespace]), 1):\n                        if namespace == 'default':\n                            t = TagText(search_widget=self.appwindow)\n                        else:\n                            t = TagText(search_widget=self.appwindow, namespace=namespace)\n                        t.setText(tag)\n                        tags_lbls.addWidget(t)\n                        t.setAutoFillBackground(True)\n            self.tags_widget.adjustSize()\n\nclass Spinner(TransparentWidget):\n    \"\"\"\n    Spinner widget\n    \"\"\"\n    activated = pyqtSignal()\n    deactivated = pyqtSignal()\n    about_to_show, about_to_hide = range(2)\n    _OFFSET_X_TOPRIGHT = [0]\n\n    def __init__(self, parent, position='topright'):\n        \"Position can be: 'center', 'topright' or QPoint\"\n        super().__init__(parent, flags=Qt.Window | Qt.FramelessWindowHint, move_listener=False)\n        self.setAttribute(Qt.WA_ShowWithoutActivating)\n        self.fps = 21\n        self.border = 2\n        self.line_width = 5\n        self.arc_length = 100\n        self.seconds_per_spin = 1\n        self.text_layout = None\n\n        self.text = ''\n        self._text_margin = 5\n\n        self._timer = QTimer(self)\n        self._timer.timeout.connect(self._on_timer_timeout)\n\n        # keep track of the current start angle to avoid\n        # unnecessary repaints\n        self._start_angle = 0\n\n        self._offset_x_topright = self._OFFSET_X_TOPRIGHT[0]\n        self.margin = 10\n        self._position = position\n        self._min_size = 0\n\n        self.state_timer = QTimer()\n        self.current_state = self.about_to_show\n        self.state_timer.timeout.connect(super().hide)\n        self.state_timer.setSingleShot(True)\n\n        # animation\n        self.fade_animation = create_animation(self, 'windowOpacity')\n        self.fade_animation.setDuration(800)\n        self.fade_animation.setStartValue(0.0)\n        self.fade_animation.setEndValue(1.0)\n        self.setWindowOpacity(0.0)\n        self._update_layout()\n        self.set_size(50)\n        self._set_position(position)\n\n    def _update_layout(self):\n        self.text_layout = text_layout(self.text, self.width() - self._text_margin, self.font(), self.fontMetrics())\n        self.setFixedHeight(self._min_size + self.text_layout.boundingRect().height())\n\n    def set_size(self, w):\n        self.setFixedWidth(w)\n        self._min_size = w\n        self._update_layout()\n        self.update()\n\n    def set_text(self, txt):\n        self.text = txt\n        self._update_layout()\n        self.update()\n\n    def _set_position(self, new_pos):\n        \"'center', 'topright' or QPoint\"\n        p = self.parent_widget\n\n        # topleft\n        if new_pos == \"topright\":\n            def topright():\n                return QPoint(p.pos().x() + p.width() - 65 - self._offset_x_topright, p.pos().y() + p.toolbar.height() + 55)\n            self.move(topright())\n            p.move_listener.connect(lambda: self.update_move(topright()))\n\n        elif new_pos == \"center\":\n            p.move_listener.connect(lambda: self.update_move(QPoint(p.pos().x() + p.width() // 2,\n                                                                p.pos().y() + p.height() // 2)))\n\n        elif isinstance(new_pos, QPoint):\n            p.move_listener.connect(lambda: self.update_move(new_pos))\n\n    def paintEvent(self, event):\n        # call the base paint event:\n        super().paintEvent(event)\n\n        painter = QPainter()\n        painter.begin(self)\n        try:\n            painter.setRenderHint(QPainter.Antialiasing)\n\n            txt_rect = QRectF(0,0,0,0)\n            if not self.text:\n                txt_rect.setHeight(self.fontMetrics().height())\n\n            painter.save()\n            painter.setPen(Qt.NoPen)\n            painter.setBrush(QBrush(QColor(88,88,88,180)))\n            painter.drawRoundedRect(QRect(0,0, self.width(), self.height() - txt_rect.height()), 5, 5)\n            painter.restore()\n\n            pen = QPen(QColor('#F2F2F2'))\n            pen.setWidth(self.line_width)\n            painter.setPen(pen)\n\n            border = self.border + int(math.ceil(self.line_width / 2.0))\n            r = QRectF((txt_rect.height()) / 2, (txt_rect.height() / 2),\n              self.width() - txt_rect.height(), self.width() - txt_rect.height())\n            r.adjust(border, border, -border, -border)\n\n            # draw the arc:\n            painter.drawArc(r, -self._start_angle * 16, self.arc_length * 16)\n\n            # draw text if there is\n            if self.text:\n                txt_rect = self.text_layout.boundingRect()\n                self.text_layout.draw(painter, QPointF(self._text_margin, self.height() - txt_rect.height() - self._text_margin / 2))\n\n            r = None\n\n        finally:\n            painter.end()\n            painter = None\n\n    def showEvent(self, event):\n        if self._position == \"topright\":\n            self._OFFSET_X_TOPRIGHT[0] += + self.width() + self.margin\n        if not self._timer.isActive():\n            self.fade_animation.start()\n            self.current_state = self.about_to_show\n            self.state_timer.stop()\n            self.activated.emit()\n            self._timer.start(1000 / max(1, self.fps))\n        super().showEvent(event)\n\n    def hideEvent(self, event):\n        self._timer.stop()\n        self.deactivated.emit()\n        super().hideEvent(event)\n\n    def before_hide(self):\n        if self.current_state == self.about_to_hide:\n            return\n        self.current_state = self.about_to_hide\n        if self._position == \"topright\":\n            self._OFFSET_X_TOPRIGHT[0] -= self.width() + self.margin\n        self.state_timer.start(5000)\n\n    def closeEvent(self, event):\n        self._timer.stop()\n        super().closeEvent(event)\n\n    def _on_timer_timeout(self):\n        if not self.isVisible():\n            return\n\n        # calculate the spin angle as a function of the current time so that all\n        # spinners appear in sync!\n        t = time.time()\n        whole_seconds = int(t)\n        p = (whole_seconds % self.seconds_per_spin) + (t - whole_seconds)\n        angle = int((360 * p) / self.seconds_per_spin)\n\n        if angle == self._start_angle:\n            return\n\n        self._start_angle = angle\n        self.update()\n\nclass GalleryMenu(QMenu):\n    delete_galleries = pyqtSignal(bool)\n    edit_gallery = pyqtSignal(object, object)\n\n    def __init__(self, view, index, sort_model, app_window, selected_indexes=None):\n        super().__init__(app_window)\n        self.parent_widget = app_window\n        self.view = view\n        self.sort_model = sort_model\n        self.index = index\n        self.gallery = index.data(Qt.UserRole + 1)\n        self.selected = selected_indexes\n        if self.view.view_type == app_constants.ViewType.Default:\n            if not self.selected:\n                favourite_act = self.addAction('Favorite',\n                                         lambda: self.parent_widget.manga_list_view.favorite(self.index))\n                favourite_act.setCheckable(True)\n                if self.gallery.fav:\n                    favourite_act.setChecked(True)\n                    favourite_act.setText('Unfavorite')\n                else:\n                    favourite_act.setChecked(False)\n            else:\n                favourite_act = self.addAction('Favorite selected', self.favourite_select)\n                favourite_act.setCheckable(True)\n                f = []\n                for idx in self.selected:\n                    if idx.data(Qt.UserRole + 1).fav:\n                        f.append(True)\n                    else:\n                        f.append(False)\n                if all(f):\n                    favourite_act.setChecked(True)\n                    favourite_act.setText('Unfavorite selected')\n                else:\n                    favourite_act.setChecked(False)\n        elif self.view.view_type == app_constants.ViewType.Addition:\n\n            send_to_lib = self.addAction('Send to library',\n                                self.send_to_lib)\n            add_to_ignore = self.addAction('Ignore and remove',\n                                  self.add_to_ignore)\n        self.addSeparator()\n        rating = self.addAction('Set rating')\n        rating_menu = QMenu(self)\n        rating.setMenu(rating_menu)\n        for x in range(0, 6):\n            rating_menu.addAction('{}'.format(x), functools.partial(self.set_rating, x))\n        self.addSeparator()\n        if not self.selected and isinstance(view, QTableView):\n            chapters_menu = self.addAction('Chapters')\n            open_chapters = QMenu(self)\n            chapters_menu.setMenu(open_chapters)\n            for number, chap in enumerate(self.gallery.chapters, 1):\n                chap_action = QAction(\"Open chapter {}\".format(number),\n                             open_chapters,\n                             triggered = functools.partial(chap.open))\n                open_chapters.addAction(chap_action)\n        if self.selected:\n            open_f_chapters = self.addAction('Open first chapters', self.open_first_chapters)\n\n        if self.view.view_type != app_constants.ViewType.Duplicate:\n            if not self.selected:\n                add_chapters = self.addAction('Add chapters', self.add_chapters)\n            if self.view.view_type == app_constants.ViewType.Default:\n                add_to_list_txt = \"Add selected to list\" if self.selected else \"Add to list\"\n                add_to_list = self.addAction(add_to_list_txt)\n                add_to_list_menu = QMenu(self)\n                add_to_list.setMenu(add_to_list_menu)\n                for g_list in sorted(app_constants.GALLERY_LISTS):\n                    add_to_list_menu.addAction(g_list.name, functools.partial(self.add_to_list, g_list))\n        self.addSeparator()\n        web_menu_act = self.addAction('Web')\n        web_menu = QMenu(self)\n        web_menu_act.setMenu(web_menu)\n\n        if not self.selected:\n            get_metadata = web_menu.addAction('Fetch metadata',\n                                    lambda: self.parent_widget.get_metadata(index.data(Qt.UserRole + 1)))\n        else:\n            gals = []\n            for idx in self.selected:\n                gals.append(idx.data(Qt.UserRole + 1))\n            get_select_metadata = web_menu.addAction('Fetch metadata for selected',\n                                        lambda: self.parent_widget.get_metadata(gals))\n\n        web_menu.addSeparator()\n\n        if self.index.data(Qt.UserRole + 1).link and not self.selected:\n            op_link = web_menu.addAction('Open URL', self.op_link)\n            web_menu.addSeparator()\n        if self.selected and all([idx.data(Qt.UserRole + 1).link for idx in self.selected]):\n            op_links = web_menu.addAction('Open URLs', lambda: self.op_link(True))\n            web_menu.addSeparator()\n\n\n        artist_lookup = web_menu.addAction(\"Lookup Artists\" if self.selected else \"Lookup Artist\", lambda: self.lookup_web(\"artist\"))\n\n        self.addSeparator()\n\n        edit = self.addAction('Edit', lambda: self.edit_gallery.emit(self.parent_widget,\n                                            self.index.data(Qt.UserRole + 1) if not self.selected else [idx.data(Qt.UserRole + 1) for idx in self.selected]))\n        \n        self.addSeparator()\n\n        if not self.selected:\n            text = 'folder' if not self.index.data(Qt.UserRole + 1).is_archive else 'archive'\n            op_folder_act = self.addAction('Open {}'.format(text), self.op_folder)\n            op_cont_folder_act = self.addAction('Show in folder', lambda: self.op_folder(containing=True))\n        else:\n            text = 'folders' if not self.index.data(Qt.UserRole + 1).is_archive else 'archives'\n            op_folder_select = self.addAction('Open {}'.format(text), lambda: self.op_folder(True))\n            op_cont_folder_select = self.addAction('Show in folders', lambda: self.op_folder(True, True))\n\n\n        remove_act = self.addAction('Remove')\n        remove_menu = QMenu(self)\n        remove_act.setMenu(remove_menu)\n        if self.view.view_type == app_constants.ViewType.Default:\n            if self.sort_model.current_gallery_list:\n                remove_f_g_list_txt = \"Remove selected from list\" if self.selected else \"Remove from list\"\n                remove_f_g_list = remove_menu.addAction(remove_f_g_list_txt, self.remove_from_list)\n        if not self.selected:\n            remove_g = remove_menu.addAction('Remove gallery',\n                                lambda: self.delete_galleries.emit(False))\n            remove_ch = remove_menu.addAction('Remove chapter')\n            remove_ch_menu = QMenu(self)\n            remove_ch.setMenu(remove_ch_menu)\n            for number, chap_number in enumerate(range(len(self.index.data(Qt.UserRole + 1).chapters)), 1):\n                chap_action = QAction(\"Remove chapter {}\".format(number),\n                          remove_ch_menu,\n                          triggered = functools.partial(self.parent_widget.manga_list_view.del_chapter,\n                              index,\n                              chap_number))\n                remove_ch_menu.addAction(chap_action)\n        else:\n            remove_select_g = remove_menu.addAction('Remove selected', lambda: self.delete_galleries.emit(False))\n        remove_menu.addSeparator()\n        if not self.selected:\n            remove_source_g = remove_menu.addAction('Remove and delete files',\n                                       lambda: self.delete_galleries.emit(True))\n        else:\n            remove_source_select_g = remove_menu.addAction('Remove selected and delete files',\n                                           lambda: self.delete_galleries.emit(True))\n        self.addSeparator()\n        advanced = self.addAction('Advanced')\n        adv_menu = QMenu(self)\n        advanced.setMenu(adv_menu)\n        if not self.selected:\n            change_cover = adv_menu.addAction('Change cover...', self.change_cover)\n\n        if self.selected:\n            allow_metadata_count = 0\n            for i in self.selected:\n                if i.data(Qt.UserRole + 1).exed:\n                    allow_metadata_count += 1\n            self.allow_metadata_exed = allow_metadata_count >= len(self.selected) // 2\n        else:\n            self.allow_metadata_exed = False if not self.gallery.exed else True\n\n        if self.selected:\n            allow_metadata_txt = \"Include selected in auto metadata fetch\" if self.allow_metadata_exed else \"Exclude selected in auto metadata fetch\"\n        else:\n            allow_metadata_txt = \"Include in 'Fetch all metadata'\" if self.allow_metadata_exed else \"Exclude in 'Fetch all metadata'\"\n        adv_menu.addAction(allow_metadata_txt, self.allow_metadata_fetch)\n        adv_menu.addAction(\"Reset read count\", self.reset_read_count)\n\n    def lookup_web(self, txt):\n        tag = []\n        if txt == 'artist':\n            if self.selected:\n                for i in self.selected:\n                    tag.append('artist:' + i.data(Qt.UserRole + 2).strip())\n            else:\n                tag.append('artist:' + self.index.data(Qt.UserRole + 2).strip())\n\n        [utils.lookup_tag(t) for t in tag]\n\n    def set_rating(self, x):\n\n        def save_rating(g):\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery,\n                                True, g.id, rating=g.rating)\n        if self.selected:\n           [(setattr(g, \"rating\", x), save_rating(g)) for g in [idx.data(Qt.UserRole + 1) for idx in self.selected]]\n        else:\n             self.gallery.rating = x\n             save_rating(self.gallery)\n\n\n    def add_to_ignore(self):\n        if self.selected:\n            gs = self.selected\n        else:\n            gs = [self.index]\n        galleries = [idx.data(Qt.UserRole + 1) for idx in gs]\n\n        paths = set()\n        for g in galleries:\n            for chap in g.chapters:\n                if not chap.in_archive:\n                    paths.add(chap.path)\n                else:\n                    paths.add(g.path)\n        app_constants.IGNORE_PATHS.extend(paths)\n\n        settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths')\n        self.delete_galleries.emit(False)\n\n    def send_to_lib(self):\n        if self.selected:\n            gs = self.selected\n        else:\n            gs = [self.index]\n        galleries = [idx.data(Qt.UserRole + 1) for idx in gs]\n        rows = len(galleries)\n        self.view.gallery_model._gallery_to_remove.extend(galleries)\n        self.view.gallery_model.removeRows(self.view.gallery_model.rowCount() - rows, rows)\n        self.parent_widget.default_manga_view.add_gallery(galleries)\n        for g in galleries:\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery,\n                                True, g.id, view=g.view)\n        self.view.sort_model.refresh()\n        self.view.clearSelection()\n\n    def allow_metadata_fetch(self):\n        exed = 0 if self.allow_metadata_exed else 1\n        if self.selected:\n            for idx in self.selected:\n                g = idx.data(Qt.UserRole + 1)\n                g.exed = exed\n                gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, g.id, {'exed':exed})\n        else:\n            self.gallery.exed = exed\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, self.gallery.id, {'exed':exed})\n\n    def reset_read_count(self):\n        if self.selected:\n            for idx in self.selected:\n                g = idx.data(Qt.UserRole + 1)\n                g.times_read = 0\n                gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, g.id, {'times_read':0})\n        else:\n            self.gallery.times_read = 0\n            gallerydb.execute(gallerydb.GalleryDB.modify_gallery, True, self.gallery.id, {'times_read':0})\n\n    def add_to_list(self, g_list):\n        galleries = []\n        if self.selected:\n            for idx in self.selected:\n                galleries.append(idx.data(Qt.UserRole + 1))\n        else:\n            galleries.append(self.gallery)\n        g_list.add_gallery(galleries)\n\n    def remove_from_list(self):\n        g_list = self.sort_model.current_gallery_list\n        if self.selected:\n            g_ids = []\n            for idx in self.selected:\n                g_ids.append(idx.data(Qt.UserRole + 1).id)\n        else:\n            g_ids = self.gallery.id\n        self.sort_model.current_gallery_list.remove_gallery(g_ids)\n        self.sort_model.init_search(self.sort_model.current_term)\n\n    def favourite_select(self):\n        for idx in self.selected:\n            self.parent_widget.manga_list_view.favorite(idx)\n\n    def change_cover(self):\n        gallery = self.index.data(Qt.UserRole + 1)\n        log_i('Attempting to change cover of {}'.format(gallery.title.encode(errors='ignore')))\n        if gallery.is_archive:\n            try:\n                zip = utils.ArchiveFile(gallery.path)\n            except utils.app_constants.CreateArchiveFail:\n                app_constants.NOTIF_BAR.add_text('Attempt to change cover failed. Could not create archive.')\n                return\n            path = zip.extract_all()\n        else:\n            path = gallery.path\n\n        new_cover = QFileDialog.getOpenFileName(self,\n                            'Select a new gallery cover',\n                            filter='Image {}'.format(utils.IMG_FILTER),\n                            directory=path)[0]\n        if new_cover and new_cover.lower().endswith(utils.IMG_FILES):\n            gallerydb.GalleryDB.clear_thumb(gallery.profile)\n            Executors.generate_thumbnail(gallery, img=new_cover, on_method=gallery.set_profile)\n            gallery.reset_profile()\n            log_i('Changed cover successfully!')\n\n    def open_first_chapters(self):\n        txt = \"Opening first chapters of selected galleries\"\n        app_constants.STAT_MSG_METHOD(txt)\n        for idx in self.selected:\n            idx.data(Qt.UserRole + 1).chapters[0].open(False)\n\n    def op_link(self, select=False):\n        if select:\n            for x in self.selected:\n                gal = x.data(Qt.UserRole + 1)\n                utils.open_web_link(gal.link)\n        else:\n            utils.open_web_link(self.index.data(Qt.UserRole + 1).link)\n            \n\n    def op_folder(self, select=False, containing=False):\n        if select:\n            for x in self.selected:\n                text = 'Opening archives...' if self.index.data(Qt.UserRole + 1).is_archive else 'Opening folders...'\n                text = 'Opening containing folders...' if containing else text\n                self.view.STATUS_BAR_MSG.emit(text)\n                gal = x.data(Qt.UserRole + 1)\n                path = os.path.split(gal.path)[0] if containing else gal.path\n                if containing:\n                    utils.open_path(path, gal.path)\n                else:\n                    utils.open_path(path)\n        else:\n            text = 'Opening archive...' if self.index.data(Qt.UserRole + 1).is_archive else 'Opening folder...'\n            text = 'Opening containing folder...' if containing else text\n            self.view.STATUS_BAR_MSG.emit(text)\n            gal = self.index.data(Qt.UserRole + 1)\n            path = os.path.split(gal.path)[0] if containing else gal.path\n            if containing:\n                utils.open_path(path, gal.path)\n            else:\n                utils.open_path(path)\n\n\n    def add_chapters(self):\n        def add_chdb(chaps_container):\n            gallery = self.index.data(Qt.UserRole + 1)\n            log_i('Adding new chapter for {}'.format(gallery.title.encode(errors='ignore')))\n            gallerydb.execute(gallerydb.ChapterDB.add_chapters_raw, False, gallery.id, chaps_container)\n        ch_widget = ChapterAddWidget(self.index.data(Qt.UserRole + 1), self.parent_widget)\n        ch_widget.CHAPTERS.connect(add_chdb)\n        ch_widget.show()\n\nclass SystemTray(QSystemTrayIcon):\n    \"\"\"\n    Pass True to minimized arg in showMessage method to only\n    show message if application is minimized.\n    \"\"\"\n    def __init__(self, icon, parent=None):\n        super().__init__(icon, parent=None)\n        self.parent_widget = parent\n\n    def showMessage(self, title, msg, icon=QSystemTrayIcon.Information,\n                 msecs=10000, minimized=False):\n        # NOTE: Crashes on linux\n        # TODO: Fix this!!\n        if not app_constants.OS_NAME == \"linux\":\n            if minimized:\n                if self.parent_widget.isMinimized() or not self.parent_widget.isActiveWindow():\n                    return super().showMessage(title, msg, icon, msecs)\n            else:\n                return super().showMessage(title, msg, icon, msecs)\n\nclass ClickedLabel(QLabel):\n    \"\"\"\n    A QLabel which emits clicked signal on click\n    \"\"\"\n    clicked = pyqtSignal(str)\n    def __init__(self, s=\"\", **kwargs):\n        super().__init__(s, **kwargs)\n        self.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)\n\n    def enterEvent(self, event):\n        if self.text():\n            self.setCursor(Qt.PointingHandCursor)\n        else:\n            self.setCursor(Qt.ArrowCursor)\n        return super().enterEvent(event)\n\n    def mousePressEvent(self, event):\n        self.clicked.emit(self.text())\n        return super().mousePressEvent(event)\n\nclass TagText(QPushButton):\n    def __init__(self, *args, **kwargs):\n        self.search_widget = kwargs.pop('search_widget', None)\n        self.namespace = kwargs.pop('namespace', None)\n        super().__init__(*args, **kwargs)\n        if self.search_widget:\n            if self.namespace:\n                self.clicked.connect(lambda: self.search_widget.search('\"{}\":\"{}\"'.format(self.namespace, self.text())))\n            else:\n                self.clicked.connect(lambda: self.search_widget.search('\"{}\"'.format(self.text())))\n\n    def mousePressEvent(self, ev):\n        assert isinstance(ev, QMouseEvent)\n        if ev.button() == Qt.RightButton:\n            if self.search_widget:\n                menu = QMenu(self)\n                menu.addAction(\"Lookup tag\",\n                               lambda: utils.lookup_tag(\n                                   self.text() if not self.namespace else '{}:{}'.format(self.namespace, self.text())))\n                menu.exec(ev.globalPos())\n\n        return super().mousePressEvent(ev)\n\n\n    def enterEvent(self, event):\n        if self.text():\n            self.setCursor(Qt.PointingHandCursor)\n        else:\n            self.setCursor(Qt.ArrowCursor)\n        return super().enterEvent(event)\n\nclass BasePopup(TransparentWidget):\n    graphics_blur = None\n    def __init__(self, parent=None, **kwargs):\n        blur = True\n        if kwargs:\n            blur = kwargs.pop('blur', True)\n            if kwargs:\n                super().__init__(parent, **kwargs)\n            else:\n                super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint)\n        else:\n            super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint)\n        main_layout = QVBoxLayout()\n        self.main_widget = QFrame()\n        self.main_widget.setFrameStyle(QFrame.StyledPanel)\n        self.setLayout(main_layout)\n        main_layout.addWidget(self.main_widget)\n        self.generic_buttons = QHBoxLayout()\n        self.generic_buttons.addWidget(Spacer('h'))\n        self.yes_button = QPushButton('Yes')\n        self.no_button = QPushButton('No')\n        self.buttons_layout = QHBoxLayout()\n        self.buttons_layout.addWidget(Spacer('h'), 3)\n        self.generic_buttons.addWidget(self.yes_button)\n        self.generic_buttons.addWidget(self.no_button)\n        self.setMaximumWidth(500)\n        self.resize(500,350)\n        self.curr_pos = QPoint()\n        if parent and blur:\n            try:\n                self.graphics_blur = parent.graphics_blur\n                parent.setGraphicsEffect(self.graphics_blur)\n            except AttributeError:\n                pass\n\n        # animation\n        self.fade_animation = create_animation(self, 'windowOpacity')\n        self.fade_animation.setDuration(800)\n        self.fade_animation.setStartValue(0.0)\n        self.fade_animation.setEndValue(1.0)\n        self.setWindowOpacity(0.0)\n\n    def mousePressEvent(self, event):\n        self.curr_pos = event.pos()\n        return super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if event.buttons() == Qt.LeftButton:\n            diff = event.pos() - self.curr_pos\n            newpos = self.pos() + diff\n            self.move(newpos)\n        return super().mouseMoveEvent(event)\n\n    def showEvent(self, event):\n        self.activateWindow()\n        self.fade_animation.start()\n        if self.graphics_blur:\n            self.graphics_blur.setEnabled(True)\n        return super().showEvent(event)\n\n    def closeEvent(self, event):\n        if self.graphics_blur:\n            self.graphics_blur.setEnabled(False)\n        return super().closeEvent(event)\n\n    def hideEvent(self, event):\n        if self.graphics_blur:\n            self.graphics_blur.setEnabled(False)\n        return super().hideEvent(event)\n\n    def add_buttons(self, *args):\n        \"\"\"\n        Pass names of buttons, from right to left.\n        Returns list of buttons in same order as they came in.\n        Note: Remember to add buttons_layout to main layout!\n        \"\"\"\n        b = []\n        for name in args:\n            button = QPushButton(name)\n            self.buttons_layout.addWidget(button)\n            b.append(button)\n        return b\n\nclass AppBubble(BasePopup):\n    \"For application notifications\"\n    def __init__(self, parent):\n        super().__init__(parent, flags= Qt.Window | Qt.FramelessWindowHint, blur=False)\n        self.hide_timer = QTimer(self)\n        self.hide_timer.timeout.connect(self.hide)\n        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum) \n        main_layout = QVBoxLayout(self.main_widget)\n        self.title = QLabel()\n        self.title.setTextFormat(Qt.RichText)\n        main_layout.addWidget(self.title)\n        self.content = QLabel()\n        self.content.setWordWrap(True)\n        self.content.setTextFormat(Qt.RichText)\n        self.content.setOpenExternalLinks(True)\n        main_layout.addWidget(self.content)\n        self.adjustSize()\n\n    def update_text(self, title, txt='', duration=20):\n        \"Duration in seconds!\"\n        if self.hide_timer.isActive():\n            self.hide_timer.stop()\n        self.title.setText('<h3>{}</h3>'.format(title))\n        self.content.setText(txt)\n        self.hide_timer.start(duration * 1000)\n        self.show()\n        self.adjustSize()\n        self.update_move()\n\n    def update_move(self):\n        if self.parent_widget:\n            tl = self.parent_widget.geometry().topLeft()\n            x = tl.x() + self.parent_widget.width() - self.width() - 10\n            y = tl.y() + self.parent_widget.height() - self.height() - self.parent_widget.statusBar().height()\n            self.move(x, y)\n\n    def mousePressEvent(self, event):\n        if event.button() == Qt.RightButton:\n            self.close()\n        super().mousePressEvent(event)\n\nclass AppDialog(BasePopup):\n\n    # modes\n    PROGRESS, MESSAGE = range(2)\n    closing_down = pyqtSignal()\n\n    def __init__(self, parent, mode=PROGRESS):\n        self.mode = mode\n        if mode == self.MESSAGE:\n            super().__init__(parent, flags=Qt.Dialog)\n        else:\n            super().__init__(parent)\n        self.parent_widget = parent\n        main_layout = QVBoxLayout()\n\n        self.info_lbl = QLabel()\n        self.info_lbl.setAlignment(Qt.AlignCenter)\n        main_layout.addWidget(self.info_lbl)\n        if mode == self.PROGRESS:\n            self.info_lbl.setText(\"Updating your galleries to newest version...\")\n            self.info_lbl.setWordWrap(True)\n            class progress(QProgressBar):\n                reached_maximum = pyqtSignal()\n                def __init__(self, parent=None):\n                    super().__init__(parent)\n\n                def setValue(self, v):\n                    if v == self.maximum():\n                        self.reached_maximum.emit()\n                    return super().setValue(v)\n\n            self.prog = progress(self)\n\n            self.prog.reached_maximum.connect(self.close)\n            main_layout.addWidget(self.prog)\n            self.note_info = QLabel(\"Note: This popup will close itself when everything is ready\")\n            self.note_info.setAlignment(Qt.AlignCenter)\n            self.restart_info = QLabel(\"Please wait.. It is safe to restart if there is no sign of progress.\")\n            self.restart_info.setAlignment(Qt.AlignCenter)\n            main_layout.addWidget(self.note_info)\n            main_layout.addWidget(self.restart_info)\n        elif mode == self.MESSAGE:\n            self.info_lbl.setText(\"<font color='red'>An exception has ben encountered.\\nContact the developer to get this fixed.\" + \"\\nStability from this point onward cannot be guaranteed.</font>\")\n            self.setWindowTitle(\"It was too big!\")\n\n        self.main_widget.setLayout(main_layout)\n        self.adjustSize()\n\n    def closeEvent(self, event):\n        self.parent_widget.setEnabled(True)\n        if self.mode == self.MESSAGE:\n            self.closing_down.emit()\n            return super().closeEvent(event)\n        else:\n            return super().closeEvent(event)\n\n    def showEvent(self, event):\n        self.parent_widget.setEnabled(False)\n        return super().showEvent(event)\n\n    def init_restart(self):\n        if self.mode == self.PROGRESS:\n            self.prog.hide()\n            self.note_info.hide()\n            self.restart_info.hide()\n            log_i('Application requires restart')\n            self.note_info.setText(\"Application requires restart!\")\n\n\nclass NotificationOverlay(QWidget):\n    \"\"\"\n    A notifaction bar\n    \"\"\"\n    clicked = pyqtSignal()\n    _show_signal = pyqtSignal()\n    _hide_signal = pyqtSignal()\n    _unset_cursor = pyqtSignal()\n    _set_cursor = pyqtSignal(object)\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self._main_layout = QHBoxLayout(self)\n        self._default_height = 20\n        self._dynamic_height = 0\n        self._lbl = QLabel()\n        self._main_layout.addWidget(self._lbl)\n        self._lbl.setAlignment(Qt.AlignCenter)\n        self.setContentsMargins(-10,-10,-10,-10)\n        self._click = False\n        self._override_hide = False\n        self.text_queue = []\n\n        self.slide_animation = create_animation(self, 'minimumHeight')\n        self.slide_animation.setDuration(500)\n        self.slide_animation.setStartValue(0)\n        self.slide_animation.setEndValue(self._default_height)\n        self.slide_animation.valueChanged.connect(self.set_dynamic_height)\n        self._show_signal.connect(self.show)\n        self._hide_signal.connect(self.hide)\n        self._unset_cursor.connect(self.unsetCursor)\n        self._set_cursor.connect(self.setCursor)\n\n    def set_dynamic_height(self, h):\n        self._dynamic_height = h\n\n    def mousePressEvent(self, event):\n        if self._click:\n            self.clicked.emit()\n        return super().mousePressEvent(event)\n\n    def set_clickable(self, d=True):\n        self._click = d\n        self._set_cursor.emit(Qt.PointingHandCursor)\n\n    def resize(self, x, y=0):\n        return super().resize(x, self._dynamic_height)\n\n    def add_text(self, text, autohide=True):\n        \"\"\"\n        Add new text to the bar, deleting the previous one\n        \"\"\"\n        try:\n            self._reset()\n        except TypeError:\n            pass\n        if not self.isVisible():\n            self._show_signal.emit()\n        self._lbl.setText(text)\n        if autohide:\n            if not self._override_hide:\n                threading.Timer(10, self._hide_signal.emit).start()\n\n    def begin_show(self):\n        \"\"\"\n        Control how long you will show notification bar.\n        end_show() must be called to hide the bar.\n        \"\"\"\n        self._override_hide = True\n        self._show_signal.emit()\n\n    def end_show(self):\n        self._override_hide = False\n        QTimer.singleShot(5000, self._hide_signal.emit)\n\n    def _reset(self):\n        self._unset_cursor.emit()\n        self._click = False\n        self.clicked.disconnect()\n\n    def showEvent(self, event):\n        self.slide_animation.start()\n        return super().showEvent(event)\n\nclass GalleryShowcaseWidget(QWidget):\n    \"\"\"\n    Pass a gallery or set a gallery via -> set_gallery\n    \"\"\"\n\n    double_clicked = pyqtSignal(gallerydb.Gallery)\n\n    def __init__(self, gallery=None, parent=None, menu=None):\n        super().__init__(parent)\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        self.main_layout = QVBoxLayout(self)\n        self.parent_widget = parent\n        if menu:\n            menu.gallery_widget = self\n        self._menu = menu\n        self.gallery = gallery\n        self.extra_text = QLabel()\n        self.profile = QLabel(self)\n        self.profile.setAlignment(Qt.AlignCenter)\n        self.text = QLabel(self)\n        self.font_M = self.text.fontMetrics()\n        self.main_layout.addWidget(self.extra_text)\n        self.extra_text.hide()\n        self.main_layout.addWidget(self.profile)\n        self.main_layout.addWidget(self.text)\n        self.h = 0\n        self.w = 0\n        if gallery:\n            self.h = 220\n            self.w = 143\n            self.set_gallery(gallery, (self.w, self.h))\n\n        self.resize(self.w, self.h)\n        self.setMouseTracking(True)\n\n    @property\n    def menu(self):\n        return self._menu\n\n    @menu.setter\n    def contextmenu(self, new_menu):\n        new_menu.gallery_widget = self\n        self._menu = new_menu\n\n    def set_pixmap(self, gallery, img):\n        self.profile.setPixmap(QPixmap.fromImage(img))\n\n    def set_gallery(self, gallery, size=app_constants.THUMB_SMALL):\n        assert isinstance(size, (list, tuple))\n        self.w = size[0]\n        self.h = size[1]\n        self.gallery = gallery\n        img = gallery.get_profile(app_constants.ProfileType.Small, self.set_pixmap)\n        if img:\n            self.profile.setPixmap(QPixmap.fromImage(img))\n        title = self.font_M.elidedText(gallery.title, Qt.ElideRight, self.w)\n        artist = self.font_M.elidedText(gallery.artist, Qt.ElideRight, self.w)\n        self.text.setText(\"{}\\n{}\".format(title, artist))\n        self.setToolTip(\"{}\\n{}\".format(gallery.title, gallery.artist))\n        self.resize(self.w, self.h + 50)\n\n    def paintEvent(self, event):\n        painter = QPainter(self)\n        if self.underMouse():\n            painter.setBrush(QBrush(QColor(164,164,164,120)))\n            painter.drawRect(self.text.pos().x() - 2, self.profile.pos().y() - 5,\n                    self.text.width() + 2, self.profile.height() + self.text.height() + 12)\n        super().paintEvent(event)\n\n    def enterEvent(self, event):\n        self.update()\n        return super().enterEvent(event)\n\n    def leaveEvent(self, event):\n        self.update()\n        return super().leaveEvent(event)\n\n    def mouseDoubleClickEvent(self, event):\n        self.double_clicked.emit(self.gallery)\n        return super().mouseDoubleClickEvent(event)\n\n    def contextMenuEvent(self, event):\n        if self._menu:\n            self._menu.exec_(event.globalPos())\n            event.accept()\n        else:\n            event.ignore()\n\nclass SingleGalleryChoices(BasePopup):\n    \"\"\"\n    Represent a single gallery with a list of choices below.\n    Pass a gallery and a list of tuple/list where the first index is a string in each\n    if text is passed, the text will be shown alongside gallery, else gallery be centered\n    \"\"\"\n    USER_CHOICE = pyqtSignal(object)\n    def __init__(self, gallery, tuple_first_idx, text=None, parent=None):\n        super().__init__(parent, flags= Qt.Dialog | Qt.FramelessWindowHint)\n        main_layout = QVBoxLayout()\n        self.main_widget.setLayout(main_layout)\n        g_showcase = GalleryShowcaseWidget()\n        g_showcase.set_gallery(gallery, (170 // 1.40, 170))\n        if text:\n            t_layout = QHBoxLayout()\n            main_layout.addLayout(t_layout)\n            t_layout.addWidget(g_showcase, 1)\n            info = QLabel(text)\n            info.setWordWrap(True)\n            t_layout.addWidget(info)\n        else:\n            main_layout.addWidget(g_showcase, 0, Qt.AlignCenter)\n        self.list_w = QListWidget(self)\n        self.list_w.setAlternatingRowColors(True)\n        self.list_w.setWordWrap(True)\n        self.list_w.setTextElideMode(Qt.ElideNone)\n        main_layout.addWidget(self.list_w, 3)\n        main_layout.addLayout(self.buttons_layout)\n        for t in tuple_first_idx:\n            item = CustomListItem(t)\n            item.setText(t[0])\n            self.list_w.addItem(item)\n        self.buttons = self.add_buttons('Skip All', 'Skip', 'Choose',)\n        self.buttons[2].clicked.connect(self.finish)\n        self.buttons[1].clicked.connect(self.skip)\n        self.buttons[0].clicked.connect(self.skipall)\n        self.resize(400, 400)\n        self.show()\n\n    def finish(self):\n        item = self.list_w.selectedItems()\n        if item:\n            item = item[0]\n            self.USER_CHOICE.emit(item.item)\n            self.close()\n\n    def skip(self):\n        self.USER_CHOICE.emit(())\n        self.close()\n\n    def skipall(self):\n        self.USER_CHOICE.emit(None)\n        self.close()\n\nclass BaseUserChoice(QDialog):\n    USER_CHOICE = pyqtSignal(object)\n    def __init__(self, parent, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        self.setAttribute(Qt.WA_TranslucentBackground)\n        main_widget = QFrame(self)\n        layout = QVBoxLayout(self)\n        layout.addWidget(main_widget)\n        self.main_layout = QFormLayout(main_widget)\n\n    def accept(self, choice):\n        self.USER_CHOICE.emit(choice)\n        super().accept()\n\nclass TorrentItem:\n    def __init__(self, url, name=\"\", date=None, size=None, seeds=None, peers=None, uploader=None):\n        self.url = url\n        self.name = name\n        self.date = date\n        self.size = size\n        self.seeds = seeds\n        self.peers = peers\n        self.uploader = uploader\n\nclass TorrentUserChoice(BaseUserChoice):\n    def __init__(self, parent, torrentitems=[], **kwargs):\n        super().__init__(parent, **kwargs)\n        title = QLabel('Torrents')\n        title.setAlignment(Qt.AlignCenter)\n        self.main_layout.addRow(title)\n        self._list_w = QListWidget(self)\n        self.main_layout.addRow(self._list_w)\n        for t in torrentitems:\n            self.add_torrent_item(t)\n\n        btn_layout = QHBoxLayout()\n        choose_btn = QPushButton('Choose')\n        choose_btn.clicked.connect(self.accept)\n        btn_layout.addWidget(Spacer('h'))\n        btn_layout.addWidget(choose_btn)\n        self.main_layout.addRow(btn_layout)\n        \n\n    def add_torrent_item(self, item):\n        list_item = CustomListItem(item)\n        list_item.setText(\"{}\\nSeeds:{}\\tPeers:{}\\tSize:{}\\tDate:{}\\tUploader:{}\".format(item.name, item.seeds, item.peers, item.size, item.date, item.uploader))\n        self._list_w.addItem(list_item)\n\n    def accept(self):\n        items = self._list_w.selectedItems()\n        if items:\n            item = items[0]\n            super().accept(item.item)\n\nclass LoadingOverlay(QWidget):\n    \n    def __init__(self, parent=None):\n        super().__init__(parent)\n        palette = QPalette(self.palette())\n        palette.setColor(palette.Background, Qt.transparent)\n        self.setPalette(palette)\n\n    def paintEngine(self, event):\n        painter = QPainter()\n        painter.begin(self)\n        painter.setRenderHint(QPainter.Antialiasing)\n        painter.fillRect(event.rect(),\n                   QBrush(QColor(255,255,255,127)))\n        painter.setPen(QPen(Qt.NoPen))\n        for i in range(6):\n            if (self.counter / 5) % 6 == i:\n                painter.setBrush(QBrush(QColor(127 + (self.counter % 5) * 32,127,127)))\n            else:\n                painter.setBrush(QBrush(QColor(127,127,127)))\n                painter.drawEllipse(self.width() / 2 + 30 * math.cos(2 * math.pi * i / 6.0) - 10,\n                        self.height() / 2 + 30 * math.sin(2 * math.pi * i / 6.0) - 10,\n                        20,20)\n\n        painter.end()\n\n    def showEvent(self, event):\n        self.timer = self.startTimer(50)\n        self.counter = 0\n        super().showEvent(event)\n\n    def timerEvent(self, event):\n        self.counter += 1\n        self.update()\n        if self.counter == 60:\n            self.killTimer(self.timer)\n            self.hide()\n\nclass FileIcon:\n    \n    def __init__(self):\n        self.ico_types = {}\n\n    def get_file_icon(self, path):\n        if os.path.isdir(path):\n            if not 'dir' in self.ico_types:\n                self.ico_types['dir'] = QFileIconProvider().icon(QFileInfo(path))\n            return self.ico_types['dir']\n        elif path.endswith(utils.ARCHIVE_FILES):\n            suff = ''\n            for s in utils.ARCHIVE_FILES:\n                if path.endswith(s):\n                    suff = s\n            if not suff in self.ico_types:\n                self.ico_types[suff] = QFileIconProvider().icon(QFileInfo(path))\n            return self.ico_types[suff]\n\n    @staticmethod\n    def get_external_file_icon():\n        if app_constants._REFRESH_EXTERNAL_VIEWER:\n            if os.path.exists(app_constants.GALLERY_EXT_ICO_PATH):\n                os.remove(app_constants.GALLERY_EXT_ICO_PATH)\n            info = QFileInfo(app_constants.EXTERNAL_VIEWER_PATH)\n            icon = QFileIconProvider().icon(info)\n            pixmap = icon.pixmap(QSize(32, 32))\n            pixmap.save(app_constants.GALLERY_EXT_ICO_PATH, quality=100)\n            app_constants._REFRESH_EXTERNAL_VIEWER = False\n\n        return QIcon(app_constants.GALLERY_EXT_ICO_PATH)\n\n    @staticmethod\n    def refresh_default_icon():\n\n        if os.path.exists(app_constants.GALLERY_DEF_ICO_PATH):\n            os.remove(app_constants.GALLERY_DEF_ICO_PATH)\n\n        def get_file(n):\n            gallery = gallerydb.GalleryDB.get_gallery_by_id(n)\n            if not gallery:\n                return False\n            file = \"\"\n            if gallery.path.endswith(tuple(ARCHIVE_FILES)):\n                try:\n                    zip = ArchiveFile(gallery.path)\n                except utils.app_constants.CreateArchiveFail:\n                    return False\n                for name in zip.namelist():\n                    if name.lower().endswith(tuple(IMG_FILES)):\n                        folder = os.path.join(app_constants.temp_dir,\n                            '{}{}'.format(name, n))\n                        zip.extract(name, folder)\n                        file = os.path.join(folder, name)\n                        break\n            else:\n                for p in scandir.scandir(gallery.chapters[0].path):\n                    if p.name.lower().endswith(tuple(IMG_FILES)):\n                        file = p.path\n                        break\n            return file\n\n        # TODO: fix this!  (When there are no ids below 300?  (because they go\n        # deleted))\n        for x in range(1, 300):\n            try:\n                file = get_file(x)\n                break\n            except FileNotFoundError:\n                continue\n            except app_constants.CreateArchiveFail:\n                continue\n\n        if not file:\n            return None\n        icon = QFileIconProvider().icon(QFileInfo(file))\n        pixmap = icon.pixmap(QSize(32, 32))\n        pixmap.save(app_constants.GALLERY_DEF_ICO_PATH, quality=100)\n        return True\n\n    @staticmethod\n    def get_default_file_icon():\n        s = True\n        if not os.path.isfile(app_constants.GALLERY_DEF_ICO_PATH):\n            s = FileIcon.refresh_default_icon()\n        if s:\n            return QIcon(app_constants.GALLERY_DEF_ICO_PATH)\n        else: return None\n\n#def center_parent(parent, child):\n#\t\"centers child window in parent\"\n#\tcenterparent = QPoint(\n#\t\t\tparent.x() + (parent.frameGeometry().width() -\n#\t\t\t\t\t child.frameGeometry().width())//2,\n#\t\t\t\t\tparent.y() + (parent.frameGeometry().width() -\n#\t\t\t\t\t   child.frameGeometry().width())//2)\n#\tdesktop = QApplication.desktop()\n#\tsg_rect = desktop.screenGeometry(desktop.screenNumber(parent))\n#\tchild_frame = child.frameGeometry()\n\n#\tif centerparent.x() < sg_rect.left():\n#\t\tcenterparent.setX(sg_rect.left())\n#\telif (centerparent.x() + child_frame.width()) > sg_rect.right():\n#\t\tcenterparent.setX(sg_rect.right() - child_frame.width())\n\n#\tif centerparent.y() < sg_rect.top():\n#\t\tcenterparent.setY(sg_rect.top())\n#\telif (centerparent.y() + child_frame.height()) > sg_rect.bottom():\n#\t\tcenterparent.setY(sg_rect.bottom() - child_frame.height())\n\n#\tchild.move(centerparent)\nclass Spacer(QWidget):\n    \"\"\"\n    To be used as a spacer.\n    Default mode is both. Specify mode with string: v, h or both\n    \"\"\"\n    def __init__(self, mode='both', parent=None):\n        super().__init__(parent)\n        if mode == 'h':\n            self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)\n        elif mode == 'v':\n            self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        else:\n            self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\nclass FlowLayout(QLayout):\n\n    def __init__(self, parent=None, margin=0, spacing=-1):\n        super(FlowLayout, self).__init__(parent)\n\n        if parent is not None:\n            self.setContentsMargins(margin, margin, margin, margin)\n\n        self.setSpacing(spacing)\n\n        self.itemList = []\n\n    def __del__(self):\n        item = self.takeAt(0)\n        while item:\n            item = self.takeAt(0)\n\n    def addItem(self, item):\n        self.itemList.append(item)\n\n    def count(self):\n        return len(self.itemList)\n\n    # to keep it in style with the others..\n    def rowCount(self):\n        return self.count()\n\n    def itemAt(self, index):\n        if index >= 0 and index < len(self.itemList):\n            return self.itemList[index]\n\n        return None\n\n    def takeAt(self, index):\n        if index >= 0 and index < len(self.itemList):\n            return self.itemList.pop(index)\n\n        return None\n\n    def expandingDirections(self):\n        return Qt.Orientations(Qt.Orientation(0))\n\n    def hasHeightForWidth(self):\n        return True\n\n    def heightForWidth(self, width):\n        height = self.doLayout(QRect(0, 0, width, 0), True)\n        return height\n\n    def setGeometry(self, rect):\n        super(FlowLayout, self).setGeometry(rect)\n        self.doLayout(rect, False)\n\n    def sizeHint(self):\n        return self.minimumSize()\n\n    def minimumSize(self):\n        size = QSize()\n\n        for item in self.itemList:\n            size = size.expandedTo(item.minimumSize())\n\n        margin, _, _, _ = self.getContentsMargins()\n\n        size += QSize(2 * margin, 2 * margin)\n        return size\n\n    def doLayout(self, rect, testOnly):\n        x = rect.x()\n        y = rect.y()\n        lineHeight = 0\n\n        for item in self.itemList:\n            wid = item.widget()\n            spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)\n            spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)\n            nextX = x + item.sizeHint().width() + spaceX\n            if nextX - spaceX > rect.right() and lineHeight > 0:\n                x = rect.x()\n                y = y + lineHeight + spaceY\n                nextX = x + item.sizeHint().width() + spaceX\n                lineHeight = 0\n\n            if not testOnly:\n                item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))\n\n            x = nextX\n            lineHeight = max(lineHeight, item.sizeHint().height())\n\n        return y + lineHeight - rect.y()\n\nclass LineEdit(QLineEdit):\n    \"\"\"\n    Custom Line Edit which sacrifices contextmenu for selectAll\n    \"\"\"\n    def __init__(self, parent=None):\n        super().__init__(parent)\n\n    def mousePressEvent(self, event):\n        if event.button() == Qt.RightButton:\n            self.selectAll()\n        else:\n            super().mousePressEvent(event)\n\n    def contextMenuEvent(self, QContextMenuEvent):\n        pass\n\n    def sizeHint(self):\n        s = super().sizeHint()\n        return QSize(400, s.height())\n\nclass PathLineEdit(QLineEdit):\n    \"\"\"\n    A lineedit which open a filedialog on right/left click\n    Set dir to false if you want files.\n    \"\"\"\n    def __init__(self, parent=None, dir=True, filters=utils.FILE_FILTER):\n        super().__init__(parent)\n        self.folder = dir\n        self.filters = filters\n        self.setPlaceholderText('Right/Left-click to open folder explorer.')\n        self.setToolTip('Right/Left-click to open folder explorer.')\n\n    def openExplorer(self):\n        if self.folder:\n            path = QFileDialog.getExistingDirectory(self,\n                                           'Choose folder')\n        else:\n            path = QFileDialog.getOpenFileName(self,\n                                      'Choose file', filter=self.filters)\n            path = path[0]\n        if len(path) != 0:\n            self.setText(path)\n\n    def mousePressEvent(self, event):\n        assert isinstance(event, QMouseEvent)\n        if len(self.text()) == 0:\n            if event.button() == Qt.LeftButton:\n                self.openExplorer()\n            else:\n                return super().mousePressEvent(event)\n        if event.button() == Qt.RightButton:\n            self.openExplorer()\n            \n        super().mousePressEvent(event)\n\nclass ChapterAddWidget(QWidget):\n    CHAPTERS = pyqtSignal(gallerydb.ChaptersContainer)\n    def __init__(self, gallery, parent=None):\n        super().__init__(parent)\n        self.setWindowFlags(Qt.Window)\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        self.current_chapters = gallery.chapters.count()\n        self.added_chaps = 0\n        self.gallery = gallery\n\n        layout = QFormLayout()\n        self.setLayout(layout)\n        lbl = QLabel('{} by {}'.format(gallery.title, gallery.artist))\n        layout.addRow('Gallery:', lbl)\n        layout.addRow('Current chapters:', QLabel('{}'.format(self.current_chapters)))\n\n        new_btn = QPushButton('Add directory')\n        new_btn.clicked.connect(lambda: self.add_new_chapter('f'))\n        new_btn.adjustSize()\n        new_btn_a = QPushButton('Add archive')\n        new_btn_a.clicked.connect(lambda: self.add_new_chapter('a'))\n        new_btn_a.adjustSize()\n        add_btn = QPushButton('Finish')\n        add_btn.clicked.connect(self.finish)\n        add_btn.adjustSize()\n        new_l = QHBoxLayout()\n        new_l.addWidget(add_btn, 1, alignment=Qt.AlignLeft)\n        new_l.addWidget(Spacer('h'))\n        new_l.addWidget(new_btn, alignment=Qt.AlignRight)\n        new_l.addWidget(new_btn_a, alignment=Qt.AlignRight)\n        layout.addRow(new_l)\n\n        frame = QFrame()\n        frame.setFrameShape(frame.StyledPanel)\n        layout.addRow(frame)\n\n        self.chapter_l = QVBoxLayout()\n        frame.setLayout(self.chapter_l)\n\n        self.setMaximumHeight(550)\n        self.setFixedWidth(500)\n        if parent:\n            self.move(parent.window().frameGeometry().topLeft() + parent.window().rect().center() - self.rect().center())\n        else:\n            frect = self.frameGeometry()\n            frect.moveCenter(QDesktopWidget().availableGeometry().center())\n            self.move(frect.topLeft())\n        self.setWindowTitle('Add Chapters')\n\n    def add_new_chapter(self, mode):\n        chap_layout = QHBoxLayout()\n        self.added_chaps += 1\n        curr_chap = self.current_chapters + self.added_chaps\n\n        chp_numb = QSpinBox(self)\n        chp_numb.setMinimum(curr_chap - 1)\n        chp_numb.setMaximum(curr_chap + 1)\n        chp_numb.setValue(curr_chap)\n        curr_chap_lbl = QLabel('Chapter {}'.format(curr_chap))\n        def ch_lbl(n): curr_chap_lbl.setText('Chapter {}'.format(n))\n        chp_numb.valueChanged[int].connect(ch_lbl)\n        if mode == 'f':\n            chp_path = PathLineEdit()\n            chp_path.setPlaceholderText('Right/Left-click to open folder explorer.' + ' Leave empty to not add.')\n        elif mode == 'a':\n            chp_path = PathLineEdit(dir=False)\n            chp_path.setPlaceholderText('Right/Left-click to open folder explorer.' + ' Leave empty to not add.')\n\n        chp_path.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        if mode == 'f':\n            chap_layout.addWidget(QLabel('D'))\n        elif mode == 'a':\n            chap_layout.addWidget(QLabel('A'))\n        chap_layout.addWidget(chp_path, 3)\n        chap_layout.addWidget(chp_numb, 0)\n        self.chapter_l.addWidget(curr_chap_lbl,\n                           alignment=Qt.AlignLeft)\n        self.chapter_l.addLayout(chap_layout)\n\n    def finish(self):\n        chapters = self.gallery.chapters\n        widgets = []\n        x = True\n        while x:\n            x = self.chapter_l.takeAt(0)\n            if x:\n                widgets.append(x)\n        for l in range(1, len(widgets), 1):\n            layout = widgets[l]\n            try:\n                line_edit = layout.itemAt(1).widget()\n                spin_box = layout.itemAt(2).widget()\n            except AttributeError:\n                continue\n            p = line_edit.text()\n            c = spin_box.value() - 1 # because of 0-based index\n            if os.path.exists(p):\n                chap = chapters.create_chapter(c)\n                chap.title = utils.title_parser(os.path.split(p)[1])['title']\n                chap.path = p\n                if os.path.isdir(p):\n                    chap.pages = len(list(scandir.scandir(p)))\n                elif p.endswith(utils.ARCHIVE_FILES):\n                    chap.in_archive = 1\n                    arch = utils.ArchiveFile(p)\n                    chap.pages = len(arch.dir_contents(''))\n                    arch.close()\n\n        self.CHAPTERS.emit(chapters)\n        self.close()\n\n\nclass CustomListItem(QListWidgetItem):\n    def __init__(self, item=None, parent=None, txt='', type=QListWidgetItem.Type):\n        super().__init__(txt, parent, type)\n        self.item = item\n\nclass CustomTableItem(QTableWidgetItem):\n    def __init__(self, item=None, txt='', type=QTableWidgetItem.Type):\n        super().__init__(txt, type)\n        self.item = item\n\nclass GalleryListView(QWidget):\n    SERIES = pyqtSignal(list)\n    def __init__(self, parent=None, modal=False):\n        super().__init__(parent)\n        self.setWindowFlags(Qt.Dialog)\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        if modal:\n            frame = QFrame()\n            frame.setFrameShape(frame.StyledPanel)\n            modal_layout = QHBoxLayout()\n            frame.setLayout(modal_layout)\n            layout.addWidget(frame)\n            info = QLabel('This mode let\\'s you add galleries from ' + 'different folders.')\n            f_folder = QPushButton('Add directories')\n            f_folder.clicked.connect(self.from_folder)\n            f_files = QPushButton('Add archives')\n            f_files.clicked.connect(self.from_files)\n            modal_layout.addWidget(info, 3, Qt.AlignLeft)\n            modal_layout.addWidget(f_folder, 0, Qt.AlignRight)\n            modal_layout.addWidget(f_files, 0, Qt.AlignRight)\n\n        check_layout = QHBoxLayout()\n        layout.addLayout(check_layout)\n        if modal:\n            check_layout.addWidget(QLabel('Please uncheck galleries you do' + ' not want to add. (Exisiting galleries won\\'t be added'),\n                             3)\n        else:\n            check_layout.addWidget(QLabel('Please uncheck galleries you do' + ' not want to add. (Existing galleries are hidden)'),\n                             3)\n        self.check_all = QCheckBox('Check/Uncheck All', self)\n        self.check_all.setChecked(True)\n        self.check_all.stateChanged.connect(self.all_check_state)\n\n        check_layout.addWidget(self.check_all)\n        self.view_list = QListWidget()\n        self.view_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)\n        self.view_list.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n        self.view_list.setAlternatingRowColors(True)\n        self.view_list.setEditTriggers(self.view_list.NoEditTriggers)\n        layout.addWidget(self.view_list)\n        \n        add_btn = QPushButton('Add checked')\n        add_btn.clicked.connect(self.return_gallery)\n\n        cancel_btn = QPushButton('Cancel')\n        cancel_btn.clicked.connect(self.close_window)\n        btn_layout = QHBoxLayout()\n\n        spacer = QWidget()\n        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        btn_layout.addWidget(spacer)\n        btn_layout.addWidget(add_btn)\n        btn_layout.addWidget(cancel_btn)\n        layout.addLayout(btn_layout)\n\n        self.resize(500,550)\n        frect = self.frameGeometry()\n        frect.moveCenter(QDesktopWidget().availableGeometry().center())\n        self.move(frect.topLeft())\n        self.setWindowTitle('Gallery List')\n        self.count = 0\n\n    def all_check_state(self, new_state):\n        row = 0\n        done = False\n        while not done:\n            item = self.view_list.item(row)\n            if item:\n                row += 1\n                if new_state == Qt.Unchecked:\n                    item.setCheckState(Qt.Unchecked)\n                else:\n                    item.setCheckState(Qt.Checked)\n            else:\n                done = True\n\n    def add_gallery(self, item, name):\n        \"\"\"\n        Constructs an widgetitem to hold the provided item,\n        and adds it to the view_list\n        \"\"\"\n        assert isinstance(name, str)\n        gallery_item = CustomListItem(item)\n        gallery_item.setText(name)\n        gallery_item.setFlags(gallery_item.flags() | Qt.ItemIsUserCheckable)\n        gallery_item.setCheckState(Qt.Checked)\n        self.view_list.addItem(gallery_item)\n        self.count += 1\n\n    def update_count(self):\n        self.setWindowTitle('Gallery List ({})'.format(self.count))\n\n    def return_gallery(self):\n        gallery_list = []\n        row = 0\n        done = False\n        while not done:\n            item = self.view_list.item(row)\n            if not item:\n                done = True\n            else:\n                if item.checkState() == Qt.Checked:\n                    gallery_list.append(item.item)\n                row += 1\n\n        self.SERIES.emit(gallery_list)\n        self.close()\n\n    def from_folder(self):\n        file_dialog = QFileDialog()\n        file_dialog.setFileMode(QFileDialog.DirectoryOnly)\n        file_dialog.setOption(QFileDialog.DontUseNativeDialog, True)\n        file_view = file_dialog.findChild(QListView, 'listView')\n        if file_view:\n            file_view.setSelectionMode(QAbstractItemView.MultiSelection)\n        f_tree_view = file_dialog.findChild(QTreeView)\n        if f_tree_view:\n            f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection)\n\n        if file_dialog.exec():\n            for path in file_dialog.selectedFiles():\n                self.add_gallery(path, os.path.split(path)[1])\n\n\n    def from_files(self):\n        gallery_list = QFileDialog.getOpenFileNames(self,\n                                             'Select 1 or more gallery to add',\n                                             filter='Archives ({})'.format(utils.FILE_FILTER))\n        for path in gallery_list[0]:\n            #Warning: will break when you add more filters\n            if len(path) != 0:\n                self.add_gallery(path, os.path.split(path)[1])\n\n    def close_window(self):\n        msgbox = QMessageBox()\n        msgbox.setText('Are you sure you want to cancel?')\n        msgbox.setStandardButtons(msgbox.Yes | msgbox.No)\n        msgbox.setDefaultButton(msgbox.No)\n        msgbox.setIcon(msgbox.Question)\n        if msgbox.exec() == QMessageBox.Yes:\n            self.close()\n\nclass Loading(BasePopup):\n    ON = False #to prevent multiple instances\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.progress = QProgressBar()\n        self.progress.setStyleSheet(\"color:white\")\n        self.text = QLabel()\n        self.text.setAlignment(Qt.AlignCenter)\n        self.text.setStyleSheet(\"color:white;background-color:transparent;\")\n        inner_layout_ = QVBoxLayout()\n        inner_layout_.addWidget(self.text, 0, Qt.AlignHCenter)\n        inner_layout_.addWidget(self.progress)\n        self.main_widget.setLayout(inner_layout_)\n        self.resize(300,100)\n        #frect = self.frameGeometry()\n        #frect.moveCenter(QDesktopWidget().availableGeometry().center())\n        #self.move(parent.window().frameGeometry().topLeft() +\n        #\tparent.window().rect().center() -\n        #\tself.rect().center() - QPoint(self.rect().width(),0))\n        #self.setAttribute(Qt.WA_DeleteOnClose)\n        #self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)\n\n    def mousePressEvent(self, QMouseEvent):\n        pass\n\n    def setText(self, string):\n        if string != self.text.text():\n            self.text.setText(string)\n\nclass CompleterTextEdit(QTextEdit):\n    \"\"\"\n    A textedit with autocomplete\n    \"\"\"\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self._completer = None\n        log_d('Instantiate CompleterTextEdit: OK')\n\n    def setCompleter(self, c):\n        if self._completer is not None:\n            self._completer.activated.disconnect()\n\n        self._completer = c\n\n        c.setWidget(self)\n        c.setCompletionMode(QCompleter.PopupCompletion)\n        c.setCaseSensitivity(Qt.CaseInsensitive)\n        c.activated.connect(self.insertCompletion)\n\n    def completer(self):\n        return self._completer\n\n    def insertCompletion(self, completion):\n        if self._completer.widget() is not self:\n            return\n\n        tc = self.textCursor()\n        extra = len(completion) - len(self._completer.completionPrefix())\n        tc.movePosition(QTextCursor.Left)\n        tc.movePosition(QTextCursor.EndOfWord)\n        tc.insertText(completion[-extra:])\n        self.setTextCursor(tc)\n\n    def textUnderCursor(self):\n        tc = self.textCursor()\n        tc.select(QTextCursor.WordUnderCursor)\n\n        return tc.selectedText()\n\n    def focusInEvent(self, e):\n        if self._completer is not None:\n            self._completer.setWidget(self)\n\n        super().focusInEvent(e)\n\n    def keyPressEvent(self, e):\n        if self._completer is not None and self._completer.popup().isVisible():\n            # The following keys are forwarded by the completer to the widget.\n            if e.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab):\n                e.ignore()\n                # Let the completer do default behavior.\n                return\n\n        isShortcut = e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_E\n        if self._completer is None or not isShortcut:\n            # Do not process the shortcut when we have a completer.\n            super().keyPressEvent(e)\n\n        ctrlOrShift = e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier)\n        if self._completer is None or (ctrlOrShift and len(e.text()) == 0):\n            return\n\n        eow = \"~!@#$%^&*()_+{}|:\\\"<>?,./;'[]\\\\-=\"\n        hasModifier = (e.modifiers() != Qt.NoModifier) and not ctrlOrShift\n        completionPrefix = self.textUnderCursor()\n\n        if not isShortcut and (hasModifier or len(e.text()) == 0 or len(completionPrefix) < 3 or e.text()[-1] in eow):\n            self._completer.popup().hide()\n            return\n\n        if completionPrefix != self._completer.completionPrefix():\n            self._completer.setCompletionPrefix(completionPrefix)\n            self._completer.popup().setCurrentIndex(self._completer.completionModel().index(0, 0))\n\n        cr = self.cursorRect()\n        cr.setWidth(self._completer.popup().sizeHintForColumn(0) + self._completer.popup().verticalScrollBar().sizeHint().width())\n        if self._completer:\n            self._completer.complete(cr)\n\nclass GCompleter(QCompleter):\n    def __init__(self, parent=None, title=True, artist=True, tags=True):\n        self.all_data = []\n        d = set()\n        for g in app_constants.GALLERY_DATA:\n            if title:\n                d.add(g.title)\n            if artist:\n                d.add(g.artist)\n            if tags:\n                for ns in g.tags:\n                    d.add(ns)\n                    for t in g.tags[ns]:\n                        d.add(t)\n\n        self.all_data.extend(d)\n        super().__init__(self.all_data, parent)\n        self.setCaseSensitivity(Qt.CaseInsensitive)\n\nclass ChapterListItem(QFrame):\n    move_pos = pyqtSignal(int, object)\n    def __init__(self, chapter, parent=None):\n        super().__init__(parent)\n        main_layout = QHBoxLayout(self)\n        chapter_layout = QFormLayout()\n        self.number_lbl = QLabel(str(chapter.number + 1), self)\n        self.number_lbl.adjustSize()\n        self.number_lbl.setFixedSize(self.number_lbl.size())\n        self.chapter_lbl = ElidedLabel(self)\n        self.set_chapter_title(chapter)\n        main_layout.addWidget(self.number_lbl)\n        chapter_layout.addRow(self.chapter_lbl)\n        g_title = ''\n        if chapter.gallery:\n            g_title = chapter.gallery.title\n        self.gallery_lbl = ElidedLabel(g_title, self)\n        g_lbl_font = QFont(self.gallery_lbl.font())\n        g_lbl_font.setPixelSize(g_lbl_font.pixelSize() - 2)\n        g_lbl_font.setItalic(True)\n        self.gallery_lbl.setFont(g_lbl_font)\n        chapter_layout.addRow(self.gallery_lbl)\n        self.chapter = chapter\n        main_layout.addLayout(chapter_layout)\n        buttons_layout = QVBoxLayout()\n        buttons_layout.setSpacing(0)\n        up_btn = QPushButton('▲')\n        up_btn.adjustSize()\n        up_btn.setFixedSize(up_btn.size())\n        up_btn.clicked.connect(lambda: self.move_pos.emit(0, self))\n        down_btn = QPushButton('▼')\n        down_btn.adjustSize()\n        down_btn.setFixedSize(down_btn.size())\n        down_btn.clicked.connect(lambda: self.move_pos.emit(1, self))\n        buttons_layout.addWidget(up_btn)\n        buttons_layout.addWidget(down_btn)\n        main_layout.addLayout(buttons_layout)\n\n    def set_chapter_title(self, chapter):\n        if chapter.title:\n            self.chapter_lbl.setText(chapter.title)\n        else:\n            self.chapter_lbl.setText(\"Chapter \" + str(chapter.number + 1))\n"
  },
  {
    "path": "version/misc_db.py",
    "content": "﻿#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\nimport pickle\nimport logging\n\nfrom PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QWidget,\n                             QVBoxLayout, QTabWidget, QAction, QGraphicsScene,\n                             QSizePolicy, QMenu, QAction, QApplication,\n                             QListWidget, QHBoxLayout, QPushButton, QStackedLayout,\n                             QFrame, QSizePolicy, QListView, QFormLayout, QLineEdit,\n                             QLabel, QStyledItemDelegate, QStyleOptionViewItem,\n                             QCheckBox, QButtonGroup, QPlainTextEdit)\nfrom PyQt5.QtCore import (Qt, QTimer, pyqtSignal, QRect, QSize, QEasingCurve,\n                          QSortFilterProxyModel, QIdentityProxyModel, QModelIndex,\n                          QPointF, QRectF, QObject)\nfrom PyQt5.QtGui import (QIcon, QStandardItem, QFont, QPainter, QColor, QBrush,\n                         QPixmap, QPalette)\n\nimport gallerydb\nimport app_constants\nimport utils\nimport misc\nimport gallery\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass ToolbarTabManager(QObject):\n    \"\"\n    def __init__(self, toolbar, parent=None):\n        super().__init__(parent)\n        self.parent_widget = parent\n        self.toolbar = toolbar\n        self._actions = []\n        self._last_selected = None\n        self.idx_widget = self.toolbar.addWidget(QWidget(self.toolbar))\n        self.idx_widget.setVisible(False)\n\n        self.agroup = QButtonGroup(self)\n        self.agroup.setExclusive(True)\n\n        self.library_btn = None\n        self.favorite_btn = self.addTab(\"Favorites\", delegate_paint=False, icon=app_constants.STAR_ICON)\n        self.library_btn = self.addTab(\"Library\", delegate_paint=False, icon=app_constants.GRIDL_ICON)\n        self.idx_widget = self.toolbar.addWidget(QWidget(self.toolbar))\n        self.idx_widget.setVisible(False)\n        self.toolbar.addSeparator()\n\n    def _manage_selected(self, b):\n        if self._last_selected == b:\n            return\n        if self._last_selected:\n            self._last_selected.selected = False\n            self._last_selected.view.list_view.sort_model.rowsInserted.disconnect(self.parent_widget.stat_row_info)\n            self._last_selected.view.list_view.sort_model.rowsRemoved.disconnect(self.parent_widget.stat_row_info)\n            self._last_selected.view.hide()\n        b.selected = True\n        self._last_selected = b\n        self.parent_widget.current_manga_view = b.view\n        b.view.list_view.sort_model.rowsInserted.connect(self.parent_widget.stat_row_info)\n        b.view.list_view.sort_model.rowsRemoved.connect(self.parent_widget.stat_row_info)\n        b.view.show()\n\n    def addTab(self, name, view_type=app_constants.ViewType.Default, delegate_paint=True, allow_sidebarwidget=False, icon=None):\n        if self.toolbar:\n            t = misc.ToolbarButton(self.toolbar, name)\n            if icon:\n                t.setIcon(icon)\n            else:\n                t.setIcon(app_constants.CIRCLE_ICON)\n            t.setCheckable(True)\n            self.agroup.addButton(t)\n            t.select.connect(self._manage_selected)\n            t.close_tab.connect(self.removeTab)\n            if self.library_btn:\n                t.view = gallery.MangaViews(view_type, self.parent_widget, allow_sidebarwidget)\n                t.view.hide()\n                t.close_tab.connect(lambda:self.library_btn.click())\n                if not allow_sidebarwidget:\n                    t.clicked.connect(self.parent_widget.sidebar_list.arrow_handle.click)\n            else:\n                t.view = self.parent_widget.default_manga_view\n            if delegate_paint:\n                t.view.list_view.manga_delegate._paint_level = 9000 # over nine thousand!!!\n            self._actions.append(self.toolbar.insertWidget(self.idx_widget, t))\n            return t\n\n    def removeTab(self, button_or_index):\n        if self.toolbar:\n            if isinstance(button_or_index, int):\n                self.toolbar.removeAction(self._actions.pop(button_or_index))\n            else:\n                act_to_remove = None\n                for act in self._actions:\n                    w = self.toolbar.widgetForAction(act)\n                    if w == button_or_index:\n                        self.toolbar.removeAction(act)\n                        act_to_remove = act\n                        break\n                if act_to_remove:\n                    self._actions.remove(act)\n\nclass NoTooltipModel(QIdentityProxyModel):\n\n    def __init__(self, model, parent=None):\n        super().__init__(parent)\n        self.setSourceModel(model)\n\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.ToolTipRole:\n            return None\n        if role == Qt.DecorationRole:\n            return app_constants.ARTIST_ICON\n        return self.sourceModel().data(index, role)\n\n\n\nclass UniqueInfoModel(QSortFilterProxyModel):\n    def __init__(self, gallerymodel, role, parent=None):\n        super().__init__(parent)\n        self.setSourceModel(NoTooltipModel(gallerymodel, parent))\n        self._unique = set()\n        self._unique_role = role\n        self.custom_filter = None\n        self.setDynamicSortFilter(True)\n\n    def filterAcceptsRow(self, source_row, parent_index):\n        if self.sourceModel():\n            idx = self.sourceModel().index(source_row, 0, parent_index)\n            if idx.isValid():\n                unique = idx.data(self._unique_role)\n                if unique:\n                    if not unique in self._unique:\n                        if self.custom_filter != None:\n                            if not idx.data(Qt.UserRole + 1) in self.custom_filter:\n                                return False\n                        self._unique.add(unique)\n                        return True\n        return False\n\n    def invalidate(self):\n        self._unique.clear()\n        super().invalidate()\n\nclass ListDelegate(QStyledItemDelegate):\n    def __init__(self, parent=None):\n        self.parent_widget = parent\n        super().__init__(parent)\n        self.create_new_list_txt = 'Create new list...'\n    \n    def sizeHint(self, option, index):\n        size = super().sizeHint(option, index)\n        if index.data(Qt.DisplayRole) == self.create_new_list_txt:\n            return size\n        return QSize(size.width(), size.height() * 2)\n\nclass GalleryArtistsList(QListView):\n    artist_clicked = pyqtSignal(str)\n\n    def __init__(self, gallerymodel, parent=None):\n        super().__init__(parent)\n        self.g_artists_model = UniqueInfoModel(gallerymodel, gallerymodel.ARTIST_ROLE, self)\n        self.setModel(self.g_artists_model)\n        self.setModelColumn(app_constants.ARTIST)\n        self.g_artists_model.setSortRole(gallerymodel.ARTIST_ROLE)\n        self.g_artists_model.sort(0)\n        self.doubleClicked.connect(self._artist_clicked)\n        self.ARTIST_ROLE = gallerymodel.ARTIST_ROLE\n\n    def _artist_clicked(self, idx):\n        if idx.isValid():\n            self.artist_clicked.emit(idx.data(self.ARTIST_ROLE))\n\n    def set_current_glist(self, g_list=None):\n        if g_list:\n            self.g_artists_model.custom_filter = g_list\n        else:\n            self.g_artists_model.custom_filter = None\n        self.g_artists_model.invalidate()\n\nclass TagsTreeView(QTreeWidget):\n    TAG_SEARCH = pyqtSignal(str)\n    NEW_LIST = pyqtSignal(str, gallerydb.GalleryList)\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.setSelectionBehavior(self.SelectItems)\n        self.setSelectionMode(self.ExtendedSelection)\n        self.clipboard = QApplication.clipboard()\n        self.itemDoubleClicked.connect(lambda i: self.search_tags([i]) if i.parent() else None)\n\n    def _convert_to_str(self, items):\n        tags = {}\n        d_tags = []\n        for item in items:\n            ns_item = item.parent()\n            if ns_item.text(0) == 'No namespace':\n                d_tags.append(item.text(0))\n                continue\n            if ns_item.text(0) in tags:\n                tags[ns_item.text(0)].append(item.text(0))\n            else:\n                tags[ns_item.text(0)] = [item.text(0)]\n            \n        search_txt = utils.tag_to_string(tags)\n        d_search_txt = ''\n        for x, d_t in enumerate(d_tags, 1):\n            if x == len(d_tags):\n                d_search_txt += '{}'.format(d_t)\n            else:\n                d_search_txt += '{}, '.format(d_t)\n        final_txt = search_txt + ', ' + d_search_txt if search_txt else d_search_txt\n        return final_txt\n\n    def search_tags(self, items):\n        self.TAG_SEARCH.emit(self._convert_to_str(items))\n\n    def create_list(self, items):\n        g_list = gallerydb.GalleryList(\"New List\", filter=self._convert_to_str(items))\n        g_list.add_to_db()\n        \n        self.NEW_LIST.emit(g_list.name, g_list)\n\n    def contextMenuEvent(self, event):\n        handled = False\n        selected = False\n        s_items = self.selectedItems()\n\n        if len(s_items) > 1:\n            selected = True\n\n        ns_count = 0\n        for item in s_items:\n            if not item.text(0).islower():\n                ns_count += 1\n        contains_ns = True if ns_count > 0 else False\n\n        def copy(with_ns=False):\n            if with_ns:\n                ns_item = s_items[0].parent()\n                ns = ns_item.text(0)\n                tag = s_items[0].text(0)\n                txt = \"{}:{}\".format(ns, tag)\n                self.clipboard.setText(txt)\n            else:\n                item = s_items[0]\n                self.clipboard.setText(item.text(0))\n\n        if s_items:\n            menu = QMenu(self)\n            if not selected:\n                copy_act = menu.addAction('Copy')\n                copy_act.triggered.connect(copy)\n                if not contains_ns:\n                    if s_items[0].parent().text(0) != 'No namespace':\n                        copy_ns_act = menu.addAction('Copy with namespace')\n                        copy_ns_act.triggered.connect(lambda: copy(True))\n            if not contains_ns:\n                search_act = menu.addAction('Search')\n                search_act.triggered.connect(lambda: self.search_tags(s_items))\n                create_list_filter_act = menu.addAction('Create list with selected')\n                create_list_filter_act.triggered.connect(lambda: self.create_list(s_items))\n            handled = True\n\n        if handled:\n            menu.exec_(event.globalPos())\n            event.accept()\n            del menu\n        else:\n            event.ignore()\n\n    def setup_tags(self):\n        self.clear()\n        tags = gallerydb.execute(gallerydb.TagDB.get_ns_tags, False)\n        items = []\n        for ns in tags:\n            top_item = QTreeWidgetItem(self)\n            if ns == 'default':\n                top_item.setText(0, 'No namespace')\n            else:\n                top_item.setText(0, ns)\n            for tag in tags[ns]:\n                child_item = QTreeWidgetItem(top_item)\n                child_item.setText(0, tag)\n        self.sortItems(0, Qt.AscendingOrder)\n\nclass GalleryListEdit(misc.BasePopup):\n    apply = pyqtSignal()\n    def __init__(self, parent=None):\n        super().__init__(parent, blur=False)\n        main_layout = QFormLayout(self.main_widget)\n        self.name_edit = QLineEdit(self)\n        main_layout.addRow(\"Name:\", self.name_edit)\n        self.filter_edit = QPlainTextEdit(self)\n        self.filter_edit.setPlaceholderText(\"tag1, namespace:tag2, namespace2:[tag1, tag2] ...\")\n        self.filter_edit.setFixedHeight(100)\n        what_is_filter = misc.ClickedLabel(\"What is Filter/Enforce? (Hover)\")\n        what_is_filter.setToolTip(app_constants.WHAT_IS_FILTER)\n        what_is_filter.setToolTipDuration(9999999999)\n        self.enforce = QCheckBox(self)\n        self.regex = QCheckBox(self)\n        self.case = QCheckBox(self)\n        self.strict = QCheckBox(self)\n        main_layout.addRow(what_is_filter)\n        main_layout.addRow(\"Filter\", self.filter_edit)\n        main_layout.addRow(\"Enforce\", self.enforce)\n        main_layout.addRow(\"Regex\", self.regex)\n        main_layout.addRow(\"Case sensitive\", self.case)\n        main_layout.addRow(\"Match whole terms\", self.strict)\n        main_layout.addRow(self.buttons_layout)\n        self.add_buttons(\"Close\")[0].clicked.connect(self.hide)\n        self.add_buttons(\"Apply\")[0].clicked.connect(self.accept)\n        old_v = self.width()\n        self.adjustSize()\n        self.resize(old_v, self.height())\n\n    def set_list(self, gallery_list, item):\n        self.gallery_list = gallery_list\n        self.name_edit.setText(gallery_list.name)\n        self.enforce.setChecked(gallery_list.enforce)\n        self.regex.setChecked(gallery_list.regex)\n        self.case.setChecked(gallery_list.case)\n        self.strict.setChecked(gallery_list.strict)\n        self.item = item\n        if gallery_list.filter:\n            self.filter_edit.setPlainText(gallery_list.filter)\n        else:\n            self.filter_edit.setPlainText('')\n\n    def accept(self):\n        name = self.name_edit.text()\n        self.item.setText(name)\n        self.gallery_list.name = name\n        self.gallery_list.filter = self.filter_edit.toPlainText()\n        self.gallery_list.enforce = self.enforce.isChecked()\n        self.gallery_list.regex = self.regex.isChecked()\n        self.gallery_list.case = self.case.isChecked()\n        self.gallery_list.strict = self.strict.isChecked()\n        gallerydb.execute(gallerydb.ListDB.modify_list, True, self.gallery_list)\n        self.apply.emit()\n        self.hide()\n\nclass GalleryListContextMenu(QMenu):\n    def __init__(self, item, sidebar):\n        super().__init__(sidebar)\n        self.sidebar_widget = sidebar\n        self.item = item\n        self.gallery_list = item.item\n        edit = self.addAction(\"Edit\", self.edit_list)\n        clear = self.addAction(\"Clear\", self.clear_list)\n        remove = self.addAction(\"Delete\", self.remove_list)\n\n    def edit_list(self):\n        self.sidebar_widget.gallery_list_edit.set_list(self.gallery_list, self.item)\n        self.sidebar_widget.gallery_list_edit.show()\n\n    def remove_list(self):\n        self.sidebar_widget.takeItem(self.sidebar_widget.row(self.item))\n        gallerydb.execute(gallerydb.ListDB.remove_list, True, self.gallery_list)\n        self.sidebar_widget.GALLERY_LIST_REMOVED.emit()\n\n    def clear_list(self):\n        self.gallery_list.clear()\n        self.sidebar_widget.GALLERY_LIST_CLICKED.emit(self.gallery_list)\n\nclass GalleryLists(QListWidget):\n    CREATE_LIST_TYPE = misc.CustomListItem.UserType + 1\n    GALLERY_LIST_CLICKED = pyqtSignal(gallerydb.GalleryList)\n    GALLERY_LIST_REMOVED = pyqtSignal()\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.gallery_list_edit = GalleryListEdit(parent.parent_widget)\n        self.gallery_list_edit.hide()\n        self._g_list_icon = app_constants.G_LISTS_ICON\n        self._font_selected = QFont(self.font())\n        self._font_selected.setBold(True)\n        self._font_selected.setUnderline(True)\n        self.itemDoubleClicked.connect(self._item_double_clicked)\n        self.setItemDelegate(ListDelegate(self))\n        self.itemDelegate().closeEditor.connect(self._add_new_list)\n        self.setEditTriggers(self.NoEditTriggers)\n        self.viewport().setAcceptDrops(True)\n        self._in_proccess_item = None\n        self.current_selected = None\n        self.gallery_list_edit.apply.connect(lambda: self._item_double_clicked(self.current_selected))\n        self.setup_lists()\n\n    def dragEnterEvent(self, event):\n        if event.mimeData().hasFormat(\"list/gallery\"):\n            event.acceptProposedAction()\n        else:\n            event.ignore()\n\n    def dragMoveEvent(self, event):\n        item = self.itemAt(event.pos())\n        self.clearSelection()\n        if item:\n            item.setSelected(True)\n        event.accept()\n\n    def dropEvent(self, event):\n        galleries = []\n\n        galleries = pickle.loads(event.mimeData().data(\"list/gallery\").data())\n\n        g_list_item = self.itemAt(event.pos())\n        if galleries and g_list_item:\n            txt = \"{} galleries\".format(len(galleries)) if len(galleries) > 1 else galleries[0].title\n            app_constants.NOTIF_BUBBLE.update_text(g_list_item.item.name, 'Added: {}!'.format(txt), 7)\n            log_i('Added {} to {}...'.format(txt, g_list_item.item.name))\n            g_list_item.item.add_gallery(galleries)\n\n        super().dropEvent(event)\n\n\n    def _add_new_list(self, lineedit=None, hint=None, gallery_list=None):\n        if not self._in_proccess_item.text():\n            self.takeItem(self.row(self._in_proccess_item))\n            return\n        new_item = self._in_proccess_item\n        if not gallery_list:\n            new_list = gallerydb.GalleryList(new_item.text())\n            new_list.add_to_db()\n        else:\n            new_list = gallery_list\n        new_item.item = new_list\n        new_item.setIcon(self._g_list_icon)\n        self.sortItems()\n\n    def create_new_list(self, name=None, gallery_list=None):\n        new_item = misc.CustomListItem()\n        self._in_proccess_item = new_item\n        new_item.setFlags(new_item.flags() | Qt.ItemIsEditable)\n        new_item.setIcon(QIcon(app_constants.LIST_ICON))\n        self.insertItem(0, new_item)\n        if name:\n            new_item.setText(name)\n            self._add_new_list(gallery_list=gallery_list)\n        else:\n            self.editItem(new_item)\n\n    def _item_double_clicked(self, item):\n        if item:\n            self._reset_selected()\n            if item.item.filter:\n                app_constants.NOTIF_BUBBLE.update_text(item.item.name, \"Updating list..\", 5)\n                gallerydb.execute(item.item.scan, True)\n            self.GALLERY_LIST_CLICKED.emit(item.item)\n            item.setFont(self._font_selected)\n            self.current_selected = item\n\n    def _reset_selected(self):\n        if self.current_selected:\n            self.current_selected.setFont(self.font())\n\n    def setup_lists(self):\n        for g_l in app_constants.GALLERY_LISTS:\n            if g_l.type == gallerydb.GalleryList.REGULAR:\n                self.create_new_list(g_l.name, g_l)\n\n    def contextMenuEvent(self, event):\n        item = self.itemAt(event.pos())\n        if item and item.type() != self.CREATE_LIST_TYPE:\n            menu = GalleryListContextMenu(item, self)\n            menu.exec_(event.globalPos())\n            event.accept()\n            return\n        event.ignore()\n\nclass SideBarWidget(QFrame):\n    \"\"\"\n    \"\"\"\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.setAcceptDrops(True)\n        self.parent_widget = parent\n        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)\n        self._widget_layout = QHBoxLayout(self)\n\n        # widget stuff\n        self._d_widget = QWidget(self)\n        self._d_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)\n        self._widget_layout.addWidget(self._d_widget)\n        self.main_layout = QVBoxLayout(self._d_widget)\n        self.main_layout.setSpacing(0)\n        self.main_layout.setContentsMargins(0,0,0,0)\n        self.arrow_handle = misc.ArrowHandle(self)\n        self.arrow_handle.CLICKED.connect(self.slide)\n\n        self._widget_layout.addWidget(self.arrow_handle)\n        self.setContentsMargins(0,0,-self.arrow_handle.width(),0)\n\n        self.show_all_galleries_btn = QPushButton(\"Show all galleries\")\n        self.show_all_galleries_btn.clicked.connect(lambda:parent.manga_list_view.sort_model.set_gallery_list())\n        self.show_all_galleries_btn.clicked.connect(self.show_all_galleries_btn.hide)\n        self.show_all_galleries_btn.setIcon(app_constants.CROSS_ICON_WH)\n        self.show_all_galleries_btn.hide()\n        self.main_layout.addWidget(self.show_all_galleries_btn)\n        self.main_buttons_layout = QHBoxLayout()\n        self.main_layout.addLayout(self.main_buttons_layout)\n\n        # buttons\n        bgroup = QButtonGroup(self)\n        bgroup.setExclusive(True)\n        self.lists_btn = QPushButton(\"\")\n        self.lists_btn.setIcon(app_constants.G_LISTS_ICON_WH)\n        self.lists_btn.setCheckable(True)\n        bgroup.addButton(self.lists_btn)\n        self.artist_btn = QPushButton(\"\")\n        self.artist_btn.setIcon(app_constants.ARTISTS_ICON)\n        self.artist_btn.setCheckable(True)\n        bgroup.addButton(self.artist_btn)\n        self.ns_tags_btn = QPushButton(\"\")\n        self.ns_tags_btn.setIcon(app_constants.NSTAGS_ICON)\n        self.ns_tags_btn.setCheckable(True)\n        bgroup.addButton(self.ns_tags_btn)\n        self.lists_btn.setChecked(True)\n\n\n        self.main_buttons_layout.addWidget(self.lists_btn)\n        self.main_buttons_layout.addWidget(self.artist_btn)\n        self.main_buttons_layout.addWidget(self.ns_tags_btn)\n\n        # buttons contents\n        self.stacked_layout = QStackedLayout()\n        self.main_layout.addLayout(self.stacked_layout)\n\n        # lists\n        gallery_lists_dummy = QWidget(self)\n        self.lists = GalleryLists(self)\n        create_new_list_btn = QPushButton()\n        create_new_list_btn.setIcon(QIcon(app_constants.PLUS_ICON))\n        create_new_list_btn.setIconSize(QSize(15, 15))\n        create_new_list_btn.clicked.connect(lambda: self.lists.create_new_list())\n        create_new_list_btn.adjustSize()\n        create_new_list_btn.setFixedSize(create_new_list_btn.width(), create_new_list_btn.height())\n        create_new_list_btn.setToolTip(\"Create a new list!\")\n        lists_l = QVBoxLayout(gallery_lists_dummy)\n        lists_l.setContentsMargins(0,0,0,0)\n        lists_l.setSpacing(0)\n        lists_l.addWidget(self.lists)\n        lists_l.addWidget(create_new_list_btn)\n        lists_index = self.stacked_layout.addWidget(gallery_lists_dummy)\n        self.lists.GALLERY_LIST_CLICKED.connect(parent.manga_list_view.sort_model.set_gallery_list)\n        self.lists.GALLERY_LIST_CLICKED.connect(self.show_all_galleries_btn.show)\n        self.lists.GALLERY_LIST_REMOVED.connect(self.show_all_galleries_btn.click)\n        self.lists_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(lists_index))\n        self.show_all_galleries_btn.clicked.connect(self.lists.clearSelection)\n        self.show_all_galleries_btn.clicked.connect(self.lists._reset_selected)\n\n        # artists\n        self.artists_list = GalleryArtistsList(parent.manga_list_view.gallery_model, self)\n        self.artists_list.artist_clicked.connect(lambda a: parent.search('artist:\"{}\"'.format(a)))\n        artists_list_index = self.stacked_layout.addWidget(self.artists_list)\n        self.artist_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(artists_list_index))\n        #self.lists.GALLERY_LIST_CLICKED.connect(self.artists_list.set_current_glist)\n        self.show_all_galleries_btn.clicked.connect(self.artists_list.clearSelection)\n        #self.show_all_galleries_btn.clicked.connect(lambda:self.artists_list.set_current_glist())\n\n        # ns_tags\n        self.tags_tree = TagsTreeView(self)\n        self.tags_tree.TAG_SEARCH.connect(parent.search)\n        self.tags_tree.NEW_LIST.connect(self.lists.create_new_list)\n        self.tags_tree.setHeaderHidden(True)\n        self.show_all_galleries_btn.clicked.connect(self.tags_tree.clearSelection)\n        self.tags_layout = QVBoxLayout(self.tags_tree)\n        ns_tags_index = self.stacked_layout.addWidget(self.tags_tree)\n        self.ns_tags_btn.clicked.connect(lambda:self.stacked_layout.setCurrentIndex(ns_tags_index))\n\n        self.slide_animation = misc.create_animation(self, \"maximumSize\")\n        self.slide_animation.stateChanged.connect(self._slide_hide)\n        self.slide_animation.setEasingCurve(QEasingCurve.InOutQuad)\n\n    def _slide_hide(self, state):\n        size = self.sizeHint()\n        if state == self.slide_animation.Stopped:\n            if self.arrow_handle.current_arrow == self.arrow_handle.OUT:\n                self._d_widget.hide()\n        elif self.slide_animation.Running:\n            if self.arrow_handle.current_arrow == self.arrow_handle.IN:\n                if not self.parent_widget.current_manga_view.allow_sidebarwidget:\n                    self.arrow_handle.current_arrow = self.arrow_handle.OUT\n                    self.arrow_handle.update()\n                else:\n                    self._d_widget.show()\n\n\n    def slide(self, state):\n        self.slide_animation.setEndValue(QSize(self.arrow_handle.width() * 2, self.height()))\n\n        if state:\n            self.slide_animation.setDirection(self.slide_animation.Forward)\n            self.slide_animation.start()\n        else:\n            self.slide_animation.setDirection(self.slide_animation.Backward)\n            self.slide_animation.start()\n\n    def showEvent(self, event):\n        super().showEvent(event)\n        if not app_constants.SHOW_SIDEBAR_WIDGET:\n            self.arrow_handle.click()\n\n    def _init_size(self, event=None):\n        h = self.parent_widget.height()\n        self._max_width = 250\n        self.updateGeometry()\n        self.setMaximumWidth(self._max_width)\n        self.slide_animation.setStartValue(QSize(self._max_width, h))\n\n    def resizeEvent(self, event):\n        self._init_size(event)\n        return super().resizeEvent(event)\n\n\nclass DBOverview(QWidget):\n    \"\"\"\n    \n    \"\"\"\n    about_to_close = pyqtSignal()\n    def __init__(self, parent, window=False):\n        if window:\n            super().__init__(None, Qt.Window)\n        else:\n            super().__init__(parent)\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        self.parent_widget = parent\n        main_layout = QVBoxLayout(self)\n        tabbar = QTabWidget(self)\n        main_layout.addWidget(tabbar)\n        \n        # Tags stats\n        self.tags_stats = QListWidget(self)\n        tabbar.addTab(self.tags_stats, 'Statistics')\n        tabbar.setTabEnabled(1, False)\n\n        # About AD\n        self.about_db = QWidget(self)\n        tabbar.addTab(self.about_db, 'DB Info')\n        tabbar.setTabEnabled(2, False)\n\n        self.resize(300, 600)\n        self.setWindowTitle('DB Overview')\n        self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))\n\n    def setup_stats(self):\n        pass\n\n    def setup_about_db(self):\n        pass\n\n    def closeEvent(self, event):\n        self.about_to_close.emit()\n        return super().closeEvent(event)"
  },
  {
    "path": "version/pewnet.py",
    "content": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport html\nimport logging\nimport os\nimport random\nimport re as regex\nimport requests\nimport shutil\nimport threading\nimport time\nimport uuid\nfrom datetime import datetime\nfrom queue import Queue\nfrom tempfile import (\n    NamedTemporaryFile,\n    mkstemp\n)\n\nfrom bs4 import BeautifulSoup\nfrom robobrowser import RoboBrowser\nfrom robobrowser.exceptions import RoboError\n\nfrom PyQt5.QtCore import QObject, pyqtSignal\n\nimport app_constants\nimport utils\nimport settings\nfrom utils import makedirs_if_not_exists\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass DownloaderItem(QObject):\n    \"Convenience class\"\n    IN_QUEUE, DOWNLOADING, FINISHED, CANCELLED = range(4)\n    file_rdy = pyqtSignal(object)\n    def __init__(self, url=\"\", session=None):\n        super().__init__()\n        self.session = session\n        self.download_url = url\n        self.file = \"\"\n        self.name = \"\"\n\n        self.total_size = 0\n        self.current_size = 0\n        self.current_state = self.IN_QUEUE\n\n    def cancel(self):\n        self.current_state = self.CANCELLED\n\n    def open(self, containing=False):\n        if self.file:\n            if containing:\n                p = os.path.split(self.file)[0]\n                utils.open_path(p, self.file)\n            else:\n                utils.open_path(self.file)\n\nclass Downloader(QObject):\n    \"\"\"\n    A download manager.\n    Emits signal item_finished with tuple of url and path to file when a download finishes\n    \"\"\"\n    _inc_queue = Queue()\n    _browser_session = None\n    _threads = []\n    item_finished = pyqtSignal(object)\n    active_items = []\n\n    def __init__(self):\n        super().__init__()\n        # download dir\n        self.base = os.path.abspath(app_constants.DOWNLOAD_DIRECTORY)\n        if not os.path.exists(self.base):\n            os.mkdir(self.base)\n\n    @staticmethod\n    def add_to_queue(item, session=None, dir=None):\n        \"\"\"\n        Add a DownloaderItem or url\n        An optional requests.Session object can be specified\n        A temp dir to be used can be specified\n\n        Returns a downloader item\n        \"\"\"\n        if isinstance(item, str):\n            item = DownloaderItem(item)\n\n        log_i(\"Adding item to download queue: {}\".format(item.download_url))\n        if dir:\n            Downloader._inc_queue.put({'dir':dir, 'item':item})\n        else:\n            Downloader._inc_queue.put(item)\n        Downloader._session = session\n\n        return item\n\n    @staticmethod\n    def remove_file(filename):\n        \"\"\"Remove file and ignore any error when doing it.\n\n        Args:\n            filename: filename to be removed.\n        \"\"\"\n        try:\n            os.remove(filename)\n        except:\n            pass\n\n    @staticmethod\n    def _get_total_size(response):\n        \"\"\"get total size from requests response.\n        Args:\n            response (requests.Response): Response from request.\n        \"\"\"\n        try:\n            return int(response.headers['content-length'])\n        except KeyError:\n            return 0\n\n    def _get_response(self, url):\n        \"\"\"get response from url.\n        Args:\n            url : Url of the response\n        Returns:\n            requests.Response: Response from url\n        \"\"\"\n        if self._browser_session:\n            r = self._browser_session.get(url, stream=True)\n        else:\n            r = requests.get(url, stream=True)\n        return r\n\n    def _get_item_and_temp_base(self):\n        \"\"\"get item and temporary folder if specified.\n\n        Returns:\n            tuple: (item, temp_base), where temp_base is the temporary folder.\n        \"\"\"\n        item = self._inc_queue.get()\n        temp_base = None\n        if isinstance(item, dict):\n            temp_base = item['dir']\n            item = item['item']\n        return item, temp_base\n\n    def _get_filename(self, item, temp_base=None):\n        \"\"\"get filename based on input.\n\n        Args:\n            item: Download item\n            temp_base: Optional temporary folder\n\n        Returns:\n            str: Edited filename\n        \"\"\"\n        file_name = item.name if item.name else str(uuid.uuid4())\n        invalid_chars = '\\\\/:*?\"<>|'\n        for x in invalid_chars:\n            file_name = file_name.replace(x, '')\n        file_name = os.path.join(self.base, file_name) if not temp_base else \\\n            os.path.join(temp_base, file_name)\n        return file_name\n\n    @staticmethod\n    def _download_with_simple_method(target_file, response, item, interrupt_state):\n        \"\"\"download single file with simple method.\n        Args:\n            target_file: Target filename where url will be downloaded.\n            response (requests.Response): Response from url.\n            item: Download item.\n            interrupt_state (bool): Interrupt state.\n        Returns:\n            tuple: (item, interrupt_state) where both variables\n                is the changed variables from input.\n        \"\"\"\n        chunk_size = 1024\n        with open(target_file, 'wb') as f:\n            for data in response.iter_content(chunk_size=chunk_size):\n                if item.current_state == item.CANCELLED:\n                    interrupt_state = True\n                    break\n                if data:\n                    item.current_size += len(data)\n                    f.write(data)\n                    f.flush()\n\n        return item, interrupt_state\n\n    @staticmethod\n    def _download_with_catch_error(\n            target_file, response, item, interrupt_state,\n            use_tempfile=False, catch_errors=None\n    ):\n        \"\"\"Download single file from url response and return changed item and interrupt state.\n\n        Args:\n            target_file: Target filename where url will be downloaded.\n            response (requests.Response): Response from url.\n            item: Download item.\n            interrupt_state (bool): Interrupt state.\n            use_tempfile (bool): Use tempfile when downloading or not.\n            catch_errors (tuple): List of error that will be catched when downloading.\n\n        Returns:\n            tuple: (item, interrupt_state) where both variables\n                is the changed variables from input.\n        \"\"\"\n        if catch_errors is None:\n            catch_errors = tuple()\n\n        # compatibility\n        DownloaderObject = Downloader\n\n        download_finished = False\n        while not download_finished:\n            try:\n                item, interrupt_state = DownloaderObject._download_single_file(\n                    target_file=target_file,\n                    response=response,\n                    item=item,\n                    interrupt_state=interrupt_state,\n                    use_tempfile=use_tempfile\n                )\n                download_finished = True\n            except catch_errors as err:\n                log_d('Redownloading because following error.\\n{}'.format(err))\n\n        return item, interrupt_state\n\n    @staticmethod\n    def _download_with_tempfile_windows(\n            target_file, response, item, interrupt_state\n    ):\n        \"\"\"Download file with tempfile return changed item and interrupt state.\n\n        method used on window taken and modified from http://stackoverflow.com/a/15259358\n\n        Args:\n            target_file: Target filename where url will be downloaded.\n            response (requests.Response): Response from url.\n            item: Download item.\n            interrupt_state (bool): Interrupt state.\n        Returns:\n            tuple: (item, interrupt_state) where both variables\n                is the changed variables from input.\n\n        \"\"\"\n        # compatibilty\n        DownloaderObject = Downloader\n        closed_ = False\n        deleted_ = False\n        file_, tempfile = mkstemp()\n        try:\n            item, interrupt_state = DownloaderObject._download_single_file(\n                    target_file=tempfile,\n                    response=response,\n                    item=item,\n                    interrupt_state=interrupt_state,\n                    use_tempfile=False\n                )\n\n            if item.current_state != item.CANCELLED:\n                    os.close(file_)\n                    closed_ = True\n                    shutil.copyfile(tempfile, target_file)\n                    os.remove(tempfile)\n                    deleted_ = True\n        finally:\n            if not closed_:\n                os.close(file_)\n            if not deleted_:\n                os.remove(tempfile)\n        return item, interrupt_state\n\n\n\n    @staticmethod\n    def _download_single_file(\n            target_file, response, item, interrupt_state,\n            use_tempfile=False, catch_errors=None\n    ):\n        \"\"\"Download single file from url response and return changed item and interrupt state.\n\n        this method is wrapper for these methods::\n\n        - _download_with_catch_error\n        - _download_with_simple_method\n\n        Note:\n            item's current size may not give exact size.\n            especially when there is multiple interupt and tempfile is used.\n\n        Args:\n            target_file: Target filename where url will be downloaded.\n            response (requests.Response): Response from url.\n            item: Download item.\n            interrupt_state (bool): Interrupt state.\n            use_tempfile (bool): Use tempfile when downloading or not.\n            catch_errors (tuple): List of error that will be catched when downloading.\n\n        Returns:\n            tuple: (item, interrupt_state) where both variables\n                is the changed variables from input.\n        \"\"\"\n        # compatibilty\n        DownloaderObject = Downloader\n\n        if catch_errors:\n            item, interrupt_state = DownloaderObject._download_with_catch_error(\n                target_file=target_file,\n                response=response,\n                item=item,\n                interrupt_state=interrupt_state,\n                use_tempfile=use_tempfile,\n                catch_errors=catch_errors\n\n            )\n        elif use_tempfile:\n            if app_constants.OS_NAME == 'windows':\n                item, interrupt_state = DownloaderObject._download_with_tempfile_windows(\n                        target_file=target_file,\n                        response=response,\n                        item=item,\n                        interrupt_state=interrupt_state,\n                    )\n            else: # unix\n                with NamedTemporaryFile() as tempfile:\n                    item, interrupt_state = DownloaderObject._download_single_file(\n                        target_file=tempfile.name,\n                        response=response,\n                        item=item,\n                        interrupt_state=interrupt_state,\n                        use_tempfile=False\n                    )\n                    if item.current_state != item.CANCELLED:\n                        shutil.copyfile(tempfile.name, target_file)\n        else:\n            item, interrupt_state = DownloaderObject._download_with_simple_method(\n                target_file=target_file,\n                response=response,\n                item=item,\n                interrupt_state=interrupt_state,\n            )\n\n        return item, interrupt_state\n\n    @staticmethod\n    def _rename_file(filename, filename_part, max_loop=100):\n        \"\"\"Custom rename file method.\n\n        Args:\n            filename: Target filename.\n            filename_part: Temporary filename\n            max_loop (int): Maximal loop  on error when renaming the file.\n\n        Returns:\n            str: Filename or filename_part\n        \"\"\"\n        # compatibility\n        file_name = filename\n        file_name_part = filename_part\n\n        n = 0\n        file_split = os.path.split(file_name)\n        while n < max_loop:\n            try:\n                if file_split[1]:\n                    src_file = file_split[0]\n                    target_file = \"({}){}\".format(n, file_split[1])\n                else:\n                    src_file = file_name_part\n                    target_file = \"({}){}\".format(n, file_name)\n                os.rename(src_file, target_file)\n                break\n            except:\n                n += 1\n        if n > max_loop:\n            file_name = file_name_part\n        return file_name\n\n    @staticmethod\n    def _get_total_size_prediction(known_filesize, urls_len):\n        \"\"\"get total size prediction.\n\n        Args:\n            known_filesize (list): List of known filesize.\n            urls_len (int): Number of urls_len\n\n        Returns:\n            int: Total size predictions.\n        \"\"\"\n        if not known_filesize:  # empty list\n            return 0\n        if len(known_filesize) == urls_len:\n            return int(sum(known_filesize))\n        return int(sum(known_filesize) * urls_len / len(known_filesize))\n\n    @staticmethod\n    def _get_local_filesize(path):\n        \"\"\"Get local filesize.\n\n        Args:\n            path: Path of the file.\n\n        Returns:\n            filesize of the file or zero.\n        \"\"\"\n        try:\n            return os.path.getsize(path)\n        except OSError:\n            return 0\n\n    def _download_item_with_multiple_dl_url(self, item, folder, interrupt_state):\n        \"\"\"download item with multiple download url.\n\n        This method is modified from _download_item_with_single_dl_url method.\n\n        Important changes::\n        - Create new folder for download.\n        - Method to calculate total size\n        - item.file is now folder name instead of filename\n\n        Args:\n            item: Item with single download url.\n            folder (str): Folder for downloaded file.\n            interrupt_state (bool): Interrupt state\n\n        Returns:\n            Modified item\n        \"\"\"\n        download_url = item.download_url\n        total_known_filesize = []\n        download_url_len = len(download_url)\n\n        makedirs_if_not_exists(folder)\n        for single_url in download_url:\n            # response\n            r = self._get_response(url=single_url)\n\n            # get total size\n            current_response_filesize = self._get_total_size(response=r)\n            total_known_filesize.append(current_response_filesize)\n            item.total_size = self._get_total_size_prediction(\n                known_filesize=total_known_filesize, urls_len=download_url_len)\n\n            url_basename = os.path.basename(single_url)\n            target_file = os.path.join(folder, url_basename)\n            target_filesize = self._get_local_filesize(path=target_file)\n            if target_filesize == current_response_filesize and target_filesize != 0:\n                item.current_size += current_response_filesize\n                log_d('File is already downloaded.\\n{}'.format(target_file))\n            else:\n                # downloading to temp file (file_name_part)\n                item, interrupt_state = self._download_single_file(\n                    target_file=target_file, response=r, item=item,\n                    interrupt_state=interrupt_state, use_tempfile=True,\n                    catch_errors=(requests.ConnectionError,)\n                    # NOTE:\n                    # You can't catch when in list, only tuple\n                    # This causes a TypeError:\n                    #   try:\n                    #     raise Exception\n                    #   except [Exception] as err:\n                    #     pass\n\n                    # But this doesn't:\n                    #   try:\n                    #     raise Exception\n                    #   except (Exception,) as err:\n                    #     pass\n                    \n                )\n\n        if not interrupt_state:\n            item.current_state = item.FINISHED\n            item.file = folder\n            # emit\n            item.file_rdy.emit(item)\n            self.item_finished.emit(item)\n        return item\n\n    def _download_item_with_single_dl_url(self, item, filename, interrupt_state):\n        \"\"\"download item with single download url.\n        Args:\n            item: Item with single download url.\n            filename (str): Filename for downloaded file.\n            interrupt_state (bool): Interrupt state\n        Returns:\n            Modified item\n        \"\"\"\n        # compatibility\n        file_name = filename\n        interrupt = interrupt_state\n        download_url = item.download_url\n        file_name_part = file_name + '.part'\n\n        # response\n        r = self._get_response(url=download_url)\n        # get total size\n        item.total_size = self._get_total_size(response=r)\n\n        # downloading to temp file (file_name_part)\n        item, interrupt = self._download_single_file(\n            target_file=file_name_part, response=r, item=item, interrupt_state=interrupt)\n\n        if not interrupt:\n            # post operation when no interrupt\n            try:\n                os.rename(file_name_part, file_name)\n            except OSError:\n                file_name = self._rename_file(\n                    filename=file_name, filename_part=file_name_part)\n\n            item.file = file_name\n            item.current_state = item.FINISHED\n            # emit\n            item.file_rdy.emit(item)\n            self.item_finished.emit(item)\n        else:\n            self.remove_file(filename=file_name_part)\n        return item\n\n    def _downloading(self):\n        \"The downloader. Put in a thread.\"\n        while True:\n            log_d(\"Download items in queue: {}\".format(self._inc_queue.qsize()))\n            interrupt = False\n            item, temp_base = self._get_item_and_temp_base()\n\n            log_d(\"Stating item download\")\n            item.current_state = item.DOWNLOADING\n\n            file_name = self._get_filename(item=item, temp_base=temp_base)\n\n            download_url = item.download_url\n            log_d(\"Download url:{}\".format(download_url))\n\n            self.active_items.append(item)\n\n            if isinstance(item.download_url, list):\n                # NOTE: file_name will be used as folder name when multiple url.\n                item = self._download_item_with_multiple_dl_url(\n                    item=item, folder=file_name, interrupt_state=interrupt)\n            else:\n                item = self._download_item_with_single_dl_url(\n                    item=item, filename=file_name, interrupt_state=interrupt)\n\n            log_d(\"Items in queue {}\".format(self._inc_queue.empty()))\n            log_d(\"Finished downloading: {}\".format(download_url))\n            self.active_items.remove(item)\n            self._inc_queue.task_done()\n\n    def start_manager(self, max_tasks):\n        \"Starts download manager where max simultaneous is mask_tasks\"\n        log_i(\"Starting download manager with {} jobs\".format(max_tasks))\n        for x in range(max_tasks):\n            thread = threading.Thread(\n                    target=self._downloading,\n                    name='Downloader {}'.format(x),\n                    daemon=True)\n            thread.start()\n            self._threads.append(thread)\n\nclass HenItem(DownloaderItem):\n    \"A convenience class that most methods in DLManager and its subclasses returns\"\n    thumb_rdy = pyqtSignal(object)\n    def __init__(self, session=None):\n        super().__init__(session=session)\n        self.thumb_url = \"\" # an url to gallery thumb\n        self.thumb = None\n        self.cost = \"0\"\n        self.size = \"\"\n        self.metadata = {}\n        self.gallery_name = \"\"\n        self.gallery_url = \"\"\n        self.download_type = app_constants.DOWNLOAD_TYPE_OTHER\n        self.torrents_found = 0\n        self.file_rdy.connect(self.check_type)\n\n    def fetch_thumb(self):\n        \"Fetches thumbnail. Emits thumb_rdy, when done\"\n        def thumb_fetched():\n            self.thumb = self._thumb_item.file\n            self.thumb_rdy.emit(self)\n        self._thumb_item = Downloader.add_to_queue(self.thumb_url, self.session, app_constants.temp_dir)\n        self._thumb_item.file_rdy.connect(thumb_fetched)\n\n    def check_type(self):\n        if self.download_type == app_constants.DOWNLOAD_TYPE_TORRENT:\n            utils.open_torrent(self.file)\n\n    def update_metadata(self, key, value):\n        \"\"\"\n        Recommended way of inserting metadata. Keeps the original EH API response structure\n        Remember to call commit_metadata when done!\n        \"\"\"\n        if not self.metadata:\n            self.metadata = {\n                    \"gmetadata\": [\n                        {\n                                \"gid\":1,\n                                \"title\": \"\",\n                                \"title_jpn\": \"\",\n                                \"category\": \"Manga\",\n                                \"uploader\": \"\",\n                                \"Posted\": \"\",\n                                \"filecount\": \"0\",\n                                \"filesize\": 0,\n                                \"expunged\": False,\n                                \"rating\": \"0\",\n                                \"torrentcount\": \"0\",\n                                \"tags\":[]\n                            }\n                        ]\n                }\n        try:\n            metadata = self.metadata['gmetadata'][0]\n        except KeyError:\n            return\n\n        metadata[key] = value\n\n    def commit_metadata(self):\n        \"Call this method when done updating metadata\"\n        g_id = 'sample'\n        try:\n            d_m = {self.metadata['gmetadata'][0]['gid']:g_id}\n        except KeyError:\n            return\n        self.metadata = EHen.parse_metadata(self.metadata, d_m)[g_id]\n\nclass DLManager(QObject):\n    \"Base class for site-specific download managers\"\n    _browser = RoboBrowser(history=True,\n                        user_agent=\"Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0\",\n                        parser='html.parser', allow_redirects=False)\n    def __init__(self, download_type=app_constants.DOWNLOAD_TYPE_OTHER):\n        super().__init__()\n        self._download_type = download_type\n\n    def _error(self):\n        pass\n\n    def from_gallery_url(self, url):\n        \"\"\"\n        Needs to be implemented in site-specific subclass\n        URL checking and class instantiating is done in GalleryDownloader class in io_misc.py\n        Basic procedure for this method:\n        - open url with self._browser and do the parsing\n        - create HenItem and fill out its attributes\n        - specify download type (important) from app_constants\n        - fetch optional thumbnail on HenItem\n        - set download url on HenItem (important)\n        - add h_item to download queue\n        - return h-item if everything went successfully, else return none\n\n        Metadata should imitiate the offical EH API response.\n        It is recommended to use update_metadata in HenItem when adding metadata\n        see the ChaikaManager class for a complete example\n        EH API: http://ehwiki.org/wiki/API\n        \"\"\"\n        raise NotImplementedError\n\n    def ensure_browser_on_url(self, url):\n        \"\"\"open browser on input url if not already.\n\n        Args:\n            url: Url where browser to open (or alreadery opened)\n        \"\"\"\n        open_url = False  # assume not opening the url\n        try:\n            current_url = self._browser.url\n            if current_url != url:\n                open_url = True\n        except RoboError:\n            open_url = True\n        if open_url:\n            self._browser.open(url)\n\nclass ChaikaManager(DLManager):\n    \"panda.chaika.moe manager\"\n\n    def __init__(self):\n        super().__init__()\n        self.url = \"http://panda.chaika.moe/\"\n        self.api = \"http://panda.chaika.moe/jsearch/?\"\n\n    def from_gallery_url(self, url):\n        h_item = HenItem(self._browser.session)\n        h_item.download_type = self._download_type\n        chaika_id = os.path.split(url)\n        if chaika_id[1]:\n            chaika_id = chaika_id[1]\n        else:\n            chaika_id = os.path.split(chaika_id[0])[1]\n\n        if '/gallery/' in url:\n            a_id = self._gallery_page(chaika_id, h_item)\n            if not a_id:\n                return\n            self._archive_page(a_id, h_item)\n        elif '/archive' in url:\n            g_id = self._archive_page(chaika_id, h_item)\n            if not g_id:\n                return\n            self._gallery_page(g_id, h_item)\n        else:\n            return\n        h_item.commit_metadata()\n        h_item.name = h_item.gallery_name+'.zip'\n        Downloader.add_to_queue(h_item, self._browser.session)\n        return h_item\n\n    def _gallery_page(self, g_id, h_item):\n        \"Returns url to archive and updates h_item metadata from the /gallery/g_id page\"\n        g_url = self.api + \"gallery={}\".format(g_id)\n        r = requests.get(g_url)\n        try:\n            r.raise_for_status()\n            chaika = r.json()\n\n            h_item.update_metadata('title', chaika['title'])\n            h_item.update_metadata('title_jpn', chaika['title_jpn'])\n            h_item.update_metadata('category', chaika['category'])\n            h_item.update_metadata('rating', chaika['rating'])\n            h_item.update_metadata('filecount', chaika['filecount'])\n            h_item.update_metadata('filesize', chaika['filesize'])\n            h_item.update_metadata('posted', chaika['posted'])\n\n            h_item.gallery_name = chaika['title']\n            h_item.gallery_url = self.url + \"gallery/{}\".format(g_id)\n            h_item.size = \"{0:.2f} MB\".format(chaika['filesize']/1048576)\n            tags = []\n            for t in chaika['tags']:\n                tag = t.replace('_', ' ')\n                tags.append(tag)\n            h_item.update_metadata('tags', tags)\n\n            if chaika['archives']:\n                h_item.download_url = self.url + chaika['archives'][0]['download'][1:]\n                return chaika['archives'][0]['id']\n        except AttributeError:\n            log.exception(\"HTML parsing error\")\n            raise app_constants.HTMLParsing\n        except requests.ConnectionError:\n            log.exception(\"Connection Error\")\n\n    def _archive_page(self, a_id, h_item):\n        \"Returns url to gallery and updates h_item metadata from the /archive/a_id page\"\n        a_url = self.api + \"archive={}\".format(a_id)\n        r = requests.get(a_url)\n        try:\n            r.raise_for_status()\n            chaika = r.json()\n            return chaika['gallery']\n        except requests.ConnectionError:\n            log.exception('Error parsing chaika')\n\nclass HenManager(DLManager):\n    \"G.e or Ex gallery manager\"\n\n    def __init__(self):\n        super().__init__()\n        if app_constants.HEN_DOWNLOAD_TYPE:\n            self._download_type = app_constants.DOWNLOAD_TYPE_TORRENT\n        else:\n            self._download_type = app_constants.DOWNLOAD_TYPE_ARCHIVE\n\n        self.e_url = 'https://e-hentai.org/'\n\n        exprops = settings.ExProperties()\n        cookies = exprops.cookies\n        if not cookies:\n            if exprops.username and exprops.password:\n                cookies = EHen.login(exprops.username, exprops.password)\n            else:\n                raise app_constants.NeedLogin\n\n        self._browser.session.cookies.update(cookies)\n\n\n    def _archive_url_d(self, gid, token, key):\n        \"Returns the archiver download url\"\n        base = self.e_url + 'archiver.php?'\n        d_url = base + 'gid=' + str(gid) + '&token=' + token + '&or=' + key\n        return d_url\n\n    def _torrent_url_d(self, gid, token):\n        \"Returns the torrent download url and filename\"\n        try:\n            base = self.e_url + 'gallerytorrents.php?'\n            torrent_page = base + 'gid=' + str(gid) + '&t=' + token\n            self._browser.open(torrent_page)\n            torrents = self._browser.find_all('table')\n            if not torrents:\n                return\n            torrent = None # [seeds, url, name]\n            for t in torrents:\n                parts = t.find_all('tr')\n                # url & name\n                url = parts[2].td.a.get('href')\n                name = parts[2].td.a.text + '.torrent'\n\n                # seeds peers etc... NOT uploader\n                meta = [x.text for x in parts[0].find_all('td')]\n                seed_txt = meta[3]\n                # extract number\n                seeds = int(seed_txt.split(' ')[1])\n\n                if not torrent:\n                    torrent = [seeds, url, name]\n                else:\n                    if seeds > torrent[0]:\n                        torrent = [seeds, url, name]\n\n            _, url, name = torrent # just get download url\n\n            # TODO: make user choose?\n            return url, name\n        except AttributeError:\n            raise app_constants.HTMLParsing\n\n    @staticmethod\n    def gtoEh(g_url):\n        \"convert g.e-h to e-h\"\n        if 'g.e-hentai' in g_url:\n            g_url = g_url.replace('g.e-hentai', 'e-hentai')\n            if not 'https' in g_url and 'http' in g_url:\n                g_url = g_url.replace('http', 'https')\n        return g_url\n\n    def from_gallery_url(self, g_url):\n        \"\"\"\n        Finds gallery download url and puts it in download queue\n        \"\"\"\n        if not g_url:\n            return False\n        if 'exhentai' in g_url:\n            hen = ExHen(settings.ExProperties().cookies)\n            if not hen.check_login(hen.cookies) == 2:\n                raise app_constants.NeedLogin\n        else:\n            hen = EHen()\n            if not hen.check_login(hen.cookies):\n                raise app_constants.NeedLogin\n        log_d(\"Using {}\".format(hen.__repr__()))\n        api_metadata, gallery_gid_dict = hen.add_to_queue(g_url, True, False)\n        gallery = api_metadata['gmetadata'][0]\n        log_d(\"EH API:\\n\\t\".format(gallery))\n\n        h_item = HenItem(self._browser.session)\n        h_item.download_type = self._download_type\n        h_item.gallery_url = g_url\n        h_item.metadata = EHen.parse_metadata(api_metadata, gallery_gid_dict)\n        try:\n            h_item.metadata = h_item.metadata[g_url]\n        except KeyError:\n            raise app_constants.WrongURL\n        h_item.thumb_url = gallery['thumb']\n        h_item.gallery_name = gallery['title']\n        h_item.size = \"{0:.2f} MB\".format(gallery['filesize']/1048576)\n\n        if self._download_type == app_constants.DOWNLOAD_TYPE_ARCHIVE:\n            try:\n                d_url = self._archive_url_d(gallery['gid'], gallery['token'], gallery['archiver_key'])\n\n                # ex/g.e\n                log_d(\"Opening {}\".format(d_url))\n                self._browser.open(d_url)\n                # check for availability\n                log_d(self._browser.parsed)\n                if 'gallery is currently unavailable' in '{}'.format(self._browser.parsed):\n                    raise app_constants.GNotAvailable\n\n                download_btn = self._browser.get_form()\n                if download_btn:\n                    log_d(\"Parsing download button!\")\n                    f_div = self._browser.find('div', id='db')\n                    divs = f_div.find_all('div')\n                    h_item.cost = divs[0].find('strong').text\n                    h_item.cost = divs[0].find('strong').text\n                    h_item.size = divs[1].find('strong').text\n                    self._browser.submit_form(download_btn)\n                    log_d(\"Submitted download button!\")\n\n                if self._browser.response.status_code == 302:\n                    self._browser.open(self._browser.response.headers['location'], \"post\")\n\n                # get dl link\n                log_d(\"Getting download URL!\")\n                continue_p = self._browser.find(\"p\", id=\"continue\")\n                if continue_p:\n                    dl = continue_p.a.get('href')\n                else:\n                    dl_a = self._browser.find('a')\n                    dl = dl_a.get('href')\n                if 'forums.e-hentai.org' in dl:\n                    raise app_constants.NeedLogin\n                self._browser.open(dl)\n                succes_test = self._browser.find('p')\n                if succes_test and 'successfully' in succes_test.text:\n                    gallery_dl = self._browser.find('a').get('href')\n                    gallery_dl = self._browser.url.split('/archive')[0] + gallery_dl\n                    f_name = succes_test.find('strong').text\n                    h_item.download_url = gallery_dl\n                    h_item.fetch_thumb()\n                    h_item.name = f_name\n                    Downloader.add_to_queue(h_item, self._browser.session)\n                    return h_item\n            except AttributeError:\n                log.exception(\"HTML parsing error\")\n                raise app_constants.HTMLParsing\n\n        elif self._download_type == app_constants.DOWNLOAD_TYPE_TORRENT:\n            h_item.torrents_found = int(gallery['torrentcount'])\n            h_item.fetch_thumb()\n            if  h_item.torrents_found > 0:\n                g_id_token = EHen.parse_url(g_url)\n                if g_id_token:\n                    url_and_file = self._torrent_url_d(g_id_token[0], g_id_token[1])\n                    if url_and_file:\n                        h_item.download_url = url_and_file[0]\n                        h_item.name = url_and_file[1]\n                        Downloader.add_to_queue(h_item, self._browser.session)\n                        return h_item\n            else:\n                return h_item\n        return False\n\nclass ExHenManager(HenManager):\n    \"ExHentai Manager\"\n    def __init__(self):\n        super().__init__()\n        self.e_url = \"https://exhentai.org/\"\n\n\nclass CommenHen:\n    \"Contains common methods\"\n    LOCK = threading.Lock()\n    TIME_RAND = app_constants.GLOBAL_EHEN_TIME\n    QUEUE = []\n    COOKIES = {}\n    LAST_USED = time.time()\n    HEADERS = {'user-agent':\"Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0\"}\n    _QUEUE_LIMIT = 25\n    _browser = RoboBrowser(user_agent=HEADERS['user-agent'], parser='html.parser')\n\n    def begin_lock(self):\n        log_d('locked')\n        self.LOCK.acquire()\n        t1 = time.time()\n        while int(time.time() - self.LAST_USED) < self.TIME_RAND:\n            t = random.randint(3, self.TIME_RAND)\n            time.sleep(t)\n        t2 = time.time() - t1\n        log_d(\"Slept for {}\".format(t2))\n\n    def end_lock(self):\n        log_d('unlocked')\n        self.LAST_USED = time.time()\n        self.LOCK.release()\n\n    def add_to_queue(self, url='', proc=False, parse=True):\n        \"\"\"Add url the the queue, when the queue has reached _QUEUE_LIMIT entries will auto process\n        :proc -> proccess queue\n        :parse -> return parsed metadata\n        \"\"\"\n        if url:\n            self.QUEUE.append(url)\n            log_i(\"Status on queue: {}/{}\".format(len(self.QUEUE), self._QUEUE_LIMIT))\n        try:\n            if proc:\n                if parse:\n                    return self.parse_metadata(*self.process_queue())\n                return self.process_queue()\n            if len(self.QUEUE) >= self._QUEUE_LIMIT:\n                if parse:\n                    return self.parse_metadata(*self.process_queue())\n                return self.process_queue()\n            else:\n                return 1\n        except TypeError:\n            return None\n\n    def process_queue(self):\n        \"\"\"\n        Process the queue if entries exists, deletes entries.\n        Note: Will only process _QUEUE_LIMIT entries (first come first out) while\n            additional entries will get deleted.\n        \"\"\"\n        log_i(\"Processing queue...\")\n        if len(self.QUEUE) < 1:\n            return None\n\n        try:\n            if len(self.QUEUE) >= self._QUEUE_LIMIT:\n                api_data, galleryid_dict = self.get_metadata(self.QUEUE[:self._QUEUE_LIMIT])\n            else:\n                api_data, galleryid_dict = self.get_metadata(self.QUEUE)\n        except TypeError:\n            return None\n        finally:\n            log_i(\"Flushing queue...\")\n            self.QUEUE.clear()\n        return api_data, galleryid_dict\n\n    @classmethod\n    def login(cls, user, password, relogin=False):\n        pass\n\n    @classmethod\n    def check_login(cls, cookies):\n        pass\n\n    def check_cookie(self, cookie):\n        cookies = self.COOKIES.keys()\n        present = []\n        for c in cookie:\n            if c in cookies:\n                present.append(True)\n            else:\n                present.append(False)\n        if not all(present):\n            log_i(\"Updating cookies...\")\n            try:\n                self.COOKIES.update(cookie)\n            except requests.cookies.CookieConflictError:\n                pass\n\n    def handle_error(self, response):\n        pass\n\n    @classmethod\n    def parse_metadata(cls, metadata_json, dict_metadata):\n        \"\"\"\n        :metadata_json <- raw data provided by site\n        :dict_metadata <- a dict with gallery id's as keys and url as value\n\n        returns a dict with url as key and gallery metadata as value\n        \"\"\"\n        pass\n\n    def get_metadata(self, list_of_urls, cookies=None):\n        \"\"\"\n        Fetches the metadata from the provided list of urls\n        returns raw api data and a dict with gallery id as key and url as value\n        \"\"\"\n        pass\n\n    @classmethod\n    def apply_metadata(cls, gallery, data, append=True):\n        \"\"\"\n        Applies fetched metadata to gallery\n        \"\"\"\n        pass\n\n    def search(self, search_string, **kwargs):\n        \"\"\"\n        Searches for the provided string or list of hashes,\n        returns a dict with search_string:[list of title & url tuples] of hits found or emtpy dict if no hits are found.\n        \"\"\"\n        pass\n\nclass NHen(CommenHen):\n    \"Fetches galleries from nhen\"\n    LOGIN_URL = \"http://nhentai.net/login/\"\n\n    @classmethod\n    def login(cls, user, password, relogin=False):\n        exprops = settings.ExProperties(settings.ExProperties.NHENTAI)\n        if not relogin:\n            if cls.COOKIES:\n                if cls.check_login(cls.COOKIES):\n                    return cls.COOKIES\n            elif exprops.cookies:\n                if cls.check_login(exprops.cookies):\n                    cls.COOKIES.update(exprops.cookies)\n                    return cls.COOKIES\n\n        cls._browser.open(cls.LOGIN_URL)\n        login_form = cls._browser.get_form()\n        if login_form:\n            login_form['username'].value = user\n            login_form['password'].value = password\n            cls._browser.submit_form(login_form)\n\n        n_c = cls._browser.session.cookies.get_dict()\n        if not cls.check_login(n_c):\n            log_w(\"NH login failed\")\n            raise app_constants.WrongLogin\n\n        log_i(\"NH login succes\")\n        exprops.cookies = n_c\n        exprops.username = user\n        exprops.password = password\n        exprops.save()\n        cls.COOKIES.update(n_c)\n        return n_c\n\n    @classmethod\n    def check_login(cls, cookies):\n        if \"sessionid\" in cookies:\n            return True\n\n    @classmethod\n    def apply_metadata(cls, gallery, data, append = True):\n        return super().apply_metadata(gallery, data, append)\n\n    def search(self, search_string, cookies = None, **kwargs):\n        pass\n\n\nclass EHen(CommenHen):\n    \"Fetches galleries from ehen\"\n    def __init__(self, cookies = None):\n        self.cookies = cookies if cookies else settings.ExProperties().cookies\n        self.e_url = \"https://e-hentai.org/api.php\"\n        self.e_url_o = \"https://e-hentai.org/\"\n\n\n    @classmethod\n    def apply_metadata(cls, g, data, append = True):\n        \"Applies metadata to gallery, returns gallery\"\n        if app_constants.USE_JPN_TITLE:\n            try:\n                title = data['title']['jpn']\n            except KeyError:\n                title = data['title']['def']\n        else:\n            title = data['title']['def']\n\n        if 'Language' in data['tags']:\n            try:\n                lang = [x for x in data['tags']['Language'] if not x == 'translated'][0].capitalize()\n            except IndexError:\n                lang = \"\"\n        else:\n            lang = \"\"\n\n        title_artist_dict = utils.title_parser(title)\n        if not append:\n            g.title = title_artist_dict['title']\n            if title_artist_dict['artist']:\n                g.artist = title_artist_dict['artist']\n            g.language = title_artist_dict['language'].capitalize()\n            if 'Artist' in data['tags']:\n                g.artist = data['tags']['Artist'][0].capitalize()\n            if lang:\n                g.language = lang\n            g.type = data['type']\n            g.pub_date = data['pub_date']\n            g.tags = data['tags']\n            if 'url' in data:\n                g.link = data['url']\n            else:\n                g.link = g.temp_url\n        else:\n            if not g.title:\n                g.title = title_artist_dict['title']\n            if not g.artist:\n                g.artist = title_artist_dict['artist']\n                if 'Artist' in data['tags']:\n                    g.artist = data['tags']['Artist'][0].capitalize()\n            if not g.language:\n                g.language = title_artist_dict['language'].capitalize()\n                if lang:\n                    g.language = lang\n            if not g.type or g.type == 'Other':\n                g.type = data['type']\n            if not g.pub_date:\n                g.pub_date = data['pub_date']\n            if not g.tags:\n                g.tags = data['tags']\n            else:\n                for ns in data['tags']:\n                    if ns in g.tags:\n                        for tag in data['tags'][ns]:\n                            if not tag in g.tags[ns]:\n                                g.tags[ns].append(tag)\n                    else:\n                        g.tags[ns] = data['tags'][ns]\n            if 'url' in data:\n                if not g.link:\n                    g.link = data['url']\n            else:\n                if not g.link:\n                    g.link = g.temp_url\n        return g\n\n    @classmethod\n    def check_login(cls, cookies):\n        \"\"\"\n        Checks if user is logged in\n        \"\"\"\n        if cookies.get('ipb_member_id') and cookies.get('ipb_pass_hash'):\n            # check if there is access to ex\n            ex = settings.ExProperties()\n            if ex.custom: # this is to avoid spamming ex with requests\n                return ex.custom.get('login')\n            else:\n                custom = {}\n                custom['login'] = 0\n\n                s = requests.Session()\n                s.cookies.update(cookies)\n                s.headers.update(cls.HEADERS)\n                try:\n                    r = cls.handle_error(cls, s.get('https://exhentai.org/'), wait=False)\n                except requests.ConnectionError:\n                    log.exception(\"connection error\")\n                    return 0\n                if r:\n                    custom['login'] = 2 # access to ex\n                if r is None:\n                    custom['login'] = 1 # we get sadpanda\n\n                ex.custom = custom\n                ex.save()\n                return custom['login']\n        return 0 # we've been banned, wrong credentials or haven't signed in\n\n    def handle_error(self, response, wait=True):\n        content_type = response.headers['content-type']\n        text = response.text\n        if 'image/gif' in content_type:\n            app_constants.NOTIF_BAR.add_text('Provided exhentai credentials are incorrect!')\n            log_e('Provided exhentai credentials are incorrect!')\n            if wait:\n                time.sleep(5)\n            return None\n        elif 'text/html' and 'Your IP address has been' in text:\n            app_constants.NOTIF_BAR.add_text(\"Your IP address has been temporarily banned from g.e-/exhentai\")\n            log_e('Your IP address has been temp banned from g.e- and ex-hentai')\n            if wait:\n                time.sleep(5)\n            return False\n        elif 'text/html' in content_type and 'You are opening' in text:\n            time.sleep(random.randint(10,50))\n        return True\n\n    @classmethod\n    def parse_url(cls, url):\n        \"Parses url into a list of gallery id and token\"\n        gallery_id_token = regex.search('(?<=g/)([0-9]+)/([a-zA-Z0-9]+)', url)\n        if not gallery_id_token:\n            log_e(\"Error extracting g_id and g_token from url: {}\".format(url))\n            return None\n        gallery_id_token = gallery_id_token.group()\n        gallery_id, gallery_token = gallery_id_token.split('/')\n        parsed_url = [int(gallery_id), gallery_token]\n        return parsed_url\n\n    def get_metadata(self, list_of_urls, cookies=None):\n        \"\"\"\n        Fetches the metadata from the provided list of urls\n        through the official API.\n        returns raw api data and a dict with gallery id as key and url as value\n        \"\"\"\n        assert isinstance(list_of_urls, list)\n        if len(list_of_urls) > 25:\n            log_e('More than 25 urls are provided. Aborting.')\n            return None\n\n        payload = {\"method\": \"gdata\",\n             \"gidlist\": [],\n             \"namespace\": 1\n             }\n        dict_metadata = {}\n        for url in list_of_urls:\n            parsed_url = EHen.parse_url(url.strip())\n            if parsed_url:\n                dict_metadata[parsed_url[0]] = url # gallery id\n                payload['gidlist'].append(parsed_url)\n\n        if payload['gidlist']:\n            self.begin_lock()\n            try:\n                if cookies:\n                    self.check_cookie(cookies)\n                    r = requests.post(self.e_url, json=payload, timeout=30, headers=self.HEADERS, cookies=self.COOKIES)\n                else:\n                    r = requests.post(self.e_url, json=payload, timeout=30, headers=self.HEADERS)\n            except requests.ConnectionError as err:\n                self.end_lock()\n                log_e(\"Could not fetch metadata: {}\".format(err))\n                raise app_constants.MetadataFetchFail(\"connection error\")\n            self.end_lock()\n            if not self.handle_error(r):\n                return 'error'\n        else: return None\n        try:\n            r.raise_for_status()\n        except:\n            log.exception('Could not fetch metadata: status error')\n            return None\n        return r.json(), dict_metadata\n\n    @classmethod\n    def parse_metadata(cls, metadata_json, dict_metadata):\n        \"\"\"\n        :metadata_json <- raw data provided by E-H API\n        :dict_metadata <- a dict with gallery id's as keys and url as value\n\n        returns a dict with url as key and gallery metadata as value\n        \"\"\"\n        def invalid_token_check(g_dict):\n            if 'error' in g_dict:\n                return False\n            else: return True\n\n        parsed_metadata = {}\n        for gallery in metadata_json['gmetadata']:\n            url = dict_metadata[gallery['gid']]\n            if invalid_token_check(gallery):\n                new_gallery = {}\n                def fix_titles(text):\n                    t = html.unescape(text)\n                    t = \" \".join(t.split())\n                    return t\n                try:\n                    gallery['title_jpn'] = fix_titles(gallery['title_jpn'])\n                    gallery['title'] = fix_titles(gallery['title'])\n                    new_gallery['title'] = {'def':gallery['title'], 'jpn':gallery['title_jpn']}\n                except KeyError:\n                    gallery['title'] = fix_titles(gallery['title'])\n                    new_gallery['title'] = {'def':gallery['title']}\n\n                new_gallery['type'] = gallery['category']\n                new_gallery['pub_date'] = datetime.fromtimestamp(int(gallery['posted']))\n                tags = {'default':[]}\n                for t in gallery['tags']:\n                    if ':' in t:\n                        ns_tag = t.split(':')\n                        namespace = ns_tag[0].capitalize()\n                        tag = ns_tag[1].lower().replace('_', ' ')\n                        if not namespace in tags:\n                            tags[namespace] = []\n                        tags[namespace].append(tag)\n                    else:\n                        tags['default'].append(t.lower().replace('_', ' '))\n                new_gallery['tags'] = tags\n                parsed_metadata[url] = new_gallery\n            else:\n                log_e(\"Error in received response with URL: {}\".format(url))\n\n        return parsed_metadata\n\n    @classmethod\n    def login(cls, user, password, relogin=False):\n        \"\"\"\n        Logs into g.e-h\n        \"\"\"\n        log_i(\"Attempting EH Login\")\n        eh_c = {}\n        exprops = settings.ExProperties()\n        if not relogin:\n            if cls.COOKIES:\n                if cls.check_login(cls.COOKIES):\n                    return cls.COOKIES\n            elif exprops.cookies:\n                if cls.check_login(exprops.cookies):\n                    cls.COOKIES.update(exprops.cookies)\n                    return cls.COOKIES\n        p = {\n            'ipb_member_id':user,\n            'ipb_pass_hash':password\n            }\n\n        s = requests.Session()\n        s.headers.update(cls.HEADERS)\n        s.cookies.update(p)\n        r =  s.get('https://e-hentai.org/')\n\n        if not cls.check_login(s.cookies):\n            log_w(\"EH login failed\")\n            raise app_constants.WrongLogin\n\n        log_i(\"EH login succes\")\n        exprops.cookies = s.cookies\n        exprops.username = user\n        exprops.password = password\n        exprops.save()\n        cls.COOKIES.update(s.cookies)\n\n        return s.cookies\n\n    def search(self, search_string, **kwargs):\n        \"\"\"\n        Searches ehentai for the provided string or list of hashes,\n        returns a dict with search_string:[list of title & url tuples] of hits found or emtpy dict if no hits are found.\n        \"\"\"\n        assert isinstance(search_string, (str, list))\n        if isinstance(search_string, str):\n            search_string = [search_string]\n\n        cookies = kwargs.pop('cookies', {})\n\n        def no_hits_found_check(soup):\n            \"return true if hits are found\"\n            if not soup:\n                log_e(\"There is no soup!\")\n            f_div = soup.body.find_all('div')\n            for d in f_div:\n                if 'No hits found' in d.text:\n                    return False\n            return True\n\n        def do_filesearch(filepath):\n            file_search_delay = 5\n            if \"exhentai\" in self.e_url_o:\n                f_url = \"https://exhentai.org/upload/image_lookup.php/\"\n            else:\n                f_url = \"https://upload.e-hentai.org/image_lookup.php/\"\n            if cookies:\n                self.check_cookie(cookies)\n                self._browser.session.cookies.update(self.COOKIES)\n            log_d(\"searching with color img: {}\".format(filepath))\n            files = {'sfile': open(filepath,'rb')}\n            values = {'fs_similar': '1'}\n            if app_constants.INCLUDE_EH_EXPUNGED:\n                values['fs_exp'] = '1'\n            try:\n                r = self._browser.session.post(f_url, files=files, data=values)\n            except requests.ConnectionError:\n                time.sleep(file_search_delay+3)\n                r = self._browser.session.post(f_url, files=files, data=values)\n                \n            s = BeautifulSoup(r.text, \"html.parser\")\n            if \"Please wait a bit longer between each file search.\" in \"{}\".format(s):\n                log_e(\"Retrying filesearch due to interval response with delay: {}\".format(file_search_delay))\n                time.sleep(file_search_delay)\n                s = do_filesearch(filepath)\n            return s\n\n\n        found_galleries = {}\n        log_i('Initiating hash search on ehentai')\n        log_d(\"search strings: \".format(search_string))\n        for h in search_string:\n            log_d('Hash search: {}'.format(h))\n            self.begin_lock()\n            try:\n                if 'color' in kwargs:\n                    soup = do_filesearch(h)\n                else:\n                    hash_url = self.e_url_o + '?f_shash='\n                    hash_search = hash_url + h\n                    if app_constants.INCLUDE_EH_EXPUNGED:\n                        hash_search + '&fs_exp=1'\n                    if cookies:\n                        self.check_cookie(cookies)\n                        r = requests.get(hash_search, timeout=30, headers=self.HEADERS, cookies=self.COOKIES)\n                    else:\n                        r = requests.get(hash_search, timeout=30, headers=self.HEADERS)\n                    log_d(\"searching with greyscale img: {}\".format(hash_search))\n                    if not self.handle_error(r):\n                        return 'error'\n                    soup = BeautifulSoup(r.text, \"html.parser\")\n            except requests.ConnectionError as err:\n                self.end_lock()\n                log.exception(\"Could not search for gallery: {}\".format(err))\n                raise app_constants.MetadataFetchFail(\"connection error\")\n            self.end_lock()\n\n            if not no_hits_found_check(soup):\n                log_e('No hits found with hash/image: {}'.format(h))\n                continue\n            log_i('Parsing html')\n            try:\n                if soup.body:\n                    found_galleries[h] = []\n                    # list view or grid view\n                    type = soup.find(attrs={'class':'itg'}).name\n                    if type == 'div':\n                        visible_galleries = soup.find_all('div', attrs={'class':'id1'})\n                    elif type == 'table':\n                        visible_galleries = soup.find_all('div', attrs={'class':'it5'})\n\n                    log_i('Found {} visible galleries'.format(len(visible_galleries)))\n                    for gallery in visible_galleries:\n                        title = gallery.text\n                        g_url = gallery.a.attrs['href']\n                        found_galleries[h].append((title,g_url))\n            except AttributeError:\n                log.exception('Unparseable html')\n                log_d(\"\\n{}\\n\".format(soup.prettify()))\n                continue\n\n        if found_galleries:\n            log_i('Found {} out of {} galleries'.format(len(found_galleries), len(search_string)))\n            return found_galleries\n        else:\n            log_w('Could not find any galleries')\n            return {}\n\nclass ExHen(EHen):\n    \"Fetches gallery metadata from exhen\"\n    def __init__(self, cookies=None):\n        super().__init__(cookies)\n        self.e_url = \"https://exhentai.org/api.php\"\n        self.e_url_o = \"https://exhentai.org/\"\n\n    def get_metadata(self, list_of_urls):\n        return super().get_metadata(list_of_urls, self.cookies)\n\n    def search(self, hash_string, **kwargs):\n        return super().search(hash_string, cookies=self.cookies, **kwargs)\n\nclass ChaikaHen(CommenHen):\n    \"Fetches gallery metadata from panda.chaika.moe\"\n    g_url = \"http://panda.chaika.moe/gallery/\"\n    g_api_url = \"http://panda.chaika.moe/jsearch?gallery=\"\n    a_api_url = \"http://panda.chaika.moe/jsearch?archive=\"\n    def __init__(self):\n        self.url = \"http://panda.chaika.moe/jsearch?sha1=\"\n        self._QUEUE_LIMIT = 1\n\n    def search(self, search_string, **kwargs):\n        \"\"\"\n        search_string should be a list of hashes\n        will actually just put urls together\n        return search_string:[list of title & url tuples]\n        \"\"\"\n        if not isinstance(search_string, (list,tuple)):\n            search_string = [search_string]\n        x = {}\n        for h in search_string:\n            x[h] = [(\"\", self.url+h)]\n        return x\n\n    def get_metadata(self, list_of_urls):\n        \"\"\"\n        Fetches the metadata from the provided list of urls\n        through the official API.\n        returns raw api data and a dict with gallery id as key and url as value\n        \"\"\"\n        data = []\n        g_id_data = {}\n        g_id = 1\n        for url in list_of_urls:\n            hash_search = True\n            chaika_g_id = None\n            old_url = url\n            re_string = \"^(http\\:\\/\\/|https\\:\\/\\/)?(www\\.)?([^\\.]?)(panda\\.chaika\\.moe\\/(archive|gallery)\\/[0-9]+)\" # to validate chaika urls\n            if regex.match(re_string, url):\n                g_or_a_id = regex.search(\"([0-9]+)\", url).group()\n                if 'gallery' in url:\n                    url = self.g_api_url+g_or_a_id\n                    chaika_g_id = g_or_a_id\n                else:\n                    url = self.a_api_url+g_or_a_id\n                hash_search = False\n            try:\n                try:\n                    r = requests.get(url)\n                except requests.ConnectionError as err:\n                    log_e(\"Could not fetch metadata: {}\".format(err))\n                    raise app_constants.MetadataFetchFail(\"connection error\")\n                r.raise_for_status()\n                if not r.json():\n                    return None\n                if hash_search:\n                    g_data = r.json()[0] # TODO: multiple archives can be returned! Please fix!\n                else:\n                    g_data = r.json()\n                if chaika_g_id:\n                    g_data['gallery'] = chaika_g_id\n                g_data['gid'] = g_id\n                data.append(g_data)\n                if hash_search:\n                    g_id_data[g_id] = url\n                else:\n                    g_id_data[g_id] = old_url\n                g_id += 1\n            except requests.RequestException:\n                log_e(\"Could not fetch metadata: status error\")\n                return None\n        return data, g_id_data\n\n    @classmethod\n    def parse_metadata(cls, data, dict_metadata):\n        \"\"\"\n        :data <- raw data provided by site\n        :dict_metadata <- a dict with gallery id's as keys and url as value\n\n        returns a dict with url as key and gallery metadata as value\n        \"\"\"\n        eh_api_data = {\n                \"gmetadata\":[]\n            }\n        g_urls = {}\n        for d in data:\n            eh_api_data['gmetadata'].append(d)\n            # to get correct gallery urls\n            g_urls[dict_metadata[d['gid']]] = cls.g_url + str(d['gallery']) + '/'\n        p_metadata =  EHen.parse_metadata(eh_api_data, dict_metadata)\n        # to get correct gallery urls instead of .....jsearch?sha1=----long-hash----\n        for url in g_urls:\n            p_metadata[url]['url'] = g_urls[url]\n        return p_metadata\n\n    @classmethod\n    def apply_metadata(cls, g, data, append = True):\n        \"Applies metadata to gallery, returns gallery\"\n        return EHen.apply_metadata(g, data, append)\n        \n\n\ndef hen_list_init():\n    h_list = []\n    for h in app_constants.HEN_LIST:\n        if h == \"ehen\":\n            h_list.append(EHen)\n        elif h == \"exhen\":\n            h_list.append(ExHen)\n        elif h == \"chaikahen\":\n            h_list.append(ChaikaHen)\n    return h_list\n"
  },
  {
    "path": "version/settings.py",
    "content": "﻿#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\nimport json, configparser, os, logging, pickle\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nif os.name == 'posix':\n    settings_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'settings.ini')\n    phappypanda_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '.happypanda')\nelse:\n    settings_path = 'settings.ini'\n    phappypanda_path = '.happypanda'\n\nif not os.path.isfile(settings_path):\n    open(settings_path, 'x')\n\nclass Config(configparser.ConfigParser):\n    def __init__(self):\n        super().__init__()\n\n    def read(self, filenames, encoding = None):\n        self.custom_cls_file = filenames\n        super().read(filenames, encoding)\n\n    def save(self, encoding = 'utf-8', space_around_delimeters=True):\n        try:\n            if not isinstance(self.custom_cls_file, str) and \\\n                hasattr(self.custom_cls_file, '__iter__'):\n                for file in self.custom_cls_file:\n                    with open(file, 'w', encoding=encoding) as cf:\n                        self.write(cf, space_around_delimeters)\n            else:\n                with open(self.custom_cls_file, 'w') as cf:\n                    self.write(cf, space_around_delimeters)\n        except PermissionError:\n            log_e('Could not save settings: PermissionError')\n        except:\n            log.exception('Could not save settings')\n\nconfig = Config()\nconfig.read(settings_path)\n\ndef save():\n    config.save()\n    ExProperties.save()\n\ndef get(default, section, key=None, type_class=str, subtype_class=None):\n    \"\"\"\n    Tries to find the given entries in config,\n    returning default if none is found. Default type\n    is str. Subtype will be used for when try_excepting fails\n    \"\"\"\n    value = default\n    try:\n        if key:\n            try:\n                value = config[section][key]\n            except KeyError:\n                value = default\n        else:\n            try:\n                value = config[section]\n            except KeyError:\n                value = default\n        try:\n            if value.lower() == 'false':\n                value = False\n            elif value.lower() == 'true':\n                value = True\n            elif value.lower() == 'none':\n                value = None\n            elif type_class in (list, tuple):\n                value = type_class([x for x in value.split('>|<') if x])\n            else:\n                if subtype_class:\n                    try:\n                        value = type_class(value)\n                    except:\n                        value = subtype_class(value)\n                else:\n                    value = type_class(value)\n        except AttributeError:\n            pass\n        except:\n            return default\n        return value\n    except:\n        return default\n\ndef set(value, section, key=None):\n    \"\"\"\n    Adds a new entry in config.\n    Remember everything is converted to string\n    \"\"\"\n    val_as_str = value\n    if not section in config:\n        config[section] = {}\n    if isinstance(value, (list, tuple)):\n        val_as_str = \"\"\n        for n, v in enumerate(value):\n            if n == len(value)-1:\n                val_as_str += \"{}\".format(v)\n            else:\n                val_as_str += \"{}>|<\".format(v)\n\n    if key:\n        config[section][key] = str(val_as_str)\n    else:\n        config[section] = str(val_as_str)\n\n\nclass Properties:\n    pass\n\n# wow this is really bad, can't be arsed to fix it\nclass ExProperties(Properties):\n    # sites\n    EHENTAI, NHENTAI = range(2)\n    sites = (EHENTAI, NHENTAI,)\n\n    _INFO = {}\n    def __init__(self, site=EHENTAI):\n        self.site = site\n        if not self._INFO:\n            if os.path.exists(phappypanda_path):\n                with open(phappypanda_path, 'rb') as f:\n                    self.__class__._INFO = pickle.load(f)\n                    for s in self.sites:\n                        if s in self.__class__._INFO:\n                            if 'custom' in self.__class__._INFO[s]:\n                                self.__class__._INFO[s]['custom'].clear()\n\n    @classmethod\n    def save(self):\n        if self._INFO:\n            with open(phappypanda_path, 'wb') as f:\n                pickle.dump(self._INFO, f, 4)\n\n    @property\n    def cookies(self):\n        if self._INFO:\n            if self.site in self._INFO:\n                return self._INFO[self.site].get('cookies')\n        return {}\n\n    @cookies.setter\n    def cookies(self, c):\n        if not self.site in self._INFO:\n            self._INFO[self.site] = {}\n        self._INFO[self.site]['cookies'] = c\n\n    @property\n    def username(self):\n        if self._INFO:\n            if self.site in self._INFO:\n                return self._INFO[self.site].get('username')\n\n    @username.setter\n    def username(self, us):\n        if not self.site in self._INFO:\n            self._INFO[self.site] = {}\n        self._INFO[self.site]['username'] = us\n\n    @property\n    def password(self):\n        if self._INFO:\n            if self.site in self._INFO:\n                return self._INFO[self.site].get('password')\n\n    @password.setter\n    def password(self, ps):\n        if not self.site in self._INFO:\n            self._INFO[self.site] = {}\n        self._INFO[self.site]['password'] = ps\n\n    @property\n    def custom(self):\n        if self._INFO:\n            if self.site in self._INFO:\n                return self._INFO[self.site].get('custom')\n\n    @custom.setter\n    def custom(self, ps):\n        if not self.site in self._INFO:\n            self._INFO[self.site] = {}\n        self._INFO[self.site]['custom'] = ps\n\nclass WinProperties(Properties):\n    def __init__(self):\n        self._resize = None\n        self._pos = (0, 0)\n\n    @property\n    def resize(self):\n        return self._resize\n\n    @resize.setter\n    def resize(self, size):\n        assert isinstance(size, list) or isinstance(size, tuple)\n        self._resize = tuple(size)\n\n    @property\n    def pos(self):\n        return self._pos\n\n    @pos.setter\n    def pos(self, point):\n        assert isinstance(point, list) or isinstance(point, tuple)\n        self._pos = tuple(point)\n\ndef win_read(cls, name):\n    \"Reads window properties\"\n    assert isinstance(name, str)\n    props = WinProperties()\n    try:\n        props.resize = (int(config[name]['resize.w']),\n                  int(config[name]['resize.h']))\n        props.pos = (int(config[name]['pos.x']),\n                   int(config[name]['pos.y']))\n    except KeyError:\n        pass\n    return props\n\ndef win_save(cls, name, winprops=None):\n    \"\"\"\n    Saves window properties.\n    Saves current window properties if no winproperties is passed\n    \"\"\"\n    assert isinstance(name, str)\n    if not winprops:\n        if not name in config:\n            config[name] = {}\n        config[name]['resize.w'] = str(cls.size().width())\n        config[name]['resize.h'] = str(cls.size().height())\n        config[name]['pos.x'] = str(cls.pos().x())\n        config[name]['pos.y'] = str(cls.pos().y())\n    else:\n        assert isinstance(winprops, WinProperties), \\\n            'You must pass a winproperties derived from WinProperties class'\n\n    config.save()\n"
  },
  {
    "path": "version/settingsdialog.py",
    "content": "﻿import logging, os, sys\n\nfrom PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QListWidget, QWidget,\n                             QListWidgetItem, QStackedLayout, QPushButton,\n                             QLabel, QTabWidget, QLineEdit, QGroupBox, QFormLayout,\n                             QCheckBox, QRadioButton, QSpinBox, QSizePolicy,\n                             QScrollArea, QFontDialog, QMessageBox, QComboBox,\n                             QFileDialog, QSlider)\nfrom PyQt5.QtCore import pyqtSignal, Qt\nfrom PyQt5.QtGui import QPalette, QPixmapCache\n\nfrom color_line_edit import ColorLineEdit\nfrom misc import FlowLayout, Spacer, PathLineEdit, AppDialog, Line\nimport misc\nimport settings\nimport app_constants\nimport misc_db\nimport gallerydb\nimport utils\nimport io_misc\nimport pewnet\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nclass SettingsDialog(QWidget):\n    \"A settings dialog\"\n    scroll_speed_changed = pyqtSignal()\n    init_gallery_rebuild = pyqtSignal(bool)\n    init_gallery_eximport = pyqtSignal(object)\n    def __init__(self, parent=None):\n        super().__init__(parent, flags=Qt.Window)\n\n        self.init_gallery_rebuild.connect(self.accept)\n\n        self.parent_widget = parent\n        self.setAttribute(Qt.WA_DeleteOnClose)\n        self.resize(700, 500)\n        self.restore_values()\n        self.initUI()\n        self.setWindowTitle('Settings')\n        self.show()\n\n    def initUI(self):\n        main_layout = QVBoxLayout(self)\n        sub_layout = QHBoxLayout()\n        # Left Panel\n        left_panel = QListWidget()\n        left_panel.setViewMode(left_panel.ListMode)\n        #left_panel.setIconSize(QSize(40,40))\n        left_panel.setTextElideMode(Qt.ElideRight)\n        left_panel.setMaximumWidth(200)\n        left_panel.itemClicked.connect(self.change)\n        #web.setText('Web')\n        self.application = QListWidgetItem()\n        self.application.setText('Application')\n        self.web = QListWidgetItem()\n        self.web.setText('Web')\n        self.visual = QListWidgetItem()\n        self.visual.setText('Visual')\n        self.advanced = QListWidgetItem()\n        self.advanced.setText('Advanced')\n        self.about = QListWidgetItem()\n        self.about.setText('About')\n\n        #main.setIcon(QIcon(os.path.join(app_constants.static_dir, 'plus2.png')))\n        left_panel.addItem(self.application)\n        left_panel.addItem(self.web)\n        left_panel.addItem(self.visual)\n        left_panel.addItem(self.advanced)\n        left_panel.addItem(self.about)\n        left_panel.setMaximumWidth(100)\n\n        # right panel\n        self.right_panel = QStackedLayout()\n        self.init_right_panel()\n\n        # bottom\n        bottom_layout = QHBoxLayout()\n        ok_btn = QPushButton('Ok')\n        ok_btn.clicked.connect(self.accept)\n        cancel_btn = QPushButton('Cancel')\n        cancel_btn.clicked.connect(self.close)\n        info_lbl = QLabel()\n        info_lbl.setText('<a href=\"https://github.com/Pewpews/happypanda\">'+\n                   'Visit GitHub Repo</a> | Options marked with * requires application restart.')\n        info_lbl.setTextFormat(Qt.RichText)\n        info_lbl.setTextInteractionFlags(Qt.TextBrowserInteraction)\n        info_lbl.setOpenExternalLinks(True)\n        self.spacer = QWidget()\n        self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        bottom_layout.addWidget(info_lbl, 0, Qt.AlignLeft)\n        bottom_layout.addWidget(self.spacer)\n        bottom_layout.addWidget(ok_btn, 0, Qt.AlignRight)\n        bottom_layout.addWidget(cancel_btn, 0, Qt.AlignRight)\n\n        sub_layout.addWidget(left_panel)\n        sub_layout.addLayout(self.right_panel)\n        main_layout.addLayout(sub_layout)\n        main_layout.addLayout(bottom_layout)\n\n        self.restore_options()\n\n\n    def change(self, item):\n        def curr_index(index):\n            if index != self.right_panel.currentIndex():\n                self.right_panel.setCurrentIndex(index)\n        if item == self.application:\n            curr_index(self.application_index)\n        elif item == self.web:\n            curr_index(self.web_index)\n        elif item == self.visual:\n            curr_index(self.visual_index)\n        elif item == self.advanced:\n            curr_index(self.advanced_index)\n        elif item == self.about:\n            curr_index(self.about_index)\n\n    def restore_values(self):\n        # Visual\n        self.high_quality_thumbs = app_constants.HIGH_QUALITY_THUMBS\n        self.style_sheet = app_constants.user_stylesheet_path\n\n        # Advanced\n        self.scroll_speed = app_constants.SCROLL_SPEED\n        self.cache_size = app_constants.THUMBNAIL_CACHE_SIZE\n        self.prefetch_item_amnt = app_constants.PREFETCH_ITEM_AMOUNT\n\n    def restore_options(self):\n\n        # App / General\n        self.g_languages.addItems(app_constants.G_LANGUAGES)\n        self.g_languages.addItems(app_constants.G_CUSTOM_LANGUAGES)\n        self._find_combobox_match(self.g_languages, app_constants.G_DEF_LANGUAGE, 0)\n        self.g_type.addItems(app_constants.G_TYPES)\n        self._find_combobox_match(self.g_type, app_constants.G_DEF_TYPE, 0)\n        self.g_status.addItems(app_constants.G_STATUS)\n        self._find_combobox_match(self.g_status, app_constants.G_DEF_STATUS, 0)\n        self.sidebar_widget_hidden.setChecked(app_constants.SHOW_SIDEBAR_WIDGET)\n        self.send_2_trash.setChecked(app_constants.SEND_FILES_TO_TRASH)\n        self.subfolder_as_chapters.setChecked(app_constants.SUBFOLDER_AS_GALLERY)\n        self.extract_gallery_before_opening.setChecked(app_constants.EXTRACT_CHAPTER_BEFORE_OPENING)\n        self.open_galleries_sequentially.setChecked(app_constants.OPEN_GALLERIES_SEQUENTIALLY)\n        self.move_imported_gs.setChecked(app_constants.MOVE_IMPORTED_GALLERIES)\n        self.move_imported_def_path.setText(app_constants.IMPORTED_GALLERY_DEF_PATH)\n        self.open_random_g_chapters.setChecked(app_constants.OPEN_RANDOM_GALLERY_CHAPTERS)\n        self.rename_g_source_group.setChecked(app_constants.RENAME_GALLERY_SOURCE)\n        self.path_to_unrar.setText(app_constants.unrar_tool_path)\n        self.keep_added_gallery.setChecked(not app_constants.KEEP_ADDED_GALLERIES)\n        # App / General / External Viewer\n        self.external_viewer_path.setText(app_constants.EXTERNAL_VIEWER_PATH)\n\n        # App / Monitor / Misc\n        self.enable_monitor.setChecked(app_constants.ENABLE_MONITOR)\n        self.look_new_gallery_startup.setChecked(app_constants.LOOK_NEW_GALLERY_STARTUP)\n\n        # App / Monitor / Folders\n        for path in app_constants.MONITOR_PATHS:\n            self.add_folder_monitor(path)\n\n        # App / Monitor / Ignore list\n        for ext in app_constants.IGNORE_EXTS:\n            if ext == 'Folder':\n                self.ignore_folder.setChecked(True)\n            if ext == 'ZIP':\n                self.ignore_zip.setChecked(True)\n            if ext == 'CBZ':\n                self.ignore_cbz.setChecked(True)\n            if ext == 'RAR':\n                self.ignore_rar.setChecked(True)\n            if ext == 'CBR':\n                self.ignore_cbr.setChecked(True)\n\n        for path in app_constants.IGNORE_PATHS:\n            self.add_ignore_path(path)\n\n        # Web / metadata\n        if 'e-hentai' in app_constants.DEFAULT_EHEN_URL:\n            self.default_ehen_url.setChecked(True)\n        else:\n            self.exhentai_ehen_url.setChecked(True)\n        \n        self.include_expunged.setChecked(app_constants.INCLUDE_EH_EXPUNGED)\n        self.replace_metadata.setChecked(app_constants.REPLACE_METADATA)\n        self.always_first_hit.setChecked(app_constants.ALWAYS_CHOOSE_FIRST_HIT)\n        self.web_time_offset.setValue(app_constants.GLOBAL_EHEN_TIME)\n        self.continue_a_metadata_fetcher.setChecked(app_constants.CONTINUE_AUTO_METADATA_FETCHER)\n        self.use_jpn_title.setChecked(app_constants.USE_JPN_TITLE)\n        self.use_gallery_link.setChecked(app_constants.USE_GALLERY_LINK)\n        self.fallback_chaika.setChecked(True) if 'chaikahen' in app_constants.HEN_LIST else None\n\n        # Web / Download\n        if app_constants.HEN_DOWNLOAD_TYPE == 0:\n            self.archive_download.setChecked(True)\n        else:\n            self.torrent_download.setChecked(True)\n\n        self.download_directory.setText(app_constants.DOWNLOAD_DIRECTORY)\n        self.torrent_client.setText(app_constants.TORRENT_CLIENT)\n        self.download_gallery_lib.setChecked(app_constants.DOWNLOAD_GALLERY_TO_LIB)\n\n        # Visual / Grid View\n        self.g_popup_width.setValue(app_constants.POPUP_WIDTH)\n        self.g_popup_height.setValue(app_constants.POPUP_HEIGHT)\n        # Visual / Grid View / Tooltip\n        self.grid_tooltip_group.setChecked(app_constants.GRID_TOOLTIP)\n        self.visual_grid_tooltip_title.setChecked(app_constants.TOOLTIP_TITLE)\n        self.visual_grid_tooltip_author.setChecked(app_constants.TOOLTIP_AUTHOR)\n        self.visual_grid_tooltip_chapters.setChecked(app_constants.TOOLTIP_CHAPTERS)\n        self.visual_grid_tooltip_status.setChecked(app_constants.TOOLTIP_STATUS)\n        self.visual_grid_tooltip_type.setChecked(app_constants.TOOLTIP_TYPE)\n        self.visual_grid_tooltip_lang.setChecked(app_constants.TOOLTIP_LANG)\n        self.visual_grid_tooltip_descr.setChecked(app_constants.TOOLTIP_DESCR)\n        self.visual_grid_tooltip_tags.setChecked(app_constants.TOOLTIP_TAGS)\n        self.visual_grid_tooltip_last_read.setChecked(app_constants.TOOLTIP_LAST_READ)\n        self.visual_grid_tooltip_times_read.setChecked(app_constants.TOOLTIP_TIMES_READ)\n        self.visual_grid_tooltip_pub_date.setChecked(app_constants.TOOLTIP_PUB_DATE)\n        self.visual_grid_tooltip_date_added.setChecked(app_constants.TOOLTIP_DATE_ADDED)\n        # Visual / Grid View / Gallery\n        self.gallery_rating.setChecked(app_constants.DISPLAY_RATING)\n        self.gallery_type_ico.setChecked(app_constants.DISPLAY_GALLERY_TYPE)\n        if app_constants.GALLERY_FONT_ELIDE:\n            self.gallery_text_elide.setChecked(True)\n        else:\n            self.gallery_text_fit.setChecked(True)\n        self.font_lbl.setText(app_constants.GALLERY_FONT[0])\n        self.font_size_lbl.setValue(app_constants.GALLERY_FONT[1])\n\n        if app_constants.SEARCH_ON_ENTER:\n            self.search_on_enter.setChecked(True)\n        else:\n            self.search_every_keystroke.setChecked(True)\n        self.gallery_size.setValue(app_constants.SIZE_FACTOR//10)\n        self.grid_spacing.setValue(app_constants.GRID_SPACING)\n        # Visual / Grid View / Colors\n        self.grid_label_color.setText(app_constants.GRID_VIEW_LABEL_COLOR)\n        self.grid_title_color.setText(app_constants.GRID_VIEW_TITLE_COLOR)\n        self.grid_artist_color.setText(app_constants.GRID_VIEW_ARTIST_COLOR)\n\n        self.colors_ribbon_group.setChecked(app_constants.DISPLAY_GALLERY_RIBBON)\n        self.ribbon_manga_color.setText(app_constants.GRID_VIEW_T_MANGA_COLOR)\n        self.ribbon_doujin_color.setText(app_constants.GRID_VIEW_T_DOUJIN_COLOR)\n        self.ribbon_artist_cg_color.setText(app_constants.GRID_VIEW_T_ARTIST_CG_COLOR)\n        self.ribbon_game_cg_color.setText(app_constants.GRID_VIEW_T_GAME_CG_COLOR)\n        self.ribbon_western_color.setText(app_constants.GRID_VIEW_T_WESTERN_COLOR)\n        self.ribbon_image_color.setText(app_constants.GRID_VIEW_T_IMAGE_COLOR)\n        self.ribbon_non_h_color.setText(app_constants.GRID_VIEW_T_NON_H_COLOR)\n        self.ribbon_cosplay_color.setText(app_constants.GRID_VIEW_T_COSPLAY_COLOR)\n        self.ribbon_other_color.setText(app_constants.GRID_VIEW_T_OTHER_COLOR)\n\n        # Advanced / Misc\n        self.external_viewer_args.setText(app_constants.EXTERNAL_VIEWER_ARGS)\n        self.force_high_dpi_support.setChecked(app_constants.FORCE_HIGH_DPI_SUPPORT)\n\n        # Advanced / Gallery / Gallery Text Fixer\n        self.g_data_regex_fix_edit.setText(app_constants.GALLERY_DATA_FIX_REGEX)\n        self.g_data_replace_fix_edit.setText(app_constants.GALLERY_DATA_FIX_REPLACE)\n        self.g_data_fixer_title.setChecked(app_constants.GALLERY_DATA_FIX_TITLE)\n        self.g_data_fixer_artist.setChecked(app_constants.GALLERY_DATA_FIX_ARTIST)\n\n    def accept(self):\n        set = settings.set\n\n        # App / General\n        app_constants.SHOW_SIDEBAR_WIDGET = self.sidebar_widget_hidden.isChecked()\n        set(app_constants.SHOW_SIDEBAR_WIDGET, 'Application', 'show sidebar widget')\n        app_constants.SEND_FILES_TO_TRASH = self.send_2_trash.isChecked()\n        set(app_constants.SEND_FILES_TO_TRASH, 'Application', 'send files to trash')\n\n        # App / General / Gallery\n\n        app_constants.KEEP_ADDED_GALLERIES = not self.keep_added_gallery.isChecked()\n        set(app_constants.KEEP_ADDED_GALLERIES, 'Application', 'keep added galleries')\n\n        g_custom_lang = []\n        for x in range(self.g_languages.count()):\n            l = self.g_languages.itemText(x).capitalize()\n            if l and not l in app_constants.G_LANGUAGES:\n                g_custom_lang.append(l)\n\n        app_constants.G_CUSTOM_LANGUAGES = g_custom_lang\n        set(app_constants.G_CUSTOM_LANGUAGES, 'General', 'gallery custom languages')\n        if self.g_languages.currentText():\n            app_constants.G_DEF_LANGUAGE = self.g_languages.currentText()\n            set(app_constants.G_DEF_LANGUAGE, 'General', 'gallery default language')\n        app_constants.G_DEF_STATUS = self.g_status.currentText()\n        set(app_constants.G_DEF_STATUS, 'General', 'gallery default status')\n        app_constants.G_DEF_TYPE = self.g_type.currentText()\n        set(app_constants.G_DEF_TYPE, 'General', 'gallery default type')\n        app_constants.SUBFOLDER_AS_GALLERY = self.subfolder_as_chapters.isChecked()\n        set(app_constants.SUBFOLDER_AS_GALLERY, 'Application', 'subfolder as gallery')\n        app_constants.EXTRACT_CHAPTER_BEFORE_OPENING = self.extract_gallery_before_opening.isChecked()\n        set(app_constants.EXTRACT_CHAPTER_BEFORE_OPENING, 'Application', 'extract chapter before opening')\n        app_constants.OPEN_GALLERIES_SEQUENTIALLY = self.open_galleries_sequentially.isChecked()\n        set(app_constants.OPEN_GALLERIES_SEQUENTIALLY, 'Application', 'open galleries sequentially')\n        app_constants.MOVE_IMPORTED_GALLERIES = self.move_imported_gs.isChecked()\n        set(app_constants.MOVE_IMPORTED_GALLERIES, 'Application', 'move imported galleries')\n        if not self.move_imported_def_path.text() or os.path.exists(self.move_imported_def_path.text()):\n            app_constants.IMPORTED_GALLERY_DEF_PATH = self.move_imported_def_path.text()\n            set(app_constants.IMPORTED_GALLERY_DEF_PATH, 'Application', 'imported gallery def path')\n        app_constants.OPEN_RANDOM_GALLERY_CHAPTERS = self.open_random_g_chapters.isChecked()\n        set(app_constants.OPEN_RANDOM_GALLERY_CHAPTERS, 'Application', 'open random gallery chapters')\n        app_constants.RENAME_GALLERY_SOURCE = self.rename_g_source_group.isChecked()\n        set(app_constants.RENAME_GALLERY_SOURCE, 'Application', 'rename gallery source')\n        app_constants.unrar_tool_path = self.path_to_unrar.text()\n        set(app_constants.unrar_tool_path, 'Application', 'unrar tool path')\n        # App / General / Search\n        app_constants.SEARCH_AUTOCOMPLETE = self.search_autocomplete.isChecked()\n        set(app_constants.SEARCH_AUTOCOMPLETE, 'Application', 'search autocomplete')\n        if self.search_on_enter.isChecked():\n            app_constants.SEARCH_ON_ENTER = True\n        else:\n            app_constants.SEARCH_ON_ENTER = False\n        set(app_constants.SEARCH_ON_ENTER, 'Application', 'search on enter')\n        # App / General / External Viewer\n        if not self.external_viewer_path.text():\n            app_constants.USE_EXTERNAL_VIEWER = False\n            set(False, 'Application', 'use external viewer')\n        else:\n            app_constants.USE_EXTERNAL_VIEWER = True\n            set(True, 'Application', 'use external viewer')\n            app_constants._REFRESH_EXTERNAL_VIEWER = True\n        app_constants.EXTERNAL_VIEWER_PATH = self.external_viewer_path.text()\n        set(app_constants.EXTERNAL_VIEWER_PATH,'Application', 'external viewer path')\n        # App / Monitor / misc\n        app_constants.ENABLE_MONITOR = self.enable_monitor.isChecked()\n        set(app_constants.ENABLE_MONITOR, 'Application', 'enable monitor')\n        app_constants.LOOK_NEW_GALLERY_STARTUP = self.look_new_gallery_startup.isChecked()\n        set(app_constants.LOOK_NEW_GALLERY_STARTUP, 'Application', 'look new gallery startup')\n        # App / Monitor / folders\n        paths = []\n        folder_p_widgets = self.take_all_layout_widgets(self.folders_layout)\n        for x, l_edit in enumerate(folder_p_widgets):\n            p = l_edit.text()\n            if p:\n                paths.append(p)\n\n        set(paths, 'Application', 'monitor paths')\n        app_constants.MONITOR_PATHS = paths\n        # App / Monitor / ignore list\n        exts_list = []\n        for ext in [self.ignore_folder, self.ignore_zip, self.ignore_cbz, self.ignore_rar, self.ignore_cbr]:\n            if ext.isChecked():\n                exts_list.append(ext.text())\n        set(exts_list, 'Application', 'ignore exts')\n        app_constants.IGNORE_EXTS = exts_list\n\n        paths = []\n        ignore_p_widgets = self.take_all_layout_widgets(self.ignore_path_l)\n        for x, l_edit in enumerate(ignore_p_widgets):\n            p = l_edit.text()\n            if p:\n                paths.append(p)\n        set(paths, 'Application', 'ignore paths')\n        app_constants.IGNORE_PATHS = paths\n\n        # Web / Downloader\n\n        if self.archive_download.isChecked():\n            app_constants.HEN_DOWNLOAD_TYPE = 0\n        else:\n            app_constants.HEN_DOWNLOAD_TYPE = 1\n        set(app_constants.HEN_DOWNLOAD_TYPE, 'Web', 'hen download type')\n\n        app_constants.DOWNLOAD_DIRECTORY = self.download_directory.text()\n        set(app_constants.DOWNLOAD_DIRECTORY, 'Web', 'download directory')\n\n        app_constants.TORRENT_CLIENT = self.torrent_client.text()\n        set(app_constants.TORRENT_CLIENT, 'Web', 'torrent client')\n\n        app_constants.DOWNLOAD_GALLERY_TO_LIB = self.download_gallery_lib.isChecked()\n        set(app_constants.DOWNLOAD_GALLERY_TO_LIB, 'Web', 'download galleries to library')\n\n        # Web / Metdata\n        if self.default_ehen_url.isChecked():\n            app_constants.DEFAULT_EHEN_URL = 'https://e-hentai.org/'\n        else:\n            app_constants.DEFAULT_EHEN_URL = 'https://exhentai.org/'\n        set(app_constants.DEFAULT_EHEN_URL, 'Web', 'default ehen url')\n\n        app_constants.INCLUDE_EH_EXPUNGED = self.include_expunged.isChecked()\n        set(app_constants.INCLUDE_EH_EXPUNGED, 'Web', 'include eh expunged')\n\n        app_constants.REPLACE_METADATA = self.replace_metadata.isChecked()\n        set(app_constants.REPLACE_METADATA, 'Web', 'replace metadata')\n\n        app_constants.ALWAYS_CHOOSE_FIRST_HIT = self.always_first_hit.isChecked()\n        set(app_constants.ALWAYS_CHOOSE_FIRST_HIT, 'Web', 'always choose first hit')\n\n        app_constants.GLOBAL_EHEN_TIME = self.web_time_offset.value()\n        set(app_constants.GLOBAL_EHEN_TIME, 'Web', 'global ehen time offset')\n\n        app_constants.CONTINUE_AUTO_METADATA_FETCHER = self.continue_a_metadata_fetcher.isChecked()\n        set(app_constants.CONTINUE_AUTO_METADATA_FETCHER, 'Web', 'continue auto metadata fetcher')\n\n        app_constants.USE_JPN_TITLE = self.use_jpn_title.isChecked()\n        set(app_constants.USE_JPN_TITLE, 'Web', 'use jpn title')\n\n        app_constants.USE_GALLERY_LINK = self.use_gallery_link.isChecked()\n        set(app_constants.USE_GALLERY_LINK, 'Web', 'use gallery link')\n        # fallback sources\n        henlist = []\n        if self.fallback_chaika.isChecked():\n            henlist.append('chaikahen')\n        app_constants.HEN_LIST = henlist\n        set(app_constants.HEN_LIST, 'Web', 'hen list')\n\n        # Visual / Grid View\n        app_constants.POPUP_WIDTH = self.g_popup_width.value()\n        set(app_constants.POPUP_WIDTH, 'Visual', 'popup.w')\n        app_constants.POPUP_HEIGHT = self.g_popup_height.value()\n        set(app_constants.POPUP_HEIGHT, 'Visual', 'popup.h')\n\n        # Visual / Grid View / Tooltip\n        app_constants.GRID_TOOLTIP = self.grid_tooltip_group.isChecked()\n        set(app_constants.GRID_TOOLTIP, 'Visual', 'grid tooltip')\n        app_constants.TOOLTIP_TITLE = self.visual_grid_tooltip_title.isChecked()\n        set(app_constants.TOOLTIP_TITLE, 'Visual', 'tooltip title')\n        app_constants.TOOLTIP_AUTHOR = self.visual_grid_tooltip_author.isChecked()\n        set(app_constants.TOOLTIP_AUTHOR, 'Visual', 'tooltip author')\n        app_constants.TOOLTIP_CHAPTERS = self.visual_grid_tooltip_chapters.isChecked()\n        set(app_constants.TOOLTIP_CHAPTERS, 'Visual', 'tooltip chapters')\n        app_constants.TOOLTIP_STATUS = self.visual_grid_tooltip_status.isChecked()\n        set(app_constants.TOOLTIP_STATUS, 'Visual', 'tooltip status')\n        app_constants.TOOLTIP_TYPE = self.visual_grid_tooltip_type.isChecked()\n        set(app_constants.TOOLTIP_TYPE, 'Visual', 'tooltip type')\n        app_constants.TOOLTIP_LANG = self.visual_grid_tooltip_lang.isChecked()\n        set(app_constants.TOOLTIP_LANG, 'Visual', 'tooltip lang')\n        app_constants.TOOLTIP_DESCR = self.visual_grid_tooltip_descr.isChecked()\n        set(app_constants.TOOLTIP_DESCR, 'Visual', 'tooltip descr')\n        app_constants.TOOLTIP_TAGS = self.visual_grid_tooltip_tags.isChecked()\n        set(app_constants.TOOLTIP_TAGS, 'Visual', 'tooltip tags')\n        app_constants.TOOLTIP_LAST_READ = self.visual_grid_tooltip_last_read.isChecked()\n        set(app_constants.TOOLTIP_LAST_READ, 'Visual', 'tooltip last read')\n        app_constants.TOOLTIP_TIMES_READ = self.visual_grid_tooltip_times_read.isChecked()\n        set(app_constants.TOOLTIP_TIMES_READ, 'Visual', 'tooltip times read')\n        app_constants.TOOLTIP_PUB_DATE = self.visual_grid_tooltip_pub_date.isChecked()\n        set(app_constants.TOOLTIP_PUB_DATE, 'Visual', 'tooltip pub date')\n        app_constants.TOOLTIP_DATE_ADDED = self.visual_grid_tooltip_date_added.isChecked()\n        set(app_constants.TOOLTIP_DATE_ADDED, 'Visual', 'tooltip date added')\n        # Visual / Grid View / Gallery\n        app_constants.DISPLAY_RATING = self.gallery_rating.isChecked()\n        set(app_constants.DISPLAY_RATING, 'Visual', 'display gallery rating')\n        app_constants.DISPLAY_GALLERY_TYPE = self.gallery_type_ico.isChecked()\n        set(app_constants.DISPLAY_GALLERY_TYPE, 'Visual', 'display gallery type')\n        if self.gallery_text_elide.isChecked():\n            app_constants.GALLERY_FONT_ELIDE = True\n        else:\n            app_constants.GALLERY_FONT_ELIDE = False\n        set(app_constants.GALLERY_FONT_ELIDE, 'Visual', 'gallery font elide')\n        app_constants.GALLERY_FONT = (self.font_lbl.text(), self.font_size_lbl.value())\n        set(app_constants.GALLERY_FONT[0], 'Visual', 'gallery font family')\n        set(app_constants.GALLERY_FONT[1], 'Visual', 'gallery font size')\n        app_constants.SIZE_FACTOR = self.gallery_size.value() * 10\n        set(app_constants.SIZE_FACTOR, 'Visual', 'size factor')\n        app_constants.GRID_SPACING = self.grid_spacing.value()\n        set(app_constants.GRID_SPACING, 'Visual', 'grid spacing')\n\n        # Visual / Grid View / Colors\n        app_constants.DISPLAY_GALLERY_RIBBON = self.colors_ribbon_group.isChecked()\n        set(app_constants.DISPLAY_GALLERY_RIBBON, 'Visual', 'display gallery ribbon')\n        if self.color_checker(self.grid_title_color.text()):\n            app_constants.GRID_VIEW_TITLE_COLOR = self.grid_title_color.text()\n            set(app_constants.GRID_VIEW_TITLE_COLOR, 'Visual', 'grid view title color')\n        if self.color_checker(self.grid_artist_color.text()):\n            app_constants.GRID_VIEW_ARTIST_COLOR = self.grid_artist_color.text()\n            set(app_constants.GRID_VIEW_ARTIST_COLOR, 'Visual', 'grid view artist color')\n        if self.color_checker(self.grid_label_color.text()):\n            app_constants.GRID_VIEW_LABEL_COLOR = self.grid_label_color.text()\n            set(app_constants.GRID_VIEW_LABEL_COLOR, 'Visual', 'grid view label color')\n\n        if self.color_checker(self.ribbon_manga_color.text()):\n            app_constants.GRID_VIEW_T_MANGA_COLOR = self.ribbon_manga_color.text()\n            set(app_constants.GRID_VIEW_T_MANGA_COLOR, 'Visual', 'grid view t manga color')\n        if self.color_checker(self.ribbon_doujin_color.text()):\n            app_constants.GRID_VIEW_T_DOUJIN_COLOR = self.ribbon_doujin_color.text()\n            set(app_constants.GRID_VIEW_T_DOUJIN_COLOR, 'Visual', 'grid view t doujin color')\n        if self.color_checker(self.ribbon_artist_cg_color.text()):\n            app_constants.GRID_VIEW_T_ARTIST_CG_COLOR = self.ribbon_artist_cg_color.text()\n            set(app_constants.GRID_VIEW_T_ARTIST_CG_COLOR, 'Visual', 'grid view t artist cg color')\n        if self.color_checker(self.ribbon_game_cg_color.text()):\n            app_constants.GRID_VIEW_T_GAME_CG_COLOR = self.ribbon_game_cg_color.text()\n            set(app_constants.GRID_VIEW_T_GAME_CG_COLOR, 'Visual', 'grid view t game cg color')\n        if self.color_checker(self.ribbon_western_color.text()):\n            app_constants.GRID_VIEW_T_WESTERN_COLOR = self.ribbon_western_color.text()\n            set(app_constants.GRID_VIEW_T_WESTERN_COLOR, 'Visual', 'grid view t western color')\n        if self.color_checker(self.ribbon_image_color.text()):\n            app_constants.GRID_VIEW_T_IMAGE_COLOR = self.ribbon_image_color.text()\n            set(app_constants.GRID_VIEW_T_IMAGE_COLOR, 'Visual', 'grid view t image color')\n        if self.color_checker(self.ribbon_non_h_color.text()):\n            app_constants.GRID_VIEW_T_NON_H_COLOR = self.ribbon_non_h_color.text()\n            set(app_constants.GRID_VIEW_T_NON_H_COLOR, 'Visual', 'grid view t non-h color')\n        if self.color_checker(self.ribbon_cosplay_color.text()):\n            app_constants.GRID_VIEW_T_COSPLAY_COLOR = self.ribbon_cosplay_color.text()\n            set(app_constants.GRID_VIEW_T_COSPLAY_COLOR, 'Visual', 'grid view t cosplay color')\n        if self.color_checker(self.ribbon_other_color.text()):\n            app_constants.GRID_VIEW_T_OTHER_COLOR = self.ribbon_other_color.text()\n            set(app_constants.GRID_VIEW_T_OTHER_COLOR, 'Visual', 'grid view t other color')\n\n\n        # Advanced / Misc\n        app_constants.EXTERNAL_VIEWER_ARGS = self.external_viewer_args.text()\n        set(app_constants.EXTERNAL_VIEWER_ARGS, 'Advanced', 'external viewer args')\n\n        # Advanced / Misc / Grid View\n        app_constants.SCROLL_SPEED = self.scroll_speed\n        set(self.scroll_speed, 'Advanced', 'scroll speed')\n        self.scroll_speed_changed.emit()\n        app_constants.THUMBNAIL_CACHE_SIZE = self.cache_size\n        set(self.cache_size[1], 'Advanced', 'cache size')\n        QPixmapCache.setCacheLimit(self.cache_size[0]*\n                             self.cache_size[1])\n\n        app_constants.FORCE_HIGH_DPI_SUPPORT = self.force_high_dpi_support.isChecked()\n        set(app_constants.FORCE_HIGH_DPI_SUPPORT, 'Advanced', 'force high dpi support')\n\n        # Advanced / General / Gallery Text Fixer\n        app_constants.GALLERY_DATA_FIX_REGEX = self.g_data_regex_fix_edit.text()\n        set(app_constants.GALLERY_DATA_FIX_REGEX, 'Advanced', 'gallery data fix regex')\n        app_constants.GALLERY_DATA_FIX_TITLE = self.g_data_fixer_title.isChecked()\n        set(app_constants.GALLERY_DATA_FIX_TITLE, 'Advanced', 'gallery data fix title')\n        app_constants.GALLERY_DATA_FIX_ARTIST = self.g_data_fixer_artist.isChecked()\n        set(app_constants.GALLERY_DATA_FIX_ARTIST, 'Advanced', 'gallery data fix artist')\n        app_constants.GALLERY_DATA_FIX_REPLACE = self.g_data_replace_fix_edit.text()\n        set(app_constants.GALLERY_DATA_FIX_REPLACE, 'Advanced', 'gallery data fix replace')\n\n        # About / DB Overview\n\n        settings.save()\n        self.close()\n\n    def init_right_panel(self):\n\n        #def title_def(title):\n        #\ttitle_lbl = QLabel(title)\n        #\tf = QFont()\n        #\tf.setPixelSize(16)\n        #\ttitle_lbl.setFont(f)\n        #\treturn title_lbl\n\n        def groupbox(name, layout, parent, add_groupbox_in_layout=None):\n            \"\"\"\n            Makes a groupbox and a layout for you\n            Returns groupbox and layout\n            \"\"\"\n            g = QGroupBox(name, parent)\n            l = layout(g)\n            if add_groupbox_in_layout:\n                if isinstance(add_groupbox_in_layout, QFormLayout):\n                    add_groupbox_in_layout.addRow(g)\n                else:\n                    add_groupbox_in_layout.addWidget(g)\n            return g, l\n\n        def option_lbl_checkbox(text, optiontext, parent=None):\n            l = QLabel(text)\n            c = QCheckBox(text, parent)\n            return l, c\n\n        def new_tab(name, parent, scroll=False):\n            \"\"\"\n            Creates a new tab.\n            Returns new tab page widget and it's layout\n            \"\"\"\n            new_t = QWidget(parent)\n            new_l = QFormLayout(new_t)\n            if scroll:\n                scr = QScrollArea(parent)\n                scr.setBackgroundRole(QPalette.Base)\n                scr.setWidget(new_t)\n                scr.setWidgetResizable(True)\n                parent.addTab(scr, name)\n                return new_t, new_l\n            else:\n                parent.addTab(new_t, name)\n            return new_t, new_l\n\n\n        # App\n        application = QTabWidget(self)\n        self.application_index = self.right_panel.addWidget(application)\n        application_general, app_general_m_l = new_tab('General', application, True)\n\n        # App / General\n        self.sidebar_widget_hidden = QCheckBox(\"Show sidebar widget on startup\")\n        app_general_m_l.addRow(self.sidebar_widget_hidden)\n        self.send_2_trash = QCheckBox(\"Send deleted files to recycle bin\", self)\n        self.send_2_trash.setToolTip(\"When unchecked, files will get deleted permanently and be unrecoverable!\")\n        app_general_m_l.addRow(self.send_2_trash)\n        self.keep_added_gallery = QCheckBox(\"Remove galleries added in inbox on exit\")\n        self.keep_added_gallery.setToolTip(\"When turned off, galleries in inbox will not be deleted on exit\")\n        app_general_m_l.addRow(self.keep_added_gallery)\n\n        # App / General / Search\n        app_search, app_search_layout = groupbox('Search', QFormLayout, application_general)\n        app_general_m_l.addRow(app_search)\n        # App / General / Search / autocomplete\n        self.search_autocomplete = QCheckBox('*')\n        self.search_autocomplete.setChecked(app_constants.SEARCH_AUTOCOMPLETE)\n        self.search_autocomplete.setToolTip('Turn autocomplete on/off')\n        app_search_layout.addRow('Autocomplete', self.search_autocomplete)\n        # App / General / Search / search behaviour\n        self.search_every_keystroke = QRadioButton('Search on every keystroke *', app_search)\n        app_search_layout.addRow(self.search_every_keystroke)\n        self.search_on_enter = QRadioButton('Search on return-key *', app_search)\n        app_search_layout.addRow(self.search_on_enter)\n\n        # App / General / External Viewer\n        app_external_viewer, app_external_viewer_l = groupbox('External Viewer', QFormLayout, application_general, app_general_m_l)\n        external_viewer_p_info = QLabel(\"Tip: If your preffered image viewer doesn't work, try changing the arguments sent in the Advanced section\")\n        external_viewer_p_info.setWordWrap(True)\n        app_external_viewer_l.addRow(external_viewer_p_info)\n        self.external_viewer_path = PathLineEdit(app_external_viewer, False, '')\n        self.external_viewer_path.setPlaceholderText('Right/Left-click to open folder explorer.'+\n                              ' Leave empty to use default viewer')\n        self.external_viewer_path.setToolTip('Right/Left-click to open folder explorer.'+\n                              ' Leave empty to use default viewer')\n        self.external_viewer_path.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)\n        app_external_viewer_l.addRow('Path:', self.external_viewer_path)\n\n        # App / General / Rar Support\n        app_rar_group, app_rar_layout = groupbox('RAR Support *', QFormLayout, self)\n        app_general_m_l.addRow(app_rar_group)\n        rar_info = QLabel('Specify the path to the unrar tool to enable rar support.\\n'+\n                    'Windows: \"unrar.exe\" should be in the \"bin\" directory if you installed from the'+\n                    ' self-extracting archive provided on github.\\nOSX: You can install this via HomeBrew.'+\n                    ' Path should be something like: \"/usr/local/bin/unrar\".\\nLinux: Should already be'+\n                    ' installed. You can just type \"unrar\". If it\\'s not installed, use your package manager: pacman -S unrar')\n        rar_info.setWordWrap(True)\n        app_rar_layout.addRow(rar_info)\n        self.path_to_unrar = PathLineEdit(self, False, filters='')\n        app_rar_layout.addRow('UnRAR tool path:', self.path_to_unrar)\n\n        # App / Gallery\n        app_gallery_page, app_gallery_l = new_tab('Gallery', application, True)\n\n        g_def_values, g_def_values_l = groupbox(\"Default values\", QFormLayout, app_gallery_page)\n        app_gallery_l.addRow(g_def_values)\n        self.g_languages = QComboBox(self)\n        self.g_languages.setInsertPolicy(QComboBox.InsertAlphabetically)\n        self.g_languages.setEditable(True)\n        g_def_values_l.addRow(\"Default Language\", self.g_languages)\n        self.g_type = QComboBox(self)\n        g_def_values_l.addRow(\"Default Type\", self.g_type)\n        self.g_status = QComboBox(self)\n        g_def_values_l.addRow(\"Default Status\", self.g_status)\n\n        self.subfolder_as_chapters = QCheckBox(\"Subdirectiories should be treated as standalone galleries instead of chapters (applies in archives too)\")\n        self.subfolder_as_chapters.setToolTip(\"This option will enable creating standalone galleries for each subdirectiories found recursively when importing.\"+\n                                        \"\\nDefault action is treating each subfolder found as chapters of a gallery.\")\n        extract_gallery_info = QLabel(\"Note: This option has no effect when turned off if path to external viewer is not specified.\")\n        self.extract_gallery_before_opening = QCheckBox(\"Extract archive before opening (Uncheck only if your viewer supports it)\")\n        self.open_galleries_sequentially = QCheckBox(\"Open chapters sequentially (Note: has no effect if path to viewer is not specified)\")\n        subf_info = QLabel(\"Behaviour of 'Scan for new galleries on startup' option will be affected.\")\n        subf_info.setWordWrap(True)\n        \n        app_gallery_l.addRow('Note:', subf_info)\n        app_gallery_l.addRow(self.subfolder_as_chapters)\n        app_gallery_l.addRow(extract_gallery_info)\n        app_gallery_l.addRow(self.extract_gallery_before_opening)\n        app_gallery_l.addRow(self.open_galleries_sequentially)\n\n        self.move_imported_gs, move_imported_gs_l = groupbox('Move imported galleries',\n                                                       QFormLayout, app_gallery_page)\n        self.move_imported_gs.setCheckable(True)\n        self.move_imported_gs.setToolTip(\"Move imported galleries to specified folder.\")\n        self.move_imported_def_path = PathLineEdit()\n        move_imported_gs_l.addRow('Directory:', self.move_imported_def_path)\n        app_gallery_l.addRow(self.move_imported_gs)\n        self.rename_g_source_group, rename_g_source_l = groupbox('Rename gallery source (Coming soon)',\n                                                      QFormLayout, app_gallery_page)\n        self.rename_g_source_group.setCheckable(True)\n        self.rename_g_source_group.setDisabled(True)\n        app_gallery_l.addRow(self.rename_g_source_group)\n        rename_g_source_l.addRow(QLabel(\"Check what to include when renaming gallery source. (Same order)\"))\n        rename_g_source_flow_l = FlowLayout()\n        rename_g_source_l.addRow(rename_g_source_flow_l)\n        self.rename_artist = QCheckBox(\"Artist\")\n        self.rename_title = QCheckBox(\"Title\")\n        self.rename_lang = QCheckBox(\"Language\")\n        self.rename_title.setChecked(True)\n        self.rename_title.setDisabled(True)\n        rename_g_source_flow_l.addWidget(self.rename_artist)\n        rename_g_source_flow_l.addWidget(self.rename_title)\n        rename_g_source_flow_l.addWidget(self.rename_lang)\n        random_gallery_opener, random_g_opener_l = groupbox('Random Gallery Opener', QFormLayout, app_gallery_page)\n        app_gallery_l.addRow(random_gallery_opener)\n        self.open_random_g_chapters = QCheckBox(\"Open random gallery chapters\")\n        random_g_opener_l.addRow(self.open_random_g_chapters)\n\n        # App / Monitor\n        app_monitor_page = QScrollArea()\n        app_monitor_page.setBackgroundRole(QPalette.Base)\n        app_monitor_dummy = QWidget()\n        app_monitor_page.setWidgetResizable(True)\n        app_monitor_page.setWidget(app_monitor_dummy)\n        application.addTab(app_monitor_page, 'Monitoring')\n        app_monitor_m_l = QVBoxLayout(app_monitor_dummy)\n        # App / Monitor / misc\n        app_monitor_misc_group = QGroupBox('General *', self)\n        app_monitor_m_l.addWidget(app_monitor_misc_group)\n        app_monitor_misc_m_l = QFormLayout(app_monitor_misc_group)\n        monitor_info = QLabel('Directory monitoring will monitor the specified directories for any'+\n                        ' filesystem events. For example if you delete a gallery source in one of your'+\n                        ' monitored directories the application will inform you and ask if'+\n                        ' you want to delete the gallery from the application as well.')\n        monitor_info.setWordWrap(True)\n        app_monitor_misc_m_l.addRow(monitor_info)\n        self.enable_monitor = QCheckBox('Enable directory monitoring')\n        app_monitor_misc_m_l.addRow(self.enable_monitor)\n        self.look_new_gallery_startup = QCheckBox('Scan for new galleries on startup')\n        app_monitor_misc_m_l.addRow(self.look_new_gallery_startup)\n\n        # App / Monitor / folders\n        app_monitor_group = QGroupBox('Directories *', self)\n        app_monitor_m_l.addWidget(app_monitor_group, 1)\n        app_monitor_folders_m_l = QVBoxLayout(app_monitor_group)\n        app_monitor_folders_add = QPushButton('+')\n        app_monitor_folders_add.clicked.connect(self.add_folder_monitor)\n        app_monitor_folders_add.setMaximumWidth(20)\n        app_monitor_folders_add.setMaximumHeight(20)\n        app_monitor_folders_m_l.addWidget(app_monitor_folders_add, 0, Qt.AlignRight)\n        self.folders_layout = QFormLayout()\n        app_monitor_folders_m_l.addLayout(self.folders_layout)\n\n        # App / Ignore\n        app_ignore, app_ignore_m_l = new_tab('Ignore', application, True)\n        ignore_ext_group, ignore_ext_l = groupbox('Folder && File extensions (Check to ignore)', QVBoxLayout, app_monitor_dummy)\n        app_ignore_m_l.addRow(ignore_ext_group)\n        ignore_ext_list_l = FlowLayout()\n        ignore_ext_l.addLayout(ignore_ext_list_l)\n        self.ignore_folder = QCheckBox(\"Folder\", ignore_ext_group)\n        ignore_ext_list_l.addWidget(self.ignore_folder)\n        self.ignore_zip = QCheckBox(\"ZIP\", ignore_ext_group)\n        ignore_ext_list_l.addWidget(self.ignore_zip)\n        self.ignore_cbz = QCheckBox(\"CBZ\", ignore_ext_group)\n        ignore_ext_list_l.addWidget(self.ignore_cbz)\n        self.ignore_rar = QCheckBox(\"RAR\", ignore_ext_group)\n        ignore_ext_list_l.addWidget(self.ignore_rar)\n        self.ignore_cbr = QCheckBox(\"CBR\", ignore_ext_group)\n        ignore_ext_list_l.addWidget(self.ignore_cbr)\n\n        app_ignore_group, app_ignore_list_l = groupbox('List', QVBoxLayout, app_monitor_dummy)\n        app_ignore_m_l.addRow(app_ignore_group)\n        add_buttons_l = QHBoxLayout()\n        app_ignore_add_a = QPushButton('Add archive')\n        app_ignore_add_a.clicked.connect(lambda: self.add_ignore_path(dir=False))\n        app_ignore_add_f = QPushButton('Add directory')\n        app_ignore_add_f.clicked.connect(self.add_ignore_path)\n        add_buttons_l.addWidget(app_ignore_add_a, 0, Qt.AlignRight)\n        add_buttons_l.addWidget(app_ignore_add_f, 1, Qt.AlignRight)\n        app_ignore_list_l.addLayout(add_buttons_l)\n        self.ignore_path_l = QFormLayout()\n        app_ignore_list_l.addLayout(self.ignore_path_l)\n\n        # Web\n        web = QTabWidget(self)\n        self.web_index = self.right_panel.addWidget(web)\n\n        # Web / Logins\n        logins_page, logins_layout = new_tab(\"Logins\", web, True)\n\n        def login(userlineedit, passlineedit, statuslbl, baseHen_class, partial_txt, relogin=False):\n            statuslbl.setText(\"Logging in...\")\n            statuslbl.show()\n            try:\n                c_h = baseHen_class.login(userlineedit.text(), passlineedit.text(), relogin)\n                result = baseHen_class.check_login(c_h)\n                if result == 1:\n                    statuslbl.setText(\"<font color='green'>{}</font>\".format(partial_txt))\n                elif result:\n                    statuslbl.setText(\"<font color='green'>Logged in!</font>\")\n                else:\n                    statuslbl.setText(\"<font color='red'>Logging in failed!</font>\")\n            except app_constants.WrongLogin:\n                statuslbl.setText(\"<font color='red'>Wrong login information!</font>\")\n        \n        def make_login_forms(layout, exprops, baseHen_class, partial_txt='You have partial access!', info=''):\n            status = QLabel(logins_page)\n            status.setText(\"<font color='red'>Not logged in!</font>\")\n            layout.addRow(status)\n            user = QLineEdit(logins_page)\n            usertxt = 'Username:'\n            passtxt = 'Password:'\n            if baseHen_class == pewnet.EHen:\n                usertxt = 'IPB Member ID:'\n                passtxt = 'IPB Pass Hash:'\n            layout.addRow(usertxt, user)\n            passw = QLineEdit(logins_page)\n            layout.addRow(passtxt, passw)\n            passw.setEchoMode(QLineEdit.Password)\n            log_btn = QPushButton(\"Login\")\n            b_l = QHBoxLayout()\n            b_l.addWidget(Spacer('h'))\n            b_l.addWidget(log_btn)\n            layout.addRow(b_l)\n            if info:\n                layout.addRow(QLabel(info))\n            result = baseHen_class.check_login(exprops.cookies)\n            if result == 1:\n                status.setText(\"<font color='orange'>{}</font>\".format(partial_txt))\n            elif result:\n                status.setText(\"<font color='green'>Logged in!</font>\")\n            if result:\n                user.setText(exprops.username)\n                passw.setText(exprops.password)\n                log_btn.setText(\"Relogin\")\n                log_btn.clicked.connect(lambda: login(user, passw, status, baseHen_class, partial_txt, True))\n            else:\n                log_btn.clicked.connect(lambda: login(user, passw, status, baseHen_class, partial_txt))\n\n            return user, passw, status\n\n        # ehentai\n        exprops = settings.ExProperties\n        ehentai_group, ehentai_l = groupbox(\"E-Hentai\", QFormLayout, logins_page)\n        logins_layout.addRow(ehentai_group)\n        ehentai_user, ehentai_pass, ehentai_status = make_login_forms(ehentai_l, exprops(), pewnet.EHen,\n                                                                \"You have partial access (e-hentai). You do not have access to exhentai.\",\n                                                                app_constants.EXHEN_COOKIE_TUTORIAL)\n\n        # nhentai\n        #nhentai_group, nhentai_l = groupbox(\"NHentai\", QFormLayout, logins_page)\n        #logins_layout.addRow(nhentai_group)\n        #nhentai_user, nhentai_pass, nhentai_status = make_login_forms(nhentai_l, exprops(exprops.NHENTAI), pewnet.NHen)\n\n\n        # Web / Downloader\n        web_downloader, web_downloader_l = new_tab('Downloader', web)\n        hen_download_group, hen_download_group_l = groupbox('E-Hentai',\n                                                      QFormLayout, web_downloader)\n        web_downloader_l.addRow(hen_download_group)\n        self.archive_download = QRadioButton('Archive', hen_download_group)\n        self.torrent_download = QRadioButton('Torrent', hen_download_group)\n        download_type_l = QHBoxLayout()\n        download_type_l.addWidget(self.archive_download)\n        download_type_l.addWidget(self.torrent_download, 1)\n        hen_download_group_l.addRow('Download Type:', download_type_l)\n        self.download_directory = PathLineEdit(web_downloader)\n        web_downloader_l.addRow('Destination:', self.download_directory)\n        self.torrent_client = PathLineEdit(web_downloader, False, '')\n        web_downloader_l.addRow(QLabel(\"Leave empty to use default torrent client.\"+\n                                 \"\\nIt is NOT recommended to import a file while it's still downloading.\"))\n        web_downloader_l.addRow('Torrent client:', self.torrent_client)\n        self.download_gallery_lib = QCheckBox(\"Send downloaded galleries directly to library\")\n        web_downloader_l.addRow(self.download_gallery_lib)\n\n        # Web / Metadata\n        web_metadata_page = QScrollArea()\n        web_metadata_page.setBackgroundRole(QPalette.Base)\n        web_metadata_page.setWidgetResizable(True)\n        web.addTab(web_metadata_page, 'Metadata')\n        web_metadata_dummy = QWidget()\n        web_metadata_page.setWidget(web_metadata_dummy)\n        web_metadata_m_l = QFormLayout(web_metadata_dummy)\n        self.default_ehen_url = QRadioButton('e-hentai.org', web_metadata_page)\n        self.exhentai_ehen_url = QRadioButton('exhentai.org (login needed)', web_metadata_page)\n        ehen_url_l = QHBoxLayout()\n        ehen_url_l.addWidget(self.default_ehen_url)\n        ehen_url_l.addWidget(self.exhentai_ehen_url, 1)\n        web_metadata_m_l.addRow('Default EH:', ehen_url_l)\n        self.include_expunged = QCheckBox('Allow fetching from expunged galleries')\n        web_metadata_m_l.addRow(self.include_expunged)\n        self.continue_a_metadata_fetcher = QCheckBox('Skip galleries that has already been processed in auto metadata fetcher')\n        web_metadata_m_l.addRow(self.continue_a_metadata_fetcher)\n        self.use_jpn_title = QCheckBox('Apply japanese title instead of english title')\n        self.use_jpn_title.setToolTip('Applies the japanese title instead of the english')\n        web_metadata_m_l.addRow(self.use_jpn_title)\n        time_offset_info = QLabel('A delay between EH requests to avoid getting temp banned.')\n        self.web_time_offset = QSpinBox()\n        self.web_time_offset.setMaximumWidth(40)\n        self.web_time_offset.setMinimum(3)\n        self.web_time_offset.setMaximum(99)\n        web_metadata_m_l.addRow(time_offset_info)\n        web_metadata_m_l.addRow('Delay in seconds:', self.web_time_offset)\n        replace_metadata_info = QLabel('By default metadata is appended to a gallery.\\n'+\n                                 'Enabling this option makes it so that a gallery\\'s old data'+\n                                 ' is deleted and replaced with the new data.')\n        replace_metadata_info.setWordWrap(True)\n        self.replace_metadata = QCheckBox('Replace old metadata with new metadata')\n        web_metadata_m_l.addRow(replace_metadata_info)\n        web_metadata_m_l.addRow(self.replace_metadata)\n        self.always_first_hit = QCheckBox('Always choose first gallery found')\n        web_metadata_m_l.addRow(self.always_first_hit)\n        use_gallery_link_info = QLabel(\"Enable this option to fetch metadata using the currently applied URL on the gallery\")\n        self.use_gallery_link = QCheckBox('Use currently applied gallery URL')\n        self.use_gallery_link.setToolTip(\"Metadata will be fetched from the current gallery URL\"+\n                                   \" if it's a supported gallery url\")\n        web_metadata_m_l.addRow(use_gallery_link_info)\n        web_metadata_m_l.addRow(self.use_gallery_link)\n        fallback_source_info = QLabel(\"Specify which sources metadata fetcher should fallback to when a gallery is not found.\")\n        fallback_source_l = FlowLayout()\n        web_metadata_m_l.addRow(fallback_source_info)\n        web_metadata_m_l.addRow(fallback_source_l)\n        self.fallback_chaika = QCheckBox(\"panda.chaika.moe\")\n        fallback_source_l.addWidget(self.fallback_chaika)\n\n\n        # Visual\n        visual = QTabWidget(self)\n        self.visual_index = self.right_panel.addWidget(visual)\n        visual_general_page = QWidget()\n        visual.addTab(visual_general_page, 'General')\n\n        # grid view\n        grid_view_general_page, grid_view_layout = new_tab(\"Grid View\", visual, True)\n        # grid view / popup\n        grid_popup, grid_popup_l = groupbox(\"Popup\", QFormLayout, grid_view_general_page)\n        grid_view_layout.addRow(grid_popup)\n        self.g_popup_width = QSpinBox(grid_popup)\n        self.g_popup_width.setRange(200, 100000)\n        self.g_popup_width.setFixedWidth(120)\n        grid_popup_l.addRow(\"Popup Width:\", self.g_popup_width)\n        self.g_popup_height = QSpinBox(grid_popup)\n        self.g_popup_height.setRange(100, 1000000)\n        self.g_popup_height.setFixedWidth(120)\n        grid_popup_l.addRow(\"Popup Height:\", self.g_popup_height)\n\n        # grid view / tooltip\n        self.grid_tooltip_group = QGroupBox('Tooltip', grid_view_general_page)\n        self.grid_tooltip_group.setCheckable(True)\n        grid_view_layout.addRow(self.grid_tooltip_group)\n        grid_tooltip_layout = QFormLayout()\n        self.grid_tooltip_group.setLayout(grid_tooltip_layout)\n        grid_tooltip_layout.addRow(QLabel('Control what is'+\n                                    ' displayed in the tooltip when hovering a gallery'))\n        grid_tooltips_hlayout = FlowLayout()\n        grid_tooltip_layout.addRow(grid_tooltips_hlayout)\n        self.visual_grid_tooltip_title = QCheckBox('Title')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_title)\n        self.visual_grid_tooltip_author = QCheckBox('Author')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_author)\n        self.visual_grid_tooltip_chapters = QCheckBox('Chapters')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_chapters)\n        self.visual_grid_tooltip_status = QCheckBox('Status')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_status)\n        self.visual_grid_tooltip_type = QCheckBox('Type')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_type)\n        self.visual_grid_tooltip_lang = QCheckBox('Language')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_lang)\n        self.visual_grid_tooltip_descr = QCheckBox('Description')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_descr)\n        self.visual_grid_tooltip_tags = QCheckBox('Tags')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_tags)\n        self.visual_grid_tooltip_last_read = QCheckBox('Last read')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_last_read)\n        self.visual_grid_tooltip_times_read = QCheckBox('Times read')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_times_read)\n        self.visual_grid_tooltip_pub_date = QCheckBox('Publication Date')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_pub_date)\n        self.visual_grid_tooltip_date_added = QCheckBox('Date added')\n        grid_tooltips_hlayout.addWidget(self.visual_grid_tooltip_date_added)\n        # grid view / gallery\n        grid_gallery_group = QGroupBox('Gallery', grid_view_general_page)\n        grid_view_layout.addRow(grid_gallery_group)\n        grid_gallery_main_l = QFormLayout()\n        grid_gallery_main_l.setFormAlignment(Qt.AlignLeft)\n        grid_gallery_group.setLayout(grid_gallery_main_l)\n        grid_gallery_display = FlowLayout()\n        grid_gallery_main_l.addRow('Display on gallery:', grid_gallery_display)\n        self.gallery_rating = QCheckBox('Rating')\n        grid_gallery_display.addWidget(self.gallery_rating)\n        self.gallery_type_ico = QCheckBox('File Type')\n        grid_gallery_display.addWidget(self.gallery_type_ico)\n        if sys.platform.startswith('darwin'):\n            self.gallery_rating.setEnabled(False)\n            self.gallery_type_ico.setEnabled(False)\n        gallery_text_mode = QWidget()\n        grid_gallery_main_l.addRow('Text Mode:', gallery_text_mode)\n        gallery_text_mode_l = QHBoxLayout()\n        gallery_text_mode.setLayout(gallery_text_mode_l)\n        self.gallery_text_elide = QRadioButton('Elide text', gallery_text_mode)\n        self.gallery_text_fit = QRadioButton('Fit text', gallery_text_mode)\n        gallery_text_mode_l.addWidget(self.gallery_text_elide, 0, Qt.AlignLeft)\n        gallery_text_mode_l.addWidget(self.gallery_text_fit, 0, Qt.AlignLeft)\n        gallery_text_mode_l.addWidget(Spacer('h'), 1, Qt.AlignLeft)\n        gallery_font = QHBoxLayout()\n        grid_gallery_main_l.addRow('Font:*', gallery_font)\n        self.font_lbl = QLabel()\n        self.font_size_lbl = QSpinBox()\n        self.font_size_lbl.setMaximum(100)\n        self.font_size_lbl.setMinimum(1)\n        self.font_size_lbl.setToolTip('Font size in pixels')\n        choose_font = QPushButton('Choose font')\n        choose_font.clicked.connect(self.choose_font)\n        gallery_font.addWidget(self.font_lbl, 0, Qt.AlignLeft)\n        gallery_font.addWidget(self.font_size_lbl, 0, Qt.AlignLeft)\n        gallery_font.addWidget(choose_font, 0, Qt.AlignLeft)\n        gallery_font.addWidget(Spacer('h'), 1, Qt.AlignLeft)\n\n        class NoWheelSlider(QSlider):\n            def __init__(self, ori, p):\n                super().__init__(ori, p)\n\n            def wheelEvent(self, ev):\n                ev.ignore()\n\n        gallery_size_lbl = QLabel(self)\n        self.gallery_size = NoWheelSlider(Qt.Horizontal, self)\n        self.gallery_size.valueChanged.connect(lambda x: gallery_size_lbl.setText(str(x+2)))\n        self.gallery_size.setMinimum(-2)\n        self.gallery_size.setMaximum(10)\n        self.gallery_size.setSingleStep(1)\n        self.gallery_size.setPageStep(3)\n        self.gallery_size.setTickInterval(1)\n        self.gallery_size.setTickPosition(QSlider.TicksBothSides)\n        self.gallery_size.setToolTip(\"Changes size of grid in gridview. Remember to re-generate thumbnails! DEFAULT=3\")\n        gallery_size_l = QHBoxLayout()\n        gallery_size_l.addWidget(gallery_size_lbl)\n        gallery_size_l.addWidget(self.gallery_size)\n        grid_gallery_main_l.addRow(QLabel(\"Note: A manual re-generation of thumbnails is required. Advanced -> Gallery\"))\n        grid_gallery_main_l.addRow(\"Thumbnail Size:*\", gallery_size_l)\n        self.grid_spacing = QSpinBox(self)\n        self.grid_spacing.setMinimum(1)\n        self.grid_spacing.setMaximum(99)\n        self.grid_spacing.setToolTip(\"Changes space between thumbnails in gridview. DEFAULT=15\")\n        self.grid_spacing.adjustSize()\n        self.grid_spacing.setFixedWidth(self.grid_spacing.width())\n        grid_gallery_main_l.addRow(\"Spacing:*\", self.grid_spacing)\n\n        # grid view / colors\n        grid_colors_group = QGroupBox('Colors', grid_view_general_page)\n        grid_view_layout.addRow(grid_colors_group)\n        grid_colors_l = QFormLayout()\n        grid_colors_group.setLayout(grid_colors_l)\n        def color_lineedit():\n            l = QLineEdit()\n            l.setPlaceholderText('Hex colors. Eg.: #323232')\n            l.setMaximumWidth(200)\n            return l\n\n        self.grid_label_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            hex_color=app_constants.GRID_VIEW_LABEL_COLOR)\n        grid_colors_l.addRow('Label color:', hbox_layout)\n        self.grid_title_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            hex_color=app_constants.GRID_VIEW_TITLE_COLOR)\n        grid_colors_l.addRow('Title color:', hbox_layout)\n        self.grid_artist_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            hex_color=app_constants.GRID_VIEW_ARTIST_COLOR)\n        grid_colors_l.addRow('Artist color:', hbox_layout)\n\n        # grid view / colors / ribbon\n        self.colors_ribbon_group, colors_ribbon_l = groupbox('Ribbon', QFormLayout, grid_colors_group)\n        self.colors_ribbon_group.setCheckable(True)\n        grid_colors_l.addRow(self.colors_ribbon_group)\n\n        self.ribbon_manga_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n             app_constants.GRID_VIEW_T_MANGA_COLOR)\n        colors_ribbon_l.addRow('Manga', hbox_layout)\n        self.ribbon_doujin_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_DOUJIN_COLOR)\n        colors_ribbon_l.addRow('Doujinshi', hbox_layout)\n        self.ribbon_artist_cg_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_ARTIST_CG_COLOR)\n        colors_ribbon_l.addRow('Artist CG', hbox_layout)\n        self.ribbon_game_cg_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_GAME_CG_COLOR)\n        colors_ribbon_l.addRow('Game CG', hbox_layout)\n        self.ribbon_western_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_WESTERN_COLOR)\n        colors_ribbon_l.addRow('Western', hbox_layout)\n        self.ribbon_image_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_IMAGE_COLOR)\n        colors_ribbon_l.addRow('Image', hbox_layout)\n        self.ribbon_non_h_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_NON_H_COLOR)\n        colors_ribbon_l.addRow('Non-H', hbox_layout)\n        self.ribbon_cosplay_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_COSPLAY_COLOR)\n        colors_ribbon_l.addRow('Cosplay', hbox_layout)\n        self.ribbon_other_color, hbox_layout = self._get_color_line_edit_and_hbox_layout(\n            app_constants.GRID_VIEW_T_OTHER_COLOR)\n        colors_ribbon_l.addRow('Other', hbox_layout)\n\n        # Style\n        style_page = QWidget(self)\n        visual.addTab(style_page, 'Style')\n        visual.setTabEnabled(0, False)\n        visual.setTabEnabled(2, False)\n        visual.setCurrentIndex(1)\n\n        # Advanced\n        advanced = QTabWidget(self)\n        self.advanced_index = self.right_panel.addWidget(advanced)\n        advanced_misc_scroll = QScrollArea(self)\n        advanced_misc_scroll.setBackgroundRole(QPalette.Base)\n        advanced_misc_scroll.setWidgetResizable(True)\n        advanced_misc = QWidget()\n        advanced_misc_scroll.setWidget(advanced_misc)\n        advanced.addTab(advanced_misc_scroll, 'Misc')\n        advanced_misc_main_layout = QVBoxLayout()\n        advanced_misc.setLayout(advanced_misc_main_layout)\n        misc_controls_layout = QFormLayout()\n        advanced_misc_main_layout.addLayout(misc_controls_layout)\n\n        high_dpi_info = QLabel(\"Warning: This option may incur some scaling or painting artifacts\")\n        misc_controls_layout.addRow(high_dpi_info)\n        self.force_high_dpi_support = QCheckBox(\"Force High DPI support *\", self)\n        misc_controls_layout.addRow(self.force_high_dpi_support)\n\n        external_view_group, external_view_l = groupbox(\"External Viewer Arguments\", QFormLayout, advanced)\n        misc_controls_layout.addRow(external_view_group)\n        external_viewer_info = QLabel(app_constants.EXTERNAL_VIEWER_INFO)\n        external_viewer_info.setWordWrap(True)\n        self.external_viewer_args = QLineEdit(advanced)\n        external_view_l.addRow(\"Available tokens:\", external_viewer_info)\n        external_view_l.addRow(\"Arguments:\", self.external_viewer_args)\n\n        # Advanced / Misc / Grid View\n        misc_gridview = QGroupBox('Grid View')\n        misc_controls_layout.addRow(misc_gridview)\n        misc_gridview_layout = QFormLayout()\n        misc_gridview.setLayout(misc_gridview_layout)\n        # Advanced / Misc / Grid View / scroll speed\n        scroll_speed_spin_box = QSpinBox()\n        scroll_speed_spin_box.setFixedWidth(60)\n        scroll_speed_spin_box.setToolTip('Control the speed when scrolling in'+\n                                   ' grid view. DEFAULT: 7')\n        scroll_speed_spin_box.setValue(self.scroll_speed)\n        def scroll_speed(v): self.scroll_speed = v\n        scroll_speed_spin_box.valueChanged[int].connect(scroll_speed)\n        misc_gridview_layout.addRow('Scroll speed:', scroll_speed_spin_box)\n        # Advanced / Misc / Grid View / cache size\n        cache_size_spin_box = QSpinBox()\n        cache_size_spin_box.setFixedWidth(120)\n        cache_size_spin_box.setMaximum(999999999)\n        cache_size_spin_box.setToolTip('This can greatly reduce lags/freezes in the grid view.' +\n                                 ' Increase the value if you experience lag when scrolling'+\n                                 ' through galleries. DEFAULT: 200 MiB')\n        def cache_size(c): self.cache_size = (self.cache_size[0], c)\n        cache_size_spin_box.setValue(self.cache_size[1])\n        cache_size_spin_box.valueChanged[int].connect(cache_size)\n        misc_gridview_layout.addRow('Cache Size (MiB):', cache_size_spin_box)\n\n        # Advanced / Gallery\n        advanced_gallery, advanced_gallery_m_l = new_tab('Gallery', advanced)\n        def rebuild_thumbs():\n            confirm_msg = QMessageBox(QMessageBox.Question, '', 'Are you sure you want to regenerate your thumbnails.',\n                             QMessageBox.Yes | QMessageBox.No, self)\n            if confirm_msg.exec() == QMessageBox.Yes:\n                clear_cache_confirm = QMessageBox(QMessageBox.Question, '',\n                                      'Do you want to delete all old thumbnails before regenerating?', QMessageBox.Yes | QMessageBox.No,\n                                      self)\n                clear_cache = False\n                if clear_cache_confirm.exec() == QMessageBox.Yes:\n                    clear_cache = True\n                app_spinner = misc.Spinner(self.parent_widget)\n                app_spinner.set_size(60)\n                app_spinner.set_text(\"Thumbnails\")\n                app_spinner.admin_db = gallerydb.AdminDB()\n                app_spinner.admin_db.moveToThread(app_constants.GENERAL_THREAD)\n                app_spinner.admin_db.DONE.connect(app_spinner.admin_db.deleteLater)\n                app_spinner.admin_db.DONE.connect(app_spinner.before_hide)\n                self.init_gallery_rebuild.connect(app_spinner.admin_db.rebuild_thumbs)\n                self.init_gallery_rebuild.emit(clear_cache)\n                app_spinner.show()\n\n        rebuild_thumbs_info = QLabel(\"Clears thumbnail cache and rebuilds it, which can take a while. Tip: Useful when changing thumbnail size.\")\n        rebuild_thumbs_btn = QPushButton('Regenerate Thumbnails')\n        rebuild_thumbs_btn.adjustSize()\n        rebuild_thumbs_btn.setFixedWidth(rebuild_thumbs_btn.width())\n        rebuild_thumbs_btn.clicked.connect(rebuild_thumbs)\n        advanced_gallery_m_l.addRow(rebuild_thumbs_info)\n        advanced_gallery_m_l.addRow(rebuild_thumbs_btn)\n        g_data_fixer_group, g_data_fixer_l =  groupbox('Gallery Renamer', QFormLayout, advanced_gallery)\n        g_data_fixer_group.setEnabled(False)\n        advanced_gallery_m_l.addRow(g_data_fixer_group)\n        g_data_regex_fix_lbl = QLabel(\"Rename a gallery through regular expression.\"+\n                                \" A regex cheatsheet is located at About -> Regex Cheatsheet.\")\n        g_data_regex_fix_lbl.setWordWrap(True)\n        g_data_fixer_l.addRow(g_data_regex_fix_lbl)\n        self.g_data_regex_fix_edit = QLineEdit()\n        self.g_data_regex_fix_edit.setPlaceholderText(\"Valid regex\")\n        g_data_fixer_l.addRow('Regex:', self.g_data_regex_fix_edit)\n        self.g_data_replace_fix_edit = QLineEdit()\n        self.g_data_replace_fix_edit.setPlaceholderText(\"Leave empty to delete matches\")\n        g_data_fixer_l.addRow('Replace with:', self.g_data_replace_fix_edit)\n        g_data_fixer_options = FlowLayout()\n        g_data_fixer_l.addRow(g_data_fixer_options)\n        self.g_data_fixer_title = QCheckBox(\"Title\", g_data_fixer_group)\n        self.g_data_fixer_artist = QCheckBox(\"Artist\", g_data_fixer_group)\n        g_data_fixer_options.addWidget(self.g_data_fixer_title)\n        g_data_fixer_options.addWidget(self.g_data_fixer_artist)\n\n        # Advanced / Database\n        advanced_db_page, advanced_db_page_l = new_tab('Database', advanced)\n        # Advanced / Database / Import/Export\n        def init_export():\n            confirm_msg = QMessageBox(QMessageBox.Question, '', 'Are you sure you want to export your database?',\n                             QMessageBox.Yes | QMessageBox.No, self)\n            if confirm_msg.exec() == QMessageBox.Yes:\n                app_popup = AppDialog(self.parent_widget)\n                app_popup.info_lbl.setText(\"Exporting database...\")\n                app_popup.export_instance = io_misc.ImportExport()\n                app_popup.export_instance.moveToThread(app_constants.GENERAL_THREAD)\n                app_popup.export_instance.finished.connect(app_popup.export_instance.deleteLater)\n                app_popup.export_instance.finished.connect(app_popup.close)\n                app_popup.export_instance.amount.connect(app_popup.prog.setMaximum)\n                app_popup.export_instance.progress.connect(app_popup.prog.setValue)\n                self.init_gallery_eximport.connect(app_popup.export_instance.export_data)\n                self.init_gallery_eximport.emit(None)\n                app_popup.adjustSize()\n                app_popup.show()\n                self.close()\n\n        def init_import():\n            path = QFileDialog.getOpenFileName(self,\n                                      'Choose happypanda database file', filter='*.hpdb')\n            path = path[0]\n            if len(path) != 0:\n                app_popup = AppDialog(self.parent_widget)\n                app_popup.restart_info.hide()\n                app_popup.info_lbl.setText(\"Importing database file...\")\n                app_popup.note_info.setText(\"Application requires a restart after importing\")\n                app_popup.import_instance = io_misc.ImportExport()\n                app_popup.import_instance.moveToThread(app_constants.GENERAL_THREAD)\n                app_popup.import_instance.finished.connect(app_popup.import_instance.deleteLater)\n                app_popup.import_instance.finished.connect(app_popup.init_restart)\n                app_popup.import_instance.amount.connect(app_popup.prog.setMaximum)\n                app_popup.import_instance.imported_g.connect(app_popup.info_lbl.setText)\n                app_popup.import_instance.progress.connect(app_popup.prog.setValue)\n                self.init_gallery_eximport.connect(app_popup.import_instance.import_data)\n                self.init_gallery_eximport.emit(path)\n                app_popup.adjustSize()\n                app_popup.show()\n                self.close()\n\n        advanced_impexp, advanced_impexp_l = groupbox('Import/Export', QFormLayout, advanced_db_page)\n        advanced_db_page_l.addRow(advanced_impexp)\n        self.export_format = QComboBox(advanced_db_page)\n        #self.export_format.addItem('Text File', 0)\n        self.export_format.addItem('HPDB', 1)\n        self.export_format.adjustSize()\n        self.export_format.setFixedWidth(self.export_format.width())\n        advanced_impexp_l.addRow('Export Format:', self.export_format)\n        self.export_path = PathLineEdit(advanced_impexp, filters='')\n        advanced_impexp_l.addRow('Export Path:', self.export_path)\n        import_btn = QPushButton('Import database')\n        import_btn.clicked.connect(init_import)\n        export_btn = QPushButton('Export database')\n        export_btn.clicked.connect(init_export)\n        ex_imp_btn_l = QHBoxLayout()\n        ex_imp_btn_l.addWidget(import_btn)\n        ex_imp_btn_l.addWidget(export_btn)\n        advanced_impexp_l.addRow(ex_imp_btn_l)\n\n\n        # About\n        about = QTabWidget(self)\n        self.about_index = self.right_panel.addWidget(about)\n        about_happypanda_page, about_layout = new_tab(\"About Happypanda\", about, False)\n        info_lbl = QLabel(app_constants.ABOUT)\n        info_lbl.setWordWrap(True)\n        info_lbl.setOpenExternalLinks(True)\n        about_layout.addWidget(info_lbl)\n        about_layout.addWidget(Spacer('v'))\n        open_hp_folder = QPushButton('Open Happypanda Directory')\n        open_hp_folder.clicked.connect(self.open_hp_folder)\n        open_hp_folder.adjustSize()\n        open_hp_folder.setFixedWidth(open_hp_folder.width())\n        about_layout.addWidget(open_hp_folder)\n\n        ## About / DB Overview\n        #about_db_overview, about_db_overview_m_l = new_tab('DB Overview', about)\n        #about_stats_tab_widget = misc_db.DBOverview(self.parent_widget)\n        #about_db_overview_m_l.addRow(about_stats_tab_widget)\n        #about_db_overview.setEnabled(False)\n\n        # About / Troubleshooting\n        about_troubleshoot_page = QWidget()\n        about.addTab(about_troubleshoot_page, 'Bug Reporting')\n        troubleshoot_layout = QVBoxLayout()\n        about_troubleshoot_page.setLayout(troubleshoot_layout)\n        guide_lbl = QLabel(app_constants.TROUBLE_GUIDE)\n        guide_lbl.setTextFormat(Qt.RichText)\n        guide_lbl.setOpenExternalLinks(True)\n        guide_lbl.setWordWrap(True)\n        troubleshoot_layout.addWidget(guide_lbl, 0, Qt.AlignTop)\n        troubleshoot_layout.addWidget(Spacer('v'))\n\n        # About / Search tutorial\n        about_search_tut, about_search_tut_l = new_tab(\"Search Guide\", about, True)\n        g_search_lbl = QLabel(app_constants.SEARCH_TUTORIAL_TAGS)\n        g_search_lbl.setWordWrap(True)\n        about_search_tut_l.addRow(g_search_lbl)\n\n        # About / Regex Cheatsheet\n        about_s_regex, about_s_regex_l = new_tab(\"Regex Cheatsheet\", about, True)\n        reg_info = QLabel(app_constants.REGEXCHEAT)\n        reg_info.setWordWrap(True)\n        about_s_regex_l.addRow(reg_info)\n\n        # About / Keyboard shortcuts\n        about_k_shortcuts, about_k_shortcuts_l = new_tab(\"Keyboard Shortcuts\", about, True)\n        k_short_info = QLabel(app_constants.KEYBOARD_SHORTCUTS_INFO)\n        k_short_info.setWordWrap(True)\n        about_k_shortcuts_l.addRow(k_short_info)\n\n    @staticmethod\n    def _get_color_line_edit_and_hbox_layout(hex_color=None):\n        \"\"\"get ColorLineEdit and hbox layout.\"\"\"\n        color_line_edit = ColorLineEdit(hex_color=hex_color)\n        hbox_layout = QHBoxLayout()\n        hbox_layout.addWidget(color_line_edit)\n        hbox_layout.addWidget(color_line_edit.button)\n        return color_line_edit, hbox_layout\n\n    def add_folder_monitor(self, path=''):\n        if not isinstance(path, str):\n            path = ''\n        l_edit = PathLineEdit()\n        l_edit.setText(path)\n        n = self.folders_layout.rowCount() + 1\n        self.folders_layout.addRow('{}'.format(n), l_edit)\n\n    def add_ignore_path(self, path='', dir=True):\n        if not isinstance(path, str):\n            path = ''\n        l_edit = PathLineEdit(dir=dir)\n        l_edit.setText(path)\n        n = self.ignore_path_l.rowCount() + 1\n        self.ignore_path_l.addRow('{}'.format(n), l_edit)\n\n    def color_checker(self, txt):\n        allow = False\n        if len(txt) == 7:\n            if txt[0] == '#':\n                allow = True\n        return allow\n\n    def take_all_layout_widgets(self, l):\n        n = l.rowCount()\n        items = []\n        for x in range(n):\n            item = l.takeAt(x+1)\n            items.append(item.widget())\n        return items\n\n\n    def choose_font(self):\n        tup = QFontDialog.getFont(self)\n        font = tup[0]\n        if tup[1]:\n            self.font_lbl.setText(font.family())\n            self.font_size_lbl.setValue(font.pointSize())\n\n    def open_hp_folder(self):\n        if os.name == 'posix':\n            utils.open_path(app_constants.posix_program_dir)\n        else:\n            utils.open_path(os.getcwd())\n\n    def reject(self):\n        self.close()\n        \n\n    def _find_combobox_match(self, combobox, key, default):\n        f_index = combobox.findText(key, Qt.MatchFixedString)\n        if f_index != -1:\n            combobox.setCurrentIndex(f_index)\n        else:\n            combobox.setCurrentIndex(default)\n\n\n"
  },
  {
    "path": "version/utils.py",
    "content": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the terms of the GNU General Public License as published by\n#the Free Software Foundation, either version 2 of the License, or\n#any later version.\n#Happypanda is distributed in the hope that it will be useful,\n#but WITHOUT ANY WARRANTY; without even the implied warranty of\n#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#GNU General Public License for more details.\n#You should have received a copy of the GNU General Public License\n#along with Happypanda.  If not, see <http://www.gnu.org/licenses/>.\n#\"\"\"\n\nimport datetime\nimport os\nimport subprocess\nimport sys\nimport logging\nimport zipfile\nimport hashlib\nimport shutil\nimport uuid\nimport re\nimport scandir\nimport rarfile\nimport json\nimport send2trash\nimport functools\nimport time\n\nfrom PyQt5.QtGui import QImage, qRgba\nfrom PIL import Image,ImageChops\n\ntry:\n    import app_constants\n    from database import db_constants\nexcept:\n    from . import app_constants\n    from .database import db_constants\n\nlog = logging.getLogger(__name__)\nlog_i = log.info\nlog_d = log.debug\nlog_w = log.warning\nlog_e = log.error\nlog_c = log.critical\n\nIMG_FILES = ('.jpg','.bmp','.png','.gif', '.jpeg')\nARCHIVE_FILES = ('.zip', '.cbz', '.rar', '.cbr')\nFILE_FILTER = '*.zip *.cbz *.rar *.cbr'\nIMG_FILTER = '*.jpg *.bmp *.png *.jpeg'\nrarfile.PATH_SEP = '/'\nrarfile.UNRAR_TOOL = app_constants.unrar_tool_path\nif not app_constants.unrar_tool_path:\n    FILE_FILTER = '*.zip *.cbz'\n    ARCHIVE_FILES = ('.zip', '.cbz')\n\nclass GMetafile:\n    def __init__(self, path=None, archive=''):\n        self.metadata = {\n            \"title\":'',\n            \"artist\":'',\n            \"type\":'',\n            \"tags\":{},\n            \"language\":'',\n            \"pub_date\":'',\n            \"link\":'',\n            \"info\":'',\n\n            }\n        self.files = []\n        if path is None:\n            return\n        if archive:\n            zip = ArchiveFile(archive)\n            c = zip.dir_contents(path)\n            for x in c:\n                if x.endswith(app_constants.GALLERY_METAFILE_KEYWORDS):\n                    self.files.append(open(zip.extract(x), encoding='utf-8'))\n        else:\n            for p in scandir.scandir(path):\n                if p.name in app_constants.GALLERY_METAFILE_KEYWORDS:\n                    self.files.append(open(p.path, encoding='utf-8'))\n        if self.files:\n            self.detect()\n        else:\n            log_d('No metafile found...')\n\n    def _eze(self, fp):\n        if not fp.name.endswith('.json'):\n            return\n        j = json.load(fp, encoding='utf-8')\n        eze = ['gallery_info', 'image_api_key', 'image_info']\n        # eze\n        if all(x in j for x in eze):\n            log_i('Detected metafile: eze')\n            ezedata = j['gallery_info']\n            t_parser = title_parser(ezedata['title'])\n            self.metadata['title'] = t_parser['title']\n            self.metadata['type'] = ezedata['category']\n            for ns in ezedata['tags']:\n                self.metadata['tags'][ns.capitalize()] = ezedata['tags'][ns]\n            self.metadata['tags']['default'] = self.metadata['tags'].pop('Misc', [])\n            self.metadata['artist'] = self.metadata['tags']['Artist'][0].capitalize()\\\n                if 'Artist' in self.metadata['tags'] else t_parser['artist']\n            self.metadata['language'] = ezedata['language']\n            d = ezedata['upload_date']\n            # should be zero padded\n            d[1] = int(\"0\" + str(d[1])) if len(str(d[1])) == 1 else d[1]\n            d[3] = int(\"0\" + str(d[1])) if len(str(d[1])) == 1 else d[1] \n            self.metadata['pub_date'] = datetime.datetime.strptime(\"{} {} {}\".format(d[0], d[1], d[3]), \"%Y %m %d\")\n            l = ezedata['source']\n            self.metadata['link'] = 'http://' + l['site'] + '.org/g/' + str(l['gid']) + '/' + l['token']\n            return True\n\n    def _hdoujindler(self, fp):\n        \"HDoujin Downloader\"\n        if fp.name.endswith('info.txt'):\n            log_i('Detected metafile: HDoujin text')\n            lines = fp.readlines()\n            if lines:\n                for line in lines:\n                    splitted = line.split(':', 1)\n                    if len(splitted) > 1:\n                        other = splitted[1].strip()\n                        if not other:\n                            continue\n                        l = splitted[0].lower()\n                        if \"title\" == l:\n                            self.metadata['title'] = other\n                        if \"artist\" == l:\n                            self.metadata['artist'] = other.capitalize()\n                        if \"tags\" == l:\n                            self.metadata['tags'].update(tag_to_dict(other))\n                        if \"description\" == l:\n                            self.metadata['info'] = other\n                        if \"circle\" in l:\n                            if not \"group\" in self.metadata['tags']:\n                                self.metadata['tags']['group'] = []\n                                self.metadata['tags']['group'].append(other.strip().lower())\n                        if \"url\" == l:\n                            self.metadata['link'] = other\n                return True\n\n        ## Doesnt work for some reason.. too lazy to debug\n        #elif fp.name.endswith('info.json'):\n        #    log_i('Detected metafile: HDoujin json')\n        #    j = json.load(fp, encoding='utf-8')\n        #    j = j['manga_info']\n        #    self.metadata['title'] = j['title']\n        #    for n, a in enumerate(j['artist']):\n        #        at = a\n        #        if not n+1 == len(j['artist']):\n        #            at += ', '\n        #        self.metadata['artist'] += at\n        #        tags = {}\n        #        for x in j['tags']:\n        #            ns = 'default' if x == 'misc' else x.capitalize()\n        #            tags[ns] = []\n        #            for y in j[tags][x]:\n        #                tags[ns].append(y.strip().lower())\n        #        self.metadata['tags'] = tags\n        #        self.metadata['link'] = j['url']\n        #        self.metadata['info'] = j['description']\n        #        for x in j['circle']:\n        #            if not \"group\" in self.metadata['tags']:\n        #                self.metadata['tags']['group'] = []\n        #                self.metadata['tags']['group'].append(x.strip().lower())\n        #        return True\n\n    def detect(self):\n        for fp in self.files:\n            with fp:\n                z = False\n                for x in [self._eze, self._hdoujindler]:\n                    try:\n                        if x(fp):\n                            z = True\n                            break\n                    except Exception:\n                        log.exception('Error in parsing metafile')\n                        continue\n                if not z:\n                    log_i('Incompatible metafiles found')\n\n    def update(self, other):\n        self.metadata.update((x, y) for x, y in other.metadata.items() if y)\n\n    def apply_gallery(self, gallery):\n        log_i('Applying metafile to gallery')\n        if self.metadata['title']:\n            gallery.title = self.metadata['title']\n        if self.metadata['artist']:\n            gallery.artist = self.metadata['artist']\n        if self.metadata['type']:\n            gallery.type = self.metadata['type']\n        if self.metadata['tags']:\n            gallery.tags = self.metadata['tags']\n        if self.metadata['language']:\n            gallery.language = self.metadata['language']\n        if self.metadata['pub_date']:\n            gallery.pub_date = self.metadata['pub_date']\n        if self.metadata['link']:\n            gallery.link = self.metadata['link']\n        if self.metadata['info']:\n            gallery.info = self.metadata['info']\n        return gallery\n\ndef backup_database(db_path=db_constants.DB_PATH):\n    log_i(\"Perfoming database backup\")\n    date = \"{}\".format(datetime.datetime.today()).split(' ')[0]\n    base_path, name = os.path.split(db_path)\n    backup_dir = os.path.join(base_path, 'backup')\n    if not os.path.isdir(backup_dir):\n        os.mkdir(backup_dir)\n    db_name = \"{}-{}\".format(date, name)\n\n    current_try = 0\n    orig_db_name = db_name\n    while current_try < 50:\n        if current_try:\n            db_name = \"{}({})-{}\".format(date, current_try, orig_db_name)\n        try:\n            dst_path = os.path.join(backup_dir, db_name)\n            if os.path.exists(dst_path):\n                raise ValueError\n            shutil.copyfile(db_path, dst_path)\n            break\n        except ValueError:\n            current_try += 1\n    log_i(\"Database backup perfomed: {}\".format(db_name))\n    return True\n\ndef get_date_age(date):\n    \"\"\"\n    Take a datetime and return its \"age\" as a string.\n    The age can be in second, minute, hour, day, month or year. Only the\n    biggest unit is considered, e.g. if it's 2 days and 3 hours, \"2 days\" will\n    be returned.\n    Make sure date is not in the future, or else it won't work.\n    \"\"\"\n\n    def formatn(n, s):\n        '''Add \"s\" if it's plural'''\n\n        if n == 1:\n            return \"1 %s\" % s\n        elif n > 1:\n            return \"%d %ss\" % (n, s)\n\n    def q_n_r(a, b):\n        '''Return quotient and remaining'''\n\n        return a / b, a % b\n\n    class PrettyDelta:\n        def __init__(self, dt):\n            now = datetime.datetime.now()\n\n            delta = now - dt\n            self.day = delta.days\n            self.second = delta.seconds\n\n            self.year, self.day = q_n_r(self.day, 365)\n            self.month, self.day = q_n_r(self.day, 30)\n            self.hour, self.second = q_n_r(self.second, 3600)\n            self.minute, self.second = q_n_r(self.second, 60)\n\n        def format(self):\n            for period in ['year', 'month', 'day', 'hour', 'minute', 'second']:\n                n = getattr(self, period)\n                if n > 0.9:\n                    return formatn(n, period)\n            return \"0 second\"\n\n    return PrettyDelta(date).format()\n\ndef all_opposite(*args):\n    \"Returns true if all items in iterable evaluae to false\"\n    for iterable in args:\n        for x in iterable:\n            if x:\n                return False\n    return True\n\ndef update_gallery_path(new_path, gallery):\n    \"Updates a gallery's chapters path\"\n    for chap in gallery.chapters:\n        head, tail = os.path.split(chap.path)\n        if gallery.path == chap.path:\n            chap.path = new_path\n        elif gallery.path == head:\n            chap.path = os.path.join(new_path, tail)\n\n    gallery.path = new_path\n    return gallery\n\ndef move_files(path, dest='', only_path=False):\n    \"\"\"\n    Move files to a new destination. If dest is not set,\n    imported_galleries_def_path will be used instead.\n    \"\"\"\n    if not dest:\n        dest = app_constants.IMPORTED_GALLERY_DEF_PATH\n        if not dest:\n            return path\n    f = os.path.split(path)[1]\n    new_path = os.path.join(dest, f)\n    if not only_path:\n        log_i(\"Moving to: {}\".format(new_path))\n    if new_path == os.path.join(*os.path.split(path)): # need to unpack to make sure we get the corrct sep\n        return path\n    if not os.path.exists(new_path):\n        app_constants.TEMP_PATH_IGNORE.append(os.path.normcase(new_path))\n        if not only_path:\n            new_path = shutil.move(path, new_path)\n    else:\n        return path\n    return new_path\n\ndef check_ignore_list(key):\n    k = os.path.normcase(key)\n    if os.path.isdir(key) and 'Folder' in app_constants.IGNORE_EXTS:\n        return False\n    _, ext = os.path.splitext(key)\n    if ext in app_constants.IGNORE_EXTS:\n        return False\n    for path in app_constants.IGNORE_PATHS:\n        p = os.path.normcase(path)\n        if p in k:\n            return False\n    return True\n\ndef gallery_text_fixer(gallery):\n    regex_str = app_constants.GALLERY_DATA_FIX_REGEX\n    if regex_str:\n        try:\n            valid_regex = re.compile(regex_str)\n        except re.error:\n            return None\n        if not valid_regex:\n            return None\n\n        def replace_regex(text):\n            new_text = re.sub(regex_str, app_constants.GALLERY_DATA_FIX_REPLACE, text)\n            return new_text\n\n        if app_constants.GALLERY_DATA_FIX_TITLE:\n            gallery.title = replace_regex(gallery.title)\n        if app_constants.GALLERY_DATA_FIX_ARTIST:\n            gallery.artist = replace_regex(gallery.artist)\n\n        return gallery\n\ndef b_search(data, key):\n    if key:\n        lo = 0\n        hi = len(data) - 1\n        while hi >= lo:\n            mid = lo + (hi - lo) // 2\n            if data[mid] < key:\n                lo = mid + 1\n            elif data[mid] > key:\n                hi = mid - 1\n            else:\n                return data[mid]\n    return None\n\ndef generate_img_hash(src):\n    \"\"\"\n    Generates sha1 hash based on the given bytes.\n    Returns hex-digits\n    \"\"\"\n    chunk = 8129\n    sha1 = hashlib.sha1()\n    buffer = src.read(chunk)\n    log_d(\"Generating hash\")\n    while len(buffer) > 0:\n        sha1.update(buffer)\n        buffer = src.read(chunk)\n    return sha1.hexdigest()\n\nclass ArchiveFile():\n    \"\"\"\n    Work with archive files, raises exception if instance fails.\n    namelist -> returns a list with all files in archive\n    extract <- Extracts one specific file to given path\n    open -> open the given file in archive, returns bytes\n    close -> close archive\n    \"\"\"\n    zip, rar = range(2)\n    def __init__(self, filepath):\n        self.type = 0\n        try:\n            if filepath.endswith(ARCHIVE_FILES):\n                if filepath.endswith(ARCHIVE_FILES[:2]):\n                    self.archive = zipfile.ZipFile(os.path.normcase(filepath))\n                    b_f = self.archive.testzip()\n                    self.type = self.zip\n                elif filepath.endswith(ARCHIVE_FILES[2:]):\n                    self.archive = rarfile.RarFile(os.path.normcase(filepath))\n                    b_f = self.archive.testrar()\n                    self.type = self.rar\n\n                # test for corruption\n                if b_f:\n                    log_w('Bad file found in archive {}'.format(filepath.encode(errors='ignore')))\n                    raise app_constants.CreateArchiveFail\n            else:\n                log_e('Archive: Unsupported file format')\n                raise app_constants.CreateArchiveFail\n        except:\n            log.exception('Create archive: FAIL')\n            raise app_constants.CreateArchiveFail\n\n    def namelist(self):\n        filelist = self.archive.namelist()\n        return filelist\n\n    def is_dir(self, name):\n        \"\"\"\n        Checks if the provided name in the archive is a directory or not\n        \"\"\"\n        if not name:\n            return False\n        if not name in self.namelist():\n            log_e('File {} not found in archive'.format(name))\n            raise app_constants.FileNotFoundInArchive\n        if self.type == self.zip:\n            if name.endswith('/'):\n                return True\n        elif self.type == self.rar:\n            info = self.archive.getinfo(name)\n            return info.isdir()\n        return False\n\n    def dir_list(self, only_top_level=False):\n        \"\"\"\n        Returns a list of all directories found recursively. For directories not in toplevel\n        a path in the archive to the diretory will be returned.\n        \"\"\"\n        \n        if only_top_level:\n            if self.type == self.zip:\n                return [x for x in self.namelist() if x.endswith('/') and x.count('/') == 1]\n            elif self.type == self.rar:\n                potential_dirs = [x for x in self.namelist() if x.count('/') == 0]\n                return [x.filename for x in [self.archive.getinfo(y) for y in potential_dirs] if x.isdir()]\n        else:\n            if self.type == self.zip:\n                return [x for x in self.namelist() if x.endswith('/') and x.count('/') >= 1]\n            elif self.type == self.rar:\n                return [x.filename for x in self.archive.infolist() if x.isdir()]\n\n    def dir_contents(self, dir_name):\n        \"\"\"\n        Returns a list of contents in the directory\n        An empty string will return the contents of the top folder\n        \"\"\"\n        if dir_name and not dir_name in self.namelist():\n            log_e('Directory {} not found in archive'.format(dir_name))\n            raise app_constants.FileNotFoundInArchive\n        if not dir_name:\n            if self.type == self.zip:\n                con = [x for x in self.namelist() if x.count('/') == 0 or \\\n                    (x.count('/') == 1 and x.endswith('/'))]\n            elif self.type == self.rar:\n                con = [x for x in self.namelist() if x.count('/') == 0]\n            return con\n        if self.type == self.zip:\n            dir_con_start = [x for x in self.namelist() if x.startswith(dir_name)]\n            return [x for x in dir_con_start if x.count('/') == dir_name.count('/') and \\\n                (x.count('/') == dir_name.count('/') and not x.endswith('/')) or \\\n                (x.count('/') == 1 + dir_name.count('/') and x.endswith('/'))]\n        elif self.type == self.rar:\n            return [x for x in self.namelist() if x.startswith(dir_name) and \\\n                x.count('/') == 1 + dir_name.count('/')]\n        return []\n\n    def extract(self, file_to_ext, path=None):\n        \"\"\"\n        Extracts one file from archive to given path\n        Creates a temp_dir if path is not specified\n        Returns path to the extracted file\n        \"\"\"\n        if not path:\n            path = os.path.join(app_constants.temp_dir, str(uuid.uuid4()))\n            os.mkdir(path)\n\n        if not file_to_ext:\n            return self.extract_all(path)\n        else:\n            if self.type == self.zip:\n                membs = []\n                for name in self.namelist():\n                    if name.startswith(file_to_ext) and name != file_to_ext:\n                        membs.append(name)\n                temp_p = self.archive.extract(file_to_ext, path)\n                for m in membs:\n                    self.archive.extract(m, path)\n            elif self.type == self.rar:\n                temp_p = os.path.join(path, file_to_ext)\n                self.archive.extract(file_to_ext, path)\n            return temp_p\n\n    def extract_all(self, path=None, member=None):\n        \"\"\"\n        Extracts all files to given path, and returns path\n        If path is not specified, a temp dir will be created\n        \"\"\"\n        if not path:\n            path = os.path.join(app_constants.temp_dir, str(uuid.uuid4()))\n            os.mkdir(path)\n        if member:\n            self.archive.extractall(path, member)\n        self.archive.extractall(path)\n        return path\n\n    def open(self, file_to_open, fp=False):\n        \"\"\"\n        Returns bytes. If fp set to true, returns file-like object.\n        \"\"\"\n        if fp:\n            return self.archive.open(file_to_open)\n        else:\n            return self.archive.open(file_to_open).read()\n\n    def close(self):\n        self.archive.close()\n\ndef check_archive(archive_path):\n    \"\"\"\n    Checks archive path for potential galleries.\n    Returns a list with a path in archive to galleries\n    if there is no directories\n    \"\"\"\n    try:\n        zip = ArchiveFile(archive_path)\n    except app_constants.CreateArchiveFail:\n        return []\n    if not zip:\n        return []\n    galleries = []\n    zip_dirs = zip.dir_list()\n    def gallery_eval(d):\n        con = zip.dir_contents(d)\n        if con:\n            gallery_probability = len(con)\n            for n in con:\n                if not n.lower().endswith(IMG_FILES):\n                    gallery_probability -= 1\n            if gallery_probability >= (len(con) * 0.8):\n                return d\n    if zip_dirs: # There are directories in the top folder\n        # check parent\n        r = gallery_eval('')\n        if r:\n            galleries.append('')\n        for d in zip_dirs:\n            r = gallery_eval(d)\n            if r:\n                galleries.append(r)\n        zip.close()\n    else: # all pages are in top folder\n        if isinstance(gallery_eval(''), str):\n            galleries.append('')\n        zip.close()\n\n    return galleries\n\ndef recursive_gallery_check(path):\n    \"\"\"\n    Recursively checks a folder for any potential galleries\n    Returns a list of paths for directories and a list of tuples where first\n    index is path to gallery in archive and second index is path to archive.\n    Like this:\n    [\"C:path/to/g\"] and [(\"path/to/g/in/a\", \"C:path/to/a\")]\n    \"\"\"\n    gallery_dirs = []\n    gallery_arch = []\n    found_paths = 0\n    for root, subfolders, files in scandir.walk(path):\n        if files:\n            for f in files:\n                if f.endswith(ARCHIVE_FILES):\n                    arch_path = os.path.join(root, f)\n                    for g in check_archive(arch_path):\n                        found_paths += 1\n                        gallery_arch.append((g, arch_path))\n                                    \n            if not subfolders:\n                if not files:\n                    continue\n                gallery_probability = len(files)\n                for f in files:\n                    if not f.lower().endswith(IMG_FILES):\n                        gallery_probability -= 1\n                if gallery_probability >= (len(files) * 0.8):\n                    found_paths += 1\n                    gallery_dirs.append(root)\n    log_i('Found {} in {}'.format(found_paths, path).encode(errors='ignore'))\n    return gallery_dirs, gallery_arch\n\ndef today():\n    \"Returns current date in a list: [dd, Mmm, yyyy]\"\n    _date = datetime.date.today()\n    day = _date.strftime(\"%d\")\n    month = _date.strftime(\"%b\")\n    year = _date.strftime(\"%Y\")\n    return [day, month, year]\n\ndef external_viewer_checker(path):\n    check_dict = app_constants.EXTERNAL_VIEWER_SUPPORT\n    viewer = os.path.split(path)[1]\n    for x in check_dict:\n        allow = False\n        for n in check_dict[x]:\n            if viewer.lower() in n.lower():\n                allow = True\n                break\n        if allow:\n            return x\n\ndef open_chapter(chapterpath, archive=None):\n    is_archive = True if archive else False\n    if not is_archive:\n        chapterpath = os.path.normpath(chapterpath)\n    temp_p = archive if is_archive else chapterpath\n\n    custom_args = app_constants.EXTERNAL_VIEWER_ARGS\n    send_folder_t = '{$folder}'\n    send_image_t = '{$file}'\n\n    send_folder = True\n\n    if app_constants.USE_EXTERNAL_VIEWER:\n        send_folder = True\n\n    if custom_args:\n        if send_folder_t in custom_args:\n            send_folder = True\n        elif send_image_t in custom_args:\n            send_folder = False\n\n    def find_f_img_folder():\n        filepath = os.path.join(temp_p, [x for x in sorted([y.name for y in scandir.scandir(temp_p)])\\\n            if x.lower().endswith(IMG_FILES) and not x.startswith('.')][0]) # Find first page\n        return temp_p if send_folder else filepath\n\n    def find_f_img_archive(extract=True):\n        zip = ArchiveFile(temp_p)\n        if extract:\n            app_constants.NOTIF_BAR.add_text('Extracting...')\n            t_p = os.path.join('temp', str(uuid.uuid4()))\n            os.mkdir(t_p)\n            if is_archive or chapterpath.endswith(ARCHIVE_FILES):\n                if os.path.isdir(chapterpath):\n                    t_p = chapterpath\n                elif chapterpath.endswith(ARCHIVE_FILES):\n                    zip2 = ArchiveFile(chapterpath)\n                    f_d = sorted(zip2.dir_list(True))\n                    if f_d:\n                        f_d = f_d[0]\n                        t_p = zip2.extract(f_d, t_p)\n                    else:\n                        t_p = zip2.extract('', t_p)\n                else:\n                    t_p = zip.extract(chapterpath, t_p)\n            else:\n                zip.extract_all(t_p) # Compatibility reasons..  TODO: REMOVE IN BETA\n            if send_folder:\n                filepath = t_p\n            else:\n                filepath = os.path.join(t_p, [x for x in sorted([y.name for y in scandir.scandir(t_p)])\\\n                    if x.lower().endswith(IMG_FILES) and not x.startswith('.')][0]) # Find first page\n                filepath = os.path.abspath(filepath)\n        else:\n            if is_archive or chapterpath.endswith(ARCHIVE_FILES):\n                con = zip.dir_contents('')\n                f_img = [x for x in sorted(con) if x.lower().endswith(IMG_FILES) and not x.startswith('.')]\n                if not f_img:\n                    log_w('Extracting archive.. There are no images in the top-folder. ({})'.format(archive))\n                    return find_f_img_archive()\n                filepath = os.path.normpath(archive)\n            else:\n                app_constants.NOTIF_BAR.add_text(\"Fatal error: Unsupported gallery!\")\n                raise ValueError(\"Unsupported gallery version\")\n        return filepath\n\n    try:\n        try: # folder\n            filepath = find_f_img_folder()\n        except NotADirectoryError: # archive\n            try:\n                if not app_constants.EXTRACT_CHAPTER_BEFORE_OPENING and app_constants.EXTERNAL_VIEWER_PATH:\n                    filepath = find_f_img_archive(False)\n                else:\n                    filepath = find_f_img_archive()\n            except app_constants.CreateArchiveFail:\n                log.exception('Could not open chapter')\n                app_constants.NOTIF_BAR.add_text('Could not open chapter. Check happypanda.log for more details.')\n                return\n    except FileNotFoundError:\n        log.exception('Could not find chapter {}'.format(chapterpath))\n        app_constants.NOTIF_BAR.add_text(\"Chapter does no longer exist!\")\n        return\n    except IndexError:\n        log.exception('No images found: {}'.format(chapterpath))\n        app_constants.NOTIF_BAR.add_text(\"No images found in chapter!\")\n        return\n\n    if send_folder_t in custom_args:\n        custom_args = custom_args.replace(send_folder_t, filepath)\n    elif send_image_t in custom_args:\n        custom_args = custom_args.replace(send_image_t, filepath)\n    else:\n        custom_args = filepath\n\n    try:\n        app_constants.NOTIF_BAR.add_text('Opening chapter...')\n        if not app_constants.USE_EXTERNAL_VIEWER:\n            if sys.platform.startswith('darwin'):\n                subprocess.call(('open', custom_args))\n            elif os.name == 'nt':\n                os.startfile(custom_args)\n            elif os.name == 'posix':\n                subprocess.call(('xdg-open', custom_args))\n        else:\n            ext_path = app_constants.EXTERNAL_VIEWER_PATH\n            viewer = external_viewer_checker(ext_path)\n            if viewer == 'honeyview':\n                if app_constants.OPEN_GALLERIES_SEQUENTIALLY:\n                    subprocess.call((ext_path, custom_args))\n                else:\n                    subprocess.Popen((ext_path, custom_args))\n            else:\n                if app_constants.OPEN_GALLERIES_SEQUENTIALLY:\n                    subprocess.check_call((ext_path, custom_args))\n                else:\n                    subprocess.Popen((ext_path, custom_args))\n    except subprocess.CalledProcessError:\n        app_constants.NOTIF_BAR.add_text(\"Could not open chapter. Invalid external viewer.\")\n        log.exception('Could not open chapter. Invalid external viewer.')\n    except:\n        app_constants.NOTIF_BAR.add_text(\"Could not open chapter for unknown reasons. Check happypanda.log!\")\n        log_e('Could not open chapter {}'.format(os.path.split(chapterpath)[1]))\n\ndef get_gallery_img(gallery_or_path, chap_number=0):\n    \"\"\"\n    Returns a path to image in gallery chapter\n    \"\"\"\n    archive = None\n    if isinstance(gallery_or_path, str):\n        path = gallery_or_path\n    else:\n        path = gallery_or_path.chapters[chap_number].path\n        if gallery_or_path.is_archive:\n            archive = gallery_or_path.path\n\n    # TODO: add chapter support\n    try:\n        name = os.path.split(path)[1]\n    except IndexError:\n        name = os.path.split(path)[0]\n    is_archive = True if archive or name.endswith(ARCHIVE_FILES) else False\n    real_path = archive if archive else path\n    img_path = None\n    if is_archive:\n        try:\n            log_i('Getting image from archive')\n            zip = ArchiveFile(real_path)\n            temp_path = os.path.join(app_constants.temp_dir, str(uuid.uuid4()))\n            os.mkdir(temp_path)\n            if not archive:\n                f_img_name = sorted([img for img in zip.namelist() if img.lower().endswith(IMG_FILES) and not img.startswith('.')])[0]\n            else:\n                f_img_name = sorted([img for img in zip.dir_contents(path) if img.lower().endswith(IMG_FILES) and not img.startswith('.')])[0]\n            img_path = zip.extract(f_img_name, temp_path)\n            zip.close()\n        except app_constants.CreateArchiveFail:\n            img_path = app_constants.NO_IMAGE_PATH\n    elif os.path.isdir(real_path):\n        log_i('Getting image from folder')\n        first_img = sorted([img.name for img in scandir.scandir(real_path) if img.name.lower().endswith(tuple(IMG_FILES)) and not img.name.startswith('.')])\n        if first_img:\n            img_path = os.path.join(real_path, first_img[0])\n\n    if img_path:\n        return os.path.abspath(img_path)\n    else:\n        log_e(\"Could not get gallery image\")\n\ndef tag_to_string(gallery_tag, simple=False):\n    \"\"\"\n    Takes gallery tags and converts it to string, returns string\n    if simple is set to True, returns a CSV string, else a dict-like string\n    \"\"\"\n    assert isinstance(gallery_tag, dict), \"Please provide a dict like this: {'namespace':['tag1']}\"\n    string = \"\"\n    if not simple:\n        for n, namespace in enumerate(sorted(gallery_tag), 1):\n            if len(gallery_tag[namespace]) != 0:\n                if namespace != 'default':\n                    string += namespace + \":\"\n\n                # find tags\n                if namespace != 'default' and len(gallery_tag[namespace]) > 1:\n                    string += '['\n                for x, tag in enumerate(sorted(gallery_tag[namespace]), 1):\n                    # if we are at the end of the list\n                    if x == len(gallery_tag[namespace]):\n                        string += tag\n                    else:\n                        string += tag + ', '\n                if namespace != 'default' and len(gallery_tag[namespace]) > 1:\n                    string += ']'\n\n                # if we aren't at the end of the list\n                if not n == len(gallery_tag):\n                    string += ', '\n    else:\n        for n, namespace in enumerate(sorted(gallery_tag), 1):\n            if len(gallery_tag[namespace]) != 0:\n                if namespace != 'default':\n                    string += namespace + \",\"\n\n                # find tags\n                for x, tag in enumerate(sorted(gallery_tag[namespace]), 1):\n                    # if we are at the end of the list\n                    if x == len(gallery_tag[namespace]):\n                        string += tag\n                    else:\n                        string += tag + ', '\n\n                # if we aren't at the end of the list\n                if not n == len(gallery_tag):\n                    string += ', '\n\n    return string\n\ndef tag_to_dict(string, ns_capitalize=True):\n    \"Receives a string of tags and converts it to a dict of tags\"\n    namespace_tags = {'default':[]}\n    level = 0 # so we know if we are in a list\n    buffer = \"\"\n    stripped_set = set() # we only need unique values\n    for n, x in enumerate(string, 1):\n\n        if x == '[':\n            level += 1 # we are now entering a list\n        if x == ']':\n            level -= 1 # we are now exiting a list\n\n\n        if x == ',': # if we meet a comma\n            # we trim our buffer if we are at top level\n            if level is 0:\n                # add to list\n                stripped_set.add(buffer.strip())\n                buffer = \"\"\n            else:\n                buffer += x\n        elif n == len(string): # or at end of string\n            buffer += x\n            # add to list\n            stripped_set.add(buffer.strip())\n            buffer = \"\"\n        else:\n            buffer += x\n\n    def tags_in_list(br_tags):\n        \"Receives a string of tags enclosed in brackets, returns a list with tags\"\n        unique_tags = set()\n        tags = br_tags.replace('[', '').replace(']','')\n        tags = tags.split(',')\n        for t in tags:\n            if len(t) != 0:\n                unique_tags.add(t.strip().lower())\n        return list(unique_tags)\n\n    unique_tags = set()\n    for ns_tag in stripped_set:\n        splitted_tag = ns_tag.split(':')\n        # if there is a namespace\n        if len(splitted_tag) > 1 and len(splitted_tag[0]) != 0:\n            if splitted_tag[0] != 'default':\n                if ns_capitalize:\n                    namespace = splitted_tag[0].capitalize()\n                else:\n                    namespace = splitted_tag[0]\n            else:\n                namespace = splitted_tag[0]\n            tags = splitted_tag[1]\n            # if tags are enclosed in brackets\n            if '[' in tags and ']' in tags:\n                tags = tags_in_list(tags)\n                tags = [x for x in tags if len(x) != 0]\n                # if namespace is already in our list\n                if namespace in namespace_tags:\n                    for t in tags:\n                        # if tag not already in ns list\n                        if not t in namespace_tags[namespace]:\n                            namespace_tags[namespace].append(t)\n                else:\n                    # to avoid empty strings\n                    namespace_tags[namespace] = tags\n            else: # only one tag\n                if len(tags) != 0:\n                    if namespace in namespace_tags:\n                        namespace_tags[namespace].append(tags)\n                    else:\n                        namespace_tags[namespace] = [tags]\n        else: # no namespace specified\n            tag = splitted_tag[0]\n            if len(tag) != 0:\n                unique_tags.add(tag.lower())\n\n    if len(unique_tags) != 0:\n        for t in unique_tags:\n            namespace_tags['default'].append(t)\n\n    return namespace_tags\n\nimport re as regex\ndef title_parser(title):\n    \"Receives a title to parse. Returns dict with 'title', 'artist' and language\"\n    log_d(\"Parsing title: {}\".format(title))\n    title = \" \".join(title.split())\n    if '/' in title:\n        try:\n            title = os.path.split(title)[1]\n            if not title:\n                title = title\n        except IndexError:\n            pass\n\n    for x in ARCHIVE_FILES:\n        if title.endswith(x):\n            title = title[:-len(x)]\n\n    parsed_title = {'title':\"\", 'artist':\"\", 'language':\"\"}\n    try:\n        a = regex.findall('((?<=\\[) *[^\\]]+( +\\S+)* *(?=\\]))', title)\n        assert len(a) != 0\n        try:\n            artist = a[0][0].strip()\n        except IndexError:\n            artist = ''\n        parsed_title['artist'] = artist\n\n        try:\n            assert a[1]\n            lang = app_constants.G_LANGUAGES + app_constants.G_CUSTOM_LANGUAGES\n            for x in a:\n                l = x[0].strip()\n                l = l.lower()\n                l = l.capitalize()\n                if l in lang:\n                    parsed_title['language'] = l\n                    break\n            else:\n                parsed_title['language'] = app_constants.G_DEF_LANGUAGE\n        except IndexError:\n            parsed_title['language'] = app_constants.G_DEF_LANGUAGE\n\n        t = title\n        for x in a:\n            t = t.replace(x[0], '')\n\n        t = t.replace('[]', '')\n        final_title = t.strip()\n        parsed_title['title'] = final_title\n    except AssertionError:\n        parsed_title['title'] = title\n\n    return parsed_title\n\nimport webbrowser\ndef open_web_link(url):\n    if not url:\n        return\n    try:\n        webbrowser.open_new_tab(url)\n    except:\n        log_e('Could not open URL in browser')\n\ndef open_path(path, select=''):\n    \"\"\n    try:\n        if sys.platform.startswith('darwin'):\n            subprocess.Popen(['open', path])\n        elif os.name == 'nt':\n            if select:\n                subprocess.Popen(r'explorer.exe /select,\"{}\"'.format(os.path.normcase(select)), shell=True)\n            else:\n                os.startfile(path)\n        elif os.name == 'posix':\n            subprocess.Popen(('xdg-open', path))\n        else:\n            app_constants.NOTIF_BAR.add_text(\"I don't know how you've managed to do this.. If you see this, you're in deep trouble...\")\n            log_e('Could not open path: no OS found')\n    except:\n        app_constants.NOTIF_BAR.add_text(\"Could not open specified location. It might not exist anymore.\")\n        log_e('Could not open path')\n\ndef open_torrent(path):\n    if not app_constants.TORRENT_CLIENT:\n        open_path(path)\n    else:\n        subprocess.Popen([app_constants.TORRENT_CLIENT, path])\n\ndef delete_path(path):\n    \"Deletes the provided recursively\"\n    s = True\n    if os.path.exists(path):\n        error = ''\n        if app_constants.SEND_FILES_TO_TRASH:\n            try:\n                send2trash.send2trash(path)\n            except:\n                log.exception(\"Unable to send file to trash\")\n                error = 'Unable to send file to trash'\n        else:\n            try:\n                if os.path.isfile(path):\n                    os.remove(path)\n                else:\n                    shutil.rmtree(path)\n            except PermissionError:\n                error = 'PermissionError'\n            except FileNotFoundError:\n                pass\n\n        if error:\n            p = os.path.split(path)[1]\n            log_e('Failed to delete: {}:{}'.format(error, p))\n            app_constants.NOTIF_BAR.add_text('An error occured while trying to delete: {}'.format(error))\n            s = False\n    return s\n\ndef regex_search(a, b, override_case=False, args=[]):\n    \"Looks for a in b\"\n    if a and b:\n        try:\n            if not app_constants.Search.Case in args or override_case:\n                if regex.search(a, b, regex.IGNORECASE):\n                    return True\n            else:\n                if regex.search(a, b):\n                    return True\n        except regex.error:\n            pass\n    return False\n\ndef search_term(a, b, override_case=False, args=[]):\n    \"Searches for a in b\"\n    if a and b:\n        if not app_constants.Search.Case in args or override_case:\n            b = b.lower()\n            a = a.lower()\n\n        if app_constants.Search.Strict in args:\n            if a == b:\n                return True\n        else:\n            if a in b:\n                return True\n    return False\n\ndef get_terms(term):\n    \"Dividies term into pieces. Returns a list with the pieces\"\n\n    # some variables we will use\n    pieces = []\n    piece = ''\n    qoute_level = 0\n    bracket_level = 0\n    brackets_tags = {}\n    current_bracket_ns = ''\n    end_of_bracket = False\n    blacklist = ['[', ']', '\"', ',']\n\n    for n, x in enumerate(term):\n        # if we meet brackets\n        if x == '[':\n            bracket_level += 1\n            brackets_tags[piece] = set() # we want unique tags!\n            current_bracket_ns = piece\n        elif x == ']':\n            bracket_level -= 1\n            end_of_bracket = True\n\n        # if we meet a double qoute\n        if x == '\"':\n            if qoute_level > 0:\n                qoute_level -= 1\n            else:\n                qoute_level += 1\n\n        # if we meet a whitespace, comma or end of term and are not in a double qoute\n        if (x == ' ' or x == ',' or n == len(term) - 1) and qoute_level == 0:\n            # if end of term and x is allowed\n            if (n == len(term) - 1) and not x in blacklist and x != ' ':\n                piece += x\n            if piece:\n                if bracket_level > 0 or end_of_bracket: # if we are inside a bracket we put piece in the set\n                    end_of_bracket = False\n                    if piece.startswith(current_bracket_ns):\n                        piece = piece[len(current_bracket_ns):]\n                    if piece:\n                        try:\n                            brackets_tags[current_bracket_ns].add(piece)\n                        except KeyError: # keyerror when there is a closing bracket without a starting bracket\n                            pass\n                else:\n                    pieces.append(piece) # else put it in the normal list\n            piece = ''\n            continue\n\n        # else append to the buffers\n        if not x in blacklist:\n            if qoute_level > 0: # we want to include everything if in double qoute\n                piece += x\n            elif x != ' ':\n                piece += x\n\n    # now for the bracket tags\n    for ns in brackets_tags:\n        for tag in brackets_tags[ns]:\n            ns_tag = ns\n            # if they want to exlucde this tag\n            if tag[0] == '-':\n                if ns_tag[0] != '-':\n                    ns_tag = '-' + ns\n                tag = tag[1:] # remove the '-'\n\n            # put them together\n            ns_tag += tag\n\n            # done\n            pieces.append(ns_tag)\n\n    return pieces\n\ndef image_greyscale(filepath):\n    \"\"\"\n    Check if image is monochrome (1 channel or 3 identical channels)\n    \"\"\"\n    log_d(\"Checking if img is monochrome: {}\".format(filepath))\n    im = Image.open(filepath).convert(\"RGB\")\n    if im.mode not in (\"L\", \"RGB\"):\n        return False\n\n    if im.mode == \"RGB\":\n        rgb = im.split()\n        if ImageChops.difference(rgb[0],rgb[1]).getextrema()[1] != 0: \n            return False\n        if ImageChops.difference(rgb[0],rgb[2]).getextrema()[1] != 0: \n            return False\n    return True\n\ndef PToQImageHelper(im):\n    \"\"\"\n    The Python Imaging Library (PIL) is\n\n    Copyright © 1997-2011 by Secret Labs AB\n    Copyright © 1995-2011 by Fredrik Lundh\n    \"\"\"\n    def rgb(r, g, b, a=255):\n        \"\"\"(Internal) Turns an RGB color into a Qt compatible color integer.\"\"\"\n        # use qRgb to pack the colors, and then turn the resulting long\n        # into a negative integer with the same bitpattern.\n        return (qRgba(r, g, b, a) & 0xffffffff)\n\n    def align8to32(bytes, width, mode):\n        \"\"\"\n        converts each scanline of data from 8 bit to 32 bit aligned\n        \"\"\"\n\n        bits_per_pixel = {\n            '1': 1,\n            'L': 8,\n            'P': 8,\n        }[mode]\n\n        # calculate bytes per line and the extra padding if needed\n        bits_per_line = bits_per_pixel * width\n        full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8)\n        bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0)\n\n        extra_padding = -bytes_per_line % 4\n\n        # already 32 bit aligned by luck\n        if not extra_padding:\n            return bytes\n\n        new_data = []\n        for i in range(len(bytes) // bytes_per_line):\n            new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + b'\\x00' * extra_padding)\n\n        return b''.join(new_data)\n\n    data = None\n    colortable = None\n\n    # handle filename, if given instead of image name\n    if hasattr(im, \"toUtf8\"):\n        # FIXME - is this really the best way to do this?\n        if str is bytes:\n            im = unicode(im.toUtf8(), \"utf-8\")\n        else:\n            im = str(im.toUtf8(), \"utf-8\")\n    if isinstance(im, (bytes, str)):\n        im = Image.open(im)\n\n    if im.mode == \"1\":\n        format = QImage.Format_Mono\n    elif im.mode == \"L\":\n        format = QImage.Format_Indexed8\n        colortable = []\n        for i in range(256):\n            colortable.append(rgb(i, i, i))\n    elif im.mode == \"P\":\n        format = QImage.Format_Indexed8\n        colortable = []\n        palette = im.getpalette()\n        for i in range(0, len(palette), 3):\n            colortable.append(rgb(*palette[i:i+3]))\n    elif im.mode == \"RGB\":\n        data = im.tobytes(\"raw\", \"BGRX\")\n        format = QImage.Format_RGB32\n    elif im.mode == \"RGBA\":\n        try:\n            data = im.tobytes(\"raw\", \"BGRA\")\n        except SystemError:\n            # workaround for earlier versions\n            r, g, b, a = im.split()\n            im = Image.merge(\"RGBA\", (b, g, r, a))\n        format = QImage.Format_ARGB32\n    else:\n        raise ValueError(\"unsupported image mode %r\" % im.mode)\n\n    # must keep a reference, or Qt will crash!\n    __data = data or align8to32(im.tobytes(), im.size[0], im.mode)\n    return {\n        'data': __data, 'im': im, 'format': format, 'colortable': colortable\n    }\n\ndef make_chapters(gallery_object):\n    chap_container = gallery_object.chapters\n    path = gallery_object.path\n    metafile = GMetafile()\n    try:\n        log_d('Listing dir...')\n        con = scandir.scandir(path) # list all folders in gallery dir\n        log_i('Gallery source is a directory')\n        log_d('Sorting')\n        chapters = sorted([sub.path for sub in con if sub.is_dir() or sub.name.endswith(ARCHIVE_FILES)]) #subfolders\n        # if gallery has chapters divided into sub folders\n        if len(chapters) != 0:\n            log_d('Chapters divided in folders..')\n            for ch in chapters:\n                chap = chap_container.create_chapter()\n                chap.title = title_parser(ch)['title']\n                chap.path = os.path.join(path, ch)\n                metafile.update(GMetafile(chap.path))\n                chap.pages = len([x for x in scandir.scandir(chap.path) if x.name.endswith(IMG_FILES)])\n\n        else: #else assume that all images are in gallery folder\n            chap = chap_container.create_chapter()\n            chap.title = title_parser(os.path.split(path)[1])['title']\n            chap.path = path\n            metafile.update(GMetafile(path))\n            chap.pages = len([x for x in scandir.scandir(path) if x.name.endswith(IMG_FILES)])\n\n    except NotADirectoryError:\n        if path.endswith(ARCHIVE_FILES):\n            gallery_object.is_archive = 1\n            log_i(\"Gallery source is an archive\")\n            archive_g = sorted(check_archive(path))\n            for g in archive_g:\n                chap = chap_container.create_chapter()\n                chap.path = g\n                chap.in_archive = 1\n                metafile.update(GMetafile(g, path))\n                arch = ArchiveFile(path)\n                chap.pages = len(arch.dir_contents(g))\n                arch.close()\n\n    metafile.apply_gallery(gallery_object)\n\ndef timeit(func):\n    @functools.wraps(func)\n    def newfunc(*args, **kwargs):\n        startTime = time.time()\n        func(*args, **kwargs)\n        elapsedTime = time.time() - startTime\n        print('function [{}] finished in {} ms'.format(\n            func.__name__, int(elapsedTime * 1000)))\n    return newfunc\n\n\ndef makedirs_if_not_exists(folder):\n    \"\"\"Create directory if not exists.\n    Args:\n        folder: Target folder.\n    \"\"\"\n    if not os.path.isdir(folder):\n        os.makedirs(folder)\n\ndef lookup_tag(tag):\n    \"Issues a tag lookup on preferred site\"\n    assert isinstance(tag, str), \"str not \" + str(type(tag))\n    # remove whitespace at edges and replace whitespace with +\n    tag = tag.strip().lower().replace(' ', '+')\n    url = app_constants.DEFAULT_EHEN_URL\n    if not url.endswith('/'):\n        url += '/'\n\n    if not ':' in tag:\n        tag = 'misc:' + tag\n\n    url += 'tag/' + tag\n\n    open_web_link(url)"
  }
]