Showing preview only (1,019K chars total). Download the full file or copy to clipboard to get everything.
Repository: Pewpews/happypanda
Branch: master
Commit: 6acf2560404c
Files: 35
Total size: 989.3 KB
Directory structure:
gitextract_sr3txlm2/
├── .gitattributes
├── .gitignore
├── CHANGELOG.md
├── INSTALL.md
├── LICENSE
├── LICENSE-3RD-PARTY
├── README.rst
├── VS.txt
├── requirements-dev.txt
├── requirements.txt
├── res/
│ ├── license.txt
│ └── style.css
├── tests/
│ ├── database/
│ │ └── test_db.py
│ └── test_utils.py
└── version/
├── app.py
├── app_constants.py
├── asm_manager.py
├── color_line_edit.py
├── database/
│ ├── __init__.py
│ ├── db.py
│ └── db_constants.py
├── executors.py
├── fetch.py
├── gallery.py
├── gallerydb.py
├── gallerydialog.py
├── hplugins.py
├── io_misc.py
├── main.py
├── misc.py
├── misc_db.py
├── pewnet.py
├── settings.py
├── settingsdialog.py
└── utils.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# custom
*cache
*.pyproj
*Qt*
*__pycache__*
version/build
version/dist
*.sln
*Thumbs.db
settings.json
build
dist
Happypanda
setup.py
*.ini
*.swp
.DS_Store
downloads
.happypanda
# pycharm
.idea/
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Roslyn cache directories
*.ide/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# If using the old MSBuild-Integrated Package Restore, uncomment this:
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
/db
/thumbnails
/version/gui/static/gallery_def_ico.ico
/version/gui/static/gallery_ext_ico.ico
*.pyperf
/temp
/main.build
/main.dist
/version/db
/test_g.zip
/version/temp
/version/modeltest.py
/[Yamatogawa] Power Play!.zip
/test.zip
/test.py
/setup.spec
/roundcorner.png
/nuitka.txt
/models.py
/info.txt
/info.json
/horopic2.jpg
/happypanda-2015-11-10 23-51-59.hpdb
/happypanda-2015-11-10 21-29-04.hpdb
/happypanda-2015-11-09 21-08-42.hpdb
/failg.zip
/exHtmlRandomFileName123.html
/deploy.bat
/Attameaikko.01.zip
/[Yamatogawa] Power Play!.zip
/[Yamatogawa] Power Play!.zip
*.zip
/res/typicons
/res/gallery_ext_ico.ico
/res/sample.png
*.hpdb
/plugins
/compilerconfig.json
/compilerconfig.json.defaults
/happypanda.VC.db
/happypanda.VC.VC.opendb
/.vs/config/applicationhost.config
/data/happypanda.db
/main.spec
================================================
FILE: CHANGELOG.md
================================================
*Happypanda v1.1*
- Fixes
- Fixed HP settings unusable without internet connection
*Happypanda v1.0*
* New stuff
- New GUI look
- New helpful color widgets added to `Settings -> Visual` [rachmadaniHaryono]
- Gallery Contextmenu:
- Added `Set rating` to quickly set gallery rating
- Added `Lookup Artist` to open a new tab on preffered site with the artist's galleries
- Added `Reset read count` under `Advanced`
- Gallery Lists are now included when exporting gallery data
- New sorting option: Gallery Rating
- It is now possible to also append tags to galleries instead of replacing when editing
- New gallery download source: `asmhentai.com` [rachmadaniHaryono]
- New [special namespaced tag](https://github.com/Pewpews/happypanda/wiki/Gallery-Searching#special-namespaced-tags): `URL`
- Use like this: `url:none` or `url:g.e-hentai`
- Many quality of life changes
* Changed stuff
- `g.e-hentai` to `e-hentai`
- Old URLs will automatically be converted to new on metadata fetch
- Displaying rating on galleries is now optional
- Improved search history
- Improved gallery downloader (now very reliable) [rachmadaniHaryono]
- Galleries will automatically be rated 5 stars on favorite
- Gallery List edit popup will now appear in the middle of the application
- Added a way to relogin into website
* Fixes
- E-Hentai login & gallery downloading
- `date added` metadata wasn't included when exporting gallery data
- `last read` Metadata wasn't included when importing gallery data
- backup database name would get unusually long [rachmadaniHaryono]
- Fixed HDoujin `info.txt` parsing
- Newly downloaded galleries would sometimes cause a crash
- Attempting to download exhentai links without being logged in would cause a crash
- Using the random gallery opener would in rare cases cause a crash
- Moving a gallery would cause a crash from a raised PermissionError exception
- Fetching metadata for galleries would return multiple unrelated galleries to choose among
- Fetching metadata for galleries with a colored cover whose gallery source is an archive would sometimes cause a crash
- Galleries with an empty tag field wouldn't show up on a `tag:none` filter search
- Gallery Deleted popup would appear when deleting gallery files from the application
- Attempting to download removed galleries would cause a crash
- Some gallery importing issues
*Happypanda v0.30*
- Someone finally convinced me into adding star ratings
- *Note:* Ratings won't be fetched from EH since I find them useless... Though I might make it an option later on.
- External viewer icon on galleries has been removed in favor of this
- Visual make-over
- Improved how thumbnails are loaded in gridview
- Moving files into a monitored folder will now automatically add the galleries into your inbox
- Added the following special namespaced tags:
- `path:none` to filter galleries that has a broken source
- `rating:integer` to filter galleries that has been rated `integer`
- read more about them [here](https://github.com/Pewpews/happypanda/wiki/Gallery-Searching)
- Updated DB to version 0.26
- Added `Rating` metadata
- Fixed bugs:
- Attempting to add galleries on a fresh install was causing an exception
- Moving files into a monitored folder, and then accepting the pop-up would cause an exception
*Happypanda v0.29*
- Increased and improved stability and perfomance
- Shortened startup time
- Galleries are now added dynamically
- New feature: Tabs
- New inbox tab for new gallery additions
- Checking for duplicates will also make a new tab
- Gallery deletion will now process smoothly
- It is now possible to edit multiple galleries
- Type and Langauge in metadata popup window are now clickable to issue a search
- Updated DB to version 0.25
- Added views in series table
- Visual changes in gridview
- Added a recently added indicator
- Gallery filetypes will now be displayed with text
- Added new options in settings
- Removed option: autoadd scanned galleries
- Fixed bugs:
+ Fixed some metadata fetchings bugs
+ Fixed database import and export issues
+ Closing gallery metadata popup window caused an exception
+ Fetching metadata with no internet connection caused an exception
+ Invalid folders/archives were being picked up by the monitor
+ Fixed default language issues
+ Metadata would sometimes fail when doing a filesearch
+ Thumbnail cache dir was not being cleared
+ Adding from directory was not possible with single gallery add method
*Happypanda v0.28.1*
- Fixed bugs:
+ Fixed typo in external viewer args
+ Fixed regex not working when in namespace
+ Fixed thumbnail generation causing an unhandled exception
+ Moved directories kept their old path
+ Fixed auto metdata fetcher failing when mixing galleries with colored covers and galleries with greyscale covers
+ Gallery Metadata window wouldn't stay open
+ Fixed a DB bug causing all kinds of errors, including:
+ Editing a gallery while fetching its metadata would cause an exception
+ Closing the gallery dialog while fetching metadata would cause an exception
*Happypanda v0.28*
- Improved perfomance of grid view significantly
- Galleries are now draggable
+ It is now possible to add galleries to a list by dragging them to the list
- Improved metadata fetching accuracy from EH
- Improved gallery lists with new options
+ It is now possible to enable several search options on per gallerylist basis
+ A new *Enforce* option to enforce the gallerylist filter
- Improved gallery search
+ New special namespaced tags: `read_count`, `date_added` and `last_read`
+ Read more about them in the gallery searching guide
+ New `<` less than and `>` greater than operator to be used with some special namespaced tags
+ Read about it in the special namespaced tags section in the gallery searching guide
- Brought back the old way of gaining access to EX
- Only to be used if you can't gain access to EX by logging in normally
- Added ability to specify arguments sent to viewer in settings (in the `Advanced` section)
+ If when opening a gallery only the first image was viewable by your viewer, try change the arguments sent to the viewer
- Updated the database to version 0.24 with the addition of new gallerylist fields
- Moved regex search option to searchbar
- Added grid spacing option in settings (`Visual->Grid View`)
- Added folder and file extensions ignoring in settings (`Application->Ignore`)
+ Folder and file extensions ignoring will work for the directory monitor and *Add gallery..* and *Populate from folder* gallery adding methods
- Added new default language: Chinese
- Improved and fixed URL parser in gallery-downloader
- Custom languages will now be parsed from filenames along with the default languages
- Tags are now sorted alphabetically everywhere
- Gallerylists in contextmenu are also now sorted
- Reason for why metdata fecthing failed is now shown in the failed-to-get-metadata-gallery popup
- The current search term will now be upkeeped (upkept?) when switching between views
- Disabled some tray messages on linux to prevent crash
- The current gallerylist context will now be shown on the statusbar
- The keys `del` and `shift + del` are now bound to gallery deletion
- Added *exclude/include in auto metadata fetcher* in contextmenu for selection
- Bug fixes:
+ No thumbnails were created for images with incorrect extensions (namely png images with .jpg extension)
+ Only accounts with access to EX were able to login
+ Some filesystem events were not being detected
+ Name parser was not parsing languages
+ Some gallery attributes to not be added to the db on initial gallery creation
+ Attempting to fetch metadata while an instance of auto metadata fetcher was already running caused an exception
+ Gallery wasn't removed in view when removing from the duplicate-galleries popup
+ Other minor bugs
*Happypanda v0.27*
- Many visual changes
+ Including new ribbon indicating gallery type in gridview
- New sidebar widget:
+ New feature: Gallery lists
+ New feature: Artists list
+ Moved *NS & Tags* treelist from settings to sidebar widget
- Metadata fetcher:
+ Galleries with multiple hits found will now come last in the fetching process
+ Added fallback system to fetch metadata from other sources than EH
+ Currently supports panda.chaika.moe
- Gallery downloader should now be more tolerant to mistakes in URLs
- Added a "gallery source is missing" indicator in grid view
- Removed EH member_id and pass_hash in favor for EH login method
- Added new sort option: *last read*
- Added option to exclude/include gallery from auto metadata fetcher in the contextmenu
- Added general key shortcuts (read about the not so obvious shortcuts [here](https://github.com/Pewpews/happypanda/wiki/Keyboard-Shortcuts))
- Added support for new metafile: *HDoujin downloader*'s default into.txt file
- Added support for panda.chaika.moe URLs when fetching metadata
- Updated database to version 0.23:
- Gallery lists addition
- New unique indexes in some tables
- Thumbnail paths are now relative (removing the need to rebuild thumbs when moving Happypanda folder)
- Settings:
+ Added option to force support for high DPI displays
+ Added option to control the gallery size in grid view
+ Enabled most *Gallery* options in the *Visual* section for OSX
+ Added options to customize gallery type ribbon colors
+ Added options to set default gallery values
+ Added a way to add custom languages in settings
+ Added option to send deleted files to recycle bin
+ Added option to hide the sidebar widget on startup
- Bug fixes:
+ Fixed a bug causing some external viewers to only be able to view the first image
+ Fixed metadata disappearance bug (hopefully, for real this time!)
+ Fixed decoding issues preventing some galleries from getting imported
+ Fixed lots of critical database issues requiring a rebuild for updating users
+ Fixed gallery downloading from g.e-hentai
+ Fixed bug causing "Show in library" to not work properly
+ Fixed a bug causing a hang while fetching metadata
+ Fixed a bug causing autometadata fetcher to sometimes fail fetching for some galleries
+ Fixed hand when checking for duplicates
+ Fixed database rebuild issues
+ Potentially fixed a bug preventing archives from being imported, courtesy of KuroiKitsu
+ Many other minor bugs
*Happypanda v0.26*
- Startup is now slighty faster
- New redesigned gallery metadata window!
+ New chapter view in the metadata window
+ Artist field is now clickable to issue a search for galleries with same artist
- Some GUI changes
- New advanced gallery search **(make sure to read the search guide found in `Settings -> About -> Search Guide`)**
+ Case sensitive searching
+ Whole terms match searching
+ Terms excluding
+ New special namespaced tags (Read about them in `Settings -> About -> Search Guide`)
- New import/export database feature found in `Settings -> About -> Database`
- Added new column in `Skipped paths` window to show what reason caused a file to be skipped
- Gallery downloader
+ Added new batch urls window to gallery downloader
+ Gallery downloading from `panda.chaika.moe` is now using its new api
+ Added context menu's to download items
+ Added download progress on download items
+ Doubleclicking on finished download items will open its containing folder
- Added autocomplete on the artist field in gallery edit dialog
- Activated the `last read` attribute on galleries
- Improved hash generation
- Introducing metafiles:
+ Files containing gallery metadata in same folder/archive is now detected on import
+ Only supports [eze](https://github.com/dnsev-h/eze)'s `info.json` files for now
- Settings
+ Moved alot of options around. **Note: Some options will be reset**
+ Reworded some options and fixed typos
+ Enabled the `Database` tab in *About* section with import/export database feature
- Updated the database to version 0.22
+ Database will now be backed up before upgrading
- Clicking on the tray icon ballon will now activate Happypanda
- Thumbnail regenerating
+ Added confirmation popup when about to regenerate thumbnails
+ Application restart is no longer required after regenerating thumbnails
+ Added confirmation popup asking about if the thumbnail cache should be cleaned before regenerating
- Renamed `Random Gallery Opener` to `Open random gallery` and also moved it to the Gallery menu on the toolbar
- `Open random gallery` will now only pick a random gallery in current view.
+ *E.g. switching to the favorite view will make it pick a random gallery among the favorites*
- Fixed bugs:
+ Fixed a bug causing archives downloaded from g.e/ex to fail when trying to add to library
+ Fixed a bug where fetching galleries from the database would sometimes throw an exception
+ Fixed a bug causing people running from source to never see the new update notification
+ Fixed some popup blur bug
+ Fixed an annoyance where the text cursor would always move to the end when searching
+ Fixed a bug where `Show in Folder` and `Open folder/archive` in gallery context menu was doing the same thing
+ Fixed a bug where tags fetched from chaika included underscores
+ Fixed bug where the notification widget would sometimes not show messages
+ Fixed bug where chapters added to gallery with directory source would not open correctly
*Happypanda v0.25*
- Added *Show in folder* entry in gallery contextmenu
- Gallery popups
+ A contextmenu will now be shown when you rightclick a gallery
+ Added *Skip* button in the metadata gallery chooser popup (the one asking you which gallery you want to extract metadata from)
+ The text in metadata gallery chooser popups will now wrap
+ Added tooltips displaying title and artist when hovering galleries in some popups
- Settings
+ A new button allowing you to recreate your thumbnail cache is now in *Settings* -> *Advanced* -> *Gallery*
+ Added new tab *Downloader* in *Web* section
+ Renamed *General* tab in *Web* section to *Metadata*
+ Some options in settings will now show a tooltip explaining the option on hover
- 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)
+ 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*)
+ Added *Use current gallery link* checkbox option in *Web* section
- Toolbar
+ Renamed *Misc* to *Tools*
+ New *Scan for new galleries* entry in *Gallery*
+ New *Gallery Downloader* entry in *Tools*
- Gallery downloading
+ Supports archive and torrent downloading
+ archives will be automatically imported while torrents will be sent to your torrent client
+ Currently supports ex/g.e gallery urls and panda.chaika.moe gallery/archive urls
- 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.
- Tray icon
+ You can now manually check for a new update by right clicking on the tray icon
+ Triggering the tray icon, i.e. clicking on it, will now activate (showing it) the Happypanda window
- Fixed bugs:
+ Fixed a bug where skipped galleries/paths would get moved
+ 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
+ Fixed a bug where you couldn't search with the Regex option turned on
+ Fixed a bug where changing gallery covers would fail if the previous cover was not deleted or found.
+ Fixed a bug where non-existent monitored folders were not detected
+ Fixed a bug in the user settings (*settings.ini*) parsing, hence the reset
+ Fixed other minor misc. bugs
*Happypanda v0.24.1*
- Fixed bugs:
+ Removing a gallery and its files should now work
+ Popups was staying on top of all windows
*Happypanda v0.24*
- Mostly gui fixes/improvements
+ Changed toolbar style and icons
+ Added new native spinners
+ Added spinner for the metadata fetching process
+ Added spinner for initial load
+ Added spinner for DB activity
+ Removed sort contextmenu and added it to the toolbar
+ Removed some space around galleries in grid view
+ Added kinetic scrolling when scrolling with middlemouse button
- New DB Overview window and tab in settings dialog
+ you can now see all namespaces and tags in the `Namespace and Tags` tab
- Pressing the return-key will now open selected galleries
- New options in settings dialog
+ Make extracting archives before opening optional in `Application -> General`
+ Open chapters sequentially or all at once in `Application -> General`
- Added a confirmation when closing while there is still DB activity to avoid data loss
- Added log file rotation
+ When happypanda.log reaches `10 mb` a new file will be made (rotating between 3 files)
- Fixed bugs:
+ Temporarily fixed a critical bug where galleries wouldn't load
+ Fixed a bug where the tray icon would stay even after closing the application
+ Fixed a bug where clicking on a tag with no namespace in the Gallery Metdata Popup would search the tag with a blank namespace
+ Fixed a minor bug where when opening the settings dialog a small window would appear first in a split second
*Happypanda v0.23*
- Stability and perfomance increase for very large libraries
+ Instant startup: Galleries are now lazily loaded
+ Application now supports very large galleries (tested with 10k galleries)
+ Gallery searching will now scale with amount of galleries (means, no freezes when searching)
+ Same with adding new galleries.
- The gallery window appearing when you click on a gallery is now interactable
+ Clicking on a link will open it in your default browser
+ Clicking on a tag will search for the tag
- Added some animation and a spinner
- Fixed bugs:
+ Fixed critical bug where slected galleries were not mapped properly. (Which sometimes resulted in wrong galleries being removed)
+ 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
+ Fixed a bug where the notificationbar would sometiems not hide itself
+ & other minor bugs
*Happypanda v0.22*
- Added support for .rar files.
+ To enable rar support, specify the path to unrar in Settings -> Application -> General. Follow the instructions for your OS.
- Fixed most (if not all) gallery importing issues
- Added a way to populate form archive.
+ Note: Subfolders will always be treated as galleries when populating from an archive.
- Fixed a bug where users who tries Happypanda for the first time would see the 'rebuilding galleries' dialog.
- & other misc. changes
*Happypanda v0.21*
- The application will now ask if you want to view skipped paths after searching for galleries
- Added 'delete successful' in the notificationbar
- Bugfixes:
+ Fixed critical bug: Could not open chapters
+ If your gallery still won't open then please try re-adding the gallery.
+ Fixed bug: Covers for archives with no folder in-between were not being found
+ & other minor bugs
*Happypanda v0.20*
- Added support for recursively importing of galleries (applies to archives)
+ Directories in archives will now be noticed when importing
+ Directories with archives as chapters will now be properly imported
- Added drag and drop feature for directories and archives
- Galleries that was unsuccesful during gallery fetching will now be displayed in a popup
- Added support for directory or archive ignoring
- Added support for changing gallery covers
- Added: move imported galleries to a specified folder feature
- Increased speed of Populate from folder and Add galleries...
- Improved title parser to now remove unneecessary whitespaces
- Improved gallery hashing to avoid unnecessary hashing
- Added 'Add archive' button in chapter dialog
- Popups will now center on parent window correctly
+ It is now possible to move popups by leftclicking and dragging
+ Added background blur effect when popups are shown
- The rebuild galleries popup will now show real progress
- Settings:
+ Added new option: Treat subfolders as galleries
+ Added new option: Move imported galleries
+ Added new option: Scroll to new galleries (disabled)
+ Added new option: Open random gallery chapters
+ Added new option: Rename gallery source (disabled)
+ Added new tab in Advanced section: Gallery
+ Added new options: Gallery renamer (disabled)
+ Added new tab in Application section: Ignore
+ Enabled General tab in Application section
+ Reenabled Display on gallery options
- Contextmenu:
+ When selecting more galleries only options that apply to selected galleries will be shown
+ It is now possible to favourite/Unfavourite selected galleries
+ Reenabled removing of selected galleries
+ Added: Advanced and Change cover
- Updated database to version 0.2
- Bugfixes:
+ Fixed critical bug: not being able to add chapters
+ Fixed bug: removing a chapter would always remove the first chapter
+ Fixed bug: fetched metadata title and artist would not be formatted correctly
+ & other minor bugs
*Happypanda v0.19*
- Improved stability
- Updated and fixed auto metadata fetcher:
+ Now twice as fast
+ No more need to restart application because it froze
+ Updated to support namespace fetching directly from the official API
- Improved tag autocompletion in gallery dialog
- Added a system tray to notify you about events such as auto metadata fetcher being done
- Sorting:
+ Added a new sort option: Publication Date
+ Added an indicator to the current sort option.
+ Your current sort option will now be saved
+ Increased pecision of date added
- Settings:
+ Added new options:
* Continue auto metadata fetcher from where it left off
* Use japanese title
+ Enabled option:
* Auto add new galleries on startup
+ Removed options:
* HTML Parsing or API
- Bugfixes:
+ Fixed critical bug: Fetching metadata from exhentai not working
+ Fixed critical bug: Duplicates were being created in database
+ Fixed a bug causing the update checker to always fail.
*Happypanda v0.18*
- Greatly improved stability
- Added numbers to show how many galleries are left when fetching for metadata
- Possibly fixed a bug causing the *"big changes are about to occur"* popup to never disappear
- Fixed auto metadata fetcher (did not work before)
*Happypanda v0.17*
- Improved UI
- Improved stability
- Improved the toolbar
- + Added a way to find duplicate galleries
+ Added a random gallery opener
+ Added a way to fetch metadata for all your galleries
- Added a way to automagically fetch metadata from g.e-/exhentai
+ Fetching metadata is now safer, and should not get you banned
- Added a new sort option: Date added
- Added a place for gallery hashes in the database
- Added folder monitoring support
+ You will now be informed when you rename, remove or add a gallery source in one of your monitored folders
+ The application will scan for new galleries in all of your monitored folders on startup
- Added a new section in settings dialog: Application
+ Added new options in settings dialog
+ Enabled the 'General' tab in the Web section
- Bugfixes:
+ Fixed a bug where you could only open the first chapter of a gallery
+ Fixed a bug causing the application to crash when populating new galleries
+ Fixed some issues occuring when adding archive files
+ Fixed some issues occuring when editing galleries
+ other small bugfixes
- Disabled gallery source type and external program viewer icons because of memory leak (will be reenabled in a later version)
- Cleaned up some code
*Happypanda v0.16*
- A more proper way to search for namespace and tags is now available
- Added support for external image viewers
- Added support for CBZ
- The settings button will now open up a real settings dialog
+ Tons of new options are now available in the settings dialog
- Restyled the grid view
- Restyled the tooltip to now show other metadata in grid view
- Added troubleshoot, regex and search guides
- Fixed bugs:
+ Application crashing when adding a gallery
+ Application crashing when refreshing
+ Namespace & tags not being shown correctly
+ & other small bugs
*Happypanda v0.15*
- More options are now available in contextmenu when rightclicking a gallery
- It's now possible to add and remove chapters from a gallery
- Added a way to select more galleries
+ More options are now available in contextmenu for selected galleries
- Added more columns to tableview
+ Language
+ Link
+ Chapters
- Tweaked the grid view to reduce the lag when scrolling
- Added 1 more way to add galleries
- Already exisiting galleries will now be ignored
- Database will now try to auto update to newest version
- Updated Database to version 0.16 (breaking previous versions)
- Bugfixes
*Happypanda v0.14*
- New tableview. Switch easily between grid view and table view with the new button beside the searchbar
- Now able to add and read ZIP archives (You don't need to extract anymore).
+ Added temp folder for when opening a chapter
- Changed icons to white icons
- Added tag autocomplete in series dialog
- Searchbar is now enabled for searching
+ Autocomplete will complete series' titles
+ Search for title or author
+ Tag searching is only partially supported.
- Added sort options in contextmenu
- Title of series is now included in the 'Opening chapter' string
- Happypanda will now check for new version on startup
- Happypanda will now log errors.
+ Added a --debug or -d option to create a detailed log
- Updated Database version to 0.15 (supports 0.14 & 0.13)
+ Now with unique tag mappings
+ A new metadata: times_read
*Happypanda v0.13*
- First public release
================================================
FILE: INSTALL.md
================================================
This guide will show you how to run from source.
A better option is dowloading the latest version
for your OS from
https://github.com/Pewpews/happypanda/releases
If you have any questions, please find me here
[](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.
First make sure you have python of minimum version 3.4 installed.
Download from here https://www.python.org/downloads/
- arch: sudo pacman -S python3
- ubuntu: apt-get install python3
- OSX: see below
*Note: make sure to mark the 'Add to path' checkbox when available on windows*
# Linux
1. Go where you want happypanda to be downloaded (E.g. `cd ~`), and write `git clone https://github.com/Pewpews/happypanda.git`
- If it fails with something like 'unrecognized command 'git'' then do: `sudo pacman -S git` (`apt-get install git` on Ubuntu), and try again
2. Install these dependencies:
- Qt5 (Install this first) >= 5.4
+ `sudo pacman -S qt5-base` (`apt-get install qt5-default` on Ubuntu)
- pip
+ Python 3.4 should've included pip on install. Incase it didn't: `sudo pacman -S python-pip`
+ Enter the happypanda folder and write `pip3 install -r requirements.txt`
- PyQt5
+ I'm pretty sure you can install this through pip3, but if not then just `sudo pacman -S python-pyqt5` on Arch
+ On Ubuntu
- `apt-get install python3-pyqt5`
- `apt-get install PyQt5`
- `apt-get install python3-pyqt5`
- `apt-get install python3-pyqt5.qtsql`
3. In the happypanda directory go to the *version* directory and write `python3 main.py`
4. The program should now be running
# Windows
1. Go to the frontpage of the happypanda repo and click Download Zip
2. Extract to desired location
3. Install these dependencies:
- Qt5 (Install this first) >= 5.4
+ Download from https://www.qt.io/download-open-source/#section-2
- pip
+ Python 3.4 should've included pip on install. Incase it didn't https://pip.pypa.io/en/latest/installing.html
Make sure python is in your PATH. (http://stackoverflow.com/questions/6318156/adding-python-path-on-windows-7)
+ Now open cmd and `cd` to the happypanda folder
+ Write: `pip install -r requirements.txt` and press enter
- PyQt5
+ I'm pretty sure you can install this through pip, but here is the download location
http://www.riverbankcomputing.com/software/pyqt/download5 (see Binary Packages for windows)
4. Finally, write `python main.py` to run the program
5. The program should now be running.
Note: Try renaming the 'main.py' file to 'main.pyw' and then just doubleclick on it to try running without console (not guaranteed to work)
# Mac OS X
(Note: PyQt5 MUST be installed via Homebrew and NOT via Pip)
1. Install Homebrew (this makes everything easier)
- Open Terminal
- Run the following
+ `ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"`
+ `brew update && brew upgrade --all`
2. To install Python3, PyQt5, and sip (*still in Terminal*)
+ `brew install PyQt5`
3. To install other dependencies
- Download HappyPanda
+ Go to github.com/Pewpews/happypanda
+ Press the "Download ZIP" button
+ UnZip happypanda-master.zip
- In Terminal, navigate to the happypanda-master folder (E.g.: `cd /where/ever/you/put/the/folder/happypanda-master`)
+ Write `pip3 install -r requirements.txt`
5. Running HappyPanda
- Run the following
+ `cd /where/ever/you/put/the/folder/happypanda-master/version`
+ (For example `cd /Users/username/Downloads/happypanda-master/version`)
+ `python3 main.py`
================================================
FILE: LICENSE
================================================
Happypanda is a cross platform manga/doujinshi manager with namespace & tag support;
Copyright (C) 2016 Pewpews
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
================================================
FILE: LICENSE-3RD-PARTY
================================================
-----------------------------------------------------------------------------
The MIT License (MIT)
applies to:
- Beautiful Soup
- Robobrowser
-----------------------------------------------------------------------------
Permission 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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE 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.
-----------------------------------------------------------------------------
Apache License, Version 2.0 (the "License")
applies to:
- requests
- watchdog
-----------------------------------------------------------------------------
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-----------------------------------------------------------------------------
applies to:
- scandir
-----------------------------------------------------------------------------
Copyright (c) 2012, Ben Hoyt
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* 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.
* Neither the name of Ben Hoyt nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS 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.
-----------------------------------------------------------------------------
The ISC License
applies to:
- rarfile
-----------------------------------------------------------------------------
Permission 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.
THE 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.
-----------------------------------------------------------------------------
The GPL v3
applies to:
- PyQt5
-----------------------------------------------------------------------------
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-----------------------------------------------------------------------------
The LGPL
applies to:
- Qt5
-----------------------------------------------------------------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-----------------------------------------------------------------------------
The "BSD" License
applies to:
- Send2Trash
-----------------------------------------------------------------------------
Copyright (c) 2013, Hardcoded Software, http://www.hardcoded.net
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* 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.
* 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.
THIS 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.
---------------------------------------------------------------------------
The Python Imaging Library (PIL) is
Copyright © 1997-2011 by Secret Labs AB
Copyright © 1995-2011 by Fredrik Lundh
---------------------------------------------------------------------------
By 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:
Permission 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.
SECRET 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.
================================================
FILE: README.rst
================================================
Work 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).
===========
Follow me on twitter to keep up to date with HPX:
.. image:: https://img.shields.io/twitter/follow/pewspew.svg?style=social&label=Follow
:target: https://twitter.com/twiddly_
This is a cross platform manga/doujinshi manager with namespace & tag
support.
Features
========
- Portable, self-contained in folder and cross-platform
- Low memory footprint
- Advanced gallery search with regex support (`learn more about it
here <https://github.com/Pewpews/happypanda/wiki/Gallery-Searching>`__)
- Gallery tagging: userdefined namespaces and tags
- Gallery metadata fetching from the web (supports various sources)
- Gallery downloading from the web (supports various sources) \*
- Folder monitoring that'll notify you of filesystem changes
- Multiple ways of adding galleries to make it as convienient as
possible!
- Recursive directory/archive scanning
- Supports ZIP/CBZ, RAR/CBR and directories with loose files
- Very customizable
- And lots more...
\* Gallery downloading from E-Hentai costs Credits/GP
Screenshots
===========
.. image:: https://github.com/Pewpews/happypanda/raw/master/misc/screenshot1.png
:width: 100%
:align: center
.. image:: https://github.com/Pewpews/happypanda/raw/master/misc/screenshot2.png
:width: 100%
:align: center
.. image:: https://github.com/Pewpews/happypanda/raw/master/misc/screenshot3.png
:width: 100%
:align: center
How to install and run
======================
Windows
^^^^^^^
#. Download the archive from
`releases <https://github.com/Pewpews/happypanda/releases>`__
#. Extract the archive to its own folder
#. Find Happypanda.exe and double click on it!
Mac and Linux
^^^^^^^^^^^^^
Install from PYPI or see `INSTALL.md <https://github.com/Pewpews/happypanda/blob/master/INSTALL.md>`__
PYPI
^^^^^^^^^^^^^
``pip install happypanda`` (thanks `@Evolution0 <https://github.com/Evolution0>`__)
and then run with ``happypanda --home``
Note: use of the ``--home`` flag will make happypanda create required files and directories at:
On windows:
``'C:\Users\YourName\AppData\Local\Pewpew\Happypanda'``
On mac:
``'/Users/YourName/Library/Application Support/Happypanda'``
On linux:
``'/home/YourName/.local/share/Happypanda'``
Updating
========
| Overwrite your previous installation.
| More info in the `wiki <https://github.com/Pewpews/happypanda/wiki>`__
PYPI
^^^^^^^^^^^^^
``pip install --upgrade happypanda``
Misc.
=====
For general documentation (how to add galleries and usage of the
search), check the
`wiki <https://github.com/Pewpews/happypanda/wiki>`__.
People wanting to import galleries from the Pururin database torrent
should find `this <https://github.com/Exedge/Convertor>`__ useful.
Dependencies
============
- Qt5 (Install this first) >= 5.4
- PyQt5 (pip)
- requests (pip)
- beautifulsoup4 (pip)
- watchdog (pip)
- scandir (pip)
- rarfile (pip)
- robobrowser (pip)
- Send2Trash (pip)
- Pillow (pip) or PIL
- python-dateutil (pip)
- QtAwesome (pip)
- appdirs (pip)
Contributing
============
Please refer to ``HappypandaX`` instead.
================================================
FILE: VS.txt
================================================
1.1
================================================
FILE: requirements-dev.txt
================================================
-r requirements.txt
pytest==3.0.3
================================================
FILE: requirements.txt
================================================
pyqt5
requests
beautifulsoup4
scandir
rarfile
watchdog
robobrowser
Send2Trash
pillow
python-dateutil
QtAwesome==0.3.3
================================================
FILE: res/license.txt
================================================
btn_star2.png |________________________________________
https://www.iconfinder.com/iconsets/woothemesiconset |
------------------------------------------------------
================================================
FILE: res/style.css
================================================
DoNotDelete {
}
QLabel#author {
font-weight:lighter;
}
AppWindow > QToolBar, QStatusBar, SideBarWidget {
background-color: #2A2D31;
border: none;
}
AppWindow > QToolBar::sunken, AppWindow > QStatusBar::item {
border: none;
}
AppWindow > QStatusBar > QWidget, QStatusBar > QLabel {
color:white;
border: none;
}
Loading > QWidget {
background-color:rgba(0, 0, 0, 0.65);
}
BasePopup QLabel, BaseUserChoice QLabel{
color:white;
}
BasePopup > QFrame, BaseUserChoice > QFrame{
background-color:rgba(0, 0, 0, 0.85);
border-radius: 1em;
}
QScrollBar:vertical {
width:1em;
}
QScrollBar:horizontal {
height:1em;
}
QScrollBar:vertical, QScrollBar:horizontal {
border: 0px solid #2A2D31;
background:none;
margin: 0px 0px 0px 0px;
}
QScrollBar::handle {
background: #2A2D31;
}
QScrollBar::handle:vertical, QScrollBar::handle:horizontal {
min-height: 7em;
}
QScrollBar::add-line:vertical {
background: #2A2D31;
height: 0px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::add-line:horizontal {
background: #2A2D31;
width: 0px;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
background: #2A2D31;
height: 0px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
background: #2A2D31;
width: 0px;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::add-page, QScrollBar::sub-page {
background: none;
}
QMenu {
background-color: #2A2D31;
color: white;
}
QMenu::item:selected {
background-color: #d64933;
}
MangaView {
border: 0px solid;
}
QLabel {
color: #d64933;
}
QPushButton {
border: 1px solid #3E4249;
border-radius: 1px;
padding: 5px;
}
QPushButton, QToolButton {
background-color: #3E4249;
color: white;
}
QPushButton:hover, QToolButton:hover {
border: 1px solid #d64933;
}
QPushButton:pressed, QPushButton:checked {
background-color: #d64933;
}
QToolTip {
border-style: none;
background-color: #2A2D31;
color: white;
}
NotificationOverlay > QLabel {
color: white;
background-color: #3E4249;
border-style: none;
}
TagText {
padding-right: 0.7em;
padding-left: 0.7em;
padding-bottom: 0.2em;
padding-top: 0.1em;
border-radius: 0.45em;
border: 0.1em solid #d64933;
background-color: rgba(62, 66, 73,0.50) !important;
}
ArrowWindow {
background-color: #2A2D31;
color: #d64933;
border: 1px solid #d64933;
border-radius: 5px;
}
GalleryMetaWindow QScrollArea QWidget, QHeaderView::section, QHeaderView::section::checked {
background-color: #2A2D31;
}
SettingsDialog, SettingsDialog QScrollArea QWidget, GalleryDialog, GalleryDialog QScrollArea QWidget {
color: #2A2D31;
}
GalleryDialog QScrollArea QWidget QPushButton, SettingsDialog QScrollArea QWidget QPushButton{
color: white;
}
QHeaderView::section {
color: white;
}
QHeaderView::section {
border-style: none;
padding: 5px;
}
QGroupBox::title, QHeaderView::down-arrow, QHeaderView::up-arrow {
color: #d64933;
}
QListView, QTableView {
border-style: none;
}
QProgressBar {
border: 2px solid #3E4249;
border-radius: 5px;
}
QProgressBar::chunk {
background-color: #d64933;
width: 20px;
}
================================================
FILE: tests/database/test_db.py
================================================
"""test db module."""
from itertools import product
from unittest import mock
import pytest
@pytest.mark.parametrize(
'path_isfile_retval, check_dbv_retval, path_is_dbc_path',
product([False, True], repeat=3)
)
def test_init_db(path_isfile_retval, check_dbv_retval, path_is_dbc_path):
"""test sqlite generation and db creation"""
with mock.patch('version.database.db.db_constants') as m_dbc, \
mock.patch('version.database.db.sqlite3') as m_sl3, \
mock.patch('version.database.db.os') as m_os, \
mock.patch('version.database.db.create_db_path') as m_create_db_path, \
mock.patch('version.database.db.check_db_version') \
as m_check_dbv:
from version.database import db
m_os.path.isfile.return_value = path_isfile_retval
m_check_dbv.return_value = check_dbv_retval
if path_is_dbc_path:
path = m_dbc.DB_PATH
else:
path = mock.Mock()
# run
res = db.init_db(path)
# test
if path_isfile_retval:
if path == m_dbc.DB_PATH and not check_dbv_retval:
m_sl3.assert_has_calls([
mock.call.connect(path, check_same_thread=False),
])
assert res is None
return
else:
m_sl3.assert_has_calls([
mock.call.connect(path, check_same_thread=False),
mock.call.connect().execute('PRAGMA foreign_keys = on')
])
else:
m_create_db_path.assert_called_once_with()
m_sl3.assert_has_calls([
mock.call.connect(path, check_same_thread=False),
mock.call.connect().cursor(),
mock.call.connect().cursor().execute(
'CREATE TABLE IF NOT EXISTS version(version REAL)'),
mock.call.connect().cursor().execute(
'INSERT INTO version(version) VALUES(?)',
(m_dbc.CURRENT_DB_VERSION,)
),
mock.call.connect().cursor().executescript(db.STRUCTURE_SCRIPT),
mock.call.connect().commit(),
mock.call.connect().execute('PRAGMA foreign_keys = on')
])
assert res == m_sl3.connect.return_value
assert res.isolation_level is None
================================================
FILE: tests/test_utils.py
================================================
"""test utils module."""
from unittest import mock
from itertools import product
import pytest
from version.utils import backup_database
@pytest.mark.parametrize(
'mock_exists_retval, mock_isdir_retval',
product([True, False], repeat=2)
)
def test_run_backup_database(mock_exists_retval, mock_isdir_retval):
"""test run with mock obj as input."""
mock_db_path = mock.Mock()
mock_base_path = mock.Mock()
mock_name = mock.Mock()
with mock.patch('version.utils.os') as mock_os, \
mock.patch('version.utils.shutil') as mock_shutil, \
mock.patch('version.utils.datetime') as mock_datetime:
mock_datetime.datetime.today.return_value = '2016-10-25 15:42:47.649416'
mock_os.path.split.return_value = (mock_base_path, mock_name)
mock_os.path.exists.return_value = mock_exists_retval
mock_os.path.isdir.return_value = mock_isdir_retval
res = backup_database(mock_db_path)
assert res
mock_datetime.datetime.today.assert_called_once_with()
os_calls = [
mock.call.path.split(mock_db_path),
mock.call.path.join(mock_base_path, 'backup'),
mock.call.path.isdir(mock_os.path.join.return_value),
mock.call.path.join(
mock_os.path.join.return_value,
"2016-10-25-{}".format(mock_name)),
mock.call.path.exists(mock_os.path.join.return_value),
]
if mock_exists_retval:
if mock_isdir_retval:
assert len(mock_os.mock_calls) == 103
else:
assert len(mock_os.mock_calls) == 104
os_calls.extend([
mock.call.path.join(
mock_os.path.join.return_value,
"2016-10-25(1)-2016-10-25-{}".format(mock_name)),
mock.call.path.join(
mock_os.path.join.return_value,
"2016-10-25(2)-2016-10-25-{}".format(mock_name)),
])
assert not mock_shutil.mock_calls
else:
if mock_isdir_retval:
assert len(mock_os.mock_calls) == 5
else:
assert len(mock_os.mock_calls) == 6
mock_shutil.copyfile.assert_called_once_with(
mock_db_path, mock_os.path.join.return_value)
if mock_isdir_retval:
assert not mock_os.mkdir.called
else:
mock_os.mkdir.assert_called_once_with(mock_os.path.join.return_value)
mock_os.assert_has_calls(os_calls, any_order=True)
================================================
FILE: version/app.py
================================================
#"""
#This file is part of Happypanda.
#Happypanda is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 2 of the License, or
#any later version.
#Happypanda is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with Happypanda. If not, see <http://www.gnu.org/licenses/>.
#"""
import sys
import logging
import os
import threading
import re
import requests
import scandir
import random
import traceback
from PyQt5.QtCore import (Qt, QSize, pyqtSignal, QThread, QEvent, QTimer,
QObject, QPoint, QPropertyAnimation)
from PyQt5.QtGui import (QPixmap, QIcon, QMoveEvent, QCursor,
QKeySequence)
from PyQt5.QtWidgets import (QMainWindow, QListView,
QHBoxLayout, QFrame, QWidget, QVBoxLayout,
QLabel, QStackedLayout, QToolBar, QMenuBar,
QSizePolicy, QMenu, QAction, QLineEdit,
QSplitter, QMessageBox, QFileDialog,
QDesktopWidget, QPushButton, QCompleter,
QListWidget, QListWidgetItem, QToolTip,
QProgressBar, QToolButton, QSystemTrayIcon,
QShortcut, QGraphicsBlurEffect, QTableWidget,
QTableWidgetItem, QActionGroup)
from executors import Executors
import app_constants
import misc
import gallery
import io_misc
import settingsdialog
import gallerydialog
import fetch
import gallerydb
import settings
import pewnet
import utils
import misc_db
import database
log = logging.getLogger(__name__)
log_i = log.info
log_d = log.debug
log_w = log.warning
log_e = log.error
log_c = log.critical
class AppWindow(QMainWindow):
"The application's main window"
move_listener = pyqtSignal()
login_check_invoker = pyqtSignal()
db_startup_invoker = pyqtSignal(list)
duplicate_check_invoker = pyqtSignal(gallery.GalleryModel)
admin_db_method_invoker = pyqtSignal(object)
db_activity_checker = pyqtSignal()
graphics_blur = QGraphicsBlurEffect()
def __init__(self, disable_excepthook=False):
super().__init__()
if not disable_excepthook:
sys.excepthook = self.excepthook
app_constants.GENERAL_THREAD = QThread(self)
app_constants.GENERAL_THREAD.finished.connect(app_constants.GENERAL_THREAD.deleteLater)
app_constants.GENERAL_THREAD.start()
self.check_site_logins()
self._db_startup_thread = QThread(self)
self._db_startup_thread.finished.connect(self._db_startup_thread.deleteLater)
self.db_startup = gallerydb.DatabaseStartup()
self._db_startup_thread.start()
self.db_startup.moveToThread(self._db_startup_thread)
self.db_startup.DONE.connect(lambda: self.scan_for_new_galleries() if app_constants.LOOK_NEW_GALLERY_STARTUP else None)
self.db_startup_invoker.connect(self.db_startup.startup)
self.setAcceptDrops(True)
self.initUI()
self.startup()
QTimer.singleShot(3000, self._check_update)
self.setFocusPolicy(Qt.NoFocus)
self.set_shortcuts()
self.graphics_blur.setParent(self)
def set_shortcuts(self):
quit = QShortcut(QKeySequence('Ctrl+Q'), self, self.close)
search_focus = QShortcut(QKeySequence(QKeySequence.Find), self, lambda:self.search_bar.setFocus(Qt.ShortcutFocusReason))
prev_view = QShortcut(QKeySequence(QKeySequence.PreviousChild), self, self.switch_display)
next_view = QShortcut(QKeySequence(QKeySequence.NextChild), self, self.switch_display)
help = QShortcut(QKeySequence(QKeySequence.HelpContents), self, lambda:utils.open_web_link("https://github.com/Pewpews/happypanda/wiki"))
def check_site_logins(self):
# checking logins
# need to do this to avoid settings dialog locking up
class LoginCheck(QObject):
def __init__(self):
super().__init__()
def check(self):
for s in settings.ExProperties.sites:
ex = settings.ExProperties(s)
if ex.cookies:
if s == settings.ExProperties.EHENTAI:
pewnet.EHen.check_login(ex.cookies)
logincheck = LoginCheck()
self.login_check_invoker.connect(logincheck.check)
logincheck.moveToThread(app_constants.GENERAL_THREAD)
self.login_check_invoker.emit()
def init_watchers(self):
def remove_gallery(g):
index = gallery.CommonView.find_index(self.get_current_view(), g.id, True)
if index:
gallery.CommonView.remove_gallery(self.get_current_view(), [index])
else:
log_e('Could not find gallery to remove from watcher')
def update_gallery(g):
index = gallery.CommonView.find_index(self.get_current_view(), g.id)
if index:
gal = index.data(gallery.GalleryModel.GALLERY_ROLE)
gal.path = g.path
gal.chapters = g.chapters
else:
log_e('Could not find gallery to update from watcher')
self.default_manga_view.replace_gallery(g, False)
def created(path):
self.gallery_populate([path])
def modified(path, gallery):
mod_popup = io_misc.ModifiedPopup(path, gallery, self)
def deleted(path, gallery):
d_popup = io_misc.DeletedPopup(path, gallery, self)
d_popup.UPDATE_SIGNAL.connect(update_gallery)
d_popup.REMOVE_SIGNAL.connect(remove_gallery)
def moved(new_path, gallery):
mov_popup = io_misc.MovedPopup(new_path, gallery, self)
mov_popup.UPDATE_SIGNAL.connect(update_gallery)
self.watchers = io_misc.Watchers()
self.watchers.gallery_handler.CREATE_SIGNAL.connect(created)
self.watchers.gallery_handler.MODIFIED_SIGNAL.connect(modified)
self.watchers.gallery_handler.MOVED_SIGNAL.connect(moved)
self.watchers.gallery_handler.DELETED_SIGNAL.connect(deleted)
def startup(self):
def normalize_first_time():
settings.set(app_constants.INTERNAL_LEVEL, 'Application', 'first time level')
settings.save()
def done(status=True):
self.db_startup_invoker.emit(gallery.MangaViews.manga_views)
#self.db_startup.startup()
if app_constants.FIRST_TIME_LEVEL != app_constants.INTERNAL_LEVEL:
normalize_first_time()
if app_constants.UPDATE_VERSION != app_constants.vs:
settings.set(app_constants.vs, 'Application', 'version')
if app_constants.UPDATE_VERSION != app_constants.vs:
pop = misc.BasePopup(self, blur=False)
ml = QVBoxLayout(pop.main_widget)
ml.addWidget(QLabel("\nGoodbye Happypanda!\n\n\nHello, this is the last release of 'old' Happypanda.\n"+
"This means that I (personally) won't be adding any new features or fix bugs.\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"+
"Please follow me on twitter (@pewspew) to keep yourself updated!\n"))
ml.addLayout(pop.buttons_layout)
pop.add_buttons("close")[0].clicked.connect(pop.close)
pop.adjustSize()
pop.show()
if app_constants.ENABLE_MONITOR and \
app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS):
self.init_watchers()
self.download_manager = pewnet.Downloader()
app_constants.DOWNLOAD_MANAGER = self.download_manager
self.download_manager.start_manager(4)
eh_url = app_constants.DEFAULT_EHEN_URL
if 'g.e-h' in eh_url or 'http://' in eh_url: # reset default hen
eh_url_n = 'https://e-hentai.org/'
settings.set(eh_url_n, 'Web', 'default ehen url')
settings.save()
app_constants.DEFAULT_EHEN_URL = eh_url_n
done()
def initUI(self):
self.center = QWidget()
self._main_layout = QHBoxLayout(self.center)
self._main_layout.setSpacing(0)
self._main_layout.setContentsMargins(0,0,0,0)
self.init_stat_bar()
self.manga_views = {}
self._current_manga_view = None
self.default_manga_view = gallery.MangaViews(app_constants.ViewType.Default, self, True)
def refresh_view():
self.current_manga_view.sort_model.refresh()
self.db_startup.DONE.connect(refresh_view)
self.manga_list_view = self.default_manga_view.list_view
self.manga_table_view = self.default_manga_view.table_view
self.manga_list_view.gallery_model.STATUSBAR_MSG.connect(self.stat_temp_msg)
self.manga_list_view.STATUS_BAR_MSG.connect(self.stat_temp_msg)
self.manga_table_view.STATUS_BAR_MSG.connect(self.stat_temp_msg)
self.sidebar_list = misc_db.SideBarWidget(self)
self.db_startup.DONE.connect(self.sidebar_list.tags_tree.setup_tags)
self._main_layout.addWidget(self.sidebar_list)
self.current_manga_view = self.default_manga_view
#self.display_widget.setSizePolicy(QSizePolicy.Expanding,
#QSizePolicy.Preferred)
self.download_window = io_misc.GalleryDownloader(self)
self.download_window.hide()
# init toolbar
self.init_toolbar()
log_d('Create statusbar: OK')
self.system_tray = misc.SystemTray(QIcon(app_constants.APP_ICO_PATH), self)
app_constants.SYSTEM_TRAY = self.system_tray
tray_menu = QMenu(self)
self.system_tray.setContextMenu(tray_menu)
self.system_tray.setToolTip('Happypanda {}'.format(app_constants.vs))
tray_quit = QAction('Quit', tray_menu)
tray_update = tray_menu.addAction('Check for update')
tray_update.triggered.connect(self._check_update)
tray_menu.addAction(tray_quit)
tray_quit.triggered.connect(self.close)
self.system_tray.show()
def tray_activate(r=None):
if not r or r == QSystemTrayIcon.Trigger:
self.showNormal()
self.activateWindow()
self.system_tray.messageClicked.connect(tray_activate)
self.system_tray.activated.connect(tray_activate)
log_d('Create system tray: OK')
#self.display.addWidget(self.chapter_main)
self.setCentralWidget(self.center)
self.setWindowIcon(QIcon(app_constants.APP_ICO_PATH))
props = settings.win_read(self, 'AppWindow')
if props.resize:
x, y = props.resize
self.resize(x, y)
else:
self.resize(app_constants.MAIN_W, app_constants.MAIN_H)
self.setMinimumWidth(600)
self.setMinimumHeight(400)
misc.centerWidget(self)
self.init_spinners()
self.show()
log_d('Show window: OK')
self.notification_bar = misc.NotificationOverlay(self)
p = self.toolbar.pos()
self.notification_bar.move(p.x(), p.y() + self.toolbar.height())
self.notification_bar.resize(self.width())
self.notif_bubble = misc.AppBubble(self)
app_constants.NOTIF_BAR = self.notification_bar
app_constants.NOTIF_BUBBLE = self.notif_bubble
log_d('Create notificationbar: OK')
log_d('Window Create: OK')
def _check_update(self):
class upd_chk(QObject):
UPDATE_CHECK = pyqtSignal(str)
def __init__(self, **kwargs):
super().__init__(**kwargs)
def fetch_vs(self):
import requests
import time
log_d('Checking Update')
time.sleep(1.5)
try:
r = requests.get("https://raw.githubusercontent.com/Pewpews/happypanda/master/VS.txt")
a = r.text
vs = a.strip()
self.UPDATE_CHECK.emit(vs)
except:
log.exception('Checking Update: FAIL')
self.UPDATE_CHECK.emit('this is a very long text which is sure to be over limit')
def check_update(vs):
log_i('Received version: {}\nCurrent version: {}'.format(vs, app_constants.vs))
if vs != app_constants.vs:
if len(vs) < 10:
self.notification_bar.begin_show()
self.notification_bar.add_text("Version {} of Happypanda is".format(vs) + " available. Click here to update!", False)
self.notification_bar.clicked.connect(lambda: utils.open_web_link('https://github.com/Pewpews/happypanda/releases'))
self.notification_bar.set_clickable(True)
else:
self.notification_bar.add_text("An error occurred while checking for new version")
self.update_instance = upd_chk()
thread = QThread(self)
self.update_instance.moveToThread(thread)
thread.started.connect(self.update_instance.fetch_vs)
self.update_instance.UPDATE_CHECK.connect(check_update)
self.update_instance.UPDATE_CHECK.connect(self.update_instance.deleteLater)
thread.finished.connect(thread.deleteLater)
thread.start()
def _web_metadata_picker(self, gallery, title_url_list, queue, parent=None):
if not parent:
parent = self
text = "Which gallery do you want to extract metadata from?"
s_gallery_popup = misc.SingleGalleryChoices(gallery, title_url_list,
text, parent)
s_gallery_popup.USER_CHOICE.connect(queue.put)
def get_metadata(self, gal=None):
if not app_constants.GLOBAL_EHEN_LOCK:
metadata_spinner = misc.Spinner(self)
metadata_spinner.set_text("Metadata")
metadata_spinner.set_size(55)
thread = QThread(self)
thread.setObjectName('App.get_metadata')
fetch_instance = fetch.Fetch()
if gal:
if not isinstance(gal, list):
galleries = [gal]
else:
galleries = gal
else:
if app_constants.CONTINUE_AUTO_METADATA_FETCHER:
galleries = [g for g in self.current_manga_view.gallery_model._data if not g.exed]
else:
galleries = self.current_manga_view.gallery_model._data
if not galleries:
self.notification_bar.add_text('All galleries has already been processed!')
return None
fetch_instance.galleries = galleries
self.notification_bar.begin_show()
fetch_instance.moveToThread(thread)
def done(status):
self.notification_bar.end_show()
gallerydb.execute(database.db.DBBase.end, True)
try:
fetch_instance.deleteLater()
except RuntimeError:
pass
if not isinstance(status, bool):
galleries = []
for tup in status:
galleries.append(tup[0])
class GalleryContextMenu(QMenu):
app_instance = self
def __init__(self, parent=None):
super().__init__(parent)
show_in_library_act = self.addAction('Show in library')
show_in_library_act.triggered.connect(self.show_in_library)
def show_in_library(self):
index = gallery.CommonView.find_index(self.app_instance.get_current_view(), self.gallery_widget.gallery.id, True)
if index:
gallery.CommonView.scroll_to_index(self.app_instance.get_current_view(), index)
g_popup = io_misc.GalleryPopup(('Fecthing metadata for these galleries failed.' + ' Check happypanda.log for details.', galleries), self, menu=GalleryContextMenu)
errors = {g[0].id: g[1] for g in status}
for g_item in g_popup.get_all_items():
g_item.extra_text.setText("<font color='red'>{}</font>".format(errors[g_item.gallery.id]))
g_item.extra_text.show()
g_popup.graphics_blur.setEnabled(False)
close_button = g_popup.add_buttons('Close')[0]
close_button.clicked.connect(g_popup.close)
database.db.DBBase.begin()
fetch_instance.GALLERY_PICKER.connect(self._web_metadata_picker)
fetch_instance.GALLERY_EMITTER.connect(self.default_manga_view.replace_gallery)
fetch_instance.AUTO_METADATA_PROGRESS.connect(self.notification_bar.add_text)
thread.started.connect(fetch_instance.auto_web_metadata)
fetch_instance.FINISHED.connect(done)
fetch_instance.FINISHED.connect(metadata_spinner.before_hide)
thread.finished.connect(thread.deleteLater)
thread.start()
#fetch_instance.auto_web_metadata()
metadata_spinner.show()
else:
self.notif_bubble.update_text("Oops!", "Auto metadata fetcher is already running...")
def init_stat_bar(self):
self.status_bar = self.statusBar()
self.status_bar.setSizeGripEnabled(False)
self.stat_info = QLabel()
self.stat_info.setIndent(5)
self.sort_main = QAction("Asc", self)
sort_menu = QMenu()
self.sort_main.setMenu(sort_menu)
s_by_title = QAction("Title", sort_menu)
s_by_artist = QAction("Artist", sort_menu)
sort_menu.addAction(s_by_title)
sort_menu.addAction(s_by_artist)
self.status_bar.addPermanentWidget(self.stat_info)
#self.status_bar.addAction(self.sort_main)
self.temp_msg = QLabel()
self.temp_timer = QTimer()
app_constants.STAT_MSG_METHOD = self.stat_temp_msg
def stat_temp_msg(self, msg):
self.temp_timer.stop()
self.temp_msg.setText(msg)
self.status_bar.addWidget(self.temp_msg)
self.temp_timer.timeout.connect(self.temp_msg.clear)
self.temp_timer.setSingleShot(True)
self.temp_timer.start(5000)
def stat_row_info(self):
r = self.current_manga_view.get_current_view().sort_model.rowCount()
t = self.current_manga_view.get_current_view().gallery_model.rowCount()
g_l = self.get_current_view().sort_model.current_gallery_list
if g_l:
self.stat_info.setText("<b><i>{}</i></b> | Showing {} of {} ".format(g_l.name, r, t))
else:
self.stat_info.setText("Showing {} of {} ".format(r, t))
def set_current_manga_view(self, v):
self.current_manga_view = v
@property
def current_manga_view(self):
return self._current_manga_view
@current_manga_view.setter
def current_manga_view(self, new_view):
if self._current_manga_view:
self._main_layout.takeAt(1)
self._current_manga_view = new_view
self._main_layout.insertLayout(1, new_view.view_layout, 1)
self.stat_row_info()
def init_spinners(self):
# fetching spinner
self.data_fetch_spinner = misc.Spinner(self, "center")
self.data_fetch_spinner.set_size(80)
self.manga_list_view.gallery_model.ADD_MORE.connect(self.data_fetch_spinner.show)
self.db_startup.START.connect(self.data_fetch_spinner.show)
self.db_startup.PROGRESS.connect(self.data_fetch_spinner.set_text)
self.manga_list_view.gallery_model.ADDED_ROWS.connect(self.data_fetch_spinner.before_hide)
self.db_startup.DONE.connect(self.data_fetch_spinner.before_hide)
## deleting spinner
#self.gallery_delete_spinner = misc.Spinner(self)
#self.gallery_delete_spinner.set_size(40,40)
##self.gallery_delete_spinner.set_text('Removing...')
#self.manga_list_view.gallery_model.rowsAboutToBeRemoved.connect(self.gallery_delete_spinner.show)
#self.manga_list_view.gallery_model.rowsRemoved.connect(self.gallery_delete_spinner.before_hide)
def search(self, srch_string):
"Args should be Search Enums"
self.search_bar.setText(srch_string)
self.search_backward.setVisible(True)
args = []
if app_constants.GALLERY_SEARCH_REGEX:
args.append(app_constants.Search.Regex)
if app_constants.GALLERY_SEARCH_CASE:
args.append(app_constants.Search.Case)
if app_constants.GALLERY_SEARCH_STRICT:
args.append(app_constants.Search.Strict)
self.current_manga_view.get_current_view().sort_model.init_search(srch_string, args)
old_cursor_pos = self._search_cursor_pos[0]
self.search_bar.end(False)
if self.search_bar.cursorPosition() != old_cursor_pos + 1:
self.search_bar.setCursorPosition(old_cursor_pos)
def switch_display(self):
"Switches between fav and catalog display"
if self.current_manga_view.fav_is_current():
self.tab_manager.library_btn.click()
else:
self.tab_manager.favorite_btn.click()
def settings(self):
sett = settingsdialog.SettingsDialog(self)
sett.scroll_speed_changed.connect(self.manga_list_view.updateGeometries)
#sett.show()
def init_toolbar(self):
self.toolbar = QToolBar()
self.toolbar.adjustSize()
#self.toolbar.setFixedHeight()
self.toolbar.setWindowTitle("Show") # text for the contextmenu
#self.toolbar.setStyleSheet("QToolBar {border:0px}") # make it user
#defined?
self.toolbar.setMovable(False)
self.toolbar.setFloatable(False)
#self.toolbar.setIconSize(QSize(20,20))
self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toolbar.setIconSize(QSize(20,20))
def switch_view(fav):
if fav:
self.default_manga_view.get_current_view().sort_model.fav_view()
else:
self.default_manga_view.get_current_view().sort_model.catalog_view()
self.tab_manager = misc_db.ToolbarTabManager(self.toolbar, self)
self.tab_manager.favorite_btn.clicked.connect(lambda: switch_view(True))
self.tab_manager.library_btn.click()
self.tab_manager.library_btn.clicked.connect(lambda: switch_view(False))
self.addition_tab = self.tab_manager.addTab("Inbox", app_constants.ViewType.Addition, icon=app_constants.INBOX_ICON)
gallery_k = QKeySequence('Alt+G')
new_gallery_k = QKeySequence('Ctrl+N')
new_galleries_k = QKeySequence('Ctrl+Shift+N')
new_populate_k = QKeySequence('Ctrl+Alt+N')
scan_galleries_k = QKeySequence('Ctrl+Alt+S')
open_random_k = QKeySequence(QKeySequence.Open)
get_all_metadata_k = QKeySequence('Ctrl+Alt+M')
gallery_downloader_k = QKeySequence('Ctrl+Alt+D')
gallery_menu = QMenu()
gallery_action = QToolButton()
gallery_action.setIcon(app_constants.PLUS_ICON)
gallery_action.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
gallery_action.setShortcut(gallery_k)
gallery_action.setText('Gallery ')
gallery_action.setPopupMode(QToolButton.InstantPopup)
gallery_action.setToolTip('Contains various gallery related features')
gallery_action.setMenu(gallery_menu)
add_gallery_icon = QIcon(app_constants.PLUS_ICON)
gallery_action_add = QAction(add_gallery_icon, "Add a gallery...", self)
gallery_action_add.triggered.connect(lambda: gallery.CommonView.spawn_dialog(self))
gallery_action_add.setToolTip('Add a single gallery thoroughly')
gallery_action_add.setShortcut(new_gallery_k)
gallery_menu.addAction(gallery_action_add)
add_more_action = QAction(add_gallery_icon, "Add galleries...", self)
add_more_action.setStatusTip('Add galleries from different folders')
add_more_action.setShortcut(new_galleries_k)
add_more_action.triggered.connect(lambda: self.populate(True))
gallery_menu.addAction(add_more_action)
populate_action = QAction(add_gallery_icon, "Populate from directory/archive...", self)
populate_action.setStatusTip('Populates the DB with galleries from a single folder or archive')
populate_action.triggered.connect(self.populate)
populate_action.setShortcut(new_populate_k)
gallery_menu.addAction(populate_action)
gallery_menu.addSeparator()
scan_galleries_action = QAction('Scan for new galleries', self)
scan_galleries_action.setIcon(app_constants.SPINNER_ICON)
scan_galleries_action.triggered.connect(self.scan_for_new_galleries)
scan_galleries_action.setStatusTip('Scan monitored folders for new galleries')
scan_galleries_action.setShortcut(scan_galleries_k)
gallery_menu.addAction(scan_galleries_action)
duplicate_check_simple = QAction("Check for duplicate galleries", self)
duplicate_check_simple.setIcon(app_constants.DUPLICATE_ICON)
duplicate_check_simple.triggered.connect(lambda: self.duplicate_check()) # triggered emits False
gallery_menu.addAction(duplicate_check_simple)
self.toolbar.addWidget(gallery_action)
spacer_tool = QWidget()
spacer_tool.setFixedSize(QSize(5, 1))
self.toolbar.addWidget(spacer_tool)
metadata_action = QToolButton()
metadata_action.setText('Fetch all metadata')
metadata_action.clicked.connect(self.get_metadata)
metadata_action.setIcon(app_constants.DOWNLOAD_ICON)
metadata_action.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
metadata_action.setShortcut(get_all_metadata_k)
self.toolbar.addWidget(metadata_action)
spacer_tool2 = QWidget()
spacer_tool2.setFixedSize(QSize(1, 1))
self.toolbar.addWidget(spacer_tool2)
gallery_action_random = QToolButton()
gallery_action_random.setText("Open random gallery")
gallery_action_random.clicked.connect(lambda: gallery.CommonView.open_random_gallery(self.get_current_view()))
gallery_action_random.setIcon(app_constants.RANDOM_ICON)
gallery_action_random.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
gallery_action_random.setShortcut(open_random_k)
self.toolbar.addWidget(gallery_action_random)
spacer_tool3 = QWidget()
spacer_tool3.setFixedSize(QSize(1, 1))
self.toolbar.addWidget(spacer_tool3)
gallery_downloader = QToolButton()
gallery_downloader.setText("Downloader")
gallery_downloader.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
gallery_downloader.clicked.connect(self.download_window.show)
gallery_downloader.setShortcut(gallery_downloader_k)
gallery_downloader.setIcon(app_constants.MANAGER_ICON)
self.toolbar.addWidget(gallery_downloader)
spacer_tool4 = QWidget()
spacer_tool4.setFixedSize(QSize(5, 1))
self.toolbar.addWidget(spacer_tool4)
# debug specfic code
if app_constants.DEBUG:
def debug_func():
pass
debug_btn = QToolButton()
debug_btn.setText("DEBUG BUTTON")
self.toolbar.addWidget(debug_btn)
debug_btn.clicked.connect(debug_func)
spacer_middle = QWidget() # aligns buttons to the right
spacer_middle.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar.addWidget(spacer_middle)
sort_k = QKeySequence('Alt+S')
sort_action = QToolButton()
sort_action.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
sort_action.setShortcut(sort_k)
sort_action.setIcon(app_constants.SORT_ICON_DESC)
sort_menu = misc.SortMenu(self, self.toolbar, sort_action)
sort_menu.set_toolbutton_text()
sort_action.setMenu(sort_menu)
sort_action.setPopupMode(QToolButton.InstantPopup)
self.toolbar.addWidget(sort_action)
def set_new_sort(s):
sort_menu.set_toolbutton_text()
self.current_manga_view.list_view.sort(s)
sort_menu.new_sort.connect(set_new_sort)
spacer_tool4 = QWidget()
spacer_tool4.setFixedSize(QSize(5, 1))
self.toolbar.addWidget(spacer_tool4)
togle_view_k = QKeySequence('Alt+Space')
self.grid_toggle_g_icon = app_constants.GRID_ICON
self.grid_toggle_l_icon = app_constants.LIST_ICON
self.grid_toggle = QToolButton()
self.grid_toggle.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.grid_toggle.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.grid_toggle.setShortcut(togle_view_k)
if self.current_manga_view.current_view == gallery.MangaViews.View.List:
self.grid_toggle.setIcon(self.grid_toggle_l_icon)
else:
self.grid_toggle.setIcon(self.grid_toggle_g_icon)
self.grid_toggle.setObjectName('gridtoggle')
self.grid_toggle.clicked.connect(self.toggle_view)
self.toolbar.addWidget(self.grid_toggle)
spacer_mid2 = QWidget()
spacer_mid2.setFixedSize(QSize(5, 1))
self.toolbar.addWidget(spacer_mid2)
search_options = QToolButton()
search_options.setIconSize(QSize(15,15))
search_options.setPopupMode(QToolButton.InstantPopup)
self.toolbar.addWidget(search_options)
search_options.setIcon(app_constants.SEARCH_ICON)
search_options_menu = QMenu(self)
search_options.setMenu(search_options_menu)
case_search_option = search_options_menu.addAction('Case Sensitive')
case_search_option.setCheckable(True)
case_search_option.setChecked(app_constants.GALLERY_SEARCH_CASE)
def set_search_case(b):
app_constants.GALLERY_SEARCH_CASE = b
settings.set(b, 'Application', 'gallery search case')
settings.save()
case_search_option.toggled.connect(set_search_case)
search_options_menu.addSeparator()
strict_search_option = search_options_menu.addAction('Match whole terms')
strict_search_option.setCheckable(True)
strict_search_option.setChecked(app_constants.GALLERY_SEARCH_STRICT)
regex_search_option = search_options_menu.addAction('Regex')
regex_search_option.setCheckable(True)
regex_search_option.setChecked(app_constants.GALLERY_SEARCH_REGEX)
def set_search_strict(b):
if b:
if regex_search_option.isChecked():
regex_search_option.toggle()
app_constants.GALLERY_SEARCH_STRICT = b
settings.set(b, 'Application', 'gallery search strict')
settings.save()
strict_search_option.toggled.connect(set_search_strict)
def set_search_regex(b):
if b:
if strict_search_option.isChecked():
strict_search_option.toggle()
app_constants.GALLERY_SEARCH_REGEX = b
settings.set(b, 'Application', 'allow search regex')
settings.save()
regex_search_option.toggled.connect(set_search_regex)
self.search_bar = misc.LineEdit()
remove_txt = self.search_bar.addAction(app_constants.CROSS_ICON, QLineEdit.LeadingPosition)
refresh_search = self.search_bar.addAction(app_constants.REFRESH_ICON, QLineEdit.TrailingPosition)
refresh_search.triggered.connect(self.current_manga_view.get_current_view().sort_model.refresh)
remove_txt.setVisible(False)
def clear_txt():
self.search_bar.setText("")
self.search_bar.returnPressed.emit()
remove_txt.triggered.connect(clear_txt)
def hide_cross(txt):
remove_txt.setVisible(bool(txt))
self.search_bar.textChanged.connect(hide_cross)
self.search_bar.setObjectName('search_bar')
self.search_timer = QTimer(self)
self.search_timer.setSingleShot(True)
self.search_timer.timeout.connect(lambda: self.search(self.search_bar.text()))
self._search_cursor_pos = [0, 0]
def set_cursor_pos(old, new):
self._search_cursor_pos[0] = old
self._search_cursor_pos[1] = new
self.search_bar.cursorPositionChanged.connect(set_cursor_pos)
if app_constants.SEARCH_AUTOCOMPLETE:
completer = QCompleter(self)
completer_view = misc.CompleterPopupView()
completer.setPopup(completer_view)
completer_view._setup()
completer.setModel(self.manga_list_view.gallery_model)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCompletionMode(QCompleter.PopupCompletion)
completer.setCompletionRole(Qt.DisplayRole)
completer.setCompletionColumn(app_constants.TITLE)
completer.setFilterMode(Qt.MatchContains)
completer.activated[str].connect(lambda a: self.search(a))
self.search_bar.setCompleter(completer)
self.search_bar.returnPressed.connect(lambda: self.search(self.search_bar.text()))
if not app_constants.SEARCH_ON_ENTER:
self.search_bar.textEdited.connect(lambda: self.search_timer.start(800))
self.search_bar.setPlaceholderText("Search title, artist, namespace & tags")
self.search_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.manga_list_view.sort_model.HISTORY_SEARCH_TERM.connect(lambda a: self.search_bar.setText(a))
self.toolbar.addWidget(self.search_bar)
def search_history(_, back=True): # clicked signal passes a bool
sort_model = self.manga_list_view.sort_model
nav = sort_model.PREV if back else sort_model.NEXT
history_term = sort_model.navigate_history(nav)
if back:
self.search_forward.setVisible(True)
back_k = QKeySequence(QKeySequence.Back)
forward_k = QKeySequence(QKeySequence.Forward)
search_backbutton = QToolButton(self.toolbar)
search_backbutton.setIcon(app_constants.ARROW_LEFT_ICON)
search_backbutton.setFixedWidth(20)
search_backbutton.clicked.connect(search_history)
search_backbutton.setShortcut(back_k)
self.search_backward = self.toolbar.addWidget(search_backbutton)
self.search_backward.setVisible(False)
search_forwardbutton = QToolButton(self.toolbar)
search_forwardbutton.setIcon(app_constants.ARROW_RIGHT_ICON)
search_forwardbutton.setFixedWidth(20)
search_forwardbutton.clicked.connect(lambda: search_history(None, False))
search_forwardbutton.setShortcut(forward_k)
self.search_forward = self.toolbar.addWidget(search_forwardbutton)
self.search_forward.setVisible(False)
spacer_end = QWidget() # aligns settings action properly
spacer_end.setFixedSize(QSize(10, 1))
self.toolbar.addWidget(spacer_end)
settings_k = QKeySequence("Ctrl+P")
settings_act = QToolButton(self.toolbar)
settings_act.setShortcut(settings_k)
settings_act.setIcon(QIcon(app_constants.SETTINGS_PATH))
settings_act.clicked.connect(self.settings)
self.toolbar.addWidget(settings_act)
self.addToolBar(self.toolbar)
def get_current_view(self):
return self.current_manga_view.get_current_view()
def toggle_view(self):
"""
Toggles the current display view
"""
if self.current_manga_view.current_view == gallery.MangaViews.View.Table:
self.current_manga_view.changeTo(self.current_manga_view.m_l_view_index)
self.grid_toggle.setIcon(self.grid_toggle_l_icon)
else:
self.current_manga_view.changeTo(self.current_manga_view.m_t_view_index)
self.grid_toggle.setIcon(self.grid_toggle_g_icon)
# TODO: Improve this so that it adds to the gallery dialog,
# so user can edit data before inserting (make it a choice)
def populate(self, mixed=None):
"Populates the database with gallery from local drive'"
if mixed:
gallery_view = misc.GalleryListView(self, True)
gallery_view.SERIES.connect(self.gallery_populate)
gallery_view.show()
else:
msg_box = misc.BasePopup(self)
l = QVBoxLayout()
msg_box.main_widget.setLayout(l)
l.addWidget(QLabel('Directory or Archive?'))
l.addLayout(msg_box.buttons_layout)
def from_dir():
path = QFileDialog.getExistingDirectory(self, "Choose a directory containing your galleries")
if not path:
return
msg_box.close()
app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True
self.gallery_populate(path, True)
def from_arch():
path = QFileDialog.getOpenFileName(self, 'Choose an archive containing your galleries',
filter=utils.FILE_FILTER)
path = [path[0]]
if not all(path) or not path:
return
msg_box.close()
app_constants.OVERRIDE_SUBFOLDER_AS_GALLERY = True
self.gallery_populate(path, True)
buttons = msg_box.add_buttons('Directory', 'Archive', 'Close')
buttons[2].clicked.connect(msg_box.close)
buttons[0].clicked.connect(from_dir)
buttons[1].clicked.connect(from_arch)
msg_box.adjustSize()
msg_box.show()
def gallery_populate(self, path, validate=False):
"Scans the given path for gallery to add into the DB"
if len(path) is not 0:
data_thread = QThread(self)
data_thread.setObjectName('General gallery populate')
self.addition_tab.click()
self.g_populate_inst = fetch.Fetch()
self.g_populate_inst.series_path = path
self._g_populate_count = 0
fetch_spinner = misc.Spinner(self)
fetch_spinner.set_size(60)
fetch_spinner.set_text("Populating")
fetch_spinner.show()
def finished(status):
fetch_spinner.hide()
if not status:
log_e('Populating DB from gallery folder: Nothing was added!')
self.notif_bubble.update_text("Gallery Populate",
"<font color='red'>Nothing was added. Check happypanda_log for details..</font>")
def skipped_gs(s_list):
"Skipped galleries"
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Question)
msg_box.setText('Do you want to view skipped paths?')
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg_box.setDefaultButton(QMessageBox.No)
if msg_box.exec() == QMessageBox.Yes:
list_wid = QTableWidget(self)
list_wid.setAttribute(Qt.WA_DeleteOnClose)
list_wid.setRowCount(len(s_list))
list_wid.setColumnCount(2)
list_wid.setAlternatingRowColors(True)
list_wid.setEditTriggers(list_wid.NoEditTriggers)
list_wid.setHorizontalHeaderLabels(['Reason', 'Path'])
list_wid.setSelectionBehavior(list_wid.SelectRows)
list_wid.setSelectionMode(list_wid.SingleSelection)
list_wid.setSortingEnabled(True)
list_wid.verticalHeader().hide()
list_wid.setAutoScroll(False)
for x, g in enumerate(s_list):
list_wid.setItem(x, 0, QTableWidgetItem(g[1]))
list_wid.setItem(x, 1, QTableWidgetItem(g[0]))
list_wid.resizeColumnsToContents()
list_wid.setWindowTitle('{} skipped paths'.format(len(s_list)))
list_wid.setWindowFlags(Qt.Window)
list_wid.resize(900,400)
list_wid.doubleClicked.connect(lambda i: utils.open_path(list_wid.item(i.row(), 1).text(), list_wid.item(i.row(), 1).text()))
list_wid.show()
def a_progress(prog):
fetch_spinner.set_text("Populating... {}/{}".format(prog, self._g_populate_count))
def add_to_model(gallery):
self.addition_tab.view.add_gallery(gallery, app_constants.KEEP_ADDED_GALLERIES)
def set_count(c):
self._g_populate_count = c
self.g_populate_inst.moveToThread(data_thread)
self.g_populate_inst.PROGRESS.connect(a_progress)
self.g_populate_inst.DATA_COUNT.connect(set_count)
self.g_populate_inst.LOCAL_EMITTER.connect(add_to_model)
self.g_populate_inst.FINISHED.connect(finished)
self.g_populate_inst.FINISHED.connect(self.g_populate_inst.deleteLater)
self.g_populate_inst.SKIPPED.connect(skipped_gs)
data_thread.finished.connect(data_thread.deleteLater)
data_thread.started.connect(self.g_populate_inst.local)
data_thread.start()
#self.g_populate_inst.local()
log_i('Populating DB from directory/archive')
def scan_for_new_galleries(self):
available_folders = app_constants.ENABLE_MONITOR and \
app_constants.MONITOR_PATHS and all(app_constants.MONITOR_PATHS)
if available_folders and not app_constants.SCANNING_FOR_GALLERIES:
app_constants.SCANNING_FOR_GALLERIES = True
self.notification_bar.add_text("Scanning for new galleries...")
log_i('Scanning for new galleries...')
try:
class ScanDir(QObject):
finished = pyqtSignal()
fetch_inst = fetch.Fetch(self)
def __init__(self, addition_view, addition_tab, parent=None):
super().__init__(parent)
self.addition_view = addition_view
self.addition_tab = addition_tab
self._switched = False
def switch_tab(self):
if not self._switched:
self.addition_tab.click()
self._switched = True
def scan_dirs(self):
paths = []
for p in app_constants.MONITOR_PATHS:
if os.path.exists(p):
dir_content = scandir.scandir(p)
for d in dir_content:
paths.append(d.path)
else:
log_e("Monitored path does not exists: {}".format(p.encode(errors='ignore')))
self.fetch_inst.series_path = paths
self.fetch_inst.LOCAL_EMITTER.connect(lambda g:self.addition_view.add_gallery(g, app_constants.KEEP_ADDED_GALLERIES))
self.fetch_inst.LOCAL_EMITTER.connect(self.switch_tab)
self.fetch_inst.local()
#contents = []
#for g in self.scanned_data:
# contents.append(g)
#paths = sorted(paths)
#new_galleries = []
#for x in contents:
# y = utils.b_search(paths, os.path.normcase(x.path))
# if not y:
# new_galleries.append(x)
self.finished.emit()
self.deleteLater()
#if app_constants.LOOK_NEW_GALLERY_AUTOADD:
# QTimer.singleShot(10000,
# self.gallery_populate(final_paths))
# return
def finished(): app_constants.SCANNING_FOR_GALLERIES = False
new_gall_spinner = misc.Spinner(self)
new_gall_spinner.set_text("Gallery Scan")
new_gall_spinner.show()
thread = QThread(self)
self.scan_inst = ScanDir(self.addition_tab.view, self.addition_tab)
self.scan_inst.moveToThread(thread)
self.scan_inst.finished.connect(finished)
self.scan_inst.finished.connect(new_gall_spinner.before_hide)
thread.started.connect(self.scan_inst.scan_dirs)
#self.scan_inst.scan_dirs()
thread.finished.connect(thread.deleteLater)
thread.start()
except:
self.notification_bar.add_text('An error occured while attempting to scan for new galleries. Check happypanda.log.')
log.exception('An error occured while attempting to scan for new galleries.')
app_constants.SCANNING_FOR_GALLERIES = False
else:
self.notification_bar.add_text("Please specify directory in settings to scan for new galleries!")
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
super().dragEnterEvent(event)
def dropEvent(self, event):
acceptable = []
unaccept = []
for u in event.mimeData().urls():
path = u.toLocalFile()
if os.path.isdir(path) or path.endswith(utils.ARCHIVE_FILES):
acceptable.append(path)
else:
unaccept.append(path)
log_i('Acceptable dropped items: {}'.format(len(acceptable)))
log_i('Unacceptable dropped items: {}'.format(len(unaccept)))
log_d('Dropped items: {}\n{}'.format(acceptable, unaccept).encode(errors='ignore'))
if acceptable:
self.notification_bar.add_text('Adding dropped items...')
log_i('Adding dropped items')
l = len(acceptable) == 1
f_item = acceptable[0]
if f_item.endswith(utils.ARCHIVE_FILES):
f_item = utils.check_archive(f_item)
else:
f_item = utils.recursive_gallery_check(f_item)
f_item_l = len(f_item) < 2
subfolder_as_c = not app_constants.SUBFOLDER_AS_GALLERY
if l and subfolder_as_c or l and f_item_l:
g_d = gallerydialog.GalleryDialog(self, acceptable[0])
g_d.show()
else:
self.gallery_populate(acceptable, True)
event.accept()
else:
text = 'File not supported' if len(unaccept) < 2 else 'Files not supported'
self.notification_bar.add_text(text)
if unaccept:
self.notification_bar.add_text('Some unsupported files did not get added')
super().dropEvent(event)
def resizeEvent(self, event):
try:
self.notification_bar.resize(event.size().width())
except AttributeError:
pass
self.move_listener.emit()
return super().resizeEvent(event)
def moveEvent(self, event):
self.move_listener.emit()
return super().moveEvent(event)
def showEvent(self, event):
return super().showEvent(event)
def cleanup_exit(self):
self.system_tray.hide()
# watchers
try:
self.watchers.stop_all()
except AttributeError:
pass
# settings
settings.set(self.manga_list_view.current_sort, 'General', 'current sort')
settings.set(app_constants.IGNORE_PATHS, 'Application', 'ignore paths')
if not self.isMaximized():
settings.win_save(self, 'AppWindow')
# temp dir
try:
for root, dirs, files in scandir.walk('temp', topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
log_d('Flush temp on exit: OK')
except:
log.exception('Flush temp on exit: FAIL')
# DB
try:
log_i("Analyzing database...")
gallerydb.GalleryDB.analyze()
log_i("Closing database...")
gallerydb.GalleryDB.close()
except:
pass
self.download_window.close()
# check if there is db activity
if not gallerydb.method_queue.empty():
class DBActivityChecker(QObject):
FINISHED = pyqtSignal()
def __init__(self, **kwargs):
super().__init__(**kwargs)
def check(self):
gallerydb.method_queue.join()
self.FINISHED.emit()
self.deleteLater()
db_activity = DBActivityChecker()
db_spinner = misc.Spinner(self)
self.db_activity_checker.connect(db_activity.check)
db_activity.moveToThread(app_constants.GENERAL_THREAD)
db_activity.FINISHED.connect(db_spinner.close)
db_spinner.set_text('DB Activity')
db_spinner.show()
self.db_activity_checker.emit()
msg_box = QMessageBox(self)
msg_box.setText('Database activity detected!')
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)")
msg_box.setIcon(QMessageBox.Critical)
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg_box.setDefaultButton(QMessageBox.No)
if msg_box.exec() == QMessageBox.Yes:
return 1
else:
return 2
else:
return 0
def duplicate_check(self, simple=True):
try:
self.duplicate_check_invoker.disconnect()
except TypeError:
pass
mode = 'simple' if simple else 'advanced'
log_i('Checking for duplicates in mode: {}'.format(mode))
notifbar = app_constants.NOTIF_BAR
notifbar.add_text('Checking for duplicates...')
duplicate_spinner = misc.Spinner(self)
duplicate_spinner.set_text("Duplicate Check")
duplicate_spinner.show()
dup_tab = self.tab_manager.addTab("Duplicate", app_constants.ViewType.Duplicate)
dup_tab.view.set_delete_proxy(self.default_manga_view.gallery_model)
class DuplicateCheck(QObject):
found_duplicates = pyqtSignal(tuple)
finished = pyqtSignal()
def __init__(self):
super().__init__()
def checkSimple(self, model):
galleries = model._data
duplicates = []
for n, g in enumerate(galleries, 1):
notifbar.add_text('Checking gallery {}'.format(n))
log_d('Checking gallery {}'.format(g.title.encode(errors="ignore")))
for y in galleries:
title = g.title.strip().lower() == y.title.strip().lower()
path = os.path.normcase(g.path) == os.path.normcase(y.path)
if g.id != y.id and (title or path):
if g not in duplicates:
duplicates.append(y)
duplicates.append(g)
self.found_duplicates.emit((g, y))
self.finished.emit()
self._d_checker = DuplicateCheck()
self._d_checker.moveToThread(app_constants.GENERAL_THREAD)
self._d_checker.found_duplicates.connect(lambda t: dup_tab.view.add_gallery(t, record_time=True))
self._d_checker.finished.connect(dup_tab.click)
self._d_checker.finished.connect(self._d_checker.deleteLater)
self._d_checker.finished.connect(duplicate_spinner.before_hide)
if simple:
self.duplicate_check_invoker.connect(self._d_checker.checkSimple)
self.duplicate_check_invoker.emit(self.default_manga_view.gallery_model)
def excepthook(self, ex_type, ex, tb):
log_c(''.join(traceback.format_tb(tb)))
log_c('{}: {}'.format(ex_type, ex))
traceback.print_exception(ex_type, ex, tb)
w = QMessageBox(self)
w.setWindowTitle("Critical Error")
w.setIcon(QMessageBox.Critical)
w.setText('A critical error has ben encountered. Stability from this point onward cannot be guaranteed.')
w.setStandardButtons(QMessageBox.Ok)
w.setDefaultButton(QMessageBox.Ok)
w.exec_()
def closeEvent(self, event):
r_code = self.cleanup_exit()
if r_code == 1:
log_d('Force Exit App: OK')
super().closeEvent(event)
elif r_code == 2:
log_d('Ignore Exit App')
event.ignore()
else:
log_d('Normal Exit App: OK')
super().closeEvent(event)
if __name__ == '__main__':
raise NotImplementedError("Unit testing not implemented yet!")
================================================
FILE: version/app_constants.py
================================================
#This file is part of Happypanda.
#Happypanda is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 2 of the License, or
#any later version.
#Happypanda is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#You should have received a copy of the GNU General Public License
#along with Happypanda. If not, see <http://www.gnu.org/licenses/>.
"""Contains constants to be used by several modules"""
import os, sys, enum
import qtawesome as qta
try:
import settings
from database import db_constants
except ImportError:
from . import settings
from .database import db_constants
# Version number
vs = '1.1'
DEBUG = False
OS_NAME = ''
if sys.platform.startswith('darwin'):
OS_NAME = "darwin"
elif os.name == 'nt':
OS_NAME = "windows"
elif os.name == 'posix':
OS_NAME = "linux"
APP_RESTART_CODE = 0
get = settings.get
posix_program_dir = os.path.dirname(os.path.realpath(__file__))
if os.name == 'posix':
static_dir = os.path.join(posix_program_dir, '../res')
bin_dir = os.path.join(posix_program_dir, 'bin')
temp_dir = os.path.join(posix_program_dir, 'temp')
else:
bin_dir = os.path.join(os.getcwd(), 'bin')
static_dir = os.path.join(os.getcwd(), "res")
temp_dir = os.path.join('temp')
# path to unrar tool binary
unrar_tool_path = get('', 'Application', 'unrar tool path')
# type of download needed by download manager for each site parser
# NOTE define here if any new type will be supported in the future.
DOWNLOAD_TYPE_ARCHIVE = 0
DOWNLOAD_TYPE_TORRENT = 1 # Note: With this type, file will be sent to torrent program
DOWNLOAD_TYPE_OTHER = 2
VALID_GALLERY_CATEGORY = (
'Doujinshi',
'Manga',
'Artist CG',
'Game CG',
'Western',
'Non-H',
'Image Set',
'Cosplay',
'Miscellaneous',
'Private'
)
#default stylesheet path
default_stylesheet_path = os.path.join(static_dir,"style.css")
user_stylesheet_path = ""
INTERNAL_LEVEL = 8
FIRST_TIME_LEVEL = get(INTERNAL_LEVEL, 'Application', 'first time level', int)
UPDATE_VERSION = get('0.30', 'Application', 'version', str)
FORCE_HIGH_DPI_SUPPORT = get(False, 'Advanced', 'force high dpi support', bool)
# sizes
MAIN_W = 1061 # main window
MAIN_H = 650 # main window
SIZE_FACTOR = get(10, 'Visual', 'size factor', int)
GRID_SPACING = get(15, 'Visual', 'grid spacing', int)
LISTBOX_H_SIZE = 190
LISTBOX_W_SIZE = 950
GRIDBOX_LBL_H = 60
THUMB_H_SIZE = 190 + SIZE_FACTOR
THUMB_W_SIZE = 133 + SIZE_FACTOR
THUMB_DEFAULT = (THUMB_W_SIZE, THUMB_H_SIZE)
THUMB_SMALL = (140, 93)
# Columns
COLUMNS = tuple(range(11))
TITLE = 0
ARTIST = 1
DESCR = 2
TAGS = 3
TYPE = 4
FAV = 5
CHAPTERS = 6
LANGUAGE = 7
LINK = 8
PUB_DATE = 9
DATE_ADDED = 10
@enum.unique
class ViewType(enum.IntEnum):
Default = 1
Addition = 2
Duplicate = 3
@enum.unique
class ProfileType(enum.Enum):
Default = 1
Small = 2
# Application
SYSTEM_TRAY = None
NOTIF_BAR = None
NOTIF_BUBBLE = None
STAT_MSG_METHOD = None
GENERAL_THREAD = None
WHEEL_SCROLL_EFFECT = 10
DOWNLOAD_MANAGER = None
# ICONS
# IMPORTANT: Neccessary because qtawesome can't function without an instanced QApplication
# IMPORTANT: called after instancing qApplication in main.py
def load_icons():
global G_LISTS_ICON_WH
global G_LISTS_ICON
global LIST_ICON
global ARTISTS_ICON
global ARTIST_ICON
global NSTAGS_ICON
global PLUS_ICON
global ARROW_RIGHT_ICON
global ARROW_LEFT_ICON
global GRID_ICON
global GRIDL_ICON
global SEARCH_ICON
global CROSS_ICON
global CROSS_ICON_WH
global MANAGER_ICON
global DOWNLOAD_ICON
global RANDOM_ICON
global DUPLICATE_ICON
global SORT_ICON_DESC
global SORT_ICON_ASC
global REFRESH_ICON
global STAR_ICON
global CIRCLE_ICON
global INBOX_ICON
global SPINNER_ICON
G_LISTS_ICON_WH = qta.icon("fa.bars", color="white")
G_LISTS_ICON = qta.icon("fa.bars", color="black")
LIST_ICON = qta.icon("fa.bars", color="white")
ARTISTS_ICON = qta.icon("fa.users", color="white")
ARTIST_ICON = qta.icon("fa.user", color="black")
NSTAGS_ICON = qta.icon("fa.sitemap", color="white")
PLUS_ICON = qta.icon("fa.plus", color="white")
ARROW_RIGHT_ICON = qta.icon("fa.angle-double-right", color="white")
ARROW_LEFT_ICON = qta.icon("fa.angle-double-left", color="white")
GRID_ICON = qta.icon("fa.th", color="white")
GRIDL_ICON = qta.icon("fa.th-large", color="white")
SEARCH_ICON = qta.icon("fa.search", color="white")
CROSS_ICON = qta.icon("fa.times", color="black")
CROSS_ICON_WH = qta.icon("fa.times", color="white")
MANAGER_ICON = qta.icon("fa.tasks", color="white")
DOWNLOAD_ICON = qta.icon("fa.arrow-circle-o-down", color="white")
RANDOM_ICON = qta.icon("fa.random", color="white")
DUPLICATE_ICON = qta.icon("fa.files-o", color="white")
SORT_ICON_DESC = qta.icon("fa.sort-amount-desc", color="white")
SORT_ICON_ASC = qta.icon("fa.sort-amount-asc", color="white")
REFRESH_ICON = qta.icon("fa.refresh", color="black")
STAR_ICON = qta.icon("fa.star", color="white")
CIRCLE_ICON = qta.icon("fa.circle", color="white")
INBOX_ICON = qta.icon("fa.inbox", color="white")
SPINNER_ICON = qta.icon("fa.spinner", color="white")
# image paths
GALLERY_DEF_ICO_PATH = os.path.join(static_dir, "gallery_def_ico.ico")
GALLERY_EXT_ICO_PATH = os.path.join(static_dir, "gallery_ext_ico.ico")
APP_ICO_PATH = os.path.join(static_dir, "happypanda.ico")
SETTINGS_PATH = os.path.join(static_dir, "settings.png")
NO_IMAGE_PATH = os.path.join(static_dir, "default.jpg")
# Monitored Paths
OVERRIDE_MONITOR = False # set true to make watchers to ignore next item (will be set to False)
LOOK_NEW_GALLERY_STARTUP = get(True, 'Application', 'look new gallery startup', bool)
ENABLE_MONITOR = get(True, 'Application', 'enable monitor', bool)
MONITOR_PATHS = [p for p in get([], 'Application', 'monitor paths', list) if os.path.exists(p)]
IGNORE_PATHS = get([], 'Application', 'ignore paths', list)
IGNORE_EXTS = get([], 'Application', 'ignore exts', list)
SCANNING_FOR_GALLERIES = False # if a scan for new galleries is being done
TEMP_PATH_IGNORE = []
# GENERAL
OVERRIDE_MOVE_IMPORTED_IN_FETCH = False # set to true to make a fetch instance ignore moving files (will be set to false)
MOVE_IMPORTED_GALLERIES = get(False, 'Application', 'move imported galleries', bool)
IMPORTED_GALLERY_DEF_PATH = get('', 'Application', 'imported gallery def path', str)
OPEN_RANDOM_GALLERY_CHAPTERS = get(False, 'Application', 'open random gallery chapters', bool)
OVERRIDE_SUBFOLDER_AS_GALLERY = False # set to true to make a fetch instance treat subfolder as galleries (will be set to false)
SUBFOLDER_AS_GALLERY = get(False, 'Application', 'subfolder as gallery', bool)
RENAME_GALLERY_SOURCE = get(False, 'Application', 'rename gallery source', bool)
EXTRACT_CHAPTER_BEFORE_OPENING = get(True, 'Application', 'extract chapter before opening', bool)
OPEN_GALLERIES_SEQUENTIALLY = get(False, 'Application', 'open galleries sequentially', bool)
SEND_FILES_TO_TRASH = get(True, 'Application', 'send files to trash', bool)
SHOW_SIDEBAR_WIDGET = get(False, 'Application', 'show sidebar widget', bool)
# ADVANCED
GALLERY_DATA_FIX_REGEX = get("", 'Advanced', 'gallery data fix regex', str)
GALLERY_DATA_FIX_TITLE = get(True, 'Advanced', 'gallery data fix title', bool)
GALLERY_DATA_FIX_ARTIST = get(True, 'Advanced', 'gallery data fix artist', bool)
GALLERY_DATA_FIX_REPLACE = get("", 'Advanced', 'gallery data fix replace', str)
EXTERNAL_VIEWER_ARGS = get("{$file}", 'Advanced', 'external viewer args', str)
# Import/Export
EXPORT_FORMAT = get(1, 'Advanced', 'export format', int)
EXPORT_PATH = ''
# HASH
HASH_GALLERY_PAGES = get('all', 'Advanced', 'hash gallery pages', int, str)
# WEB
INCLUDE_EH_EXPUNGED = get(False, 'Web', 'include eh expunged', bool)
GLOBAL_EHEN_TIME = get(5, 'Web', 'global ehen time offset', int)
GLOBAL_EHEN_LOCK = False
DEFAULT_EHEN_URL = get('https://e-hentai.org/', 'Web', 'default ehen url', str)
REPLACE_METADATA = get(False, 'Web', 'replace metadata', bool)
ALWAYS_CHOOSE_FIRST_HIT = get(False, 'Web', 'always choose first hit', bool)
USE_GALLERY_LINK = get(True, 'Web', 'use gallery link', bool)
USE_JPN_TITLE = get(False, 'Web', 'use jpn title', bool)
CONTINUE_AUTO_METADATA_FETCHER = get(True, 'Web', 'continue auto metadata fetcher', bool)
HEN_DOWNLOAD_TYPE = get(DOWNLOAD_TYPE_ARCHIVE, 'Web', 'hen download type', int)
DOWNLOAD_DIRECTORY = get('downloads', 'Web', 'download directory', str)
TORRENT_CLIENT = get('', 'Web', 'torrent client', str)
HEN_LIST = get(['chaikahen'], 'Web', 'hen list', list)
DOWNLOAD_GALLERY_TO_LIB = get(False, 'Web', 'download galleries to library', bool)
# External Viewer
EXTERNAL_VIEWER_SUPPORT = {'honeyview':['Honeyview.exe']}
USE_EXTERNAL_VIEWER = get(False, 'Application', 'use external viewer', bool)
EXTERNAL_VIEWER_PATH = os.path.normcase(get('', 'Application', 'external viewer path', str))
_REFRESH_EXTERNAL_VIEWER = False
# controls
THUMBNAIL_CACHE_SIZE = (1024, get(200, 'Advanced', 'cache size', int)) #1024 is 1mib
PREFETCH_ITEM_AMOUNT = get(50, 'Advanced', 'prefetch item amount', int)# amount of items to prefetch
SCROLL_SPEED = get(7, 'Advanced', 'scroll speed', int) # controls how many steps it takes when scrolling
# POPUP
POPUP_WIDTH = get(500, 'Visual', 'popup.w', int)
POPUP_HEIGHT = get(300, 'Visual', 'popup.h', int)
# Gallery
APPEND_TAGS_GALLERIES = get(True, 'Application', 'append tags to gallery', bool)
KEEP_ADDED_GALLERIES = get(True, 'Application', 'keep added galleries', bool)
GALLERY_METAFILE_KEYWORDS = ('info.json', 'info.txt')
CURRENT_SORT = get('title', 'General', 'current sort')
HIGH_QUALITY_THUMBS = get(False, 'Visual', 'high quality thumbs', bool)
DISPLAY_RATING = get(True, 'Visual', 'display gallery rating', bool)
DISPLAY_GALLERY_TYPE = get(False, 'Visual', 'display gallery type', bool) if not sys.platform.startswith('darwin') else False
DISPLAY_GALLERY_RIBBON = get(True, 'Visual', 'display gallery ribbon', bool)
GALLERY_FONT = (get('Segoe UI', 'Visual', 'gallery font family', str),
get(11, 'Visual', 'gallery font size', int))
GALLERY_FONT_ELIDE = get(True, 'Visual', 'gallery font elide', bool)
G_DEF_LANGUAGE = get('English', 'General', 'gallery default language', str)
G_CUSTOM_LANGUAGES = get([], 'General', 'gallery custom languages', list)
G_DEF_STATUS = get('Completed', 'General', 'gallery default status', str)
G_DEF_TYPE = get('Doujinshi', 'General', 'gallery default type', str)
G_LANGUAGES = ["English", "Japanese", "Chinese", "Other"]
G_STATUS = ["Ongoing", "Completed", "Unknown"]
G_TYPES = ["Manga", "Doujinshi", "Artist CG Sets", "Game CG Sets", "Western", "Image Sets", "Non-H", "Cosplay", "Other"]
@enum.unique
class GalleryState(enum.Enum):
Default = 1
New = 2
# Colors
GRID_VIEW_TITLE_COLOR = get('#ffffff', 'Visual', 'grid view title color', str)
GRID_VIEW_ARTIST_COLOR = get('#e2e2e2', 'Visual', 'grid view artist color', str)
GRID_VIEW_LABEL_COLOR = get('#d64933', 'Visual', 'grid view label color', str)
GRID_VIEW_T_MANGA_COLOR = get('#3498db', 'Visual', 'grid view t manga color', str)
GRID_VIEW_T_DOUJIN_COLOR = get('#e74c3c', 'Visual', 'grid view t doujin color', str)
GRID_VIEW_T_ARTIST_CG_COLOR = get('#16a085', 'Visual', 'grid view t artist cg color', str)
GRID_VIEW_T_GAME_CG_COLOR = get('#2ecc71', 'Visual', 'grid view t game cg color', str)
GRID_VIEW_T_WESTERN_COLOR = get('#ecf0f1', 'Visual', 'grid view t western color', str)
GRID_VIEW_T_IMAGE_COLOR = get('#f39c12', 'Visual', 'grid view t image color', str)
GRID_VIEW_T_NON_H_COLOR = get('#f1c40f', 'Visual', 'grid view t non-h color', str)
GRID_VIEW_T_COSPLAY_COLOR = get('#9b59b6', 'Visual', 'grid view t cosplay color', str)
GRID_VIEW_T_OTHER_COLOR = get('#34495e', 'Visual', 'grid view t other color', str)
# Search
SEARCH_AUTOCOMPLETE = get(True, 'Application', 'search autocomplete', bool)
GALLERY_SEARCH_REGEX = get(False, 'Application', 'allow search regex', bool)
SEARCH_ON_ENTER = get(False, 'Application', 'search on enter', bool)
GALLERY_SEARCH_STRICT = get(False, 'Application', 'gallery search strict', bool)
GALLERY_SEARCH_CASE = get(False, 'Application', 'gallery search case', bool)
@enum.unique
class Search(enum.Enum):
Strict = 1
Case = 2
Regex = 3
# Grid Tooltip
GRID_TOOLTIP = get(True, 'Visual', 'grid tooltip', bool)
TOOLTIP_TITLE = get(False, 'Visual', 'tooltip title', bool)
TOOLTIP_AUTHOR = get(False, 'Visual', 'tooltip author', bool)
TOOLTIP_CHAPTERS = get(True, 'Visual', 'tooltip chapters', bool)
TOOLTIP_STATUS = get(True, 'Visual', 'tooltip status', bool)
TOOLTIP_TYPE = get(True, 'Visual', 'tooltip type', bool)
TOOLTIP_LANG = get(False, 'Visual', 'tooltip lang', bool)
TOOLTIP_DESCR = get(False, 'Visual', 'tooltip descr', bool)
TOOLTIP_TAGS = get(False, 'Visual', 'tooltip tags', bool)
TOOLTIP_LAST_READ = get(True, 'Visual', 'tooltip last read', bool)
TOOLTIP_TIMES_READ = get(True, 'Visual', 'tooltip times read', bool)
TOOLTIP_PUB_DATE = get(False, 'Visual', 'tooltip pub date', bool)
TOOLTIP_DATE_ADDED = get(True, 'Visual', 'tooltip date added', bool)
GALLERY_ADDITION_DATA = []
GALLERY_DATA = [] # contains the most up to date gallery data
GALLERY_LISTS = set() # contains the most up to dat gallery lists
# Exceptions
class MetadataFetchFail(Exception): pass
class InternalPagesMismatch(Exception): pass
class ChapterExists(Exception): pass
class ChapterWrongParentGallery(Exception): pass
class CreateArchiveFail(Exception): pass
class FileNotFoundInArchive(Exception): pass
class WrongURL(Exception): pass
class NeedLogin(Exception): pass
class WrongLogin(Exception): pass
class HTMLParsing(Exception): pass
class GNotAvailable(Exception): pass
class TitleParsingError(Exception): pass
EXTERNAL_VIEWER_INFO =\
"""{$folder} = path to folder
{$file} = path to first image
Tip: IrfanView uses {$file}
"""
WHAT_IS_FILTER =\
"""[FILTER]
Filters are basically predefined gallery search terms.
Every time a gallery matches the specific filter it gets automatically added to the list!
Filter works the same way a gallery search does so make sure to read the guide in
Settings -> About -> Search Guide.
You can write any valid gallery search term.
[ENFORCE]
With Enforce enabled the list will only allow galleries that match the specified filter into the list.
"""
SUPPORTED_DOWNLOAD_URLS=\
"""Supported URLs:
- exhentai/g.e-hentai/e-hentai gallery urls, e.g.: https://e-hentai.org/g/618395/0439fa3666/
- panda.chaika.moe gallery and archive urls
http://panda.chaika.moe/[0]/[1]/ where [0] is 'gallery' or 'archive' and [1] are numbers
- asmhentai.com gallery urls, e.g: http://asmhentai.com/g/102845/
"""
SUPPORTED_METADATA_URLS=\
"""Supported gallery URLs:
- exhentai/g.e-hentai gallery urls, e.g.: http://g.e-hentai.org/g/618395/0439fa3666/
- panda.chaika.moe gallery and archive urls
http://panda.chaika.moe/[0]/[1]/ where [0] is 'gallery' or 'archive' and [1] is numbers
"""
EXHEN_COOKIE_TUTORIAL =\
"""
How do I find these two values? <br \>
<b>All browsers</b> <br \>
1. Navigate to e-hentai.org (needs to be logged in) or exhentai.org <br \>
2. Right click on page --> Inspect element <br \>
3. Go on 'Console' tab <br \>
4. Write : 'document.cookie' <br \>
5. A line of values should appear that correspond to active cookies <br \>
6. Look for the 'ipb_member_id' and 'ipb_pass_hash' values <br \>
"""
REGEXCHEAT =\
"""
<!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">
<h4><a id="Characters_I_1"></a>Characters I</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>.</td>
<td>Match any character except newline</td>
</tr>
<tr>
<td>^</td>
<td>Match the start of the string</td>
</tr>
<tr>
<td>$</td>
<td>Match the end of the string</td>
</tr>
<tr>
<td>*</td>
<td>Match 0 or more repetitions</td>
</tr>
<tr>
<td>+</td>
<td>Match 1 or more repetitions</td>
</tr>
<tr>
<td>?</td>
<td>Match 0 or 1 repetitions</td>
</tr>
</tbody>
</table>
<h4><a id="Special_Sequences_I_12"></a>Special Sequences I</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>\A</td>
<td>Match only at start of string</td>
</tr>
<tr>
<td>\\b</td>
<td>Match empty string, only at beginning or end of a word</td>
</tr>
<tr>
<td>\B</td>
<td>Match empty string, only when it is not at beginning or end of word</td>
</tr>
<tr>
<td>\d</td>
<td>Match digits # same as [0-9]</td>
</tr>
<tr>
<td>\D</td>
<td>Match any non digit # same as [^0-9]</td>
</tr>
</tbody>
</table>
<h4><a id="Characters_II_22"></a>Characters II</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>*?</td>
<td>Match 0 or more repetitions non-greedy</td>
</tr>
<tr>
<td>+?</td>
<td>Match 1 or more repetitions non-greedy</td>
</tr>
<tr>
<td>??</td>
<td>Match 0 or 1 repetitions non-greedy</td>
</tr>
<tr>
<td>\</td>
<td>Escape special characters</td>
</tr>
<tr>
<td>[]</td>
<td>Match a set of characters</td>
</tr>
<tr>
<td>[a-z]</td>
<td>Match any lowercase ASCII letter</td>
</tr>
<tr>
<td>[lower-upper]</td>
<td>Match a set of characters from lower to upper</td>
</tr>
<tr>
<td>[^]</td>
<td>Match characters NOT in a set</td>
</tr>
<tr>
<td>A|B</td>
<td>Match either A or B regular expressions (non-greedy)</td>
</tr>
</tbody>
</table>
<h4><a id="Special_Sequences_II_36"></a>Special Sequences II</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>\s</td>
<td>Match whitespace characters # same as [ \t\n\r\f\v]</td>
</tr>
<tr>
<td>\S</td>
<td>Match non whitespace characters #same as [^ \t\n\r\f\v]</td>
</tr>
<tr>
<td>\w</td>
<td>Match unicode word characters # same as [a-zA-Z0-9_]</td>
</tr>
<tr>
<td>\W</td>
<td>Match any character not a Unicode word character # same as [^a-zA-Z0-9_]</td>
</tr>
<tr>
<td>\Z</td>
<td>Match only at end of string</td>
</tr>
</tbody>
</table>
<h4><a id="Characters_III_46"></a>Characters III</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>{m}</td>
<td>Match exactly m copies</td>
</tr>
<tr>
<td>{m,n}</td>
<td>Match from m to n repetitions</td>
</tr>
<tr>
<td>{,n}</td>
<td>Match from 0 to n repetitions</td>
</tr>
<tr>
<td>{m,}</td>
<td>Match from m to infinite repetitions</td>
</tr>
<tr>
<td>{m,n}?</td>
<td>Match from m to n repetitions non-greedy (as few as possible)</td>
</tr>
</tbody>
</table>
<h4><a id="Groups_I_56"></a>Groups I</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>(match)</td>
<td>Use to specify a group for which match can be retrieved later</td>
</tr>
<tr>
<td>(?:match)</td>
<td>Non-capturing version parenthesis (match cannot be retrieved later)</td>
</tr>
<tr>
<td>(?P<name>)</td>
<td>Capture group with name “name”</td>
</tr>
<tr>
<td>(?P=name)</td>
<td>Back reference group named “name” in same pattern</td>
</tr>
<tr>
<td>(?#comment)</td>
<td>Comment</td>
</tr>
</tbody>
</table>
<h4><a id="Lookahead__Behind_I_66"></a>Lookahead / Behind I</h4>
<table>
<thead>
<tr>
<th>Expression</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>(?=match)</td>
<td>Lookahead assertion - match if contents matches next, but don’t consume any of the string.</td>
</tr>
<tr>
<td>(?!match)</td>
<td>Negative lookahead assertion - match if contents do not match next</td>
</tr>
<tr>
<td>(?<=match)</td>
<td>Positive lookbehind assertion - match if current position in string is preceded by match</td>
</tr>
<tr>
<td>(?<!match)</td>
<td>Negative lookbehind assertion - match if current position is not preceded by match</td>
</tr>
<tr>
<td>(?(id/name)yes|no)</td>
<td>Match “yes” pattern if id or name exists, otherwise match “no” pattern</td>
</tr>
</tbody>
</table>
</body></html>
"""
ABOUT =\
"""
<!DOCTYPE html><html><head></head><body>
<p><strong>Creator</strong>: <a href="https://github.com/Pewpews">Pewpews</a></p>
<p>Twitter: <a href="https://twitter.com/pewspew">@pewspew</a></p>
<p>Chat: <a href="https://gitter.im/Pewpews/happypanda">Gitter chat</a></p>
<p>Email: <code>happypandabugs@gmail.com</code></p>
<p><strong>Current version</strong>: {}</p>
<p><strong>Current database version</strong>: {}
gitextract_sr3txlm2/
├── .gitattributes
├── .gitignore
├── CHANGELOG.md
├── INSTALL.md
├── LICENSE
├── LICENSE-3RD-PARTY
├── README.rst
├── VS.txt
├── requirements-dev.txt
├── requirements.txt
├── res/
│ ├── license.txt
│ └── style.css
├── tests/
│ ├── database/
│ │ └── test_db.py
│ └── test_utils.py
└── version/
├── app.py
├── app_constants.py
├── asm_manager.py
├── color_line_edit.py
├── database/
│ ├── __init__.py
│ ├── db.py
│ └── db_constants.py
├── executors.py
├── fetch.py
├── gallery.py
├── gallerydb.py
├── gallerydialog.py
├── hplugins.py
├── io_misc.py
├── main.py
├── misc.py
├── misc_db.py
├── pewnet.py
├── settings.py
├── settingsdialog.py
└── utils.py
SYMBOL INDEX (875 symbols across 22 files)
FILE: tests/database/test_db.py
function test_init_db (line 12) | def test_init_db(path_isfile_retval, check_dbv_retval, path_is_dbc_path):
FILE: tests/test_utils.py
function test_run_backup_database (line 14) | def test_run_backup_database(mock_exists_retval, mock_isdir_retval):
FILE: version/app.py
class AppWindow (line 63) | class AppWindow(QMainWindow):
method __init__ (line 74) | def __init__(self, disable_excepthook=False):
method set_shortcuts (line 97) | def set_shortcuts(self):
method check_site_logins (line 104) | def check_site_logins(self):
method init_watchers (line 121) | def init_watchers(self):
method startup (line 159) | def startup(self):
method initUI (line 200) | def initUI(self):
method _check_update (line 281) | def _check_update(self):
method _web_metadata_picker (line 320) | def _web_metadata_picker(self, gallery, title_url_list, queue, parent=...
method get_metadata (line 328) | def get_metadata(self, gal=None):
method init_stat_bar (line 401) | def init_stat_bar(self):
method stat_temp_msg (line 420) | def stat_temp_msg(self, msg):
method stat_row_info (line 428) | def stat_row_info(self):
method set_current_manga_view (line 437) | def set_current_manga_view(self, v):
method current_manga_view (line 441) | def current_manga_view(self):
method current_manga_view (line 445) | def current_manga_view(self, new_view):
method init_spinners (line 452) | def init_spinners(self):
method search (line 471) | def search(self, srch_string):
method switch_display (line 488) | def switch_display(self):
method settings (line 495) | def settings(self):
method init_toolbar (line 500) | def init_toolbar(self):
method get_current_view (line 801) | def get_current_view(self):
method toggle_view (line 804) | def toggle_view(self):
method populate (line 817) | def populate(self, mixed=None):
method gallery_populate (line 855) | def gallery_populate(self, path, validate=False):
method scan_for_new_galleries (line 931) | def scan_for_new_galleries(self):
method dragEnterEvent (line 1007) | def dragEnterEvent(self, event):
method dropEvent (line 1013) | def dropEvent(self, event):
method resizeEvent (line 1050) | def resizeEvent(self, event):
method moveEvent (line 1058) | def moveEvent(self, event):
method showEvent (line 1062) | def showEvent(self, event):
method cleanup_exit (line 1065) | def cleanup_exit(self):
method duplicate_check (line 1133) | def duplicate_check(self, simple=True):
method excepthook (line 1181) | def excepthook(self, ex_type, ex, tb):
method closeEvent (line 1193) | def closeEvent(self, event):
FILE: version/app_constants.py
class ViewType (line 110) | class ViewType(enum.IntEnum):
class ProfileType (line 116) | class ProfileType(enum.Enum):
function load_icons (line 133) | def load_icons():
class GalleryState (line 284) | class GalleryState(enum.Enum):
class Search (line 311) | class Search(enum.Enum):
class MetadataFetchFail (line 336) | class MetadataFetchFail(Exception): pass
class InternalPagesMismatch (line 337) | class InternalPagesMismatch(Exception): pass
class ChapterExists (line 338) | class ChapterExists(Exception): pass
class ChapterWrongParentGallery (line 339) | class ChapterWrongParentGallery(Exception): pass
class CreateArchiveFail (line 340) | class CreateArchiveFail(Exception): pass
class FileNotFoundInArchive (line 341) | class FileNotFoundInArchive(Exception): pass
class WrongURL (line 342) | class WrongURL(Exception): pass
class NeedLogin (line 343) | class NeedLogin(Exception): pass
class WrongLogin (line 344) | class WrongLogin(Exception): pass
class HTMLParsing (line 345) | class HTMLParsing(Exception): pass
class GNotAvailable (line 346) | class GNotAvailable(Exception): pass
class TitleParsingError (line 347) | class TitleParsingError(Exception): pass
FILE: version/asm_manager.py
class AsmManager (line 26) | class AsmManager(DLManagerObject):
method _find_tags (line 36) | def _find_tags(browser):
method _get_metadata (line 59) | def _get_metadata(self, g_url):
method _get_server_id (line 81) | def _get_server_id(self, link_parts):
method _split_href_links_to_parts (line 101) | def _split_href_links_to_parts(links):
method _get_dl_urls (line 112) | def _get_dl_urls(self, g_url):
method _set_ehen_metadata (line 137) | def _set_ehen_metadata(h_item, dict_metadata):
method _set_metadata (line 175) | def _set_metadata(h_item, dict_metadata):
method from_gallery_url (line 201) | def from_gallery_url(self, g_url):
FILE: version/color_line_edit.py
class ColorLineEdit (line 24) | class ColorLineEdit(QLineEdit):
method __init__ (line 43) | def __init__(self, parent=None, hex_color=None):
method init_ui (line 48) | def init_ui(self, hex_color=None):
method button_click (line 68) | def button_click(self):
method update_button_color (line 84) | def update_button_color(self):
FILE: version/database/db.py
function hashes_sql (line 26) | def hashes_sql(cols=False):
function series_sql (line 44) | def series_sql(cols=False):
function chapters_sql (line 75) | def chapters_sql(cols=False):
function namespaces_sql (line 93) | def namespaces_sql(cols=False):
function tags_sql (line 105) | def tags_sql(cols=False):
function tags_mappings_sql (line 117) | def tags_mappings_sql(cols=False):
function series_tags_mappings_sql (line 133) | def series_tags_mappings_sql(cols=False):
function list_sql (line 148) | def list_sql(cols=False):
function series_list_map_sql (line 167) | def series_list_map_sql(cols=False):
function global_db_convert (line 185) | def global_db_convert(conn):
function add_db_revisions (line 229) | def add_db_revisions(old_db):
function create_db_path (line 247) | def create_db_path(db_path=db_constants.DB_PATH):
function check_db_version (line 256) | def check_db_version(conn):
function init_db (line 274) | def init_db(path=db_constants.DB_PATH):
class DBBase (line 312) | class DBBase:
method __init__ (line 318) | def __init__(self, **kwargs):
method begin (line 322) | def begin(cls):
method end (line 331) | def end(cls):
method execute (line 342) | def execute(self, *args):
method executemany (line 357) | def executemany(self, *args):
method commit (line 369) | def commit(self):
method analyze (line 373) | def analyze(cls):
method close (line 377) | def close(cls):
FILE: version/database/db_constants.py
class NoDatabaseConnection (line 35) | class NoDatabaseConnection(Exception): pass
FILE: version/executors.py
function _rounded_qimage (line 18) | def _rounded_qimage(qimg, radius):
function _task_thumbnail (line 32) | def _task_thumbnail(gallery_or_path, img=None, width=app_constants.THUMB...
function _task_load_thumbnail (line 78) | def _task_load_thumbnail(ppath, thumb_size, on_method=None, **kwargs):
class Executors (line 90) | class Executors:
method generate_thumbnail (line 95) | def generate_thumbnail(cls, gallery_or_path, img=None, width=app_const...
method load_thumbnail (line 109) | def load_thumbnail(cls, ppath, thumb_size=app_constants.THUMB_DEFAULT,...
FILE: version/fetch.py
class Fetch (line 35) | class Fetch(QObject):
method __init__ (line 58) | def __init__(self, parent=None):
method _refresh_filter_list (line 80) | def _refresh_filter_list(self):
method create_gallery (line 87) | def create_gallery(self, path, folder_name, do_chapters=True, archive=...
method local (line 201) | def local(self, s_path=None):
method _return_gallery_metadata (line 282) | def _return_gallery_metadata(self, gallery):
method fetch_metadata (line 290) | def fetch_metadata(self, gallery=None, hen=None, proc=False):
method _auto_metadata_process (line 338) | def _auto_metadata_process(self, galleries, hen, valid_url, **kwargs):
method _website_checker (line 475) | def _website_checker(self, url):
method auto_web_metadata (line 489) | def auto_web_metadata(self):
FILE: version/gallery.py
class GallerySearch (line 106) | class GallerySearch(QObject):
method __init__ (line 108) | def __init__(self, data):
method set_gallery_list (line 117) | def set_gallery_list(self, g_list):
method set_data (line 120) | def set_data(self, new_data):
method set_fav (line 124) | def set_fav(self, new_fav):
method search (line 127) | def search(self, term, args):
method _filter (line 134) | def _filter(self, terms, args):
class SortFilterModel (line 158) | class SortFilterModel(QSortFilterProxyModel):
method __init__ (line 171) | def __init__(self, parent):
method navigate_history (line 190) | def navigate_history(self, direction=PREV):
method set_gallery_list (line 204) | def set_gallery_list(self, g_list=None):
method fav_view (line 209) | def fav_view(self):
method catalog_view (line 214) | def catalog_view(self):
method setup_search (line 219) | def setup_search(self):
method refresh (line 232) | def refresh(self):
method init_search (line 235) | def init_search(self, term, args=None, **kwargs):
method filterAcceptsRow (line 266) | def filterAcceptsRow(self, source_row, parent_index):
method change_model (line 280) | def change_model(self, model):
method change_data (line 286) | def change_data(self, data):
method status_b_msg (line 289) | def status_b_msg(self, msg):
method canDropMimeData (line 292) | def canDropMimeData(self, data, action, row, coloumn, index):
method dropMimeData (line 298) | def dropMimeData(self, data, action, row, coloumn, index):
method mimeTypes (line 326) | def mimeTypes(self):
method mimeData (line 329) | def mimeData(self, index_list):
method flags (line 339) | def flags(self, index):
method supportedDragActions (line 349) | def supportedDragActions(self):
class StarRating (line 352) | class StarRating():
method __init__ (line 358) | def __init__(self, starCount=1, maxStarCount=5):
method starCount (line 374) | def starCount(self):
method maxStarCount (line 377) | def maxStarCount(self):
method setStarCount (line 380) | def setStarCount(self, starCount):
method setMaxStarCount (line 383) | def setMaxStarCount(self, maxStarCount):
method sizeHint (line 386) | def sizeHint(self):
method paint (line 389) | def paint(self, painter, rect, editMode=ReadOnly):
class GalleryModel (line 415) | class GalleryModel(QAbstractTableModel):
method __init__ (line 438) | def __init__(self, data, parent=None):
method status_b_msg (line 461) | def status_b_msg(self, msg):
method data (line 464) | def data(self, index, role=Qt.DisplayRole):
method rowCount (line 615) | def rowCount(self, index=QModelIndex()):
method columnCount (line 620) | def columnCount(self, parent=QModelIndex()):
method headerData (line 623) | def headerData(self, section, orientation, role=Qt.DisplayRole):
method insertRows (line 654) | def insertRows(self, position, rows, index=QModelIndex()):
method replaceRows (line 665) | def replaceRows(self, list_of_gallery, position, rows=1, index=QModelI...
method removeRows (line 672) | def removeRows(self, position, rows, index=QModelIndex()):
class GridDelegate (line 683) | class GridDelegate(QStyledItemDelegate):
method __init__ (line 689) | def __init__(self, app_inst, parent):
method key (line 724) | def key(self, key):
method _increment_paint_level (line 733) | def _increment_paint_level(self):
method paint (line 737) | def paint(self, painter, option, index):
method _ribbon_color (line 1100) | def _ribbon_color(self, gallery_type):
method sizeHint (line 1122) | def sizeHint(self, option, index):
class MangaView (line 1125) | class MangaView(QListView):
method __init__ (line 1132) | def __init__(self, model, v_type, filter_model=None, parent=None):
method scroll_speed (line 1197) | def scroll_speed(self):
method _calculate_scroll_speed (line 1200) | def _calculate_scroll_speed(self):
method get_visible_indexes (line 1217) | def get_visible_indexes(self, column=0):
method wheelEvent (line 1242) | def wheelEvent(self, event):
method mouseMoveEvent (line 1247) | def mouseMoveEvent(self, event):
method keyPressEvent (line 1251) | def keyPressEvent(self, event):
method favorite (line 1263) | def favorite(self, index):
method del_chapter (line 1278) | def del_chapter(self, index, chap_numb):
method sort (line 1294) | def sort(self, name):
method contextMenuEvent (line 1325) | def contextMenuEvent(self, event):
method updateGeometries (line 1328) | def updateGeometries(self):
class MangaTableView (line 1332) | class MangaTableView(QTableView):
method __init__ (line 1335) | def __init__(self, v_type, parent=None):
method keyPressEvent (line 1381) | def keyPressEvent(self, event):
method contextMenuEvent (line 1393) | def contextMenuEvent(self, event):
class CommonView (line 1396) | class CommonView:
method remove_selected (line 1402) | def remove_selected(view_cls, source=False):
method remove_gallery (line 1412) | def remove_gallery(view_cls, index_list, local=False):
method find_index (line 1455) | def find_index(view_cls, gallery_id, sort_model=False):
method open_random_gallery (line 1469) | def open_random_gallery(view_cls):
method scroll_to_index (line 1490) | def scroll_to_index(view_cls, idx, select=True):
method contextMenuEvent (line 1508) | def contextMenuEvent(view_cls, event):
method spawn_dialog (line 1557) | def spawn_dialog(app_inst, gallery=None):
class MangaViews (line 1561) | class MangaViews:
class View (line 1566) | class View(enum.Enum):
method __init__ (line 1570) | def __init__(self, v_type, parent, allow_sidebarwidget=False):
method _delegate_delete (line 1613) | def _delegate_delete(self):
method set_delete_proxy (line 1619) | def set_delete_proxy(self, other_model):
method add_gallery (line 1623) | def add_gallery(self, gallery, db=False, record_time=False):
method replace_gallery (line 1654) | def replace_gallery(self, list_of_gallery, db_optimize=True):
method changeTo (line 1683) | def changeTo(self, idx):
method get_current_view (line 1691) | def get_current_view(self):
method fav_is_current (line 1697) | def fav_is_current(self):
method hide (line 1703) | def hide(self):
method show (line 1706) | def show(self):
FILE: version/gallerydb.py
class PriorityObject (line 53) | class PriorityObject:
method __init__ (line 54) | def __init__(self, priority, data):
method __lt__ (line 58) | def __lt__(self, other):
function process_methods (line 61) | def process_methods():
function execute (line 105) | def execute(method, no_return, *args, **kwargs):
function chapter_map (line 121) | def chapter_map(row, chapter):
function gallery_map (line 129) | def gallery_map(row, gallery, chapters=True, tags=True, hashes=True):
function default_chap_exec (line 175) | def default_chap_exec(gallery_or_id, chap, only_values=False):
function default_exec (line 198) | def default_exec(object):
class GalleryDB (line 234) | class GalleryDB(DBBase):
method __init__ (line 251) | def __init__(self):
method rebuild_thumb (line 255) | def rebuild_thumb(gallery):
method clear_thumb (line 270) | def clear_thumb(path):
method clear_thumb_dir (line 286) | def clear_thumb_dir():
method rebuild_gallery (line 293) | def rebuild_gallery(gallery, thumb=False):
method modify_gallery (line 326) | def modify_gallery(cls, series_id, title=None, profile=None, artist=No...
method get_all_gallery (line 399) | def get_all_gallery(cls, chapters=True, tags=True, hashes=True):
method gen_galleries (line 409) | def gen_galleries(gallery_dict, chapters=True, tags=True, hashes=True):
method get_gallery_by_path (line 426) | def get_gallery_by_path(cls, path):
method get_gallery_by_id (line 440) | def get_gallery_by_id(cls, id):
method add_gallery (line 454) | def add_gallery(cls, object, test_mode=False):
method gallery_count (line 473) | def gallery_count(cls):
method del_gallery (line 481) | def del_gallery(cls, list_of_gallery, local=False):
method check_exists (line 512) | def check_exists(name, galleries=None, filter=True):
class ChapterDB (line 546) | class ChapterDB(DBBase):
method __init__ (line 560) | def __init__(self):
method update_chapter (line 564) | def update_chapter(cls, chapter_container, numbers=[]):
method add_chapters (line 587) | def add_chapters(cls, gallery_object):
method add_chapters_raw (line 599) | def add_chapters_raw(cls, series_id, chapters_container):
method get_chapters_for_gallery (line 613) | def get_chapters_for_gallery(cls, series_id):
method get_chapter (line 629) | def get_chapter(cls, series_id, chap_numb):
method get_chapter_id (line 646) | def get_chapter_id(cls, series_id, chapter_number):
method chapter_size (line 662) | def chapter_size(gallery_id):
method del_all_chapters (line 669) | def del_all_chapters(cls, series_id):
method del_chapter (line 675) | def del_chapter(cls, series_id, chap_number):
class TagDB (line 682) | class TagDB(DBBase):
method __init__ (line 701) | def __init__(self):
method del_tags (line 705) | def del_tags(list_of_tags_id):
method del_gallery_mapping (line 710) | def del_gallery_mapping(cls, series_id):
method get_gallery_tags (line 718) | def get_gallery_tags(cls, series_id):
method add_tags (line 760) | def add_tags(cls, object):
method modify_tags (line 840) | def modify_tags(series_id, dict_of_tags):
method get_tag_gallery (line 855) | def get_tag_gallery(tag):
method get_ns_tags (line 860) | def get_ns_tags(cls):
method get_tags_from_namespace (line 887) | def get_tags_from_namespace(namespace):
method get_ns_tags_to_gallery (line 892) | def get_ns_tags_to_gallery(ns_tags):
method get_all_tags (line 900) | def get_all_tags(cls):
method get_all_ns (line 909) | def get_all_ns(cls):
class ListDB (line 917) | class ListDB(DBBase):
method init_lists (line 923) | def init_lists(cls):
method query_gallery (line 947) | def query_gallery(cls, gallery):
method modify_list (line 957) | def modify_list(cls, gallery_list):
method add_list (line 968) | def add_list(cls, gallery_list):
method _g_id_or_list (line 983) | def _g_id_or_list(cls, gallery_or_id_or_list):
method add_gallery_to_list (line 995) | def add_gallery_to_list(cls, gallery_or_id_or_list, gallery_list):
method remove_list (line 1004) | def remove_list(cls, gallery_list):
method remove_gallery_from_list (line 1015) | def remove_gallery_from_list(cls, gallery_or_id_or_list, gallery_list):
class HashDB (line 1024) | class HashDB(DBBase):
method find_gallery (line 1036) | def find_gallery(cls, hashes):
method get_gallery_hashes (line 1075) | def get_gallery_hashes(cls, gallery_id):
method get_gallery_hash (line 1088) | def get_gallery_hash(cls, gallery_id, chapter, page=None):
method gen_gallery_hash (line 1115) | def gen_gallery_hash(cls, gallery, chapter, page=None, color_img=False...
method gen_gallery_hashes (line 1305) | def gen_gallery_hashes(cls, gallery):
method rebuild_gallery_hashes (line 1310) | def rebuild_gallery_hashes(gallery):
method del_gallery_hashes (line 1320) | def del_gallery_hashes(cls, gallery_id):
class GalleryList (line 1324) | class GalleryList:
method __init__ (line 1337) | def __init__(self, name, list_of_galleries=[], filter=None, id=None, _...
method add_gallery (line 1352) | def add_gallery(self, gallery_or_list_of, _db=True, _check_filter=True):
method remove_gallery (line 1371) | def remove_gallery(self, gallery_id_or_list_of):
method clear (line 1390) | def clear(self):
method galleries (line 1397) | def galleries(self):
method __contains__ (line 1401) | def __contains__(self, g):
method add_to_db (line 1404) | def add_to_db(self):
method scan (line 1408) | def scan(self, galleries=None):
method __lt__ (line 1451) | def __lt__(self, other):
class Gallery (line 1454) | class Gallery:
method __init__ (line 1483) | def __init__(self):
method path (line 1521) | def path(self):
method path (line 1525) | def path(self, n_p):
method set_defaults (line 1531) | def set_defaults(self):
method reset_profile (line 1539) | def reset_profile(self):
method _profile_loaded (line 1543) | def _profile_loaded(self, img, ptype=None, method=None):
method get_profile (line 1548) | def get_profile(self, ptype, on_method=None):
method set_profile (line 1567) | def set_profile(self, future):
method chapters (line 1574) | def chapters(self):
method chapters (line 1578) | def chapters(self, chp_cont):
method merge (line 1583) | def merge(galleries):
method gen_hashes (line 1587) | def gen_hashes(self):
method validate (line 1599) | def validate(self):
method invalidities (line 1614) | def invalidities(self):
method _keyword_search (line 1621) | def _keyword_search(self, ns, tag, args=[]):
method __contains__ (line 1697) | def __contains__(self, key):
method contains (line 1702) | def contains(self, key, args=[]):
method move_gallery (line 1798) | def move_gallery(self, new_path=''):
method __lt__ (line 1825) | def __lt__(self, other):
method __str__ (line 1828) | def __str__(self):
class Chapter (line 1834) | class Chapter:
method __init__ (line 1846) | def __init__(self, parent, gallery, number=0, path='', pages=0, in_arc...
method __lt__ (line 1855) | def __lt__(self, other):
method __str__ (line 1858) | def __str__(self):
method next_chapter (line 1869) | def next_chapter(self):
method previous_chapter (line 1876) | def previous_chapter(self):
method open (line 1882) | def open(self, stat_msg=True):
class ChaptersContainer (line 1899) | class ChaptersContainer:
method __init__ (line 1907) | def __init__(self, gallery=None):
method set_parent (line 1914) | def set_parent(self, gallery):
method add_chapter (line 1921) | def add_chapter(self, chp, overwrite=True, db=False):
method create_chapter (line 1940) | def create_chapter(self, number=None):
method update_chapter_pages (line 1959) | def update_chapter_pages(self, number):
method pages (line 1974) | def pages(self):
method get_chapter (line 1980) | def get_chapter(self, number):
method get_all_chapters (line 1983) | def get_all_chapters(self):
method count (line 1986) | def count(self):
method pop (line 1989) | def pop(self, key, default=None):
method __len__ (line 1992) | def __len__(self):
method __getitem__ (line 1995) | def __getitem__(self, key):
method __setitem__ (line 1998) | def __setitem__(self, key, value):
method __delitem__ (line 2006) | def __delitem__(self, key):
method __iter__ (line 2009) | def __iter__(self):
method __bool__ (line 2012) | def __bool__(self):
method __str__ (line 2015) | def __str__(self):
method __contains__ (line 2023) | def __contains__(self, key):
class AdminDB (line 2029) | class AdminDB(QObject):
method __init__ (line 2033) | def __init__(self, parent=None):
method from_v021_to_v022 (line 2036) | def from_v021_to_v022(self, old_db_path=db_constants.DB_PATH):
method rebuild_database (line 2118) | def rebuild_database(self):
method rebuild_galleries (line 2151) | def rebuild_galleries(self):
method rebuild_thumbs (line 2161) | def rebuild_thumbs(self, clear_first):
class DatabaseStartup (line 2177) | class DatabaseStartup(QObject):
method __init__ (line 2189) | def __init__(self):
method startup (line 2199) | def startup(self, manga_views):
method fetch_galleries (line 2221) | def fetch_galleries(self, f, t, manga_views):
method fetch_chapters (line 2234) | def fetch_chapters(self):
method fetch_tags (line 2238) | def fetch_tags(self):
method fetch_hashes (line 2242) | def fetch_hashes(self):
FILE: version/gallerydialog.py
class GalleryDialog (line 26) | class GalleryDialog(QWidget):
method __init__ (line 33) | def __init__(self, parent, arg=None):
method commonUI (line 100) | def commonUI(self):
method resizeEvent (line 252) | def resizeEvent(self, event):
method _find_combobox_match (line 257) | def _find_combobox_match(self, combobox, key, default):
method setGallery (line 266) | def setGallery(self, gallery):
method newUI (line 342) | def newUI(self):
method choose_dir (line 362) | def choose_dir(self, mode):
method check (line 415) | def check(self):
method reject (line 431) | def reject(self):
method web_metadata (line 443) | def web_metadata(self, url, btn_widget, pgr_widget):
method set_web_metadata (line 488) | def set_web_metadata(self, metadata):
method make_gallery (line 502) | def make_gallery(self, new_gallery, add_to_model=True, new=False):
method link_set (line 569) | def link_set(self):
method link_modify (line 577) | def link_modify(self):
method _disconnect (line 585) | def _disconnect(self):
method delayed_close (line 593) | def delayed_close(self):
method accept (line 600) | def accept(self):
method accept_edit (line 604) | def accept_edit(self):
method reject_edit (line 613) | def reject_edit(self):
FILE: version/hplugins.py
class PluginError (line 16) | class PluginError(ValueError):
class PluginIDError (line 19) | class PluginIDError(PluginError):
class PluginNameError (line 22) | class PluginNameError(PluginIDError):
class PluginMethodError (line 25) | class PluginMethodError(PluginError):
class Plugins (line 28) | class Plugins:
method register (line 36) | def register(self, plugin):
method _connectHooks (line 42) | def _connectHooks(self):
method __getattr__ (line 60) | def __getattr__(self, key):
class HPluginMeta (line 68) | class HPluginMeta(pyqtWrapperType):
method __init__ (line 70) | def __init__(cls, name, bases, dct):
method connectPlugin (line 124) | def connectPlugin(cls, pluginid, plugin_name):
method connectHook (line 153) | def connectHook(self, pluginid, hook_name, handler):
method createHook (line 165) | def createHook(self, hook_name):
method __getattr__ (line 193) | def __getattr__(self, key):
FILE: version/io_misc.py
class GalleryDownloaderUrlExtracter (line 32) | class GalleryDownloaderUrlExtracter(QWidget):
method __init__ (line 36) | def __init__(self, parent=None):
method add_to_queue (line 51) | def add_to_queue(self):
class GalleryDownloaderItem (line 59) | class GalleryDownloaderItem(QObject):
method __init__ (line 64) | def __init__(self, hitem):
method check_progress (line 105) | def check_progress(self):
method done (line 115) | def done(self):
class GalleryDownloaderList (line 122) | class GalleryDownloaderList(QTableWidget):
method __init__ (line 126) | def __init__(self, app_inst, parent=None):
method add_entry (line 171) | def add_entry(self, hitem):
method _get_hitem (line 186) | def _get_hitem(self, idx):
method contextMenuEvent (line 190) | def contextMenuEvent(self, event):
method _init_gallery (line 212) | def _init_gallery(self, download_item):
method _gallery_to_model (line 231) | def _gallery_to_model(self):
method clear_list (line 255) | def clear_list(self):
class GalleryDownloader (line 261) | class GalleryDownloader(QWidget):
method __init__ (line 265) | def __init__(self, parent):
method add_download_entry (line 311) | def add_download_entry(self, url=None, extractor=False):
method website_validator (line 356) | def website_validator(self, url):
method show (line 388) | def show(self):
method closeEvent (line 394) | def closeEvent(self, QCloseEvent):
class GalleryPopup (line 397) | class GalleryPopup(misc.BasePopup):
method __init__ (line 403) | def __init__(self, tup_gallery, parent = None, menu = None):
method get_all_items (line 438) | def get_all_items(self):
class ModifiedPopup (line 446) | class ModifiedPopup(misc.BasePopup):
method __init__ (line 447) | def __init__(self, path, gallery_id, parent=None):
class MovedPopup (line 481) | class MovedPopup(misc.BasePopup):
method __init__ (line 483) | def __init__(self, new_path, gallery, parent=None):
class DeletedPopup (line 523) | class DeletedPopup(misc.BasePopup):
method __init__ (line 526) | def __init__(self, path, gallery, parent=None):
class GalleryHandler (line 587) | class GalleryHandler(FileSystemEventHandler, QObject):
method __init__ (line 593) | def __init__(self):
method file_filter (line 597) | def file_filter(self, event):
method on_created (line 618) | def on_created(self, event):
method on_deleted (line 632) | def on_deleted(self, event):
method on_modified (line 642) | def on_modified(self, event):
method on_moved (line 645) | def on_moved(self, event):
class Watchers (line 655) | class Watchers:
method __init__ (line 656) | def __init__(self):
method stop_all (line 670) | def stop_all(self):
class GalleryImpExpData (line 674) | class GalleryImpExpData:
method __init__ (line 678) | def __init__(self, format=1):
method get_pages (line 686) | def get_pages(self, pages):
method add_data (line 700) | def add_data(self, name, data):
method save (line 706) | def save(self, file_path):
method find_pair (line 713) | def find_pair(self, found_pairs):
class ListImpData (line 780) | class ListImpData:
class ImportExport (line 783) | class ImportExport(QObject):
method __init__ (line 789) | def __init__(self):
method import_data (line 792) | def import_data(self, path):
method export_data (line 849) | def export_data(self, gallery=None):
FILE: version/main.py
function start (line 29) | def start(test=False):
FILE: version/misc.py
function text_layout (line 70) | def text_layout(text, width, font, font_metrics, alignment=Qt.AlignCenter):
function centerWidget (line 92) | def centerWidget(widget, parent_widget=None):
function clearLayout (line 103) | def clearLayout(layout):
function create_animation (line 112) | def create_animation(parent, prop):
class ArrowHandle (line 116) | class ArrowHandle(QWidget):
method __init__ (line 120) | def __init__(self, parent):
method paintEvent (line 128) | def paintEvent(self, event):
method click (line 153) | def click(self):
method mousePressEvent (line 162) | def mousePressEvent(self, event):
class Line (line 167) | class Line(QFrame):
method __init__ (line 169) | def __init__(self, orentiation, parent=None):
class CompleterPopupView (line 178) | class CompleterPopupView(QListView):
method __init__ (line 179) | def __init__(self, *args, **kwargs):
method _setup (line 182) | def _setup(self):
method showEvent (line 190) | def showEvent(self, event):
class ElidedLabel (line 195) | class ElidedLabel(QLabel):
method __init__ (line 196) | def __init__(self, *args, **kwargs):
method paintEvent (line 198) | def paintEvent(self, event):
class BaseMoveWidget (line 204) | class BaseMoveWidget(QWidget):
method __init__ (line 205) | def __init__(self, parent=None, **kwargs):
method update_move (line 216) | def update_move(self, new_size=None):
class SortMenu (line 225) | class SortMenu(QMenu):
method __init__ (line 227) | def __init__(self, app_inst, parent=None, toolbutton=None):
method set_toolbutton_text (line 261) | def set_toolbutton_text(self):
method set_current_sort (line 266) | def set_current_sort(self):
method asc_desc (line 287) | def asc_desc(self):
method showEvent (line 297) | def showEvent(self, event):
class ToolbarButton (line 301) | class ToolbarButton(QPushButton):
method __init__ (line 304) | def __init__(self, parent=None, txt=''):
method selected (line 312) | def selected(self):
method selected (line 316) | def selected(self, b):
method contextMenuEvent (line 319) | def contextMenuEvent(self, event):
class TransparentWidget (line 328) | class TransparentWidget(BaseMoveWidget):
method __init__ (line 329) | def __init__(self, parent = None, **kwargs):
class ArrowWindow (line 333) | class ArrowWindow(TransparentWidget):
method __init__ (line 336) | def __init__(self, parent):
method arrow_size (line 345) | def arrow_size(self):
method arrow_size (line 349) | def arrow_size(self, w_h_tuple):
method paintEvent (line 363) | def paintEvent(self, event):
class GalleryMetaWindow (line 428) | class GalleryMetaWindow(ArrowWindow):
method __init__ (line 430) | def __init__(self, parent):
method show (line 452) | def show(self):
method focusOutEvent (line 463) | def focusOutEvent(self, event):
method _mouse_in_gallery (line 467) | def _mouse_in_gallery(self):
method mouseMoveEvent (line 475) | def mouseMoveEvent(self, event):
method delayed_hide (line 482) | def delayed_hide(self):
method show_gallery (line 486) | def show_gallery(self, index, view):
method closeEvent (line 575) | def closeEvent(self, ev):
method _set_gallery (line 579) | def _set_gallery(self, gallery):
class GalleryLayout (line 595) | class GalleryLayout(QFrame):
class ChapterList (line 596) | class ChapterList(QTableWidget):
method __init__ (line 597) | def __init__(self, parent):
method set_chapters (line 620) | def set_chapters(self, chapter_container):
method _get_chap (line 645) | def _get_chap(self, idx):
method contextMenuEvent (line 650) | def contextMenuEvent(self, event):
method __init__ (line 669) | def __init__(self, parent, appwindow):
method has_tags (line 761) | def has_tags(self, tags):
method apply_gallery (line 771) | def apply_gallery(self, gallery):
class Spinner (line 820) | class Spinner(TransparentWidget):
method __init__ (line 829) | def __init__(self, parent, position='topright'):
method _update_layout (line 870) | def _update_layout(self):
method set_size (line 874) | def set_size(self, w):
method set_text (line 880) | def set_text(self, txt):
method _set_position (line 885) | def _set_position(self, new_pos):
method paintEvent (line 903) | def paintEvent(self, event):
method showEvent (line 945) | def showEvent(self, event):
method hideEvent (line 956) | def hideEvent(self, event):
method before_hide (line 961) | def before_hide(self):
method closeEvent (line 969) | def closeEvent(self, event):
method _on_timer_timeout (line 973) | def _on_timer_timeout(self):
class GalleryMenu (line 990) | class GalleryMenu(QMenu):
method __init__ (line 994) | def __init__(self, view, index, sort_model, app_window, selected_index...
method lookup_web (line 1157) | def lookup_web(self, txt):
method set_rating (line 1168) | def set_rating(self, x):
method add_to_ignore (line 1180) | def add_to_ignore(self):
method send_to_lib (line 1199) | def send_to_lib(self):
method allow_metadata_fetch (line 1215) | def allow_metadata_fetch(self):
method reset_read_count (line 1226) | def reset_read_count(self):
method add_to_list (line 1236) | def add_to_list(self, g_list):
method remove_from_list (line 1245) | def remove_from_list(self):
method favourite_select (line 1256) | def favourite_select(self):
method change_cover (line 1260) | def change_cover(self):
method open_first_chapters (line 1283) | def open_first_chapters(self):
method op_link (line 1289) | def op_link(self, select=False):
method op_folder (line 1298) | def op_folder(self, select=False, containing=False):
method add_chapters (line 1322) | def add_chapters(self):
class SystemTray (line 1331) | class SystemTray(QSystemTrayIcon):
method __init__ (line 1336) | def __init__(self, icon, parent=None):
method showMessage (line 1340) | def showMessage(self, title, msg, icon=QSystemTrayIcon.Information,
class ClickedLabel (line 1351) | class ClickedLabel(QLabel):
method __init__ (line 1356) | def __init__(self, s="", **kwargs):
method enterEvent (line 1360) | def enterEvent(self, event):
method mousePressEvent (line 1367) | def mousePressEvent(self, event):
class TagText (line 1371) | class TagText(QPushButton):
method __init__ (line 1372) | def __init__(self, *args, **kwargs):
method mousePressEvent (line 1382) | def mousePressEvent(self, ev):
method enterEvent (line 1395) | def enterEvent(self, event):
class BasePopup (line 1402) | class BasePopup(TransparentWidget):
method __init__ (line 1404) | def __init__(self, parent=None, **kwargs):
method mousePressEvent (line 1444) | def mousePressEvent(self, event):
method mouseMoveEvent (line 1448) | def mouseMoveEvent(self, event):
method showEvent (line 1455) | def showEvent(self, event):
method closeEvent (line 1462) | def closeEvent(self, event):
method hideEvent (line 1467) | def hideEvent(self, event):
method add_buttons (line 1472) | def add_buttons(self, *args):
class AppBubble (line 1485) | class AppBubble(BasePopup):
method __init__ (line 1487) | def __init__(self, parent):
method update_text (line 1503) | def update_text(self, title, txt='', duration=20):
method update_move (line 1514) | def update_move(self):
method mousePressEvent (line 1521) | def mousePressEvent(self, event):
class AppDialog (line 1526) | class AppDialog(BasePopup):
method __init__ (line 1532) | def __init__(self, parent, mode=PROGRESS):
method closeEvent (line 1574) | def closeEvent(self, event):
method showEvent (line 1582) | def showEvent(self, event):
method init_restart (line 1586) | def init_restart(self):
class NotificationOverlay (line 1595) | class NotificationOverlay(QWidget):
method __init__ (line 1604) | def __init__(self, parent=None):
method set_dynamic_height (line 1627) | def set_dynamic_height(self, h):
method mousePressEvent (line 1630) | def mousePressEvent(self, event):
method set_clickable (line 1635) | def set_clickable(self, d=True):
method resize (line 1639) | def resize(self, x, y=0):
method add_text (line 1642) | def add_text(self, text, autohide=True):
method begin_show (line 1657) | def begin_show(self):
method end_show (line 1665) | def end_show(self):
method _reset (line 1669) | def _reset(self):
method showEvent (line 1674) | def showEvent(self, event):
class GalleryShowcaseWidget (line 1678) | class GalleryShowcaseWidget(QWidget):
method __init__ (line 1685) | def __init__(self, gallery=None, parent=None, menu=None):
method menu (line 1714) | def menu(self):
method contextmenu (line 1718) | def contextmenu(self, new_menu):
method set_pixmap (line 1722) | def set_pixmap(self, gallery, img):
method set_gallery (line 1725) | def set_gallery(self, gallery, size=app_constants.THUMB_SMALL):
method paintEvent (line 1739) | def paintEvent(self, event):
method enterEvent (line 1747) | def enterEvent(self, event):
method leaveEvent (line 1751) | def leaveEvent(self, event):
method mouseDoubleClickEvent (line 1755) | def mouseDoubleClickEvent(self, event):
method contextMenuEvent (line 1759) | def contextMenuEvent(self, event):
class SingleGalleryChoices (line 1766) | class SingleGalleryChoices(BasePopup):
method __init__ (line 1773) | def __init__(self, gallery, tuple_first_idx, text=None, parent=None):
method finish (line 1805) | def finish(self):
method skip (line 1812) | def skip(self):
method skipall (line 1816) | def skipall(self):
class BaseUserChoice (line 1820) | class BaseUserChoice(QDialog):
method __init__ (line 1822) | def __init__(self, parent, **kwargs):
method accept (line 1831) | def accept(self, choice):
class TorrentItem (line 1835) | class TorrentItem:
method __init__ (line 1836) | def __init__(self, url, name="", date=None, size=None, seeds=None, pee...
class TorrentUserChoice (line 1845) | class TorrentUserChoice(BaseUserChoice):
method __init__ (line 1846) | def __init__(self, parent, torrentitems=[], **kwargs):
method add_torrent_item (line 1864) | def add_torrent_item(self, item):
method accept (line 1869) | def accept(self):
class LoadingOverlay (line 1875) | class LoadingOverlay(QWidget):
method __init__ (line 1877) | def __init__(self, parent=None):
method paintEngine (line 1883) | def paintEngine(self, event):
method showEvent (line 1901) | def showEvent(self, event):
method timerEvent (line 1906) | def timerEvent(self, event):
class FileIcon (line 1913) | class FileIcon:
method __init__ (line 1915) | def __init__(self):
method get_file_icon (line 1918) | def get_file_icon(self, path):
method get_external_file_icon (line 1933) | def get_external_file_icon():
method refresh_default_icon (line 1946) | def refresh_default_icon():
method get_default_file_icon (line 1994) | def get_default_file_icon():
class Spacer (line 2024) | class Spacer(QWidget):
method __init__ (line 2029) | def __init__(self, mode='both', parent=None):
class FlowLayout (line 2038) | class FlowLayout(QLayout):
method __init__ (line 2040) | def __init__(self, parent=None, margin=0, spacing=-1):
method __del__ (line 2050) | def __del__(self):
method addItem (line 2055) | def addItem(self, item):
method count (line 2058) | def count(self):
method rowCount (line 2062) | def rowCount(self):
method itemAt (line 2065) | def itemAt(self, index):
method takeAt (line 2071) | def takeAt(self, index):
method expandingDirections (line 2077) | def expandingDirections(self):
method hasHeightForWidth (line 2080) | def hasHeightForWidth(self):
method heightForWidth (line 2083) | def heightForWidth(self, width):
method setGeometry (line 2087) | def setGeometry(self, rect):
method sizeHint (line 2091) | def sizeHint(self):
method minimumSize (line 2094) | def minimumSize(self):
method doLayout (line 2105) | def doLayout(self, rect, testOnly):
class LineEdit (line 2129) | class LineEdit(QLineEdit):
method __init__ (line 2133) | def __init__(self, parent=None):
method mousePressEvent (line 2136) | def mousePressEvent(self, event):
method contextMenuEvent (line 2142) | def contextMenuEvent(self, QContextMenuEvent):
method sizeHint (line 2145) | def sizeHint(self):
class PathLineEdit (line 2149) | class PathLineEdit(QLineEdit):
method __init__ (line 2154) | def __init__(self, parent=None, dir=True, filters=utils.FILE_FILTER):
method openExplorer (line 2161) | def openExplorer(self):
method mousePressEvent (line 2172) | def mousePressEvent(self, event):
class ChapterAddWidget (line 2184) | class ChapterAddWidget(QWidget):
method __init__ (line 2186) | def __init__(self, gallery, parent=None):
method add_new_chapter (line 2233) | def add_new_chapter(self, mode):
method finish (line 2263) | def finish(self):
class CustomListItem (line 2296) | class CustomListItem(QListWidgetItem):
method __init__ (line 2297) | def __init__(self, item=None, parent=None, txt='', type=QListWidgetIte...
class CustomTableItem (line 2301) | class CustomTableItem(QTableWidgetItem):
method __init__ (line 2302) | def __init__(self, item=None, txt='', type=QTableWidgetItem.Type):
class GalleryListView (line 2306) | class GalleryListView(QWidget):
method __init__ (line 2308) | def __init__(self, parent=None, modal=False):
method all_check_state (line 2371) | def all_check_state(self, new_state):
method add_gallery (line 2385) | def add_gallery(self, item, name):
method update_count (line 2398) | def update_count(self):
method return_gallery (line 2401) | def return_gallery(self):
method from_folder (line 2417) | def from_folder(self):
method from_files (line 2433) | def from_files(self):
method close_window (line 2442) | def close_window(self):
class Loading (line 2451) | class Loading(BasePopup):
method __init__ (line 2453) | def __init__(self, parent=None):
method mousePressEvent (line 2473) | def mousePressEvent(self, QMouseEvent):
method setText (line 2476) | def setText(self, string):
class CompleterTextEdit (line 2480) | class CompleterTextEdit(QTextEdit):
method __init__ (line 2484) | def __init__(self, **kwargs):
method setCompleter (line 2489) | def setCompleter(self, c):
method completer (line 2500) | def completer(self):
method insertCompletion (line 2503) | def insertCompletion(self, completion):
method textUnderCursor (line 2514) | def textUnderCursor(self):
method focusInEvent (line 2520) | def focusInEvent(self, e):
method keyPressEvent (line 2526) | def keyPressEvent(self, e):
class GCompleter (line 2560) | class GCompleter(QCompleter):
method __init__ (line 2561) | def __init__(self, parent=None, title=True, artist=True, tags=True):
class ChapterListItem (line 2579) | class ChapterListItem(QFrame):
method __init__ (line 2581) | def __init__(self, chapter, parent=None):
method set_chapter_title (line 2617) | def set_chapter_title(self, chapter):
FILE: version/misc_db.py
class ToolbarTabManager (line 41) | class ToolbarTabManager(QObject):
method __init__ (line 43) | def __init__(self, toolbar, parent=None):
method _manage_selected (line 62) | def _manage_selected(self, b):
method addTab (line 77) | def addTab(self, name, view_type=app_constants.ViewType.Default, deleg...
method removeTab (line 101) | def removeTab(self, button_or_index):
class NoTooltipModel (line 116) | class NoTooltipModel(QIdentityProxyModel):
method __init__ (line 118) | def __init__(self, model, parent=None):
method data (line 122) | def data(self, index, role=Qt.DisplayRole):
class UniqueInfoModel (line 131) | class UniqueInfoModel(QSortFilterProxyModel):
method __init__ (line 132) | def __init__(self, gallerymodel, role, parent=None):
method filterAcceptsRow (line 140) | def filterAcceptsRow(self, source_row, parent_index):
method invalidate (line 154) | def invalidate(self):
class ListDelegate (line 158) | class ListDelegate(QStyledItemDelegate):
method __init__ (line 159) | def __init__(self, parent=None):
method sizeHint (line 164) | def sizeHint(self, option, index):
class GalleryArtistsList (line 170) | class GalleryArtistsList(QListView):
method __init__ (line 173) | def __init__(self, gallerymodel, parent=None):
method _artist_clicked (line 183) | def _artist_clicked(self, idx):
method set_current_glist (line 187) | def set_current_glist(self, g_list=None):
class TagsTreeView (line 194) | class TagsTreeView(QTreeWidget):
method __init__ (line 197) | def __init__(self, parent):
method _convert_to_str (line 204) | def _convert_to_str(self, items):
method search_tags (line 227) | def search_tags(self, items):
method create_list (line 230) | def create_list(self, items):
method contextMenuEvent (line 236) | def contextMenuEvent(self, event):
method setup_tags (line 284) | def setup_tags(self):
class GalleryListEdit (line 299) | class GalleryListEdit(misc.BasePopup):
method __init__ (line 301) | def __init__(self, parent=None):
method set_list (line 329) | def set_list(self, gallery_list, item):
method accept (line 342) | def accept(self):
class GalleryListContextMenu (line 355) | class GalleryListContextMenu(QMenu):
method __init__ (line 356) | def __init__(self, item, sidebar):
method edit_list (line 365) | def edit_list(self):
method remove_list (line 369) | def remove_list(self):
method clear_list (line 374) | def clear_list(self):
class GalleryLists (line 378) | class GalleryLists(QListWidget):
method __init__ (line 382) | def __init__(self, parent):
method dragEnterEvent (line 400) | def dragEnterEvent(self, event):
method dragMoveEvent (line 406) | def dragMoveEvent(self, event):
method dropEvent (line 413) | def dropEvent(self, event):
method _add_new_list (line 428) | def _add_new_list(self, lineedit=None, hint=None, gallery_list=None):
method create_new_list (line 442) | def create_new_list(self, name=None, gallery_list=None):
method _item_double_clicked (line 454) | def _item_double_clicked(self, item):
method _reset_selected (line 464) | def _reset_selected(self):
method setup_lists (line 468) | def setup_lists(self):
method contextMenuEvent (line 473) | def contextMenuEvent(self, event):
class SideBarWidget (line 482) | class SideBarWidget(QFrame):
method __init__ (line 485) | def __init__(self, parent):
method _slide_hide (line 586) | def _slide_hide(self, state):
method slide (line 600) | def slide(self, state):
method showEvent (line 610) | def showEvent(self, event):
method _init_size (line 615) | def _init_size(self, event=None):
method resizeEvent (line 622) | def resizeEvent(self, event):
class DBOverview (line 627) | class DBOverview(QWidget):
method __init__ (line 632) | def __init__(self, parent, window=False):
method setup_stats (line 657) | def setup_stats(self):
method setup_about_db (line 660) | def setup_about_db(self):
method closeEvent (line 663) | def closeEvent(self, event):
FILE: version/pewnet.py
class DownloaderItem (line 50) | class DownloaderItem(QObject):
method __init__ (line 54) | def __init__(self, url="", session=None):
method cancel (line 65) | def cancel(self):
method open (line 68) | def open(self, containing=False):
class Downloader (line 76) | class Downloader(QObject):
method __init__ (line 87) | def __init__(self):
method add_to_queue (line 95) | def add_to_queue(item, session=None, dir=None):
method remove_file (line 116) | def remove_file(filename):
method _get_total_size (line 128) | def _get_total_size(response):
method _get_response (line 138) | def _get_response(self, url):
method _get_item_and_temp_base (line 151) | def _get_item_and_temp_base(self):
method _get_filename (line 164) | def _get_filename(self, item, temp_base=None):
method _download_with_simple_method (line 183) | def _download_with_simple_method(target_file, response, item, interrup...
method _download_with_catch_error (line 208) | def _download_with_catch_error(
method _download_with_tempfile_windows (line 249) | def _download_with_tempfile_windows(
method _download_single_file (line 296) | def _download_single_file(
method _rename_file (line 366) | def _rename_file(filename, filename_part, max_loop=100):
method _get_total_size_prediction (line 400) | def _get_total_size_prediction(known_filesize, urls_len):
method _get_local_filesize (line 417) | def _get_local_filesize(path):
method _download_item_with_multiple_dl_url (line 431) | def _download_item_with_multiple_dl_url(self, item, folder, interrupt_...
method _download_item_with_single_dl_url (line 500) | def _download_item_with_single_dl_url(self, item, filename, interrupt_...
method _downloading (line 541) | def _downloading(self):
method start_manager (line 571) | def start_manager(self, max_tasks):
class HenItem (line 582) | class HenItem(DownloaderItem):
method __init__ (line 585) | def __init__(self, session=None):
method fetch_thumb (line 598) | def fetch_thumb(self):
method check_type (line 606) | def check_type(self):
method update_metadata (line 610) | def update_metadata(self, key, value):
method commit_metadata (line 641) | def commit_metadata(self):
class DLManager (line 650) | class DLManager(QObject):
method __init__ (line 655) | def __init__(self, download_type=app_constants.DOWNLOAD_TYPE_OTHER):
method _error (line 659) | def _error(self):
method from_gallery_url (line 662) | def from_gallery_url(self, url):
method ensure_browser_on_url (line 682) | def ensure_browser_on_url(self, url):
class ChaikaManager (line 698) | class ChaikaManager(DLManager):
method __init__ (line 701) | def __init__(self):
method from_gallery_url (line 706) | def from_gallery_url(self, url):
method _gallery_page (line 732) | def _gallery_page(self, g_id, h_item):
method _archive_page (line 766) | def _archive_page(self, a_id, h_item):
class HenManager (line 777) | class HenManager(DLManager):
method __init__ (line 780) | def __init__(self):
method _archive_url_d (line 800) | def _archive_url_d(self, gid, token, key):
method _torrent_url_d (line 806) | def _torrent_url_d(self, gid, token):
method gtoEh (line 842) | def gtoEh(g_url):
method from_gallery_url (line 850) | def from_gallery_url(self, g_url):
class ExHenManager (line 948) | class ExHenManager(HenManager):
method __init__ (line 950) | def __init__(self):
class CommenHen (line 955) | class CommenHen:
method begin_lock (line 966) | def begin_lock(self):
method end_lock (line 976) | def end_lock(self):
method add_to_queue (line 981) | def add_to_queue(self, url='', proc=False, parse=True):
method process_queue (line 1003) | def process_queue(self):
method login (line 1026) | def login(cls, user, password, relogin=False):
method check_login (line 1030) | def check_login(cls, cookies):
method check_cookie (line 1033) | def check_cookie(self, cookie):
method handle_error (line 1048) | def handle_error(self, response):
method parse_metadata (line 1052) | def parse_metadata(cls, metadata_json, dict_metadata):
method get_metadata (line 1061) | def get_metadata(self, list_of_urls, cookies=None):
method apply_metadata (line 1069) | def apply_metadata(cls, gallery, data, append=True):
method search (line 1075) | def search(self, search_string, **kwargs):
class NHen (line 1082) | class NHen(CommenHen):
method login (line 1087) | def login(cls, user, password, relogin=False):
method check_login (line 1119) | def check_login(cls, cookies):
method apply_metadata (line 1124) | def apply_metadata(cls, gallery, data, append = True):
method search (line 1127) | def search(self, search_string, cookies = None, **kwargs):
class EHen (line 1131) | class EHen(CommenHen):
method __init__ (line 1133) | def __init__(self, cookies = None):
method apply_metadata (line 1140) | def apply_metadata(cls, g, data, append = True):
method check_login (line 1209) | def check_login(cls, cookies):
method handle_error (line 1240) | def handle_error(self, response, wait=True):
method parse_url (line 1260) | def parse_url(cls, url):
method get_metadata (line 1271) | def get_metadata(self, list_of_urls, cookies=None):
method parse_metadata (line 1317) | def parse_metadata(cls, metadata_json, dict_metadata):
method login (line 1367) | def login(cls, user, password, relogin=False):
method search (line 1405) | def search(self, search_string, **kwargs):
class ExHen (line 1514) | class ExHen(EHen):
method __init__ (line 1516) | def __init__(self, cookies=None):
method get_metadata (line 1521) | def get_metadata(self, list_of_urls):
method search (line 1524) | def search(self, hash_string, **kwargs):
class ChaikaHen (line 1527) | class ChaikaHen(CommenHen):
method __init__ (line 1532) | def __init__(self):
method search (line 1536) | def search(self, search_string, **kwargs):
method get_metadata (line 1549) | def get_metadata(self, list_of_urls):
method parse_metadata (line 1599) | def parse_metadata(cls, data, dict_metadata):
method apply_metadata (line 1621) | def apply_metadata(cls, g, data, append = True):
function hen_list_init (line 1627) | def hen_list_init():
FILE: version/settings.py
class Config (line 33) | class Config(configparser.ConfigParser):
method __init__ (line 34) | def __init__(self):
method read (line 37) | def read(self, filenames, encoding = None):
method save (line 41) | def save(self, encoding = 'utf-8', space_around_delimeters=True):
function save (line 59) | def save():
function get (line 63) | def get(default, section, key=None, type_class=str, subtype_class=None):
function set (line 106) | def set(value, section, key=None):
class Properties (line 128) | class Properties:
class ExProperties (line 132) | class ExProperties(Properties):
method __init__ (line 138) | def __init__(self, site=EHENTAI):
method save (line 150) | def save(self):
method cookies (line 156) | def cookies(self):
method cookies (line 163) | def cookies(self, c):
method username (line 169) | def username(self):
method username (line 175) | def username(self, us):
method password (line 181) | def password(self):
method password (line 187) | def password(self, ps):
method custom (line 193) | def custom(self):
method custom (line 199) | def custom(self, ps):
class WinProperties (line 204) | class WinProperties(Properties):
method __init__ (line 205) | def __init__(self):
method resize (line 210) | def resize(self):
method resize (line 214) | def resize(self, size):
method pos (line 219) | def pos(self):
method pos (line 223) | def pos(self, point):
function win_read (line 227) | def win_read(cls, name):
function win_save (line 240) | def win_save(cls, name, winprops=None):
FILE: version/settingsdialog.py
class SettingsDialog (line 30) | class SettingsDialog(QWidget):
method __init__ (line 35) | def __init__(self, parent=None):
method initUI (line 48) | def initUI(self):
method change (line 109) | def change(self, item):
method restore_values (line 124) | def restore_values(self):
method restore_options (line 134) | def restore_options(self):
method accept (line 266) | def accept(self):
method init_right_panel (line 537) | def init_right_panel(self):
method _get_color_line_edit_and_hbox_layout (line 1305) | def _get_color_line_edit_and_hbox_layout(hex_color=None):
method add_folder_monitor (line 1313) | def add_folder_monitor(self, path=''):
method add_ignore_path (line 1321) | def add_ignore_path(self, path='', dir=True):
method color_checker (line 1329) | def color_checker(self, txt):
method take_all_layout_widgets (line 1336) | def take_all_layout_widgets(self, l):
method choose_font (line 1345) | def choose_font(self):
method open_hp_folder (line 1352) | def open_hp_folder(self):
method reject (line 1358) | def reject(self):
method _find_combobox_match (line 1362) | def _find_combobox_match(self, combobox, key, default):
FILE: version/utils.py
class GMetafile (line 59) | class GMetafile:
method __init__ (line 60) | def __init__(self, path=None, archive=''):
method _eze (line 90) | def _eze(self, fp):
method _hdoujindler (line 117) | def _hdoujindler(self, fp):
method detect (line 172) | def detect(self):
method update (line 187) | def update(self, other):
method apply_gallery (line 190) | def apply_gallery(self, gallery):
function backup_database (line 210) | def backup_database(db_path=db_constants.DB_PATH):
function get_date_age (line 235) | def get_date_age(date):
function all_opposite (line 279) | def all_opposite(*args):
function update_gallery_path (line 287) | def update_gallery_path(new_path, gallery):
function move_files (line 299) | def move_files(path, dest='', only_path=False):
function check_ignore_list (line 322) | def check_ignore_list(key):
function gallery_text_fixer (line 335) | def gallery_text_fixer(gallery):
function b_search (line 356) | def b_search(data, key):
function generate_img_hash (line 370) | def generate_img_hash(src):
class ArchiveFile (line 384) | class ArchiveFile():
method __init__ (line 393) | def __init__(self, filepath):
method namelist (line 417) | def namelist(self):
method is_dir (line 421) | def is_dir(self, name):
method dir_list (line 438) | def dir_list(self, only_top_level=False):
method dir_contents (line 456) | def dir_contents(self, dir_name):
method extract (line 481) | def extract(self, file_to_ext, path=None):
method extract_all (line 507) | def extract_all(self, path=None, member=None):
method open (line 520) | def open(self, file_to_open, fp=False):
method close (line 529) | def close(self):
function check_archive (line 532) | def check_archive(archive_path):
function recursive_gallery_check (line 572) | def recursive_gallery_check(path):
function today (line 605) | def today():
function external_viewer_checker (line 613) | def external_viewer_checker(path):
function open_chapter (line 625) | def open_chapter(chapterpath, archive=None):
function get_gallery_img (line 749) | def get_gallery_img(gallery_or_path, chap_number=0):
function tag_to_string (line 794) | def tag_to_string(gallery_tag, simple=False):
function tag_to_dict (line 842) | def tag_to_dict(string, ns_capitalize=True):
function title_parser (line 926) | def title_parser(title):
function open_web_link (line 980) | def open_web_link(url):
function open_path (line 988) | def open_path(path, select=''):
function open_torrent (line 1007) | def open_torrent(path):
function delete_path (line 1013) | def delete_path(path):
function regex_search (line 1042) | def regex_search(a, b, override_case=False, args=[]):
function search_term (line 1056) | def search_term(a, b, override_case=False, args=[]):
function get_terms (line 1071) | def get_terms(term):
function image_greyscale (line 1146) | def image_greyscale(filepath):
function PToQImageHelper (line 1163) | def PToQImageHelper(im):
function make_chapters (line 1250) | def make_chapters(gallery_object):
function timeit (line 1293) | def timeit(func):
function makedirs_if_not_exists (line 1304) | def makedirs_if_not_exists(folder):
function lookup_tag (line 1312) | def lookup_tag(tag):
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,039K chars).
[
{
"path": ".gitattributes",
"chars": 2517,
"preview": "###############################################################################\n# Set default behavior to automatically "
},
{
"path": ".gitignore",
"chars": 3657,
"preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# cust"
},
{
"path": "CHANGELOG.md",
"chars": 26006,
"preview": "*Happypanda v1.1*\n- Fixes\n - Fixed HP settings unusable without internet connection\n \n*Happypanda v1.0*\n* New stuff\n"
},
{
"path": "INSTALL.md",
"chars": 3738,
"preview": "This guide will show you how to run from source.\n\nA better option is dowloading the latest version\nfor your OS from\nhttp"
},
{
"path": "LICENSE",
"chars": 795,
"preview": "Happypanda is a cross platform manga/doujinshi manager with namespace & tag support;\nCopyright (C) 2016 Pewpews\n\nThis p"
},
{
"path": "LICENSE-3RD-PARTY",
"chars": 9618,
"preview": "-----------------------------------------------------------------------------\n The MIT License (M"
},
{
"path": "README.rst",
"chars": 3244,
"preview": "\nWork on this program has been halted in favor of its successor `HappyPanda X <https://github.com/happypandax/server>`__"
},
{
"path": "VS.txt",
"chars": 4,
"preview": "1.1\n"
},
{
"path": "requirements-dev.txt",
"chars": 35,
"preview": "-r requirements.txt\n\npytest==3.0.3\n"
},
{
"path": "requirements.txt",
"chars": 117,
"preview": "pyqt5\nrequests\nbeautifulsoup4\nscandir\nrarfile\nwatchdog\nrobobrowser\nSend2Trash\npillow\npython-dateutil\nQtAwesome==0.3.3"
},
{
"path": "res/license.txt",
"chars": 170,
"preview": "btn_star2.png |\u001f\u001f________________________________________\r\nhttps://www.iconfinder.com/iconsets/woothemesiconset |\r\n----"
},
{
"path": "res/style.css",
"chars": 3205,
"preview": "DoNotDelete {\n}\n\nQLabel#author {\n\tfont-weight:lighter;\n}\n\nAppWindow > QToolBar, QStatusBar, SideBarWidget {\n\tbackground-"
},
{
"path": "tests/database/test_db.py",
"chars": 2375,
"preview": "\"\"\"test db module.\"\"\"\nfrom itertools import product\nfrom unittest import mock\n\nimport pytest\n\n\n@pytest.mark.parametrize("
},
{
"path": "tests/test_utils.py",
"chars": 2564,
"preview": "\"\"\"test utils module.\"\"\"\nfrom unittest import mock\nfrom itertools import product\n\nimport pytest\n\nfrom version.utils impo"
},
{
"path": "version/app.py",
"chars": 54164,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the"
},
{
"path": "version/app_constants.py",
"chars": 294335,
"preview": "#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the ter"
},
{
"path": "version/asm_manager.py",
"chars": 7733,
"preview": "\"\"\"asmhentai module.\"\"\"\nimport logging\nfrom pprint import pformat\n\nfrom app_constants import DOWNLOAD_TYPE_OTHER, VALID_"
},
{
"path": "version/color_line_edit.py",
"chars": 3142,
"preview": "\"\"\"LineEdit for color input.\"\"\"\nimport sys\nimport logging\n\nfrom PyQt5 import QtWidgets\nfrom PyQt5.QtWidgets import (\n "
},
{
"path": "version/database/__init__.py",
"chars": 627,
"preview": "\"\"\"\nThis file is part of Happypanda.\nHappypanda is free software: you can redistribute it and/or modify\nit under the ter"
},
{
"path": "version/database/db.py",
"chars": 11550,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the"
},
{
"path": "version/database/db_constants.py",
"chars": 1307,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under th"
},
{
"path": "version/executors.py",
"chars": 3343,
"preview": "import logging, uuid, os\n\nfrom concurrent import futures\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtGui import QImage, QP"
},
{
"path": "version/fetch.py",
"chars": 27681,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under th"
},
{
"path": "version/gallery.py",
"chars": 68927,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under th"
},
{
"path": "version/gallerydb.py",
"chars": 85276,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under th"
},
{
"path": "version/gallerydialog.py",
"chars": 26557,
"preview": "import queue, os, threading, random, logging, time, scandir\nfrom datetime import datetime\n\nfrom PyQt5.QtWidgets import ("
},
{
"path": "version/hplugins.py",
"chars": 5253,
"preview": "import logging\nimport os\nimport uuid\nimport threading\nimport sys\n\nfrom PyQt5.QtCore import pyqtWrapperType\n\nlog = loggin"
},
{
"path": "version/io_misc.py",
"chars": 38181,
"preview": "import logging, os, json, datetime, random, re, queue\n\nfrom watchdog.events import FileSystemEventHandler, DirDeletedEv"
},
{
"path": "version/main.py",
"chars": 9528,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the"
},
{
"path": "version/misc.py",
"chars": 103140,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under th"
},
{
"path": "version/misc_db.py",
"chars": 26933,
"preview": "#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the ter"
},
{
"path": "version/pewnet.py",
"chars": 59393,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the"
},
{
"path": "version/settings.py",
"chars": 7849,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under th"
},
{
"path": "version/settingsdialog.py",
"chars": 72629,
"preview": "import logging, os, sys\n\nfrom PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QListWidget, QWidget,\n "
},
{
"path": "version/utils.py",
"chars": 47493,
"preview": "#\"\"\"\n#This file is part of Happypanda.\n#Happypanda is free software: you can redistribute it and/or modify\n#it under the"
}
]
About this extraction
This page contains the full source code of the Pewpews/happypanda GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (989.3 KB), approximately 251.1k tokens, and a symbol index with 875 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.