├── .gitignore ├── LICENSE ├── README.md ├── client-PC ├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── README.md ├── background │ ├── background_window.js │ ├── file-scanner.js │ └── index.js ├── main │ ├── index.js │ ├── main_menu.js │ └── main_window.js ├── package-lock.json ├── package.json ├── public │ ├── background │ │ └── index.html │ └── index.html ├── renderer │ ├── actions │ │ └── index.js │ ├── assets │ │ ├── icons │ │ │ ├── icon.icns │ │ │ ├── icon.ico │ │ │ └── icon.png │ │ └── images │ │ │ ├── add.png │ │ │ ├── add@2x.png │ │ │ ├── add@3x.png │ │ │ ├── avatar.png │ │ │ ├── book-default.png │ │ │ ├── book-default@2x.png │ │ │ ├── book-default@3x.png │ │ │ ├── close.png │ │ │ ├── close@2x.png │ │ │ ├── close@3x.png │ │ │ ├── files.png │ │ │ ├── files@2x.png │ │ │ ├── files@3x.png │ │ │ ├── magnify.png │ │ │ ├── magnify@2x.png │ │ │ ├── magnify@3x.png │ │ │ ├── radio-icon.png │ │ │ ├── radio-icon@2x.png │ │ │ ├── radio-icon@3x.png │ │ │ ├── right.png │ │ │ ├── right@2x.png │ │ │ ├── right@3x.png │ │ │ ├── star-lighted.png │ │ │ ├── star-lighted@2x.png │ │ │ ├── star-lighted@3x.png │ │ │ ├── star.png │ │ │ ├── star@2x.png │ │ │ └── star@3x.png │ ├── common │ │ ├── reset.css │ │ ├── utils.js │ │ └── variables.scss │ ├── components │ │ ├── avatar │ │ │ ├── avatar.scss │ │ │ └── index.jsx │ │ ├── book-list │ │ │ ├── book-list.scss │ │ │ └── index.jsx │ │ ├── file-input │ │ │ ├── file-input.scss │ │ │ └── index.jsx │ │ ├── search-input │ │ │ ├── index.jsx │ │ │ └── search-input.scss │ │ ├── table │ │ │ ├── index.jsx │ │ │ ├── table.scss │ │ │ └── trow │ │ │ │ ├── index.jsx │ │ │ │ └── trow.scss │ │ └── top-fixed │ │ │ ├── index.jsx │ │ │ └── top-fixed.scss │ ├── containers │ │ ├── book-add │ │ │ ├── book-add.scss │ │ │ └── index.jsx │ │ ├── book-search │ │ │ ├── book-search.scss │ │ │ └── index.jsx │ │ ├── preferences │ │ │ ├── index.jsx │ │ │ └── preferences.scss │ │ └── recently-read │ │ │ ├── index.jsx │ │ │ └── recently-read.scss │ ├── entry.scss │ ├── index.js │ └── reducers │ │ └── index.js ├── webpack.config.electron.js └── webpack.config.react.js ├── doc └── release-plan │ └── v1.md └── v1 ├── README.md ├── __init__.py ├── doc ├── design │ └── design1_02.png └── figure │ ├── overview.png │ └── scan.png ├── frame_connect.py ├── frame_overview.py ├── frame_scan.py ├── lib ├── ObjectListView │ ├── CellEditor.py │ ├── Filter.py │ ├── ListCtrlPrinter.py │ ├── OLVEvent.py │ ├── OLVPrinter.py │ ├── ObjectListView.py │ ├── WordWrapRenderer.py │ └── __init__.py ├── __init__.py └── util.py ├── media_repo.py ├── requirement.txt └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | #project ignore 2 | local_settings.py 3 | main.js 4 | bundle.js 5 | bookhub-metainfo.db 6 | 7 | *.py[cod] 8 | 9 | #vim 10 | *.swp 11 | 12 | *.ropeproject/* 13 | 14 | # SVN 15 | .svn 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | nosetests.xml 27 | 28 | # Translations 29 | *.mo 30 | 31 | # Mr Developer 32 | .mr.developer.cfg 33 | .project 34 | .pydevproject 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | release-builds/ 44 | build/Release 45 | .eslintcache 46 | 47 | # Dependency directory 48 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 49 | node_modules 50 | app/node_modules 51 | 52 | # OSX 53 | .DS_Store 54 | 55 | # flow-typed 56 | flow-typed/npm/* 57 | !flow-typed/npm/module_vx.x.x.js 58 | 59 | # App packaged 60 | release 61 | app/main.prod.js 62 | app/main.prod.js.map 63 | app/renderer.prod.js 64 | app/renderer.prod.js.map 65 | app/style.css 66 | app/style.css.map 67 | dist 68 | dll 69 | 70 | # react 71 | built 72 | 73 | .idea 74 | npm-debug.log.* 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present C. T. Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bookhub 2 | ======= 3 | 4 | 工具还在开发中,client-PC 目录下是 active 的代码。 5 | 6 | - [UI Design Preview](http://jackon.me/bookhub/) 7 | - [Release Plan](doc/release-plan) 8 | 9 | 调查问卷 https://jinshuju.net/f/3wSWpy 10 | 11 | 调查的结果,以邮件的方式 share 给填写了邮箱的有效回答。昵称/邮箱/微信/坐标不会 share。 12 | 13 | ## 日常痛点 14 | 15 | #### 1. 花了那么多时间整理电子书,找书的时候依旧一脸懵。 16 | 17 | 信息大爆炸的时代,最好的交互工具就是“搜索引擎“ 18 | 19 | - 互联网上的 Google 20 | - Mac 上的 SpotLight 21 | - Windows 下的 Everything(软件名) 22 | 23 | 能不能做一个软件,不需要太多时间整理书,但找书的时候,搜一下就够了。 24 | 25 | 随着使用时间的增加,搜索可以越来越 AI 智能(障) 26 | 27 | #### 2. 电子书管理工具和笔记管理,一直是两条平行线, 为什么? 28 | 29 | 或许,开发成本/技术栈是个主要原因。 30 | 31 | 现在,我们有 electron 了,无所不能的 node.js 什么都可以写。 32 | 33 | #### 3. Calibre 可以根据文件信息自动从豆瓣等网站匹配作者、出版社等信息 34 | 35 | 如果有一个云端,就可以把电子书文件及其匹配结果共享出来,不用每个人重复做一遍了。 36 | 37 | #### 4. 电子书管理和文献管理之间的距离,很近,又很远。 38 | 39 | 文献管理,只考虑怎么能简单的生成参考文献。 40 | 41 | 电子书管理,似乎从不在乎科研群体。 42 | 43 | ## 一个好的阅读工具,是可以做图书推荐的。 44 | 45 | #### 举个例子: 46 | 47 | 最近读 <机器学习高手攻略>, 看不懂。 48 | 49 | A 读过,很喜欢。B 读过,也很喜欢。他们在读这本书以前,都读了《机器学习入门》。 50 | 51 | 那么,我们可以认为,我应该先读一读这本书。 52 | 53 | A 和 B 之后又都读过 《颈椎病指南》,我看懂这本书之前,不要给我推荐,看不懂的。 54 | 55 | #### 从图书推荐到知识地图 56 | 57 | 做图书推荐的时候,阅读顺序是很重要的。 58 | 59 | 这其实已经有知识地图的概念了, 60 | 61 | 豆瓣也有这些数据,但那个标记一本书的成本太低,装 X 的因素也很重。 62 | 63 | 标记自己全部阅读记录的成本又太高, 64 | 65 | 无法做到基于阅读顺序的推荐。 66 | 67 | 电子书工具,可以获取用户完整的阅读记录,甚至打开一本书的时间和频率。 68 | 69 | 数据更真实,推荐更靠谱。 70 | 71 | ## 技术栈 72 | 73 | #### PC Client 74 | 75 | Electron + React + Redux 76 | 77 | #### Data Platform 78 | 79 | Python 80 | -------------------------------------------------------------------------------- /client-PC/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-rest-spread"], 3 | "presets": [ 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /client-PC/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 160 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /client-PC/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | root: true, 6 | parser: 'babel-eslint', 7 | parserOptions: { 8 | sourceType: 'module' 9 | }, 10 | env: { 11 | browser: true, 12 | }, 13 | globals: { 14 | React: true, 15 | }, 16 | extends: 'airbnb', 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': path.join(__dirname, 'webpack.config.react.js'), 22 | }, 23 | }, 24 | 'import/alias': { 25 | '@p': path.join(__dirname, 'renderer/', 'components'), 26 | '@n': path.join(__dirname, 'renderer/', 'containers'), 27 | } 28 | }, 29 | // add your custom rules here 30 | 'rules': { 31 | // don't require .js .jsx extension when importing 32 | 'import/extensions': ['error', 'always', { 33 | 'js': 'never', 34 | 'jsx': 'never' 35 | }], 36 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 37 | 'arrow-parens': ['error', 'as-needed'], 38 | // allow debugger during development 39 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 40 | 'jsx-a11y/role-has-required-aria-props': 1, 41 | 'jsx-a11y/interactive-supports-focus': 1, 42 | 'jsx-a11y/click-events-have-key-events': 1 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client-PC/README.md: -------------------------------------------------------------------------------- 1 | # BookHub PC Client 2 | 3 | - [UI Design Preview](http://jackon.me/bookhub/) 4 | - [Release Plan](../doc/release-plan) 5 | 6 | ## Development 7 | 8 | the client is built using Electron + React + Redux. 9 | 10 | I am new to these stacks, too. 11 | here are tutorial projects that help me learn them: 12 | [https://github.com/JackonYang/tutorial](https://github.com/JackonYang/tutorial) 13 | 14 | ```bash 15 | npm install 16 | npm start # you might need to wait several seconds before the page is loaded 17 | ``` 18 | -------------------------------------------------------------------------------- /client-PC/background/background_window.js: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserWindow, 3 | } from 'electron'; 4 | 5 | import url from 'url'; 6 | import path from 'path'; 7 | 8 | function createBackgroundWindow() { 9 | const backgroundWindow = new BrowserWindow({ 10 | show: false, 11 | nodeIntegrationInWorker: true, 12 | }); 13 | 14 | // Load html into window 15 | backgroundWindow.loadURL(url.format({ 16 | pathname: path.join(__dirname, 'public/background/index.html'), 17 | protocol: 'file:', 18 | slashes: true, 19 | })); 20 | return backgroundWindow; 21 | } 22 | 23 | export default createBackgroundWindow; 24 | -------------------------------------------------------------------------------- /client-PC/background/file-scanner.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const wildcard = require('wildcard'); 4 | const md5File = require('md5-file'); 5 | 6 | 7 | const ignorePaths = [ 8 | '.*', 9 | 'node_modules', 10 | 'Library', // Mac 11 | 'log', 12 | 'logs', 13 | 'video-course', 14 | 'interview', 15 | ]; 16 | 17 | const targetPtn = [ 18 | '.pdf', 19 | ]; 20 | 21 | // let STOP_FLAG = false; 22 | 23 | function isIgnoreDir(filePath) { 24 | return ignorePaths.some(ptn => wildcard(ptn, filePath)); 25 | } 26 | 27 | function scanPath(targetPath, dispatchMsg) { 28 | if (!fs.existsSync(targetPath)) { 29 | dispatchMsg('scan:error', `path not exists. path=${targetPath}`); 30 | return 0; 31 | } 32 | 33 | const stat = fs.lstatSync(targetPath); 34 | 35 | if (stat.isDirectory()) { 36 | dispatchMsg('scan:heartbeat', `scanning ${targetPath}`); 37 | 38 | let cntAdded = 0; 39 | const subPaths = fs.readdirSync(targetPath).filter(ele => !isIgnoreDir(ele)); 40 | subPaths.forEach(ele => { 41 | cntAdded += scanPath(path.join(targetPath, ele), dispatchMsg); // recurse 42 | }); 43 | return cntAdded; 44 | } 45 | 46 | // else -> file 47 | // match target rule 48 | const extname = path.extname(targetPath); 49 | if (!targetPtn.includes(extname)) { 50 | return 0; 51 | } 52 | 53 | const fileInfo = { 54 | md5: md5File.sync(targetPath), 55 | srcFullPath: targetPath, 56 | extname, 57 | sizeBytes: stat.size, 58 | }; 59 | dispatchMsg('scan:file:found', fileInfo); 60 | return 1; 61 | } 62 | 63 | // scanPath('/Users/'); 64 | 65 | export default scanPath; 66 | -------------------------------------------------------------------------------- /client-PC/background/index.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | import scanPath from './file-scanner'; 4 | 5 | // ipcRenderer.send('bg:started', 'background started!'); 6 | 7 | ipcRenderer.on('bg:scan:task:new', (e, targetPath) => { 8 | scanPath(targetPath, (msgKey, payload) => { 9 | // msgKey may be: 10 | // - scan:file:found 11 | // - path not exists 12 | // etc 13 | ipcRenderer.send(msgKey, payload); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /client-PC/main/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | Menu, 4 | ipcMain, 5 | } from 'electron'; 6 | import url from 'url'; 7 | import path from 'path'; 8 | 9 | import createBackgroundWindow from 'background_window'; 10 | 11 | import createMainWindow from './main_window'; 12 | import createMainMenu from './main_menu'; 13 | 14 | 15 | let mainWindow; 16 | let backgroundWindow; 17 | 18 | function switchToSearchBooks() { 19 | mainWindow.webContents.send('windown:location:change', '#/'); 20 | } 21 | 22 | function switchToAddBooks() { 23 | mainWindow.webContents.send('windown:location:change', '#/add-books'); 24 | } 25 | 26 | function switchToPreferences() { 27 | mainWindow.webContents.send('windown:location:change', '#/preferences'); 28 | } 29 | 30 | function switchToRecentlyReads() { 31 | mainWindow.webContents.send('windown:location:change', '#/recently-read'); 32 | } 33 | 34 | // Listen for app to be ready 35 | app.on('ready', () => { 36 | mainWindow = createMainWindow(); 37 | 38 | mainWindow.loadURL(url.format({ 39 | pathname: path.join(__dirname, 'public/index.html'), 40 | protocol: 'file:', 41 | hash: '#/recently-read', 42 | slashes: false, 43 | })); 44 | 45 | mainWindow.on('close', e => { 46 | e.preventDefault(); 47 | mainWindow.webContents.send('windown:close:dump'); 48 | mainWindow.destroy(); // wait until data dumped 49 | }); 50 | 51 | // Quit app when closed 52 | mainWindow.on('closed', () => { 53 | app.quit(); 54 | }); 55 | 56 | backgroundWindow = createBackgroundWindow(); 57 | 58 | // build menu from template 59 | Menu.setApplicationMenu(createMainMenu( 60 | switchToAddBooks, 61 | switchToPreferences, 62 | switchToSearchBooks, 63 | switchToRecentlyReads, 64 | )); 65 | }); 66 | 67 | // ipcMain.on('bg:started', (e, msg) => { 68 | // console.log(msg); 69 | // }); 70 | 71 | ipcMain.on('scan:task:new', (e, targetPath) => { 72 | backgroundWindow.webContents.send('bg:scan:task:new', targetPath); 73 | }); 74 | 75 | ipcMain.on('scan:heartbeat', (e, msg) => { 76 | console.log(msg); 77 | }); 78 | 79 | ipcMain.on('scan:error', (e, msg) => { 80 | console.log(msg); 81 | }); 82 | 83 | ipcMain.on('scan:file:found', (e, fileMetaInfo) => { 84 | mainWindow.webContents.send('scan:file:found', fileMetaInfo); 85 | }); 86 | -------------------------------------------------------------------------------- /client-PC/main/main_menu.js: -------------------------------------------------------------------------------- 1 | import { 2 | Menu, 3 | app, 4 | } from 'electron'; 5 | 6 | function createMainMenu( 7 | switchToAddBooks, 8 | switchToPreferences, 9 | switchToSearchBooks, 10 | switchToRecentlyReads, 11 | ) { 12 | const mainMenuTemplate = [ 13 | { 14 | label: 'File', 15 | submenu: [ 16 | { 17 | label: 'Add Books', 18 | accelerator: process.platform === 'darwin' ? 'Command+N' : 'Ctrl+N', 19 | click() { 20 | switchToAddBooks(); 21 | }, 22 | }, 23 | { 24 | label: 'Search Books', 25 | accelerator: process.platform === 'darwin' ? 'Command+l' : 'Ctrl+l', 26 | click() { 27 | switchToSearchBooks(); 28 | }, 29 | }, 30 | { 31 | label: 'Recently Read', 32 | accelerator: process.platform === 'darwin' ? 'Command+h' : 'Ctrl+h', 33 | click() { 34 | switchToRecentlyReads(); 35 | }, 36 | }, 37 | { 38 | label: 'Preferences', 39 | accelerator: process.platform === 'darwin' ? 'Command+,' : 'Ctrl+,', 40 | click() { 41 | switchToPreferences(); 42 | }, 43 | }, 44 | { 45 | label: 'Close Current Window', 46 | accelerator: process.platform === 'darwin' ? 'Command+W' : 'Ctrl+W', 47 | click(item, focusedWindow) { 48 | focusedWindow.close(); 49 | }, 50 | }, 51 | { 52 | label: 'Quit', 53 | accelerator: process.platform === 'darwin' ? 'Command+Q' : 'Ctrl+Q', 54 | click() { 55 | app.quit(); 56 | }, 57 | }, 58 | ], 59 | }, 60 | ]; 61 | 62 | // if Mac, add empty object to menu 63 | if (process.platform === 'darwin') { 64 | mainMenuTemplate.unshift({}); 65 | } 66 | 67 | // Add developer tools if not in prod 68 | if (process.env.NODE_ENV !== 'production') { 69 | mainMenuTemplate.push({ 70 | label: 'Developer Tools', 71 | submenu: [ 72 | { 73 | label: 'Toggle DevTools', 74 | accelerator: process.platform === 'darwin' ? 'Command+I' : 'Ctrl+I', 75 | click(item, focusedWindow) { 76 | focusedWindow.toggleDevTools(); 77 | }, 78 | }, 79 | { 80 | role: 'reload', 81 | }, 82 | ], 83 | }); 84 | } 85 | 86 | return Menu.buildFromTemplate(mainMenuTemplate); 87 | } 88 | 89 | export default createMainMenu; 90 | -------------------------------------------------------------------------------- /client-PC/main/main_window.js: -------------------------------------------------------------------------------- 1 | import electron, { 2 | BrowserWindow, 3 | } from 'electron'; 4 | 5 | function createMainWindow() { 6 | const electronScreen = electron.screen; 7 | const size = electronScreen.getPrimaryDisplay().workAreaSize; 8 | 9 | const mainWindow = new BrowserWindow({ 10 | width: size.width, 11 | height: size.height, 12 | }); 13 | return mainWindow; 14 | } 15 | 16 | export default createMainWindow; 17 | -------------------------------------------------------------------------------- /client-PC/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookhub", 3 | "version": "1.0.0", 4 | "description": "book management and sharing", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "concurrently \"npm run watch\" \"npm run start-electron\"", 8 | "start-electron": "webpack --config webpack.config.electron.js && electron .", 9 | "watch": "./node_modules/.bin/webpack-dev-server --config webpack.config.react.js", 10 | "build-react": "webpack --config webpack.config.react.js", 11 | "package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=renderer/assets/icons/icon.icns --prune=true --out=release-builds", 12 | "package-win": "electron-packager . --overwrite --asar=true --platform=win32 --arch=ia32 --icon=renderer/assets/icons/icon.ico --prune=true --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName=\"BookHub\"", 13 | "package-linux": "electron-packager . --overwrite --asar=true --platform=linux --arch=x64 --icon=renderer/assets/icons/icon.png --prune=true --out=release-builds" 14 | }, 15 | "author": "Jackon Yang", 16 | "license": "MIT", 17 | "dependencies": { 18 | "concurrently": "^3.5.1", 19 | "electron": "^1.7.12", 20 | "filesize": "^3.6.0", 21 | "jsonfile": "^4.0.0", 22 | "md5-file": "^3.2.3", 23 | "prop-types": "^15.6.0", 24 | "react": "^16.2.0", 25 | "react-dom": "^16.2.0", 26 | "react-redux": "^5.0.6", 27 | "react-router": "^4.2.0", 28 | "react-router-dom": "^4.2.2", 29 | "react-scripts": "1.1.0", 30 | "redux": "^3.7.2", 31 | "wildcard": "^1.1.2" 32 | }, 33 | "devDependencies": { 34 | "babel": "^6.23.0", 35 | "babel-core": "^6.26.0", 36 | "babel-loader": "^7.1.2", 37 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 38 | "css-loader": "^0.28.9", 39 | "electron-packager": "^10.1.2", 40 | "electron-react-devtools": "^0.5.3", 41 | "eslint-config-airbnb": "^16.1.0", 42 | "eslint-import-resolver-webpack": "^0.8.4", 43 | "extract-text-webpack-plugin": "^3.0.2", 44 | "is-electron-renderer": "^2.0.1", 45 | "less": "^2.7.3", 46 | "less-loader": "^4.0.5", 47 | "node-sass": "^4.7.2", 48 | "sass-loader": "^6.0.6", 49 | "style-loader": "^0.20.1", 50 | "url-loader": "^0.6.2", 51 | "webpack": "^3.10.0", 52 | "webpack-dev-server": "^3.1.11", 53 | "webpack-target-electron-renderer": "^0.4.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client-PC/public/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client-PC/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BookHub 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client-PC/renderer/actions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * action types 3 | */ 4 | 5 | export const BOOK_SCANNED = 'BOOK_SCANNED'; 6 | export const ADD_BOOK_TO_REPO = 'ADD_BOOK_TO_REPO'; 7 | export const CLEAR_SCAN_LOG = 'CLEAR_SCAN_LOG'; 8 | // Select for AddBooks 9 | export const SELECT_ALL = 'SELECT_ALL'; 10 | export const SELECT_NONE = 'SELECT_NONE'; 11 | export const TOGGLE_SELECT = 'TOGGLE_SELECT'; 12 | export const TOGGLE_STAR = 'TOGGLE_STAR'; 13 | export const UPDATE_QUERY = 'UPDATE_QUERY'; 14 | 15 | /* 16 | * action creators 17 | */ 18 | 19 | export function addFileInfo(fileInfo) { 20 | return { type: BOOK_SCANNED, ...fileInfo }; 21 | } 22 | 23 | export function addBookToRepo(md5, srcFullPath, bookMeta) { 24 | return { 25 | type: ADD_BOOK_TO_REPO, 26 | md5, 27 | srcFullPath, 28 | bookMeta, 29 | }; 30 | } 31 | 32 | export function clearScanLog() { 33 | return { type: CLEAR_SCAN_LOG }; 34 | } 35 | 36 | export function selectAll() { 37 | return { type: SELECT_ALL }; 38 | } 39 | 40 | export function selectNone() { 41 | return { type: SELECT_NONE }; 42 | } 43 | 44 | export function toggleStar(idx) { 45 | return { type: TOGGLE_STAR, idx }; 46 | } 47 | 48 | export function toggleSelect(idx) { 49 | return { type: TOGGLE_SELECT, idx }; 50 | } 51 | 52 | export function updateQuery(query) { 53 | return { type: UPDATE_QUERY, query }; 54 | } 55 | -------------------------------------------------------------------------------- /client-PC/renderer/assets/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/icons/icon.icns -------------------------------------------------------------------------------- /client-PC/renderer/assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/icons/icon.ico -------------------------------------------------------------------------------- /client-PC/renderer/assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/icons/icon.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/add.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/add@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/add@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/add@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/add@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/avatar.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/book-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/book-default.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/book-default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/book-default@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/book-default@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/book-default@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/close.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/close@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/close@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/files.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/files@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/files@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/files@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/files@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/magnify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/magnify.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/magnify@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/magnify@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/magnify@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/magnify@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/radio-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/radio-icon.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/radio-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/radio-icon@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/radio-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/radio-icon@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/right.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/right@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/right@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/right@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/star-lighted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/star-lighted.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/star-lighted@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/star-lighted@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/star-lighted@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/star-lighted@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/star.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/star@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/star@2x.png -------------------------------------------------------------------------------- /client-PC/renderer/assets/images/star@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/client-PC/renderer/assets/images/star@3x.png -------------------------------------------------------------------------------- /client-PC/renderer/common/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | html5doctor.com Reset Stylesheet 3 | v1.4.1 4 | 2010-03-01 5 | Author: Richard Clark - http://richclarkdesign.com 6 | */ 7 | 8 | html, body, div, span, object, iframe, 9 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 10 | abbr, address, cite, code, 11 | del, dfn, em, img, ins, kbd, q, samp, 12 | small, strong, sub, sup, var, 13 | b, i, 14 | dl, dt, dd, ol, ul, li, 15 | fieldset, form, label, legend, 16 | table, caption, tbody, tfoot, thead, tr, th, td, 17 | article, aside, canvas, details, figcaption, figure, 18 | footer, header, hgroup, menu, nav, section, summary, 19 | time, mark, audio, video { 20 | margin:0; 21 | padding:0; 22 | border:0; 23 | outline:0; 24 | font-size:100%; 25 | vertical-align:baseline; 26 | background:transparent; 27 | } 28 | 29 | body { 30 | line-height:1; 31 | } 32 | html,body { 33 | height: 100%; 34 | font-family: PingFangSC-Regular; 35 | } 36 | 37 | :focus { 38 | outline: 1; 39 | } 40 | 41 | article,aside,canvas,details,figcaption,figure, 42 | footer,header,hgroup,menu,nav,section,summary { 43 | display:block; 44 | } 45 | 46 | nav ul { 47 | list-style:none; 48 | } 49 | 50 | blockquote, q { 51 | quotes:none; 52 | } 53 | 54 | blockquote:before, blockquote:after, 55 | q:before, q:after { 56 | content:''; 57 | content:none; 58 | } 59 | 60 | a { 61 | margin:0; 62 | padding:0; 63 | border:0; 64 | font-size:100%; 65 | vertical-align:baseline; 66 | background:transparent; 67 | } 68 | 69 | abbr[title], dfn[title] { 70 | border-bottom:1px dotted #000; 71 | cursor:help; 72 | } 73 | 74 | table { 75 | border-collapse:collapse; 76 | border-spacing:0; 77 | } 78 | 79 | hr { 80 | display:block; 81 | height:1px; 82 | border:0; 83 | border-top:1px solid #cccccc; 84 | margin:1em 0; 85 | padding:0; 86 | } 87 | 88 | input, select { 89 | vertical-align:middle; 90 | } 91 | 92 | button { 93 | padding: 0; 94 | border: none; 95 | cursor: pointer; 96 | } 97 | 98 | [role="button"] { 99 | cursor: pointer; 100 | } 101 | 102 | button:focus { 103 | outline: none; 104 | } 105 | 106 | ul { 107 | list-style:none; 108 | } 109 | 110 | /*私有*/ 111 | #app { 112 | height: 100%; 113 | } 114 | -------------------------------------------------------------------------------- /client-PC/renderer/common/utils.js: -------------------------------------------------------------------------------- 1 | import { execFile } from 'child_process'; 2 | 3 | function openFile(srcPath) { 4 | const platformCmd = { 5 | win32: 'start', // win7 32bit, win7 64bit 6 | cygwin: 'start', // cygwin 7 | linux2: 'xdg-open', // ubuntu 12.04 64bit 8 | darwin: 'open', // Mac 9 | }; 10 | return () => { 11 | execFile(platformCmd[process.platform], [srcPath[0]]); 12 | }; 13 | } 14 | 15 | export default { 16 | openFile, 17 | }; 18 | -------------------------------------------------------------------------------- /client-PC/renderer/common/variables.scss: -------------------------------------------------------------------------------- 1 | $fontSize: 14px; 2 | $fontColor: #757575; 3 | $weightColor: #777; 4 | $secondColor: #979797; 5 | $secondActive: #000; 6 | 7 | $mainBg: #F5F5F5; 8 | $secondBg: #FFF; 9 | 10 | $mainColor: #2196F3; 11 | 12 | $paddingLeft: 70px; 13 | $opacityBg: rgba(255, 255, 255, .7); 14 | -------------------------------------------------------------------------------- /client-PC/renderer/components/avatar/avatar.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | position: absolute; 3 | top: 24px; 4 | right: 25px; 5 | width: 55px; 6 | height: 55px; 7 | font-size: 0; 8 | border-radius: 50%; 9 | overflow: hidden; 10 | } 11 | .avatar { 12 | width: 55px; 13 | cursor: pointer; 14 | } 15 | -------------------------------------------------------------------------------- /client-PC/renderer/components/avatar/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | 4 | import AvatarPic from 'assets/images/avatar.png'; 5 | 6 | import styles from './avatar.scss'; 7 | 8 | /* eslint-disable react/prefer-stateless-function */ 9 | class Avatar extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | }; 14 | } 15 | render() { 16 | return ( 17 |
18 | 头像 19 |
20 | ); 21 | } 22 | } 23 | 24 | // Avatar.propTypes = { 25 | // }; 26 | 27 | // Avatar.defaultProps = { 28 | // }; 29 | export default Avatar; 30 | -------------------------------------------------------------------------------- /client-PC/renderer/components/book-list/book-list.scss: -------------------------------------------------------------------------------- 1 | @import '../../common/variables.scss'; 2 | 3 | .books { 4 | width: 1000px; 5 | li { 6 | display: inline-block; 7 | margin: 0 93px 30px 0; 8 | 9 | p { 10 | text-align: center; 11 | line-height: 20px; 12 | width: 100px; 13 | white-space: nowrap; 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | } 17 | } 18 | } 19 | .img { 20 | width: 100px; 21 | height: 140px; 22 | box-shadow: 0 2px 4px 0 rgba(0,0,0,0.50); 23 | } 24 | 25 | .name { 26 | margin-top: 11px; 27 | } 28 | .author { 29 | color: $secondColor; 30 | } 31 | -------------------------------------------------------------------------------- /client-PC/renderer/components/book-list/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import utils from 'common/utils'; 6 | import bookDefault from 'assets/images/book-default.png'; 7 | 8 | import styles from './book-list.scss'; 9 | 10 | function filterBooks(fullData, query) { 11 | if (!query) { 12 | return fullData; 13 | } 14 | return fullData.filter(t => t.titleDisplay.toLowerCase().includes(query)); 15 | } 16 | 17 | const mapStateToProps = (state, ownProps) => ({ 18 | bookList: filterBooks(state.bookList, state.query), 19 | ...ownProps, 20 | }); 21 | 22 | const mapDispatchToProps = () => ({ 23 | }); 24 | 25 | const bookRender = (book, idx) => ( 26 |
  • 27 | {book.rawname} 28 |

    {book.titleDisplay}

    29 |

    {book.author && book.author.length > 0 ? book.author : 'Unknown Author'}

    30 |
  • 31 | ); 32 | 33 | function ConnectedBookList(props) { 34 | const lis = props.bookList.map((book, idx) => bookRender(book, idx)); 35 | // const lis = this.store.getState().bookList.map(bookRender()); 36 | return ( 37 | 40 | ); 41 | } 42 | 43 | ConnectedBookList.propTypes = { 44 | bookList: PropTypes.arrayOf(PropTypes.shape({ 45 | md5: PropTypes.string.isRequired, 46 | })).isRequired, 47 | }; 48 | 49 | const BookList = connect(mapStateToProps, mapDispatchToProps)(ConnectedBookList); 50 | 51 | export default BookList; 52 | -------------------------------------------------------------------------------- /client-PC/renderer/components/file-input/file-input.scss: -------------------------------------------------------------------------------- 1 | @import "../../common/variables.scss"; 2 | 3 | $barHeight: 44px; 4 | $filesWidth: 20px; 5 | $filesHeight: 17px; 6 | $barLeft: 50px; 7 | 8 | .inputWrap { 9 | width: 550px; 10 | height: 44px; 11 | padding-left: $barLeft; 12 | box-sizing: border-box; 13 | border-radius: 137.63px; 14 | background: $mainBg; 15 | line-height: 28px; 16 | cursor: pointer; 17 | position: relative; 18 | display: inline-block; 19 | white-space: nowrap; 20 | font-family: sans-serif; 21 | text-align: left; 22 | vertical-align: middle; 23 | 24 | &:before { 25 | content:""; 26 | display:block; 27 | position:absolute; 28 | top: 50%; 29 | left: 18px; 30 | vertical-align: middle; 31 | } 32 | 33 | } 34 | .path { 35 | display: inline-block; 36 | height: $barHeight; 37 | line-height: $barHeight; 38 | color: $fontColor; 39 | } 40 | .file { 41 | input { 42 | position:absolute; 43 | width:0; 44 | overflow:hidden; 45 | opacity:0; 46 | } 47 | 48 | &:before { 49 | width: $filesWidth; 50 | height: $filesHeight; 51 | margin-top: -($filesHeight/2); 52 | background: no-repeat center url("assets/images/files.png"); 53 | } 54 | 55 | &[title]:not([title=""]):before{ 56 | content:attr(title); 57 | color:#162f44; 58 | } 59 | 60 | &.afterIcon{ 61 | &:after { 62 | content: ""; 63 | display: inline-block; 64 | position: absolute; 65 | top: 50%; 66 | right: 12px; 67 | width: 20px; 68 | height: 20px; 69 | margin-top: -(20px/2); 70 | line-height: 20px; 71 | background: url("assets/images/add@2x.png") no-repeat center; 72 | background-size: cover; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client-PC/renderer/components/file-input/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './file-input.scss'; 4 | 5 | /* eslint-disable react/prefer-stateless-function */ 6 | class FileIput extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | fileKey: '', 11 | }; 12 | this.handleFileChange = this.handleFileChange.bind(this); 13 | this.handleClear = this.handleClear.bind(this); 14 | } 15 | componentWillReceiveProps(nextProps) { 16 | if (nextProps.isClear !== this.props.isClear && nextProps.isClear) { 17 | this.setState({ fileKey: null }); 18 | } 19 | } 20 | handleFileChange() { 21 | if (this.fileDom.files.length < 1) return; 22 | const fileKey = this.fileDom.files[0].path; 23 | this.setState({ fileKey }); 24 | this.props.onFileChangeCb(fileKey); 25 | } 26 | // 父组件 isClear 为 true 时候执行,【取消键】 27 | handleClear() { 28 | this.setState({ fileKey: null }); 29 | } 30 | render() { 31 | const { fileKey } = this.state; 32 | return ( 33 | 47 | ); 48 | } 49 | } 50 | 51 | FileIput.propTypes = { 52 | onFileChangeCb: PropTypes.func, 53 | id: PropTypes.string, 54 | showAfter: PropTypes.bool, 55 | defaultText: PropTypes.string, 56 | isClear: PropTypes.bool, 57 | }; 58 | 59 | FileIput.defaultProps = { 60 | onFileChangeCb: () => {}, 61 | id: 'file-input', 62 | showAfter: true, 63 | defaultText: null, 64 | isClear: false, 65 | }; 66 | export default FileIput; 67 | -------------------------------------------------------------------------------- /client-PC/renderer/components/search-input/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { updateQuery } from 'actions'; 6 | 7 | import styles from './search-input.scss'; 8 | 9 | const mapStateToProps = state => ({ 10 | query: state.query, 11 | }); 12 | 13 | const mapDispatchToProps = dispatch => ({ 14 | updateQuery: query => dispatch(updateQuery(query)), 15 | }); 16 | 17 | function ConnectedSearchInput(props) { 18 | return ( 19 | 30 | ); 31 | } 32 | 33 | ConnectedSearchInput.propTypes = { 34 | query: PropTypes.string.isRequired, 35 | updateQuery: PropTypes.func.isRequired, 36 | }; 37 | 38 | const SearchInput = connect(mapStateToProps, mapDispatchToProps)(ConnectedSearchInput); 39 | 40 | export default SearchInput; 41 | -------------------------------------------------------------------------------- /client-PC/renderer/components/search-input/search-input.scss: -------------------------------------------------------------------------------- 1 | @import "../../common/variables.scss"; 2 | 3 | $searchWidth: 22px; 4 | $searchHeight: 22px; 5 | $barLeft: 50px; 6 | // global 7 | 8 | .inputWrap { 9 | width: 550px; 10 | height: 44px; 11 | padding-left: $barLeft; 12 | box-sizing: border-box; 13 | border-radius: 137.63px; 14 | background: $mainBg; 15 | line-height: 28px; 16 | cursor: pointer; 17 | position: relative; 18 | display: inline-block; 19 | white-space: nowrap; 20 | font-family: sans-serif; 21 | text-align: left; 22 | 23 | &:before { 24 | content:""; 25 | display:block; 26 | position:absolute; 27 | top: 50%; 28 | left: 18px; 29 | vertical-align: middle; 30 | } 31 | 32 | } 33 | 34 | .search { 35 | &:before { 36 | width: $searchWidth; 37 | height: $searchHeight; 38 | margin-top: -($searchHeight/2); 39 | background: no-repeat center url("assets/images/magnify.png"); 40 | } 41 | 42 | input { 43 | position:absolute; 44 | // left: 50px right: 20px 45 | width: calc(100% - 50px - 20px); 46 | padding: 12px 18px 12px 0; 47 | line-height: 20px; 48 | border: none; 49 | font-size: $fontSize; 50 | color: $fontColor; 51 | background: transparent; 52 | 53 | &:focus { 54 | outline: none; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client-PC/renderer/components/table/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import styles from './table.scss'; 6 | import Trow from './trow/index'; 7 | 8 | const mapStateToProps = (state, ownProps) => ({ 9 | ...ownProps, 10 | }); 11 | 12 | const mapDispatchToProps = () => ({ 13 | }); 14 | 15 | class ConnectedTable extends React.PureComponent { 16 | // componentWillMount() { 17 | // console.log('componentWillMount'); 18 | // } 19 | // componentDidMount() { 20 | // // console.log('componentDidMount'); 21 | // this.interval = setInterval(this.tick.bind(this), 1000); 22 | // } 23 | // shouldComponentUpdate(nextProps, nextState) { 24 | // // console.log('shouldComponentUpdate', nextProps, nextState, this.state.secondsElapsed); 25 | // return this.state.secondsElapsed !== nextState.secondsElapsed; 26 | // } 27 | // componentWillUpdate() { 28 | // console.log('componentWillUpdate'); 29 | // } 30 | // componentWillUnmount() { 31 | // clearInterval(this.interval); 32 | // } 33 | // tick() { 34 | // // console.log(this.state); 35 | // this.setState({ secondsElapsed: this.state.secondsElapsed + 1 }); 36 | // } 37 | 38 | // componentWillMount() { 39 | // console.log('componentWillMount'); 40 | // console.log('isSelectAll', this.store.getState().isSelectAll); 41 | // console.log('isUnSelectAll', this.store.getState().isUnSelectAll); 42 | // } 43 | // componentWillReceiveProps(nextProps) { 44 | // console.log('componentWillReceiveProps'); 45 | // console.log(nextProps); 46 | // console.log('isSelectAll', this.store.getState().isSelectAll, 47 | // nextProps.store.getState().isSelectAll); 48 | // console.log('isUnSelectAll', this.store.getState().isUnSelectAll, 49 | // nextProps.store.getState().isUnSelectAll); 50 | // } 51 | 52 | render() { 53 | let ths = []; 54 | let trows = []; 55 | // console.log('render', this.props.type, this.store.getState().bookList); 56 | 57 | ths = this.props.colTitles.map(th => ( 58 |
    62 | {th.text} 63 |
    64 | )); 65 | 66 | if (this.props.type === 'book-search') { // search 页面 67 | ths.unshift((
    )); 72 | } else { // add 页面 73 | ths.unshift(
    ); 78 | } 79 | 80 | trows = this.props.bookList.map((row, idx) => ()); 87 | 88 | return ( 89 |
    90 |
    91 | {ths} 92 |
    93 | {trows} 94 |
    95 | ); 96 | } 97 | } 98 | 99 | ConnectedTable.propTypes = { 100 | type: PropTypes.oneOf(['add-book', 'book-search']).isRequired, 101 | colTitles: PropTypes.arrayOf(PropTypes.shape({ 102 | text: PropTypes.string.isRequired, 103 | file: PropTypes.string.isRequired, 104 | })).isRequired, 105 | bookList: PropTypes.arrayOf(PropTypes.shape({ 106 | md5: PropTypes.string.isRequired, 107 | })).isRequired, 108 | }; 109 | 110 | const Table = connect(mapStateToProps, mapDispatchToProps)(ConnectedTable); 111 | 112 | export default Table; 113 | -------------------------------------------------------------------------------- /client-PC/renderer/components/table/table.scss: -------------------------------------------------------------------------------- 1 | @import "../../common/variables.scss"; 2 | 3 | $thFontSize: 12px; 4 | $thColor: #9E9E9E; 5 | $tdHeight: 48px; 6 | 7 | .table { 8 | width: 100%; 9 | display: table; 10 | box-sizing: border-box; 11 | color: $weightColor; 12 | line-height: 20px; 13 | @media screen and (max-width: 580px) { 14 | display: block; 15 | } 16 | } 17 | 18 | .rawname { 19 | width: 300px; 20 | } 21 | 22 | .cell { 23 | display: table-cell; 24 | height: $tdHeight; 25 | line-height: $tdHeight; 26 | border-top: 8px solid $mainBg; 27 | 28 | &.selecte { 29 | // background: #9c9; 30 | width: 40px; 31 | text-align: center; 32 | } 33 | } 34 | 35 | .row { 36 | display: table-row; 37 | background: $secondBg; 38 | 39 | &.header { 40 | color: $thColor; 41 | font-size: $thFontSize; 42 | background: transparent; 43 | padding: 0; 44 | border-bottom: 1px solid #E0E0E0; 45 | 46 | .cell { 47 | height: auto; 48 | padding: 17px 0 15px; 49 | line-height: 15px; 50 | border-bottom: 1px solid #E0E0E0; 51 | border-top: none; 52 | } 53 | 54 | &+.row { 55 | .cell { 56 | border-top: 16px solid $mainBg; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client-PC/renderer/components/table/trow/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import utils from 'common/utils'; 6 | import { toggleSelect, toggleStar } from 'actions'; 7 | 8 | import radioIcon from 'assets/images/radio-icon.png'; 9 | import radioIconChecked from 'assets/images/right.png'; 10 | import star from 'assets/images/star@3x.png'; 11 | import starLighted from 'assets/images/star-lighted@3x.png'; 12 | 13 | import styles from './trow.scss'; 14 | 15 | const mapStateToProps = (state, ownProps) => ({ 16 | ...ownProps, 17 | }); 18 | 19 | const mapDispatchToProps = dispatch => ({ 20 | toggleSelect: idx => dispatch(toggleSelect(idx)), 21 | toggleStar: idx => dispatch(toggleStar(idx)), 22 | }); 23 | 24 | function ConnectedTableRow(props) { 25 | const { 26 | row, 27 | thArrays, 28 | idx, 29 | type, 30 | } = props; 31 | // 适配两种不同 type 的 row 32 | const darkSelect = type === 'book-search' ? star : radioIcon; 33 | const lightedSelect = type === 'book-search' ? starLighted : radioIconChecked; 34 | const isSelected = type === 'book-search' ? row.isStared : row.isSelected; 35 | const handleSelect = type === 'book-search' ? props.toggleStar : props.toggleSelect; 36 | 37 | const tds = thArrays.map((th, i) => ( 38 |
    42 | 43 | {row[th.file]} 44 | 45 |
    46 | )); 47 | 48 | /* eslint-disable function-paren-newline */ 49 | /* eslint-disable react/jsx-no-bind */ 50 | tds.unshift( 51 |
    handleSelect(idx)} 56 | className={`${styles.cell} ${styles.selecte}`} 57 | > 58 | radio 59 |
    ); 60 | 61 | return ( 62 |
    67 | {tds} 68 |
    69 | ); 70 | } 71 | 72 | /* eslint-disable react/forbid-prop-types */ 73 | ConnectedTableRow.propTypes = { 74 | row: PropTypes.object.isRequired, 75 | type: PropTypes.oneOf(['add-book', 'book-search']).isRequired, 76 | idx: PropTypes.number.isRequired, 77 | thArrays: PropTypes.arrayOf(PropTypes.object).isRequired, 78 | toggleSelect: PropTypes.func.isRequired, 79 | toggleStar: PropTypes.func.isRequired, 80 | }; 81 | 82 | const Trow = connect(mapStateToProps, mapDispatchToProps)(ConnectedTableRow); 83 | 84 | export default Trow; 85 | -------------------------------------------------------------------------------- /client-PC/renderer/components/table/trow/trow.scss: -------------------------------------------------------------------------------- 1 | @import "../../../common/variables.scss"; 2 | 3 | $thFontSize: 12px; 4 | $thColor: #9E9E9E; 5 | $tdHeight: 48px; 6 | 7 | .cell { 8 | display: table-cell; 9 | // padding: 6px 12px; 10 | height: $tdHeight; 11 | line-height: $tdHeight; 12 | border-top: 8px solid $mainBg; 13 | 14 | &.selecte { 15 | // background: #9c9; 16 | width: 40px; 17 | text-align: center; 18 | img { 19 | width: 18px; 20 | height: 18px; 21 | vertical-align: middle; 22 | } 23 | } 24 | span { 25 | display: inline-block; 26 | vertical-align: middle; 27 | overflow: hidden; 28 | white-space: nowrap; 29 | text-overflow: ellipsis; 30 | box-sizing: border-box; 31 | padding-right: 15px; 32 | } 33 | } 34 | 35 | .row { 36 | display: table-row; 37 | background: $secondBg; 38 | 39 | &.header { 40 | color: $thColor; 41 | font-size: $thFontSize; 42 | background: transparent; 43 | padding: 0; 44 | border-bottom: 1px solid #E0E0E0; 45 | 46 | &+.row { 47 | .cell { 48 | border-top: 16px solid $mainBg; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client-PC/renderer/components/top-fixed/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import SearchInput from 'components/search-input/index'; 6 | import FileInput from 'components/file-input/index'; 7 | 8 | import styles from './top-fixed.scss'; 9 | 10 | // https://github.com/electron/electron/issues/9920 11 | const { ipcRenderer } = window.require('electron'); 12 | 13 | const mapStateToProps = (state, ownProps) => ({ 14 | bookList: state.bookList, 15 | ...ownProps, 16 | }); 17 | 18 | const mapDispatchToProps = () => ({ 19 | }); 20 | 21 | function handleFileChange(path) { 22 | ipcRenderer.send('scan:task:new', path); 23 | } 24 | 25 | function handleClose(e) { 26 | e.preventDefault(); 27 | window.location.assign('#/recently-read'); 28 | } 29 | 30 | function ConnectedTopFixed(props) { 31 | // 上传文件 32 | const fileInput = () => ( 33 | 34 | ); 35 | 36 | return ( 37 |
    38 | {props.hasClose ?
    41 | ); 42 | } 43 | 44 | ConnectedTopFixed.propTypes = { 45 | type: PropTypes.oneOf(['add-book', 'book-search']).isRequired, 46 | hasClose: PropTypes.bool, 47 | }; 48 | 49 | ConnectedTopFixed.defaultProps = { 50 | hasClose: true, 51 | }; 52 | 53 | const TopFixed = connect(mapStateToProps, mapDispatchToProps)(ConnectedTopFixed); 54 | 55 | export default TopFixed; 56 | -------------------------------------------------------------------------------- /client-PC/renderer/components/top-fixed/top-fixed.scss: -------------------------------------------------------------------------------- 1 | @import "../../common/variables.scss"; 2 | 3 | $barHeight: 44px; 4 | $filesWidth: 20px; 5 | $filesHeight: 17px; 6 | $barLeft: 50px; 7 | // global 8 | 9 | .wrap { 10 | box-sizing: border-box; 11 | height: 112px; 12 | padding: 34px 70px; 13 | font-size: $fontSize; 14 | color: $fontColor; 15 | background: $secondBg; 16 | box-shadow: 0 2px 4px 0 rgba(0,0,0,0.35); 17 | } 18 | 19 | .close { 20 | display: inline-block; 21 | float: right; 22 | width: 30px; 23 | height: 30px; 24 | background: no-repeat center url("assets/images/close.png"); 25 | border: none; 26 | 27 | &:focus { 28 | outline: none; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/book-add/book-add.scss: -------------------------------------------------------------------------------- 1 | @import "../../common/variables.scss"; 2 | $operationGropHeight: 36px; 3 | $bodyPadding: 70px; 4 | $selectBtnFz: 12px; 5 | 6 | .wrap, 7 | .contentWrap { 8 | background-color: $mainBg; 9 | } 10 | .wrap { 11 | height: 100%; 12 | } 13 | .contentWrap, 14 | .operationGrop { 15 | padding: 0 $bodyPadding; 16 | } 17 | .operationGrop { 18 | position: fixed; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | padding: 20px 0; 23 | text-align: center; 24 | background-color: $opacityBg; 25 | 26 | .leftBtnGrop { 27 | display: inline-block; 28 | position: absolute; 29 | top: 50%; 30 | left: $bodyPadding + 18px; 31 | margin-top: -($selectBtnFz/2); 32 | } 33 | 34 | .selectBtn { 35 | margin-right: 14px; 36 | line-height: 1; 37 | font-size: $selectBtnFz; 38 | color: $mainColor; 39 | 40 | &:hover, 41 | &:active { 42 | text-decoration: underline; 43 | } 44 | 45 | &:first-child { 46 | margin-left: 14px; 47 | } 48 | } 49 | 50 | .addHub { 51 | padding: 0 33px; 52 | height: $operationGropHeight; 53 | line-height: $operationGropHeight; 54 | background-color: $mainColor; 55 | color: #fff; 56 | font-size: 14px; 57 | line-height: 20px; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/book-add/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import TopFixed from 'components/top-fixed/index'; 6 | import Table from 'components/table/index'; 7 | 8 | import { 9 | addFileInfo, 10 | addBookToRepo, 11 | clearScanLog, 12 | selectAll, 13 | selectNone, 14 | } from 'actions'; 15 | 16 | import styles from './book-add.scss'; 17 | 18 | const mapStateToProps = (state, ownProps) => ({ 19 | bookList: state.scanLog, 20 | ...ownProps, 21 | }); 22 | 23 | const mapDispatchToProps = dispatch => ({ 24 | addFileInfo: fileInfo => dispatch(addFileInfo(fileInfo)), 25 | addBookToRepo: (md5, srcFullPath, bookMeta) => ( 26 | dispatch(addBookToRepo(md5, srcFullPath, bookMeta)) 27 | ), 28 | clearScanLog: () => dispatch(clearScanLog()), 29 | selectAll: () => dispatch(selectAll()), 30 | selectNone: () => dispatch(selectNone()), 31 | }); 32 | 33 | // https://github.com/electron/electron/issues/9920 34 | const { ipcRenderer } = window.require('electron'); 35 | 36 | const colTitles = [ 37 | { 38 | text: 'Title', 39 | file: 'titleDisplay', 40 | }, 41 | { 42 | text: 'Type', 43 | file: 'extname', 44 | }, 45 | { 46 | text: 'Size', 47 | file: 'sizeReadable', 48 | }, 49 | // { 50 | // text: 'MD5', 51 | // file: 'md5', 52 | // }, 53 | { 54 | text: 'Path', 55 | file: 'srcFullPath', 56 | }, 57 | ]; 58 | 59 | /* eslint-disable react/prefer-stateless-function */ 60 | class ConnectedBookAdd extends React.Component { 61 | constructor() { 62 | super(); 63 | this.addBooksToRepo = this.addBooksToRepo.bind(this); 64 | } 65 | componentDidMount() { 66 | ipcRenderer.on('scan:file:found', (e, fileInfo) => { 67 | this.props.addFileInfo(fileInfo); 68 | }); 69 | } 70 | addBooksToRepo() { 71 | this.props.bookList.forEach(bookMeta => { 72 | if (bookMeta.isSelected && bookMeta.md5 && bookMeta.srcFullPath) { 73 | this.props.addBookToRepo(bookMeta.md5, bookMeta.srcFullPath, bookMeta); 74 | } 75 | // log error is !bookMeta.md5 or !bookMeta.srcFullPath 76 | }); 77 | this.props.clearScanLog(); 78 | window.location.assign('#/recently-read'); 79 | } 80 | render() { 81 | return ( 82 |
    83 | 84 |
    85 | 90 | 91 |
    92 |
    93 | All 94 | None 95 |
    96 | 102 |
    103 | 104 | ); 105 | } 106 | } 107 | 108 | ConnectedBookAdd.propTypes = { 109 | bookList: PropTypes.arrayOf(PropTypes.shape({ 110 | md5: PropTypes.string.isRequired, 111 | })).isRequired, 112 | addFileInfo: PropTypes.func.isRequired, 113 | addBookToRepo: PropTypes.func.isRequired, 114 | clearScanLog: PropTypes.func.isRequired, 115 | selectAll: PropTypes.func.isRequired, 116 | selectNone: PropTypes.func.isRequired, 117 | }; 118 | 119 | const BookAdd = connect(mapStateToProps, mapDispatchToProps)(ConnectedBookAdd); 120 | 121 | export default BookAdd; 122 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/book-search/book-search.scss: -------------------------------------------------------------------------------- 1 | @import '../book-add/book-add.scss'; 2 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/book-search/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import TopFixed from 'components/top-fixed/index'; 6 | import Table from 'components/table/index'; 7 | 8 | import styles from './book-search.scss'; 9 | 10 | function filterBooks(fullData, query) { 11 | if (!query) { 12 | return fullData; 13 | } 14 | return fullData.filter(t => t.titleDisplay.toLowerCase().includes(query)); 15 | } 16 | 17 | const mapStateToProps = (state, ownProps) => ({ 18 | bookList: filterBooks(state.bookList, state.query), 19 | ...ownProps, 20 | }); 21 | 22 | const mapDispatchToProps = () => ({ 23 | }); 24 | 25 | const colTitles = [ 26 | { 27 | text: 'Title', 28 | file: 'titleDisplay', 29 | }, 30 | { 31 | text: 'Author', 32 | file: 'author', 33 | }, 34 | { 35 | text: 'Type', 36 | file: 'extname', 37 | }, 38 | { 39 | text: 'Last Read', 40 | file: 'lastRead', 41 | }, 42 | // { 43 | // text: 'Tags', 44 | // file: 'tags', 45 | // }, 46 | ]; 47 | 48 | // function searchMore() { 49 | // console.log('searchMore'); 50 | // } 51 | 52 | function ConnectedBookSearch(props) { 53 | return ( 54 |
    55 | 56 |
    57 |
    62 | 63 | {/*
    64 | 65 |
    */} 66 | 67 | ); 68 | } 69 | 70 | ConnectedBookSearch.propTypes = { 71 | bookList: PropTypes.arrayOf(PropTypes.shape({ 72 | md5: PropTypes.string.isRequired, 73 | })).isRequired, 74 | }; 75 | 76 | const BookSearch = connect(mapStateToProps, mapDispatchToProps)(ConnectedBookSearch); 77 | 78 | export default BookSearch; 79 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/preferences/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import FileIput from 'components/file-input/index'; 4 | import Avatar from 'components/avatar/index'; 5 | 6 | import styles from './preferences.scss'; 7 | 8 | /* eslint-disable react/prefer-stateless-function */ 9 | class Preferences extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | library: null, 14 | monitors: [''], 15 | defaultText: 'user/book', 16 | isClearFiles: false, 17 | }; 18 | this.handleLibraryChange = this.handleLibraryChange.bind(this); 19 | this.handleMonitorChange = this.handleMonitorChange.bind(this); 20 | this.handleAddMonitor = this.handleAddMonitor.bind(this); 21 | this.save = this.save.bind(this); 22 | this.cancel = this.cancel.bind(this); 23 | } 24 | // 当 自组件 FileInput 还原后,再将 isClearFiles 设为 false, 否则无法再次还原。 25 | /* eslint-disable react/no-did-update-set-state */ 26 | componentDidUpdate(...rest) { 27 | if (rest[1].isClearFiles) { 28 | this.setState({ isClearFiles: false }); 29 | } 30 | } 31 | handleLibraryChange(key) { 32 | if (key === this.state.library || key === '') return; 33 | this.setState({ 34 | library: key, 35 | }); 36 | } 37 | handleMonitorChange(key, i) { 38 | this.setState(({ monitors }) => ({ 39 | monitors: [ 40 | ...monitors.slice(0, i), 41 | key, 42 | ...monitors.slice(i + 1), 43 | ], 44 | })); 45 | } 46 | handleAddMonitor() { 47 | this.setState(preState => ({ monitors: [...preState.monitors, ''] })); 48 | } 49 | save() { 50 | console.log('save', this.state.monitors, this.state.library); 51 | } 52 | cancel() { 53 | this.setState({ 54 | library: null, 55 | monitors: [''], 56 | isClearFiles: true, 57 | }); 58 | } 59 | render() { 60 | const { monitors, defaultText, isClearFiles } = this.state; 61 | /* eslint-disable react/no-array-index-key */ 62 | /* eslint-disable react/jsx-no-bind */ 63 | const monitorEls = monitors.map((item, i) =>
  • this.handleMonitorChange(key, i)} isClear={isClearFiles} />
  • ); 64 | return ( 65 |
    66 | 67 |
    68 |
    69 |

    70 | Settings 71 |

    72 |
    73 | Library Path 74 | 75 |
    76 |
    77 | Monitor Path 78 |
      79 | { monitorEls } 80 |
    81 | 82 |
    83 |
    84 |
    85 | 86 | 87 |
    88 |
    89 |
    90 | ); 91 | } 92 | } 93 | 94 | export default Preferences; 95 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/preferences/preferences.scss: -------------------------------------------------------------------------------- 1 | @import "../../common/variables.scss"; 2 | 3 | $btncolor: #007CBB; 4 | 5 | .wrap { 6 | padding-top: 82px; 7 | } 8 | .content { 9 | position: relative; 10 | width: 76%; 11 | height: 566px; 12 | margin: auto; 13 | box-shadow: 0 2px 4px 0 rgba(0,0,0,0.50); 14 | border-radius: 8px; 15 | box-sizing: border-box; 16 | 17 | h1 { 18 | margin: 0 auto 42px; 19 | color: #000; 20 | font-size: 42.39px; 21 | text-align: center; 22 | } 23 | } 24 | .scrollContent { 25 | height: 100%; 26 | box-sizing: border-box; 27 | overflow: auto; 28 | padding: 48px 30px 0; 29 | } 30 | 31 | .form { 32 | 33 | } 34 | 35 | .label { 36 | display: inline-block; 37 | width: 135px; 38 | margin-right: 28px; 39 | text-align: right; 40 | font-size: 20px; 41 | } 42 | 43 | .libraryWrap { 44 | margin-bottom: 36px; 45 | min-width: 765px; 46 | } 47 | 48 | .monitorWrap { 49 | display: flex; 50 | min-width: 765px; 51 | margin-bottom: 100px; 52 | 53 | .label { 54 | margin-top: 10px; 55 | } 56 | .add { 57 | display: inline-block; 58 | margin: 0 0 10px 21px; 59 | align-self: flex-end; 60 | width: 25px; 61 | height: 25px; 62 | background: url("assets/images/add@3x.png") no-repeat center; 63 | background-size: cover; 64 | } 65 | 66 | li:not(:last-child) { 67 | margin-bottom: 22px; 68 | } 69 | } 70 | 71 | .btnWrap { 72 | position: absolute; 73 | left: 0; 74 | right: 0; 75 | bottom: 0; 76 | padding: 20px 0 30px; 77 | font-size: 12px; 78 | text-align: center; 79 | background-color: $opacityBg; 80 | 81 | &>button { 82 | line-height: 17px; 83 | width: 78px; 84 | height: 36px; 85 | border-radius: 3px; 86 | } 87 | 88 | .cancel { 89 | color: $btncolor; 90 | background-color: $secondBg; 91 | border: 1px solid $btncolor; 92 | } 93 | .save { 94 | color: $secondBg; 95 | background-color: $btncolor; 96 | margin-left: 11px; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/recently-read/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | 4 | import TopFixed from 'components/top-fixed/index'; 5 | import BookList from 'components/book-list/index'; 6 | import Avatar from 'components/avatar/index'; 7 | 8 | import styles from './recently-read.scss'; 9 | 10 | // const BtnsText = ['Author', 'Title', 'Tag']; 11 | 12 | // handleSwitchList(type) { 13 | // console.log(`switch type: ${type}`); 14 | // this.setState({ type }); 15 | // } 16 | 17 | function RecentRead() { 18 | // const btns = BtnsText.map(btn => ( 19 | // 26 | // )); 27 | return ( 28 |
    29 | 30 | 31 |
    32 |

    Recently Read

    33 | {/*
    34 | {btns} 35 |
    */} 36 | 37 |
    38 |
    39 | ); 40 | } 41 | 42 | export default RecentRead; 43 | -------------------------------------------------------------------------------- /client-PC/renderer/containers/recently-read/recently-read.scss: -------------------------------------------------------------------------------- 1 | @import '../../common/variables.scss'; 2 | 3 | .wrap { 4 | h1 { 5 | margin-top: 22px; 6 | line-height: 60px; 7 | // font-family: PingFangSC-Semibold; 8 | color: #000000; 9 | font-size: 42.39px; 10 | letter-spacing: -1.93px; 11 | } 12 | } 13 | 14 | .contentWrap { 15 | padding-left: $paddingLeft; 16 | } 17 | .sortRole { 18 | color: $secondColor; 19 | margin: 45px 0 63px 0; 20 | 21 | button { 22 | background: none; 23 | cursor: pointer; 24 | font-size: 14px; 25 | margin-right: 32px; 26 | color: $secondColor; 27 | 28 | &:hover, 29 | &:active, 30 | &.active { 31 | color: $secondActive; 32 | } 33 | } 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /client-PC/renderer/entry.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /client-PC/renderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter as Router, Route } from 'react-router-dom'; 4 | 5 | import { createStore } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | // import PropTypes from 'prop-types'; 8 | 9 | import path from 'path'; 10 | import jsonfile from 'jsonfile'; 11 | 12 | import BookAdd from 'containers/book-add/index'; 13 | import BookSearch from 'containers/book-search/index'; 14 | import Preferences from 'containers/preferences/index'; 15 | import RecentlyRead from 'containers/recently-read/index'; 16 | 17 | /* eslint-disable import/extensions */ 18 | import 'common/reset.css?raw'; 19 | import styles from 'entry.scss'; 20 | 21 | import rootReducer from 'reducers'; 22 | 23 | const { remote, ipcRenderer } = window.require('electron'); 24 | 25 | const dbFile = path.join(remote.app.getPath('userData'), 'bookhub-metainfo.db'); 26 | 27 | const store = createStore(rootReducer); 28 | 29 | const routes = [ 30 | { 31 | path: '/', 32 | exact: true, 33 | sidebar: () =>
    Search Books
    , 34 | main: BookSearch, 35 | }, 36 | { 37 | path: '/add-books', 38 | sidebar: () =>
    Add Books
    , 39 | main: BookAdd, 40 | }, 41 | { 42 | path: '/preferences', 43 | sidebar: () =>
    Preferences
    , 44 | main: Preferences, 45 | }, 46 | { 47 | path: '/recently-read', 48 | sidebar: () =>
    Recently Read
    , 49 | main: RecentlyRead, 50 | }, 51 | ]; 52 | 53 | // class Navbar extends React.Component { 54 | // render() { 55 | // return ( 56 | //
      57 | // {routes.map((route, index) => { 58 | // return
    • 59 | // {route.sidebar()} 60 | //
    • 61 | // })} 62 | //
    63 | // ); 64 | // } 65 | // } 66 | 67 | class Index extends React.Component { 68 | componentDidMount() { 69 | ipcRenderer.on('windown:location:change', (e, newLocation) => { 70 | window.location.assign(newLocation); 71 | }); 72 | ipcRenderer.on('windown:close:dump', () => { 73 | jsonfile.writeFileSync(dbFile, store.getState()); 74 | }); 75 | } 76 | render() { 77 | return ( 78 | 79 |
    80 | {routes.map(route => ( 81 | } 86 | />))} 87 |
    88 |
    89 | ); 90 | } 91 | } 92 | 93 | function render() { 94 | ReactDOM.render( 95 | 96 | 97 | , 98 | document.getElementById('app'), 99 | ); 100 | } 101 | 102 | store.subscribe(render); 103 | 104 | render(); 105 | -------------------------------------------------------------------------------- /client-PC/renderer/reducers/index.js: -------------------------------------------------------------------------------- 1 | import filesize from 'filesize'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import jsonfile from 'jsonfile'; 5 | 6 | import { 7 | BOOK_SCANNED, 8 | ADD_BOOK_TO_REPO, 9 | CLEAR_SCAN_LOG, 10 | SELECT_ALL, 11 | SELECT_NONE, 12 | TOGGLE_SELECT, 13 | TOGGLE_STAR, 14 | UPDATE_QUERY, 15 | } from 'actions'; 16 | 17 | const { remote } = window.require('electron'); 18 | 19 | let initialState; 20 | const dbFile = path.join(remote.app.getPath('userData'), 'bookhub-metainfo.db'); 21 | 22 | if (fs.existsSync(dbFile)) { 23 | initialState = jsonfile.readFileSync(dbFile); 24 | } else { 25 | initialState = { 26 | bookList: [], 27 | query: '', 28 | scanLog: [], 29 | }; 30 | } 31 | 32 | export default (state = initialState, action) => { 33 | switch (action.type) { 34 | case BOOK_SCANNED: { 35 | let updated = false; 36 | const newState = Object.assign({}, state, { 37 | scanLog: state.scanLog.map(bookMeta => { 38 | if (bookMeta.md5 === action.md5) { 39 | updated = true; 40 | return Object.assign({}, bookMeta, { 41 | srcFullPath: [ 42 | action.srcFullPath, 43 | ...bookMeta.srcFullPath, 44 | ], 45 | }); 46 | } 47 | return bookMeta; 48 | }), 49 | }); 50 | 51 | if (!updated) { 52 | const { 53 | extname, 54 | srcFullPath, 55 | sizeBytes, 56 | } = action; 57 | 58 | // better algorithm. add titleAlias, standardTitle etc. 59 | const titleDisplay = path.basename(srcFullPath, extname); 60 | 61 | const sizeReadable = filesize(sizeBytes); 62 | 63 | newState.scanLog.push({ 64 | md5: action.md5, 65 | isSelected: true, 66 | titleDisplay, 67 | extname, 68 | sizeBytes, 69 | sizeReadable, 70 | // local info 71 | srcFullPath: [srcFullPath], 72 | }); 73 | } 74 | return newState; 75 | } 76 | case ADD_BOOK_TO_REPO: { 77 | let updated = false; 78 | const newState = Object.assign({}, state, { 79 | bookList: state.bookList.map(bookMeta => { 80 | if (bookMeta.md5 === action.md5) { 81 | updated = true; 82 | return Object.assign({}, bookMeta, { 83 | srcFullPath: action.srcFullPath, 84 | }); 85 | } 86 | return bookMeta; 87 | }), 88 | }); 89 | 90 | if (!updated) { 91 | newState.bookList.push(action.bookMeta); 92 | } 93 | 94 | return newState; 95 | } 96 | case CLEAR_SCAN_LOG: { 97 | return Object.assign({}, state, { 98 | scanLog: [], 99 | }); 100 | } 101 | 102 | case UPDATE_QUERY: { 103 | return Object.assign({}, state, { 104 | query: action.query.toLowerCase(), 105 | }); 106 | } 107 | 108 | // Select for AddBooks Page 109 | case TOGGLE_SELECT: { 110 | return Object.assign({}, state, { 111 | scanLog: state.scanLog.map((bookMeta, idx) => { 112 | if (idx === action.idx) { 113 | return Object.assign({}, bookMeta, { 114 | isSelected: !bookMeta.isSelected, 115 | }); 116 | } 117 | return bookMeta; 118 | }), 119 | }); 120 | } 121 | case SELECT_ALL: { 122 | return Object.assign({}, state, { 123 | scanLog: state.scanLog.map(bookMeta => Object.assign({}, bookMeta, { 124 | isSelected: true, 125 | })), 126 | }); 127 | } 128 | case SELECT_NONE: { 129 | return Object.assign({}, state, { 130 | scanLog: state.scanLog.map(bookMeta => Object.assign({}, bookMeta, { 131 | isSelected: false, 132 | })), 133 | }); 134 | } 135 | 136 | case TOGGLE_STAR: { 137 | return Object.assign({}, state, { 138 | bookList: state.bookList.map((bookMeta, idx) => { 139 | if (idx === action.idx) { 140 | return Object.assign({}, bookMeta, { 141 | isStared: !bookMeta.isStared, 142 | }); 143 | } 144 | return bookMeta; 145 | }), 146 | }); 147 | } 148 | default: 149 | return state; 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /client-PC/webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | target: 'electron-renderer', 6 | entry: { 7 | main: './main/index.js', 8 | './public/background/bundle': './background/index.js', 9 | }, 10 | output: { 11 | path: __dirname, 12 | // publicPath: path.join(__dirname, 'src'), 13 | filename: "[name].js" 14 | }, 15 | // https://github.com/electron/electron/issues/5107 16 | node: { 17 | __dirname: false, 18 | __filename: false 19 | }, 20 | resolve: { 21 | extensions: ['.js', '.jsx'], 22 | modules: [ 23 | path.resolve(__dirname, 'background'), 24 | 'node_modules', 25 | ], 26 | }, 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.(js|jsx)$/, 31 | loader: 'eslint-loader', 32 | enforce: 'pre', 33 | exclude: /(node_modules)/, 34 | options: { 35 | extends: path.join(__dirname, '/.eslintrc.js'), 36 | configFile: '.eslintrc.js', 37 | // failOnWarning: true, 38 | // failOnError: true, 39 | cache: false, 40 | }, 41 | }, 42 | ] 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /client-PC/webpack.config.react.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: { 6 | app: ['webpack/hot/dev-server', './renderer/index.js'], 7 | }, 8 | // https://jlongster.com/Backend-Apps-with-Webpack--Part-I 9 | target: 'node', 10 | output: { 11 | path: path.join(__dirname, 'public/built'), 12 | filename: 'bundle.js', 13 | publicPath: './built/' 14 | }, 15 | devServer: { 16 | contentBase: path.join(__dirname, 'public'), 17 | publicPath: 'http://localhost:8080/built/' 18 | }, 19 | resolve: { 20 | extensions: ['.js', '.jsx'], 21 | modules: [ 22 | path.resolve(__dirname, 'background'), 23 | path.resolve(__dirname, 'renderer'), 24 | 'node_modules', 25 | ], 26 | alias: { 27 | '@': path.join(__dirname, 'renderer/'), 28 | '@p': path.join(__dirname, 'renderer/', 'components'), 29 | '@n': path.join(__dirname, 'renderer/', 'containers'), 30 | } 31 | }, 32 | module: { 33 | loaders: [ 34 | { 35 | test: /\.(js|jsx)$/, 36 | loader: 'eslint-loader', 37 | enforce: 'pre', 38 | exclude: /(node_modules)/, 39 | options: { 40 | extends: path.join(__dirname, '/.eslintrc.js'), 41 | configFile: '.eslintrc.js', 42 | // failOnWarning: true, 43 | // failOnError: true, 44 | cache: false, 45 | }, 46 | }, 47 | { 48 | test: /\.(js|jsx)$/, 49 | exclude: /(node_modules)/, 50 | use: { 51 | loader: 'babel-loader', 52 | } 53 | }, 54 | { 55 | test: /\.css$/, 56 | oneOf: [{ 57 | resourceQuery: /^\?raw$/, 58 | use: [ 59 | require.resolve('style-loader'), 60 | require.resolve('css-loader') 61 | ] 62 | }, { 63 | use: [ 64 | require.resolve('style-loader'), 65 | { 66 | loader: require.resolve('css-loader'), 67 | options: { 68 | importLoaders: 1, 69 | modules: true, 70 | localIdentName: '[name]__[local]___[hash:base64:5]' 71 | } 72 | }, 73 | ] 74 | }] 75 | // loader: 'style-loader!css-loader?modules&localIdentName=[name]__[local]-[hash:base64:5]' 76 | }, 77 | { 78 | test: /\.scss$/, 79 | loader: 'style-loader!css-loader?modules&localIdentName=[name]__[local]-[hash:base64:5]!sass-loader' 80 | }, 81 | { 82 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 83 | loader: 'url-loader', 84 | options: { 85 | limit: 10000, 86 | } 87 | }, 88 | ] 89 | }, 90 | plugins: [ 91 | new webpack.HotModuleReplacementPlugin() 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /doc/release-plan/v1.md: -------------------------------------------------------------------------------- 1 | # Release Plan -- v1 2 | 3 | ## Goal 4 | 5 | user is able to add and search books. 6 | 7 | highlight points for users 8 | 9 | 1. collect books into one repo(directory), well-originized. 10 | 2. start the reading immediately by searching keywords in the title. 11 | 12 | ## UI design 13 | 14 | [UI v1](http://jackon.me/bookhub/v1/) 15 | 16 | we have 4 pages: 17 | 18 | #### 1. Add books 19 | 20 | this is where user starts using our product. 21 | 22 | workflow: 23 | 24 | 1. user choose a local directory 25 | 2. bookhub PC client will scan the directory and find books. it detect whether it is a book by the extension name. 26 | 3. if book found, bookhub ompare it with the existing book library to if it is a new book. 27 | 4. display all the new books on the page, user can select which books he want to add to the database. all are selected by default. 28 | 5. user click 'Add to Library' button, bookhub adds selected books to the book library. 29 | 30 | #### 2. Search Results 31 | 32 | it is the same as: 33 | 34 | - Google on the Internet 35 | - spotlight on Mac 36 | - [everything](https://en.wikipedia.org/wiki/Everything_(software)) on windows 37 | 38 | Search box is the best interface to retrieve infomation. 39 | 40 | The interface can learn and grow smarter if trained by more data. 41 | 42 | to make development easier, it will share the same layout as AddBooks window. 43 | 44 | #### 3. Recently Read 45 | 46 | some times I don't want to think and search, just give me some surprise or a list to pick up. 47 | 48 | some times I just want to continue my reading. 49 | 50 | Adding book covers make the page / bookhub more beautiful. 51 | 52 | #### 4. Settings ( Preferences ) 53 | 54 | no need to explain more. 55 | 56 | #### 5. main page. 57 | 58 | not sure if we do it in version 1. -------------------------------------------------------------------------------- /v1/README.md: -------------------------------------------------------------------------------- 1 | bookhub 2 | ======= 3 | 4 | calibre 可以管理图书信息,与豆瓣等匹配。但是,每个人都是手动匹配,很麻烦。电子书,转来转去都是那些书。完全可以通过 md5 去共享这个信息。 5 | 6 | 对本地电子书进行扫描,与豆瓣 / 亚马逊 / google 图书匹配,建立个人图书信息库,可以分享、合并别人的图书信息库。当信息库足够大时,我们不再需要手动整理本地电子书。记录个人阅读期间的操作,可以形成个人阅读 DNA 图谱。 7 | 8 | Feature and Demo 9 | ---------------- 10 | 11 | #### Management 12 | 13 | 1. `F2` to rename display filename 14 | 2. Double click to open selected file 15 | 3. `Del` to delete selected files -- TODO 16 | 4. Edit book meta info -- TODO 17 | 5. Collect reading info for improving reading habit -- TODO 18 | 19 | ![overview](doc/figure/overview.png) 20 | 21 | #### Sharing via Github 22 | 23 | TODO 24 | 25 | #### Collection 26 | 27 | Scan and collect e-book with specific extensions on your disk 28 | 29 | ![scan](doc/figure/scan.png) 30 | 31 | Dev-env 32 | ------- 33 | 34 | #### Dependency 35 | 36 | python, wxPython, MongoDb 37 | 38 | TODO list 39 | --------- 40 | -------------------------------------------------------------------------------- /v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/v1/__init__.py -------------------------------------------------------------------------------- /v1/doc/design/design1_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/v1/doc/design/design1_02.png -------------------------------------------------------------------------------- /v1/doc/figure/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/v1/doc/figure/overview.png -------------------------------------------------------------------------------- /v1/doc/figure/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/v1/doc/figure/scan.png -------------------------------------------------------------------------------- /v1/frame_connect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!/usr/bin/python 3 | import wx 4 | 5 | 6 | def LabelText(label, default_value, parent=None): 7 | return (wx.StaticText(parent, label=label), # Label 8 | wx.TextCtrl(parent, size=(150, -1), # Panel 9 | value=default_value, style=wx.TE_PROCESS_ENTER) 10 | ) 11 | 12 | 13 | class ConnectDialog(wx.Dialog): 14 | 15 | def __init__(self, db, parent=None): 16 | wx.Dialog.__init__(self, parent=parent, id=-1) 17 | 18 | self.db = db # db handler. connect() method is required 19 | self.SetTitle("Connect Mongo") 20 | 21 | # widgets 22 | labelHost, self.inputHost = LabelText('Host: ', 'localhost', self) 23 | labelPort, self.inputPort = LabelText('Port: ', '27017', self) 24 | btnConn = wx.Button(self, label='Connect') 25 | btnCancel = wx.Button(self, id=wx.ID_CANCEL, label="Cancel") 26 | 27 | # event handler 28 | self.Bind(wx.EVT_BUTTON, self.OnConnect, btnConn) 29 | # connet if user press enter 30 | self.Bind(wx.EVT_TEXT_ENTER, self.OnConnect, self.inputHost) 31 | self.Bind(wx.EVT_TEXT_ENTER, self.OnConnect, self.inputPort) 32 | 33 | # default settings 34 | self.inputHost.SetFocus() 35 | 36 | # Layout-inputs 37 | gridInputs = wx.FlexGridSizer(2, 2, 10, 10) 38 | gridInputs.SetFlexibleDirection = wx.HORIZONTAL 39 | gridInputs.AddMany([(labelHost), (self.inputHost, 0, wx.EXPAND), 40 | (labelPort), (self.inputPort, 0, wx.EXPAND), 41 | ]) 42 | # Layout-action button 43 | sizer_act = wx.BoxSizer(wx.HORIZONTAL) 44 | sizer_act.Add(btnConn, 1, wx.ALIGN_CENTER | wx.FIXED_MINSIZE, 10) 45 | sizer_act.Add(btnCancel, 1, wx.ALIGN_CENTER | wx.FIXED_MINSIZE, 10) 46 | # main sizer 47 | sizer_main = wx.BoxSizer(wx.VERTICAL) 48 | sizer_main.Add(gridInputs, 2, flag=wx.ALL | wx.EXPAND, border=10) 49 | sizer_main.Add(sizer_act, 1, wx.ALIGN_CENTER | wx.FIXED_MINSIZE, 10) 50 | self.SetSizer(sizer_main) 51 | self.SetAutoLayout(1) 52 | sizer_main.Fit(self) 53 | 54 | def OnConnect(self, event=None): 55 | host = self.inputHost.GetValue() 56 | port = int(self.inputPort.GetValue()) 57 | if db.connect(host, port): 58 | self.EndModal(wx.ID_OK) 59 | else: 60 | msg_error = 'Error connecting to host(%s)' % host 61 | wx.MessageBox(msg_error, 'Error', wx.OK | wx.ICON_ERROR) 62 | 63 | 64 | class TestApp(wx.App): 65 | 66 | def OnInit(self): 67 | dlg = ConnectDialog(db, parent=None) 68 | res = dlg.ShowModal() 69 | if res == wx.ID_OK: 70 | print 'Connected' 71 | else: 72 | print 'Unconnect' 73 | dlg.Destroy() 74 | return True 75 | 76 | 77 | if __name__ == '__main__': 78 | from lib.mongo_hdlr import MongodbHandler 79 | db = MongodbHandler() 80 | app = TestApp() 81 | -------------------------------------------------------------------------------- /v1/frame_overview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Required: MediaRepo 4 | +get_booklist() 5 | +open_book(BookMetaObj) 6 | 7 | """ 8 | import wx 9 | from lib.ObjectListView import ObjectListView, ColumnDefn 10 | from lib.ObjectListView import Filter 11 | import lib.util as util 12 | import subprocess 13 | 14 | showlist = ['title', 'language', 'size', 'md5'] 15 | cols = {'title': ColumnDefn("Title", "left", 330, "get_dispname", stringConverter='%s', valueSetter='set_dispname'), 16 | 'language': ColumnDefn("Language", "center", 80, "get_book_language", stringConverter='%s', isEditable=False), 17 | 'size': ColumnDefn("Size", "right", 80, "getSizeString", stringConverter='%s', isEditable=False), 18 | 'md5': ColumnDefn("MD5", "center", 320, "md5", stringConverter='%s', isEditable=False), 19 | } 20 | 21 | class OverViewFrame(wx.Frame): 22 | def __init__(self, repo): 23 | FrameStyle = wx.CAPTION | wx.RESIZE_BORDER | wx.SYSTEM_MENU |\ 24 | wx.MINIMIZE_BOX | wx.MAXIMIZE_BOX | wx.CLOSE_BOX 25 | wx.Frame.__init__(self, parent=None, id=-1, title="BookHub", 26 | pos=(100, 100), size=(500, 600), style=FrameStyle) 27 | 28 | self.BuildUI() 29 | self.InitObjectListView(repo) 30 | self.InitSearchCtrls() 31 | 32 | def BuildUI(self): 33 | self.SearchFile = wx.SearchCtrl(self) 34 | self.myOlv = ObjectListView(self, -1, 35 | style=wx.LC_REPORT | wx.SUNKEN_BORDER) 36 | size_main = wx.BoxSizer(wx.VERTICAL) 37 | size_main.Add(self.SearchFile, 1, wx.ALL | wx.EXPAND, 2) 38 | size_main.Add(self.myOlv, 20, wx.ALL | wx.EXPAND, 4) 39 | self.SetSizer(size_main) 40 | self.Layout() 41 | self.CenterOnScreen() 42 | 43 | self.myOlv.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnOpenFile) 44 | self.myOlv.Bind(wx.EVT_LIST_KEY_DOWN, self.OnKeyDown) 45 | 46 | def InitObjectListView(self, repo): 47 | self.repo = repo 48 | self.myOlv.SetColumns([cols[k.lower()] for k in showlist]) 49 | self.myOlv.SetObjects(self.repo.get_booklist()) 50 | self.myOlv.cellEditMode = ObjectListView.CELLEDIT_SINGLECLICK 51 | 52 | def InitSearchCtrls(self): 53 | """Initialize the search controls""" 54 | for (searchCtrl, olv) in [(self.SearchFile, self.myOlv)]: 55 | 56 | def _handleText(evt, searchCtrl=searchCtrl, olv=olv): 57 | self.OnTextSearchCtrl(evt, searchCtrl, olv) 58 | 59 | def _handleCancel(evt, searchCtrl=searchCtrl, olv=olv): 60 | self.OnCancelSearchCtrl(evt, searchCtrl, olv) 61 | 62 | searchCtrl.Bind(wx.EVT_TEXT, _handleText) 63 | searchCtrl.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, _handleCancel) 64 | searchCtrl.SetFocus() 65 | olv.SetFilter(Filter.TextSearch(olv, olv.columns[0:4])) 66 | 67 | def OnOpenFile(self, event): 68 | obj = self.myOlv.GetSelectedObject() 69 | path = self.repo.getFilePath(obj) 70 | if path is None: 71 | wx.MessageBox('File not exists', 'Bookhub Message') 72 | return 73 | cmd = util.cmd_open_file(path) 74 | res = subprocess.call(cmd, shell=True) 75 | if res != 0: 76 | wx.MessageBox('Open File Error. returncode %s' % res, 'Bookhub Message') 77 | 78 | def OnKeyDown(self, event): 79 | objs = self.myOlv.GetSelectedObjects() 80 | key = event.GetKeyCode() 81 | if wx.WXK_DELETE == key: 82 | self.DoDelete(objs) 83 | elif 3 == key: # wx.WXK_CONTROL_C 84 | self.DoCopyFileid(objs) 85 | 86 | def DoDelete(self, objs): 87 | for obj in objs: 88 | pass 89 | # obj.delete() 90 | self.myOlv.RemoveObjects(objs) 91 | 92 | def DoCopyFileid(self, objs): 93 | self.dataObj = wx.TextDataObject() 94 | file_ids = ','.join([obj.file_id for obj in objs]) 95 | wx.MessageBox(file_ids, "MD5 code") 96 | # self.dataObj.SetText(file_ids) 97 | # if wx.TheClipboard.Open(): 98 | # wx.TheClipboard.SetData(self.dataObj) 99 | # wx.TheClipboard.Close() 100 | #else: 101 | # wx.MessageBox("Unable to open the clipboard", "Error") 102 | 103 | def OnTextSearchCtrl(self, event, searchCtrl, olv): 104 | searchCtrl.ShowCancelButton(len(searchCtrl.GetValue())) 105 | olv.GetFilter().SetText(searchCtrl.GetValue()) 106 | olv.RepopulateList() 107 | 108 | def OnCancelSearchCtrl(self, event, searchCtrl, olv): 109 | searchCtrl.SetValue("") 110 | self.OnTextSearchCtrl(event, searchCtrl, olv) 111 | 112 | 113 | class TestApp(wx.App): 114 | 115 | def OnInit(self): 116 | from media_repo import MediaRepo 117 | repo = MediaRepo() 118 | frame = OverViewFrame(repo) 119 | self.SetTopWindow(frame) 120 | frame.Show() 121 | return True 122 | 123 | 124 | if __name__ == '__main__': 125 | app = TestApp() 126 | app.MainLoop() 127 | -------------------------------------------------------------------------------- /v1/frame_scan.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import wx 3 | import threading 4 | import os 5 | import settings 6 | import lib.util as util 7 | from media_repo import MediaRepo 8 | 9 | def add_file(src_path, file_meta): 10 | repo = MediaRepo() 11 | repo.add_bookinfo(file_meta) 12 | repo.add_history(file_meta['md5'], src_path) 13 | repo.add_file(src_path, file_meta) 14 | return 1 15 | 16 | 17 | class FileScan(threading.Thread): 18 | 19 | def __init__(self, tar_path, window): 20 | threading.Thread.__init__(self) 21 | self.stopFlag = False 22 | self.ignore_seq = settings.ignore_seq or set() 23 | self.ignore_hidden = settings.ignore_hidden 24 | self.ext_pool = settings.ext_pool 25 | 26 | self.file_hdlr = add_file 27 | self.tar_path = tar_path 28 | self.window = window 29 | self.cnt_scanned = 0 30 | 31 | def stop(self): 32 | self.stopFlag = True 33 | 34 | def run(self): 35 | cnt_found = self.scan_path(self.tar_path) 36 | wx.CallAfter(self.window.scan_stopped, self.tar_path, cnt_found, self.cnt_scanned) 37 | 38 | def scan_path(self, src_path): 39 | """scan path to detect target files 40 | 41 | @src_path: unicode encoding is required""" 42 | if not os.path.exists(src_path): # not exists 43 | return None 44 | 45 | if os.path.isfile(src_path): # file 46 | rawname, ext = os.path.splitext(os.path.basename(src_path)) 47 | if not ext or ext not in self.ext_pool: # file extension check 48 | return 0 49 | file_meta = {'rawname': [rawname], 50 | 'ext': ext, 51 | 'md5': util.md5_for_file(src_path), 52 | 'bytes': os.path.getsize(src_path), 53 | } 54 | wx.CallAfter(self.window.file_found, src_path, file_meta['md5']) 55 | return self.file_hdlr(src_path, file_meta) or 0 56 | else: # dir 57 | added = 0 58 | # ignore log/.git etc 59 | tar_path = set(os.listdir(src_path)) - self.ignore_seq 60 | self.cnt_scanned += len(tar_path) 61 | wx.CallAfter(self.window.file_scanned, self.cnt_scanned) 62 | for rel_path in tar_path: 63 | if self.stopFlag: 64 | return added 65 | abs_path = os.path.join(src_path, rel_path) 66 | if self.ignore_hidden and util.is_hiden(abs_path): 67 | continue # ignore hidden 68 | else: 69 | added += self.scan_path(abs_path) or 0 70 | return added 71 | 72 | 73 | class ScanFrame(wx.Frame): 74 | 75 | def __init__(self): 76 | wx.Frame.__init__(self, parent=None, title="Scan Books", 77 | pos=(100, 100), size=(1180, 600)) 78 | self.threads = [] 79 | self.buildUI() 80 | 81 | def buildUI(self): 82 | frameStyle = wx.TE_AUTO_SCROLL | wx.TE_MULTILINE 83 | self.scan_log = wx.TextCtrl(parent=self, style=frameStyle) 84 | self.scan_log.SetEditable(False) 85 | 86 | # toolbox 87 | self.startBtn = wx.Button(parent=self, label="Start") 88 | self.stopBtn = wx.Button(parent=self, label="Stop") 89 | self.startBtn.Enable() 90 | self.stopBtn.Disable() 91 | self.scan_cnt_label = wx.StaticText(parent=self, label='Files Scanned:', style=wx.ALIGN_CENTER) 92 | self.scan_cnt_value = wx.StaticText(parent=self, label='0', style=wx.ALIGN_CENTER) 93 | 94 | self.toolbox = wx.BoxSizer(wx.VERTICAL) 95 | self.toolbox.Add(self.startBtn, 1, wx.ALL | wx.EXPAND, 5, 0) 96 | self.toolbox.Add(self.stopBtn, 1, wx.ALL | wx.EXPAND, 5, 0) 97 | self.toolbox.Add(self.scan_cnt_label, 1, wx.ALL | wx.EXPAND, 5, 0) 98 | self.toolbox.Add(self.scan_cnt_value, 1, wx.ALIGN_CENTER, 5, 0) 99 | 100 | self.mainbox = wx.BoxSizer(wx.HORIZONTAL) 101 | self.mainbox.Add(self.scan_log, 1, wx.ALL | wx.EXPAND, 5, 5) 102 | self.mainbox.Add(self.toolbox, 0, wx.NORMAL, 0, 0) 103 | 104 | self.SetSizer(self.mainbox) 105 | self.CenterOnScreen() 106 | 107 | self.startBtn.Bind(wx.EVT_BUTTON, self.OnStartScan) 108 | self.stopBtn.Bind(wx.EVT_BUTTON, self.OnStopScan) 109 | 110 | def file_found(self, filepath, md5): 111 | self.scan_log.AppendText('add %s, md5: %s\n' % (filepath, md5)) 112 | 113 | def file_scanned(self, cnt): 114 | self.scan_cnt_value.SetLabel(str(cnt)) 115 | 116 | def scan_stopped(self, scanned_path, cnt_found, cnt_scanned): 117 | self.startBtn.Enable() 118 | self.stopBtn.Disable() 119 | msg = '\nscan finished! %s/%s (found/scanned) in %s\n'\ 120 | % (cnt_found, cnt_scanned, os.path.abspath(scanned_path)) 121 | self.scan_log.AppendText(msg) 122 | 123 | def OnStartScan(self, event): 124 | # clear log if too big 125 | if len(self.scan_log.GetValue()) > 1024: 126 | self.scan_log.SetValue('') 127 | dlg = wx.DirDialog(self, "Choose a directory:") 128 | if dlg.ShowModal() == wx.ID_OK: 129 | self.startBtn.Disable() 130 | self.stopBtn.Enable() 131 | scan_thread = FileScan(dlg.GetPath(), self) 132 | self.threads.append(scan_thread) 133 | scan_thread.start() 134 | 135 | def OnStopScan(self, event): 136 | while self.threads: 137 | thread = self.threads[0] 138 | thread.stop() 139 | self.threads.remove(thread) 140 | 141 | 142 | class TestApp(wx.App): 143 | 144 | def OnInit(self): 145 | frame = ScanFrame() 146 | frame.Show(True) 147 | self.SetTopWindow(frame) 148 | return True 149 | 150 | if __name__ == "__main__": 151 | app = TestApp() 152 | app.MainLoop() 153 | -------------------------------------------------------------------------------- /v1/lib/ObjectListView/CellEditor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #---------------------------------------------------------------------------- 3 | # Name: CellEditor.py 4 | # Author: Phillip Piper 5 | # Created: 3 April 2008 6 | # SVN-ID: $Id$ 7 | # Copyright: (c) 2008 by Phillip Piper, 2008 8 | # License: wxWindows license 9 | #---------------------------------------------------------------------------- 10 | # Change log: 11 | # 2009/06/09 JPP All cell editors start life 0 sized to prevent flickering 12 | # 2008/05/26 JPP Fixed pyLint annoyances 13 | # 2008/04/04 JPP Initial version complete 14 | #---------------------------------------------------------------------------- 15 | # To do: 16 | # - there has to be a better DateTimeEditor somewhere!! 17 | 18 | """ 19 | The *CellEditor* module provides some editors for standard types that can be installed 20 | in an *ObjectListView*. It also provides a *Registry* that maps between standard types 21 | and functions that will create editors for that type. 22 | 23 | Cell Editors 24 | 25 | A cell editor can be any subclass of wx.Window provided that it supports the 26 | following protocol: 27 | 28 | SetValue(self, value) 29 | The editor should show the given value for editing 30 | 31 | GetValue(self) 32 | The editor should return the value that it holds. Return None to indicate 33 | an invalid value. The returned value should be of the correct type, i.e. 34 | don't return a string if the editor was registered for the bool type. 35 | 36 | The editor should invoke FinishCellEdit() on its parent ObjectListView when it 37 | loses focus or when the user commits the change by pressing Return or Enter. 38 | 39 | The editor should invoke CancelCellEdit() on its parent ObjectListView when 40 | the user presses Escape. 41 | 42 | Editor Registry 43 | 44 | The editor registry remembers a function that will be called to create 45 | an editor for a given type. 46 | """ 47 | 48 | __author__ = "Phillip Piper" 49 | __date__ = "3 May 2008" 50 | __version__ = "1.0" 51 | 52 | import datetime 53 | import wx 54 | 55 | #====================================================================== 56 | # Editor Registry 57 | 58 | # Module level variable 59 | _cellEditorRegistrySingleton = None 60 | 61 | def CellEditorRegistry(): 62 | """ 63 | Return the registry that is managing type to creator functions 64 | """ 65 | global _cellEditorRegistrySingleton 66 | 67 | if _cellEditorRegistrySingleton is None: 68 | _cellEditorRegistrySingleton = EditorRegistry() 69 | 70 | return _cellEditorRegistrySingleton 71 | 72 | 73 | class EditorRegistry: 74 | """ 75 | An *EditorRegistry* manages a mapping of types onto creator functions. 76 | 77 | When called, creator functions will create the appropriate kind of cell editor 78 | """ 79 | 80 | def __init__(self): 81 | self.typeToFunctionMap = {} 82 | 83 | # Standard types and their creator functions 84 | self.typeToFunctionMap[str] = self._MakeStringEditor 85 | self.typeToFunctionMap[unicode] = self._MakeStringEditor 86 | self.typeToFunctionMap[bool] = self._MakeBoolEditor 87 | self.typeToFunctionMap[int] = self._MakeIntegerEditor 88 | self.typeToFunctionMap[long] = self._MakeLongEditor 89 | self.typeToFunctionMap[float] = self._MakeFloatEditor 90 | self.typeToFunctionMap[datetime.datetime] = self._MakeDateTimeEditor 91 | self.typeToFunctionMap[datetime.date] = self._MakeDateEditor 92 | self.typeToFunctionMap[datetime.time] = self._MakeTimeEditor 93 | 94 | # TODO: Install editors for mxDateTime if installed 95 | 96 | def GetCreatorFunction(self, aValue): 97 | """ 98 | Return the creator function that is register for the type of the given value. 99 | Return None if there is no registered function for the type. 100 | """ 101 | return self.typeToFunctionMap.get(type(aValue), None) 102 | 103 | def RegisterCreatorFunction(self, aType, aFunction): 104 | """ 105 | Register the given function to be called when we need an editor for the given type. 106 | 107 | The function must accept three parameter: an ObjectListView, row index, and subitem index. 108 | It should return a wxWindow that is parented on the listview, and that responds to: 109 | 110 | - SetValue(newValue) 111 | 112 | - GetValue() to return the value shown in the editor 113 | 114 | """ 115 | self.typeToFunctionMap[aType] = aFunction 116 | 117 | #---------------------------------------------------------------------------- 118 | # Creator functions for standard types 119 | 120 | @staticmethod 121 | def _MakeStringEditor(olv, rowIndex, subItemIndex): 122 | return BaseCellTextEditor(olv, subItemIndex) 123 | 124 | @staticmethod 125 | def _MakeBoolEditor(olv, rowIndex, subItemIndex): 126 | return BooleanEditor(olv) 127 | 128 | @staticmethod 129 | def _MakeIntegerEditor(olv, rowIndex, subItemIndex): 130 | return IntEditor(olv, subItemIndex, validator=NumericValidator()) 131 | 132 | @staticmethod 133 | def _MakeLongEditor(olv, rowIndex, subItemIndex): 134 | return LongEditor(olv, subItemIndex) 135 | 136 | @staticmethod 137 | def _MakeFloatEditor(olv, rowIndex, subItemIndex): 138 | return FloatEditor(olv, subItemIndex, validator=NumericValidator("0123456789-+eE.")) 139 | 140 | @staticmethod 141 | def _MakeDateTimeEditor(olv, rowIndex, subItemIndex): 142 | dte = DateTimeEditor(olv, subItemIndex) 143 | 144 | column = olv.columns[subItemIndex] 145 | if isinstance(column.stringConverter, basestring): 146 | dte.formatString = column.stringConverter 147 | 148 | return dte 149 | 150 | @staticmethod 151 | def _MakeDateEditor(olv, rowIndex, subItemIndex): 152 | dte = DateEditor(olv, style=wx.DP_DROPDOWN | wx.DP_SHOWCENTURY | wx.WANTS_CHARS) 153 | #dte.SetValidator(MyValidator(olv)) 154 | return dte 155 | 156 | @staticmethod 157 | def _MakeTimeEditor(olv, rowIndex, subItemIndex): 158 | editor = TimeEditor(olv, subItemIndex) 159 | 160 | column = olv.columns[subItemIndex] 161 | if isinstance(column.stringConverter, basestring): 162 | editor.formatString = column.stringConverter 163 | 164 | return editor 165 | 166 | #====================================================================== 167 | # Cell editors 168 | 169 | 170 | class BooleanEditor(wx.Choice): 171 | """This is a simple editor to edit a boolean value that can be used in an 172 | ObjectListView""" 173 | 174 | def __init__(self, *args, **kwargs): 175 | kwargs["choices"] = ["True", "False"] 176 | wx.Choice.__init__(self, *args, **kwargs) 177 | 178 | def GetValue(self): 179 | "Get the value from the editor" 180 | return self.GetSelection() == 0 181 | 182 | def SetValue(self, value): 183 | "Put a new value into the editor" 184 | if value: 185 | self.Select(0) 186 | else: 187 | self.Select(1) 188 | 189 | #---------------------------------------------------------------------------- 190 | 191 | class BaseCellTextEditor(wx.TextCtrl): 192 | """This is a base text editor for text-like editors used in an ObjectListView""" 193 | 194 | def __init__(self, olv, subItemIndex, **kwargs): 195 | style = wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB 196 | # Allow for odd case where parent isn't an ObjectListView 197 | if hasattr(olv, "columns"): 198 | if olv.HasFlag(wx.LC_ICON): 199 | style |= (wx.TE_CENTRE | wx.TE_MULTILINE) 200 | else: 201 | style |= olv.columns[subItemIndex].GetAlignmentForText() 202 | wx.TextCtrl.__init__(self, olv, style=style, size=(0,0), **kwargs) 203 | 204 | # With the MULTILINE flag, the text control always has a vertical 205 | # scrollbar, which looks stupid. I don't know how to get rid of it. 206 | # This doesn't do it: 207 | # self.ToggleWindowStyle(wx.VSCROLL) 208 | 209 | #---------------------------------------------------------------------------- 210 | 211 | class IntEditor(BaseCellTextEditor): 212 | """This is a text editor for integers for use in an ObjectListView""" 213 | 214 | def GetValue(self): 215 | "Get the value from the editor" 216 | s = wx.TextCtrl.GetValue(self).strip() 217 | try: 218 | return int(s) 219 | except ValueError: 220 | return None 221 | 222 | def SetValue(self, value): 223 | "Put a new value into the editor" 224 | if isinstance(value, int): 225 | value = repr(value) 226 | wx.TextCtrl.SetValue(self, value) 227 | 228 | #---------------------------------------------------------------------------- 229 | 230 | class LongEditor(BaseCellTextEditor): 231 | """This is a text editor for long values for use in an ObjectListView""" 232 | 233 | def GetValue(self): 234 | "Get the value from the editor" 235 | s = wx.TextCtrl.GetValue(self).strip() 236 | try: 237 | return long(s) 238 | except ValueError: 239 | return None 240 | 241 | def SetValue(self, value): 242 | "Put a new value into the editor" 243 | if isinstance(value, long): 244 | value = repr(value) 245 | wx.TextCtrl.SetValue(self, value) 246 | 247 | #---------------------------------------------------------------------------- 248 | 249 | class FloatEditor(BaseCellTextEditor): 250 | """This is a text editor for floats for use in an ObjectListView. 251 | 252 | Because of the trouble of precisely converting floats to strings, 253 | this editor sometimes behaves a little strangely.""" 254 | 255 | def GetValue(self): 256 | "Get the value from the editor" 257 | s = wx.TextCtrl.GetValue(self).strip() 258 | try: 259 | return float(s) 260 | except ValueError: 261 | return None 262 | 263 | def SetValue(self, value): 264 | "Put a new value into the editor" 265 | if isinstance(value, float): 266 | value = repr(value) 267 | wx.TextCtrl.SetValue(self, value) 268 | 269 | #---------------------------------------------------------------------------- 270 | 271 | class DateTimeEditor(BaseCellTextEditor): 272 | """ 273 | A DateTimeEditor allows the user to enter a date/time combination, where the time is optional 274 | and many formats of date and time are allowed. 275 | 276 | The control accepts these date formats (in all cases, the year can be only 2 digits): 277 | - '31/12/2008' 278 | - '2008/12/31' 279 | - '12/31/2008' 280 | - '31 December 2008' 281 | - '31 Dec 2008' 282 | - 'Dec 31 2008' 283 | - 'December 31 2008' 284 | 285 | Slash character can also be '-' or ' '. Consecutive whitespace are collapsed. 286 | 287 | The control accepts these time formats: 288 | - '23:59:59' 289 | - '11:59:59pm' 290 | - '23:59' 291 | - '11:59pm' 292 | - '11pm' 293 | 294 | The colons are required. The am/pm is case insensitive. 295 | 296 | The implementation uses a brute force approach to parsing the data. 297 | """ 298 | # Acceptable formats: 299 | # '31/12/2008', '2008/12/31', '12/31/2008', '31 December 2008', '31 Dec 2008', 'Dec 31 2007' 300 | # second line is the same but with two-digit year. 301 | # slash character can also be '-' or ' '. Consecutive whitespace are collapsed. 302 | STD_DATE_FORMATS = ['%d %m %Y', '%Y %m %d', '%m %d %Y', '%d %B %Y', '%d %b %Y', '%b %d %Y', '%B %d %Y', 303 | '%d %m %y', '%y %m %d', '%m %d %y', '%d %B %y', '%d %b %y', '%b %d %y', '%B %d %y'] 304 | 305 | STD_DATE_WITHOUT_YEAR_FORMATS = ['%d %m', '%m %d', '%d %B', '%d %b', '%B %d', '%b %d'] 306 | 307 | # Acceptable formats: '23:59:59', '11:59:59pm', '23:59', '11:59pm', '11pm' 308 | STD_TIME_FORMATS = ['%H:%M:%S', '%I:%M:%S %p', '%H:%M', '%I:%M %p', '%I %p'] 309 | 310 | # These separators are treated as whitespace 311 | STD_SEPARATORS = "/-," 312 | 313 | def __init__(self, *args, **kwargs): 314 | BaseCellTextEditor.__init__(self, *args, **kwargs) 315 | self.formatString = "%X %x" 316 | 317 | self.allDateTimeFormats = [] 318 | for dtFmt in self.STD_DATE_FORMATS: 319 | self.allDateTimeFormats.append(dtFmt) 320 | for timeFmt in self.STD_TIME_FORMATS: 321 | self.allDateTimeFormats.append("%s %s" % (dtFmt, timeFmt)) 322 | 323 | self.allDateTimeWithoutYearFormats = [] 324 | for dtFmt in self.STD_DATE_WITHOUT_YEAR_FORMATS: 325 | self.allDateTimeWithoutYearFormats.append(dtFmt) 326 | for timeFmt in self.STD_TIME_FORMATS: 327 | self.allDateTimeWithoutYearFormats.append("%s %s" % (dtFmt, timeFmt)) 328 | 329 | 330 | def SetValue(self, value): 331 | "Put a new value into the editor" 332 | if isinstance(value, datetime.datetime): 333 | value = value.strftime(self.formatString) 334 | wx.TextCtrl.SetValue(self, value) 335 | 336 | 337 | def GetValue(self): 338 | "Get the value from the editor" 339 | s = wx.TextCtrl.GetValue(self).strip() 340 | return self._ParseDateTime(s) 341 | 342 | 343 | def _ParseDateTime(self, s): 344 | # Try the installed format string first 345 | try: 346 | return datetime.datetime.strptime(s, self.formatString) 347 | except ValueError: 348 | pass 349 | 350 | for x in self.STD_SEPARATORS: 351 | s = s.replace(x, " ") 352 | 353 | # Because of the logic of strptime, we have to check shorter patterns first. 354 | # For example: 355 | # "31 12" matches "%d %m %y" => datetime(2012, 1, 3, 0, 0) ?? 356 | # but we want: 357 | # "31 12" to match "%d %m" => datetime(1900, 12, 31, 0, 0) 358 | # JPP 4/4/2008 Python 2.5.1 359 | for fmt in self.allDateTimeWithoutYearFormats: 360 | try: 361 | dt = datetime.datetime.strptime(s, fmt) 362 | return dt.replace(year=datetime.datetime.today().year) 363 | except ValueError: 364 | pass 365 | 366 | for fmt in self.allDateTimeFormats: 367 | try: 368 | return datetime.datetime.strptime(s, fmt) 369 | except ValueError: 370 | pass 371 | 372 | return None 373 | 374 | #---------------------------------------------------------------------------- 375 | 376 | class NumericValidator(wx.PyValidator): 377 | """This validator only accepts numeric keys""" 378 | 379 | def __init__(self, acceptableChars="0123456789+-"): 380 | wx.PyValidator.__init__(self) 381 | self.Bind(wx.EVT_CHAR, self._OnChar) 382 | self.acceptableChars = acceptableChars 383 | self.acceptableCodes = [ord(x) for x in self.acceptableChars] 384 | stdEditKeys = [wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER, wx.WXK_ESCAPE, wx.WXK_CANCEL, 385 | wx.WXK_TAB, wx.WXK_BACK, wx.WXK_DELETE, wx.WXK_HOME, wx.WXK_END, 386 | wx.WXK_LEFT, wx.WXK_RIGHT] 387 | self.acceptableCodes.extend(stdEditKeys) 388 | 389 | def Clone(self): 390 | "Make a new copy of this validator" 391 | return NumericValidator(self.acceptableChars) 392 | 393 | def _OnChar(self, event): 394 | "Handle the OnChar event by rejecting non-numerics" 395 | if event.GetModifiers() != 0 and event.GetModifiers() != wx.MOD_SHIFT: 396 | event.Skip() 397 | return 398 | 399 | if event.GetKeyCode() in self.acceptableCodes: 400 | event.Skip() 401 | return 402 | 403 | wx.Bell() 404 | 405 | #---------------------------------------------------------------------------- 406 | 407 | class DateEditor(wx.DatePickerCtrl): 408 | """ 409 | This control uses standard datetime. 410 | wx.DatePickerCtrl works only with wx.DateTime, but they are strange beasts. 411 | wx.DataTime use 0 indexed months, i.e. January==0 and December==11. 412 | """ 413 | 414 | def __init__(self, *args, **kwargs): 415 | #kwargs["size"] = (0,0) 416 | wx.DatePickerCtrl.__init__(self, *args, **kwargs) 417 | self.SetValue(None) 418 | 419 | def SetValue(self, value): 420 | if value: 421 | dt = wx.DateTime() 422 | dt.Set(value.day, value.month-1, value.year) 423 | else: 424 | dt = wx.DateTime.Today() 425 | wx.DatePickerCtrl.SetValue(self, dt) 426 | 427 | def GetValue(self): 428 | "Get the value from the editor" 429 | dt = wx.DatePickerCtrl.GetValue(self) 430 | if dt.IsOk(): 431 | return datetime.date(dt.Year, dt.Month+1, dt.Day) 432 | else: 433 | return None 434 | 435 | #---------------------------------------------------------------------------- 436 | 437 | class TimeEditor(BaseCellTextEditor): 438 | """A text editor that expects and return time values""" 439 | 440 | # Acceptable formats: '23:59', '11:59pm', '11pm' 441 | STD_TIME_FORMATS = ['%X', '%H:%M', '%I:%M %p', '%I %p'] 442 | 443 | def __init__(self, *args, **kwargs): 444 | BaseCellTextEditor.__init__(self, *args, **kwargs) 445 | self.formatString = "%X" 446 | 447 | def SetValue(self, value): 448 | "Put a new value into the editor" 449 | value = value or "" 450 | if isinstance(value, datetime.time): 451 | value = value.strftime(self.formatString) 452 | wx.TextCtrl.SetValue(self, value) 453 | 454 | def GetValue(self): 455 | "Get the value from the editor" 456 | s = wx.TextCtrl.GetValue(self).strip() 457 | fmts = self.STD_TIME_FORMATS[:] 458 | if self.formatString not in fmts: 459 | fmts.insert(0, self.formatString) 460 | for fmt in fmts: 461 | try: 462 | dt = datetime.datetime.strptime(s, fmt) 463 | return dt.time() 464 | except ValueError: 465 | pass 466 | 467 | return None 468 | 469 | #====================================================================== 470 | # Auto complete controls 471 | 472 | def MakeAutoCompleteTextBox(olv, columnIndex, maxObjectsToConsider=10000): 473 | """ 474 | Return a TextCtrl that lets the user choose from all existing values in this column. 475 | Do not call for large lists 476 | """ 477 | col = olv.columns[columnIndex] 478 | #THINK: We could make this time based, i.e. it escapes after 1 second. 479 | maxObjectsToConsider = min(maxObjectsToConsider, olv.GetItemCount()) 480 | options = set(col.GetStringValue(olv.GetObjectAt(i)) for i in range(maxObjectsToConsider)) 481 | tb = BaseCellTextEditor(olv, columnIndex) 482 | AutoCompleteHelper(tb, list(options)) 483 | return tb 484 | 485 | def MakeAutoCompleteComboBox(olv, columnIndex, maxObjectsToConsider=10000): 486 | """ 487 | Return a ComboBox that lets the user choose from all existing values in this column. 488 | Do not call for large lists 489 | """ 490 | col = olv.columns[columnIndex] 491 | maxObjectsToConsider = min(maxObjectsToConsider, olv.GetItemCount()) 492 | options = set(col.GetStringValue(olv.GetObjectAt(i)) for i in range(maxObjectsToConsider)) 493 | cb = wx.ComboBox(olv, choices=list(options), 494 | style=wx.CB_DROPDOWN|wx.CB_SORT|wx.TE_PROCESS_ENTER) 495 | AutoCompleteHelper(cb) 496 | return cb 497 | 498 | 499 | #------------------------------------------------------------------------- 500 | 501 | class AutoCompleteHelper(object): 502 | """ 503 | This class operates on a text control or combobox, and automatically completes the 504 | text typed by the user from a list of entries in a given list. 505 | 506 | """ 507 | 508 | def __init__(self, control, possibleValues=None): 509 | self.control = control 510 | self.lastUserEnteredString = self.control.GetValue() 511 | self.control.Bind(wx.EVT_TEXT, self._OnTextEvent) 512 | if isinstance(self.control, wx.ComboBox): 513 | self.possibleValues = self.control.GetStrings() 514 | else: 515 | self.possibleValues = possibleValues or [] 516 | self.lowerCasePossibleValues = [x.lower() for x in self.possibleValues] 517 | 518 | 519 | def _OnTextEvent(self, evt): 520 | evt.Skip() 521 | # After the SetValue() we want to ignore this event. If we get this event 522 | # and the value hasn't been modified, we know it was a SetValue() call. 523 | if hasattr(self.control, "IsModified") and not self.control.IsModified(): 524 | return 525 | 526 | # If the text has changed more than the user just typing one letter, 527 | # then don't try to autocomplete it. 528 | if len(evt.GetString()) != len(self.lastUserEnteredString)+1: 529 | self.lastUserEnteredString = evt.GetString() 530 | return 531 | 532 | self.lastUserEnteredString = evt.GetString() 533 | s = evt.GetString().lower() 534 | for i, x in enumerate(self.lowerCasePossibleValues): 535 | if x.startswith(s): 536 | self._AutocompleteWith(self.possibleValues[i]) 537 | break 538 | 539 | 540 | def _AutocompleteWith(self, newValue): 541 | """Suggest the given value by autocompleting it.""" 542 | # GetInsertionPoint() doesn't seem reliable under linux 543 | insertIndex = len(self.control.GetValue()) 544 | self.control.SetValue(newValue) 545 | if isinstance(self.control, wx.ComboBox): 546 | self.control.SetMark(insertIndex, len(newValue)) 547 | else: 548 | # Seems that under linux, selecting only seems to work here if we do it 549 | # outside of the text event 550 | wx.CallAfter(self.control.SetSelection, insertIndex, len(newValue)) 551 | -------------------------------------------------------------------------------- /v1/lib/ObjectListView/Filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #---------------------------------------------------------------------------- 3 | # Name: Filter.py 4 | # Author: Phillip Piper 5 | # Created: 26 August 2008 6 | # Copyright: (c) 2008 Phillip Piper 7 | # SVN-ID: $Id$ 8 | # License: wxWindows license 9 | #---------------------------------------------------------------------------- 10 | # Change log: 11 | # 2008/08/26 JPP First version 12 | #---------------------------------------------------------------------------- 13 | # To do: 14 | # 15 | 16 | """ 17 | Filters provide a structured mechanism to display only some of the model objects 18 | given to an ObjectListView. Only those model objects which are 'chosen' by 19 | an installed filter will be presented to the user. 20 | 21 | Filters are simple callable objects which accept a single parameter, which 22 | is the list of model objects to be filtered, and returns a collection of 23 | those objects which will be presented to the user. 24 | 25 | This module provides some standard filters. 26 | 27 | Filters almost always impose a performance penalty on the ObjectListView. 28 | The penalty is normally O(n) since the filter normally examines each model 29 | object to see if it should be included. Head() and Tail() are exceptions 30 | to this observation. 31 | """ 32 | 33 | def Predicate(predicate): 34 | """ 35 | Display only those objects that match the given predicate 36 | 37 | Example:: 38 | self.olv.SetFilter(Filter.Predicate(lambda x: x.IsOverdue())) 39 | """ 40 | return lambda modelObjects: [x for x in modelObjects if predicate(x)] 41 | 42 | 43 | def Head(num): 44 | """ 45 | Display at most the first N of the model objects 46 | 47 | Example:: 48 | self.olv.SetFilter(Filter.Head(1000)) 49 | """ 50 | return lambda modelObjects: modelObjects[:num] 51 | 52 | 53 | def Tail(num): 54 | """ 55 | Display at most the last N of the model objects 56 | 57 | Example:: 58 | self.olv.SetFilter(Filter.Tail(1000)) 59 | """ 60 | return lambda modelObjects: modelObjects[-num:] 61 | 62 | 63 | class TextSearch(object): 64 | """ 65 | Return only model objects that match a given string. If columns is not empty, 66 | only those columns will be considered when searching for the string. Otherwise, 67 | all columns will be searched. 68 | 69 | Example:: 70 | self.olv.SetFilter(Filter.TextSearch(self.olv, text="findthis")) 71 | self.olv.RepopulateList() 72 | """ 73 | 74 | def __init__(self, objectListView, columns=(), text=""): 75 | """ 76 | Create a filter that includes on modelObject that have 'self.text' somewhere in the given columns. 77 | """ 78 | self.objectListView = objectListView 79 | self.columns = columns 80 | self.text = text 81 | 82 | def __call__(self, modelObjects): 83 | """ 84 | Return the model objects that contain our text in one of the columns to consider 85 | """ 86 | if not self.text: 87 | return modelObjects 88 | 89 | # In non-report views, we can only search the primary column 90 | if self.objectListView.InReportView(): 91 | cols = self.columns or self.objectListView.columns 92 | else: 93 | cols = [self.objectListView.columns[0]] 94 | 95 | textToFind = self.text.lower() 96 | 97 | def _containsText(modelObject): 98 | for col in cols: 99 | if textToFind in col.GetStringValue(modelObject).lower(): 100 | return True 101 | return False 102 | 103 | return [x for x in modelObjects if _containsText(x)] 104 | 105 | def SetText(self, text): 106 | """ 107 | Set the text that this filter will match. Set this to None or "" to disable the filter. 108 | """ 109 | self.text = text 110 | 111 | 112 | class Chain(object): 113 | """ 114 | Return only model objects that match all of the given filters. 115 | 116 | Example:: 117 | # Show at most 100 people whose salary is over 50,000 118 | salaryFilter = Filter.Predicate(lambda person: person.GetSalary() > 50000) 119 | self.olv.SetFilter(Filter.Chain(salaryFilter, Filter.Tail(100))) 120 | self.olv.RepopulateList() 121 | """ 122 | 123 | def __init__(self, *filters): 124 | """ 125 | Create a filter that performs all the given filters. 126 | 127 | The order of the filters is important. 128 | """ 129 | self.filters = filters 130 | 131 | 132 | def __call__(self, modelObjects): 133 | """ 134 | Return the model objects that match all of our filters 135 | """ 136 | for filter in self.filters: 137 | modelObjects = filter(modelObjects) 138 | return modelObjects 139 | -------------------------------------------------------------------------------- /v1/lib/ObjectListView/OLVEvent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #---------------------------------------------------------------------------- 3 | # Name: OLVEvent.py 4 | # Author: Phillip Piper 5 | # Created: 3 April 2008 6 | # SVN-ID: $Id$ 7 | # Copyright: (c) 2008 by Phillip Piper, 2008 8 | # License: wxWindows license 9 | #---------------------------------------------------------------------------- 10 | # Change log: 11 | # 2008/08/18 JPP Added CELL_EDIT_STARTED and CELL_EDIT_FINISHED events 12 | # 2008/07/16 JPP Added group-related events 13 | # 2008/06/19 JPP Added EVT_SORT 14 | # 2008/05/26 JPP Fixed pyLint annoyances 15 | # 2008/04/04 JPP Initial version complete 16 | #---------------------------------------------------------------------------- 17 | # To do: 18 | 19 | """ 20 | The OLVEvent module holds all the events used by the ObjectListView module. 21 | """ 22 | 23 | __author__ = "Phillip Piper" 24 | __date__ = "3 August 2008" 25 | __version__ = "1.1" 26 | 27 | import wx 28 | 29 | #====================================================================== 30 | # Event ids and types 31 | 32 | def _EventMaker(): 33 | evt = wx.NewEventType() 34 | return (evt, wx.PyEventBinder(evt)) 35 | 36 | (olv_EVT_CELL_EDIT_STARTING, EVT_CELL_EDIT_STARTING) = _EventMaker() 37 | (olv_EVT_CELL_EDIT_STARTED, EVT_CELL_EDIT_STARTED) = _EventMaker() 38 | (olv_EVT_CELL_EDIT_FINISHING, EVT_CELL_EDIT_FINISHING) = _EventMaker() 39 | (olv_EVT_CELL_EDIT_FINISHED, EVT_CELL_EDIT_FINISHED) = _EventMaker() 40 | (olv_EVT_SORT, EVT_SORT) = _EventMaker() 41 | (olv_EVT_GROUP_CREATING, EVT_GROUP_CREATING) = _EventMaker() 42 | (olv_EVT_GROUP_SORT, EVT_GROUP_SORT) = _EventMaker() 43 | (olv_EVT_EXPANDING, EVT_EXPANDING) = _EventMaker() 44 | (olv_EVT_EXPANDED, EVT_EXPANDED) = _EventMaker() 45 | (olv_EVT_COLLAPSING, EVT_COLLAPSING) = _EventMaker() 46 | (olv_EVT_COLLAPSED, EVT_COLLAPSED) = _EventMaker() 47 | 48 | #====================================================================== 49 | # Event parameter blocks 50 | 51 | class VetoableEvent(wx.PyCommandEvent): 52 | """ 53 | Base class for all cancellable actions 54 | """ 55 | 56 | def __init__(self, evtType): 57 | wx.PyCommandEvent.__init__(self, evtType, -1) 58 | self.veto = False 59 | 60 | def Veto(self, isVetoed=True): 61 | """ 62 | Veto (or un-veto) this event 63 | """ 64 | self.veto = isVetoed 65 | 66 | def IsVetoed(self): 67 | """ 68 | Has this event been vetod? 69 | """ 70 | return self.veto 71 | 72 | #---------------------------------------------------------------------------- 73 | 74 | class CellEditEvent(VetoableEvent): 75 | """ 76 | Base class for all cell editing events 77 | """ 78 | 79 | def SetParameters(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor): 80 | self.objectListView = objectListView 81 | self.rowIndex = rowIndex 82 | self.subItemIndex = subItemIndex 83 | self.rowModel = rowModel 84 | self.cellValue = cellValue 85 | self.editor = editor 86 | 87 | #---------------------------------------------------------------------------- 88 | 89 | class CellEditStartedEvent(CellEditEvent): 90 | """ 91 | A cell has started to be edited. 92 | 93 | All attributes are public and should be considered read-only. 94 | """ 95 | 96 | def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, cellBounds, editor): 97 | CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_STARTED) 98 | self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) 99 | self.cellBounds = cellBounds 100 | 101 | #---------------------------------------------------------------------------- 102 | 103 | class CellEditStartingEvent(CellEditEvent): 104 | """ 105 | A cell is about to be edited. 106 | 107 | All attributes are public and should be considered read-only. Methods are provided for 108 | information that can be changed. 109 | """ 110 | 111 | def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, cellBounds, editor): 112 | CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_STARTING) 113 | self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) 114 | self.cellBounds = cellBounds 115 | self.newEditor = None 116 | self.shouldConfigureEditor = True 117 | 118 | def SetCellBounds(self, rect): 119 | """ 120 | Change where the editor will be placed. 121 | rect is a list: [left, top, width, height] 122 | """ 123 | self.cellBounds = rect 124 | 125 | def SetNewEditor(self, control): 126 | """ 127 | Use the given control instead of the editor. 128 | """ 129 | self.newEditor = control 130 | 131 | def DontConfigureEditor(self): 132 | """ 133 | The editor will not be automatically configured. 134 | 135 | If this is called, the event handler must handle all configuration. In 136 | particular, it must configure its own event handlers to that 137 | ObjectListView.CancelCellEdit() is called when the user presses Escape, 138 | and ObjectListView.CommitCellEdit() is called when the user presses 139 | Enter/Return or when the editor loses focus. """ 140 | self.shouldConfigureEditor = False 141 | 142 | #---------------------------------------------------------------------------- 143 | 144 | class CellEditFinishedEvent(CellEditEvent): 145 | """ 146 | The user has finished editing a cell. 147 | """ 148 | def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, userCancelled): 149 | CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_FINISHED) 150 | self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, None, None) 151 | self.userCancelled = userCancelled 152 | 153 | #---------------------------------------------------------------------------- 154 | 155 | class CellEditFinishingEvent(CellEditEvent): 156 | """ 157 | The user is finishing editing a cell. 158 | 159 | If this event is vetoed, the edit will be cancelled silently. This is useful if the 160 | event handler completely handles the model updating. 161 | """ 162 | def __init__(self, objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor, userCancelled): 163 | CellEditEvent.__init__(self, olv_EVT_CELL_EDIT_FINISHING) 164 | self.SetParameters(objectListView, rowIndex, subItemIndex, rowModel, cellValue, editor) 165 | self.userCancelled = userCancelled 166 | 167 | def SetCellValue(self, value): 168 | """ 169 | If the event handler sets the cell value here, this value will be used to update the model 170 | object, rather than the value that was actually in the cell editor 171 | """ 172 | self.cellValue = value 173 | 174 | #---------------------------------------------------------------------------- 175 | 176 | class SortEvent(VetoableEvent): 177 | """ 178 | The user wants to sort the ObjectListView. 179 | 180 | When sortModelObjects is True, the event handler should sort the model objects used by 181 | the given ObjectListView. If the "modelObjects" instance variable is not None, that 182 | collection of objects should be sorted, otherwise the "modelObjects" collection of the 183 | ObjectListView should be sorted. For a VirtualObjectListView, "modelObjects" will 184 | always be None and the programmer must sort the object in whatever backing store is 185 | being used. 186 | 187 | When sortModelObjects is False, the event handler must sort the actual ListItems in 188 | the OLV. It does this by calling SortListItemsBy(), passing a callable that accepts 189 | two model objects as parameters. sortModelObjects must be True for a 190 | VirtualObjectListView (or a FastObjectListView) since virtual lists cannot sort items. 191 | 192 | If the handler calls Veto(), no further default processing will be done. 193 | If the handler calls Handled(), default processing concerned with UI will be done. This 194 | includes updating sort indicators. 195 | If the handler calls neither of these, all default processing will be done. 196 | """ 197 | def __init__(self, objectListView, sortColumnIndex, sortAscending, sortModelObjects, modelObjects=None): 198 | VetoableEvent.__init__(self, olv_EVT_SORT) 199 | self.objectListView = objectListView 200 | self.sortColumnIndex = sortColumnIndex 201 | self.sortAscending = sortAscending 202 | self.sortModelObjects = sortModelObjects 203 | self.modelObjects = modelObjects 204 | self.wasHandled = False 205 | 206 | def Handled(self, wasHandled=True): 207 | """ 208 | Indicate that the event handler has sorted the ObjectListView. 209 | The OLV will handle other tasks like updating sort indicators 210 | """ 211 | self.wasHandled = wasHandled 212 | 213 | #---------------------------------------------------------------------------- 214 | 215 | class GroupCreationEvent(wx.PyCommandEvent): 216 | """ 217 | The user is about to create one or more groups. 218 | 219 | The handler can mess with the list of groups before they are created: change their 220 | names, give them icons, remove them from the list to stop them being created 221 | (that last behaviour could be very confusing for the users). 222 | """ 223 | def __init__(self, objectListView, groups): 224 | wx.PyCommandEvent.__init__(self, olv_EVT_GROUP_CREATING, -1) 225 | self.objectListView = objectListView 226 | self.groups = groups 227 | 228 | #---------------------------------------------------------------------------- 229 | 230 | class ExpandCollapseEvent(VetoableEvent): 231 | """ 232 | The user wants to expand or collapse one or more groups, or has just done so. 233 | 234 | If the handler calls Veto() for a Expanding or Collapsing event, 235 | the expand/collapse action will be cancelled. 236 | 237 | Calling Veto() has no effect on a Expanded or Collapsed event 238 | """ 239 | def __init__(self, eventType, objectListView, groups, isExpand): 240 | VetoableEvent.__init__(self, eventType) 241 | self.objectListView = objectListView 242 | self.groups = groups 243 | self.isExpand = isExpand 244 | 245 | def ExpandingCollapsingEvent(objectListView, groups, isExpand): 246 | if isExpand: 247 | return ExpandCollapseEvent(olv_EVT_EXPANDING, objectListView, groups, True) 248 | else: 249 | return ExpandCollapseEvent(olv_EVT_COLLAPSING, objectListView, groups, False) 250 | 251 | def ExpandedCollapsedEvent(objectListView, groups, isExpand): 252 | if isExpand: 253 | return ExpandCollapseEvent(olv_EVT_EXPANDED, objectListView, groups, True) 254 | else: 255 | return ExpandCollapseEvent(olv_EVT_COLLAPSED, objectListView, groups, False) 256 | 257 | #---------------------------------------------------------------------------- 258 | 259 | class SortGroupsEvent(wx.PyCommandEvent): 260 | """ 261 | The given list of groups needs to be sorted. 262 | 263 | Both the groups themselves and the model objects within the group should be sorted. 264 | 265 | The handler should rearrange the list of groups in the order desired. 266 | """ 267 | def __init__(self, objectListView, groups, sortColumn, sortAscending): 268 | wx.PyCommandEvent.__init__(self, olv_EVT_GROUP_SORT, -1) 269 | self.objectListView = objectListView 270 | self.groups = groups 271 | self.sortColumn = sortColumn 272 | self.sortAscending = sortAscending 273 | self.wasHandled = False 274 | 275 | def Handled(self, wasHandled=True): 276 | """ 277 | Indicate that the event handler has sorted the groups. 278 | """ 279 | self.wasHandled = wasHandled 280 | -------------------------------------------------------------------------------- /v1/lib/ObjectListView/OLVPrinter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!/usr/bin/env python 3 | #---------------------------------------------------------------------------- 4 | # Name: OLVPrinter.py 5 | # Author: Phillip Piper 6 | # Created: 17 July 2008 7 | # SVN-ID: $Id$ 8 | # Copyright: (c) 2008 by Phillip Piper, 2008 9 | # License: wxWindows license 10 | #---------------------------------------------------------------------------- 11 | # Change log: 12 | # 2008/07/17 JPP Initial version 13 | #---------------------------------------------------------------------------- 14 | # To do: 15 | 16 | """ 17 | An OLVPrinter takes an ObjectListView and turns it into a pretty report. 18 | 19 | As always, the goal is for this to be as easy to use as possible. A typical 20 | usage should be as simple as:: 21 | 22 | printer = OLVPrinter(self.myOlv, "My Report Title") 23 | printer.PrintPreview() 24 | 25 | """ 26 | 27 | import wx 28 | 29 | from ObjectListView import GroupListView 30 | from WordWrapRenderer import WordWrapRenderer 31 | 32 | #====================================================================== 33 | 34 | class OLVPrinter(wx.Printout): 35 | """ 36 | An OLVPrinter creates a pretty report from an ObjectListView. 37 | """ 38 | 39 | def __init__(self, objectListView=None, title="ObjectListView Printing"): 40 | """ 41 | """ 42 | wx.Printout.__init__(self, title) 43 | self.engine = ReportEngine() 44 | 45 | self.printData = wx.PrintData() 46 | self.printData.SetPaperId(wx.PAPER_A4) 47 | self.printData.SetPrintMode(wx.PRINT_MODE_PRINTER) 48 | 49 | if objectListView is not None: 50 | self.engine.AddListCtrl(objectListView, title) 51 | 52 | #---------------------------------------------------------------------------- 53 | # Accessing 54 | 55 | def HasPage(self, page): 56 | print "HasPage(%d)" % page 57 | return page <= self.engine.GetTotalPages() 58 | 59 | def GetPageInfo(self): 60 | print "GetPageInfo" 61 | return (1, self.engine.GetTotalPages(), 1, 1) 62 | 63 | def GetReportFormat(self): 64 | """ 65 | Return the ReportFormat object that controls the appearance of this printout 66 | """ 67 | return self.engine.reportFormat 68 | 69 | def SetReportFormat(self, fmt): 70 | """ 71 | Set the ReportFormat object that controls the appearance of this printout 72 | """ 73 | self.engine.reportFormat = fmt 74 | 75 | ReportFormat = property(GetReportFormat, SetReportFormat) 76 | 77 | #---------------------------------------------------------------------------- 78 | # Commands 79 | 80 | def PageSetup(self): 81 | """ 82 | Show a Page Setup dialog that will change the configuration of this printout 83 | """ 84 | psdd = wx.PageSetupDialogData(self.printData) 85 | psdd.CalculatePaperSizeFromId() 86 | dlg = wx.PageSetupDialog(self, psdd) 87 | dlg.ShowModal() 88 | 89 | # this makes a copy of the wx.PrintData instead of just saving 90 | # a reference to the one inside the PrintDialogData that will 91 | # be destroyed when the dialog is destroyed 92 | self.printData = wx.PrintData(dlg.GetPageSetupData().GetPrintData()) 93 | 94 | dlg.Destroy() 95 | 96 | 97 | def PrintPreview(self, parent=None, title="ObjectListView Print Preview", bounds=(20, 50, 800, 800)): 98 | """ 99 | Show a Print Preview of this report 100 | """ 101 | data = wx.PrintDialogData(self.printData) 102 | #TODO: Implement some proper way to copy the printer 103 | #forPrinter = OLVPrinter() 104 | #forPrinter.ReportFormat = self.ReportFormat 105 | #forPrinter.engine.objectListViews = list(self.engine.objectListViews) 106 | self.preview = wx.PrintPreview(self, None, data) 107 | 108 | if not self.preview.Ok(): 109 | return False 110 | 111 | pfrm = wx.PreviewFrame(self.preview, parent, title) 112 | 113 | pfrm.Initialize() 114 | pfrm.SetPosition(bounds[0:2]) 115 | pfrm.SetSize(bounds[2:4]) 116 | pfrm.Show(True) 117 | 118 | return True 119 | 120 | 121 | def DoPrint(self, parent=None): 122 | """ 123 | Send the report to the configured printer 124 | """ 125 | pdd = wx.PrintDialogData(self.printData) 126 | printer = wx.Printer(pdd) 127 | 128 | if printer.Print(parent, self, True): 129 | self.printData = wx.PrintData(printer.GetPrintDialogData().GetPrintData()) 130 | else: 131 | wx.MessageBox("There was a problem printing.\nPerhaps your current printer is not set correctly?", "Printing", wx.OK) 132 | 133 | printout.Destroy() 134 | 135 | 136 | #---------------------------------------------------------------------------- 137 | # Event handlers 138 | 139 | def OnPreparePrinting(self): 140 | """ 141 | Prepare for printing. This event is sent before any of the others 142 | """ 143 | print "OnPreparePrinting" 144 | print "self.GetDC() = %s" % self.GetDC() 145 | self.engine.CalculateTotalPages(self.GetDC()) 146 | self.engine.StartPrinting() 147 | 148 | def OnBeginDocument(self, start, end): 149 | """ 150 | Begin printing one copy of the document. Return False to cancel the job 151 | """ 152 | print "OnBeginDocument(%d, %d)" % (start, end) 153 | if not super(OLVPrinter, self).OnBeginDocument(start, end): 154 | return False 155 | 156 | return True 157 | 158 | def OnEndDocument(self): 159 | print "OnEndDocument" 160 | super(OLVPrinter, self).OnEndDocument() 161 | 162 | def OnBeginPrinting(self): 163 | print "OnBeginPrinting" 164 | super(OLVPrinter, self).OnBeginPrinting() 165 | 166 | def OnEndPrinting(self): 167 | print "OnEndPrinting" 168 | super(OLVPrinter, self).OnEndPrinting() 169 | 170 | def OnPrintPage(self, page): 171 | print "OnPrintPage(%d)" % page 172 | return self.engine.PrintPage(self.GetDC(), page) 173 | 174 | 175 | #====================================================================== 176 | 177 | class ReportEngine(object): 178 | """ 179 | A ReportEngine handles all the work of actually producing a report. 180 | """ 181 | 182 | def __init__(self): 183 | """ 184 | """ 185 | self.currentPage = -1 186 | self.totalPages = -1 187 | self.blocks = list() 188 | self.blockInsertionIndex = 0 189 | self.objectListViews = list() 190 | 191 | self.reportFormat = ReportFormat() 192 | 193 | self.isColumnHeadingsOnEachPage = True 194 | self.alwaysCenterColumnHeader = True 195 | self.reportHeaderText = "Report Header Text" 196 | self.reportFooterText = "Report Footer Text" 197 | self.pageHeaderText = "This is the header" 198 | self.pageFooterText = "This is the footer" 199 | self.isPrintSelectionOnly = False 200 | self.isShrinkToFit = False 201 | self.canCellsWrap = True 202 | 203 | self.watermarkText = "WATERMARK" 204 | self.watermarkFont = None 205 | self.watermarkColor = None 206 | 207 | #---------------------------------------------------------------------------- 208 | # Accessing 209 | 210 | def GetNamedFormat(self, name): 211 | """ 212 | Return the given format 213 | """ 214 | return self.reportFormat.GetNamedFormat(name) 215 | 216 | 217 | def GetTotalPages(self): 218 | """ 219 | Return the total number of pages that this report will produce. 220 | 221 | CalculateTotalPages() must be called before this is accurate. 222 | """ 223 | return self.totalPages 224 | 225 | #---------------------------------------------------------------------------- 226 | # Calculating 227 | 228 | def CalculateTotalPages(self, dc): 229 | """ 230 | Do the work of calculating how many pages this report will occupy? 231 | 232 | This is expensive because it basically prints the whole report. 233 | """ 234 | self.StartPrinting() 235 | self.totalPages = 1 236 | while self.PrintOnePage(dc, self.totalPages): 237 | self.totalPages += 1 238 | dc.Clear() 239 | 240 | 241 | def CalculateBounds(self, dc): 242 | """ 243 | Calculate our page and work bounds 244 | """ 245 | self.pageBounds = (0, 0) + dc.GetSizeTuple() 246 | self.workBounds = list(self.pageBounds) 247 | 248 | #---------------------------------------------------------------------------- 249 | # Commands 250 | 251 | def AddBlock(self, block): 252 | """ 253 | Add the given block at the current insertion point 254 | """ 255 | self.blocks.insert(self.blockInsertionIndex, block) 256 | self.blockInsertionIndex += 1 257 | block.engine = self 258 | 259 | 260 | def AddListCtrl(self, objectListView, title=None): 261 | """ 262 | Add the given list to those that will be printed by this report. 263 | """ 264 | if objectListView.InReportView(): 265 | self.objectListViews.append([objectListView, title]) 266 | 267 | 268 | def DropCurrentBlock(self): 269 | """ 270 | Remove the current block from our list of blocks 271 | """ 272 | self.blocks.pop(0) 273 | self.blockInsertionIndex = 1 274 | 275 | #---------------------------------------------------------------------------- 276 | # Printing 277 | 278 | def StartPrinting(self): 279 | """ 280 | Initial a print job on this engine 281 | """ 282 | self.currentPage = 0 283 | self.blockInsertionIndex = 0 284 | self.blocks = list() 285 | self.AddBlock(ReportBlock()) 286 | self.runningBlocks = list() 287 | self.AddRunningBlock(PageHeaderBlock(self)) 288 | self.AddRunningBlock(PageFooterBlock(self)) 289 | 290 | def AddRunningBlock(self, block): 291 | """ 292 | A running block is printed on every page until it is removed 293 | """ 294 | self.runningBlocks.append(block) 295 | block.engine = self 296 | 297 | def RemoveRunningBlock(self, block): 298 | """ 299 | A running block is printed on every page until it is removed 300 | """ 301 | self.runningBlocks.remove(block) 302 | 303 | 304 | def PrintPage(self, dc, pageNumber): 305 | """ 306 | Print the given page on the given device context. 307 | """ 308 | #try: 309 | # pdc = wx.GCDC(dc) 310 | #except: 311 | # pdc = dc 312 | pdc = dc 313 | 314 | # If the request page isn't next in order, we have to restart 315 | # the printing process and advance until we reach the desired page 316 | if pageNumber != self.currentPage + 1: 317 | print "Skipping pages..." 318 | self.StartPrinting() 319 | for i in range(1, pageNumber): 320 | self.PrintOnePage(pdc, i) 321 | dc.Clear() 322 | print "...finished skipping." 323 | 324 | return self.PrintOnePage(pdc, pageNumber) 325 | 326 | 327 | def PrintOnePage(self, dc, pageNumber): 328 | """ 329 | Print the current page on the given device context. 330 | 331 | Return true if there is still more to print. 332 | """ 333 | self.currentPage = pageNumber 334 | self.CalculateBounds(dc) 335 | self.ApplyPageDecorations(dc) 336 | 337 | for x in self.runningBlocks: 338 | x.Print(dc) 339 | 340 | while len(self.blocks) and self.blocks[0].Print(dc): 341 | self.DropCurrentBlock() 342 | 343 | return len(self.blocks) > 0 344 | 345 | 346 | def ApplyPageDecorations(self, dc): 347 | """ 348 | """ 349 | fmt = self.GetNamedFormat("Page") 350 | 351 | # Draw the page decorations 352 | bounds = list(self.pageBounds) 353 | fmt.DrawDecorations(dc, bounds, self) 354 | 355 | # Subtract the area used from the work area 356 | self.workBounds = fmt.SubtractDecorations(dc, self.workBounds) 357 | 358 | 359 | #====================================================================== 360 | 361 | class ReportFormat(object): 362 | """ 363 | A ReportFormat defines completely how a report is formatted. 364 | 365 | It holds a collection of BlockFormat objects which control the 366 | formatting of individual blocks of the report 367 | 368 | """ 369 | 370 | def __init__(self): 371 | """ 372 | """ 373 | self.formats = [ 374 | "Page", 375 | "ReportHeader", 376 | "PageHeader", 377 | "ListHeader", 378 | "GroupTitle", 379 | "List", 380 | "ColumnHeader", 381 | "ListRows", 382 | "Row", 383 | "ListFooter", 384 | "PageFooter", 385 | "ReportFooter" 386 | ] 387 | for x in self.formats: 388 | setattr(self, x, BlockFormat()) 389 | 390 | def GetNamedFormat(self, name): 391 | """ 392 | Return the format used in to format a block with the given name. 393 | """ 394 | return getattr(self, name) 395 | 396 | @staticmethod 397 | def Normal(fontName="Arial"): 398 | """ 399 | Return a reasonable default format for a report 400 | """ 401 | fmt = ReportFormat() 402 | fmt.PageHeader.Font = wx.FFont(24, wx.FONTFAMILY_DEFAULT, face=fontName) 403 | fmt.PageHeader.TextAlignment = wx.ALIGN_CENTRE 404 | fmt.PageHeader.Add(FrameDecoration(pen=wx.Pen(wx.BLUE, 1), space=5)) 405 | #fmt.PageHeader.Add(LineDecoration(pen=wx.Pen(wx.BLUE, 2), space=5)) 406 | 407 | fmt.ReportHeader.Font = wx.FFont(36, wx.FONTFAMILY_DEFAULT, face=fontName) 408 | fmt.ReportHeader.TextColor = wx.RED 409 | fmt.ReportHeader.Padding = (0, 12, 0, 12) 410 | 411 | fmt.ListHeader.Add(LineDecoration(side=Decoration.BOTTOM, pen=wx.Pen(wx.GREEN, 1))) 412 | 413 | fmt.PageFooter.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=fontName) 414 | fmt.PageFooter.TextAlignment = wx.ALIGN_RIGHT 415 | fmt.PageFooter.Add(LineDecoration(side=Decoration.TOP, pen=wx.Pen(wx.BLUE, 1), space=3)) 416 | 417 | fmt.Row.Font = wx.FFont(12, wx.FONTFAMILY_DEFAULT, face=fontName) 418 | #fmt.ColumnHeader.CellPadding=25 419 | fmt.ColumnHeader.GridPen=wx.Pen(wx.RED, 1) 420 | fmt.Row.CellPadding=(10, 10, 0, 10) 421 | fmt.Row.GridPen=wx.Pen(wx.BLUE, 1) 422 | #fmt.ColumnHeader.Add(FrameDecoration(pen=wx.Pen(wx.RED, 1))) 423 | #fmt.Row.Add(FrameDecoration(pen=wx.Pen(wx.RED, 10))) 424 | #fmt.Row.Add(LineDecoration(side=Decoration.BOTTOM, pen=wx.Pen(wx.GREEN, 1))) 425 | 426 | return fmt 427 | 428 | #====================================================================== 429 | 430 | class BlockFormat(object): 431 | """ 432 | A block format defines how a Block is formatted. 433 | 434 | """ 435 | 436 | def __init__(self): 437 | """ 438 | """ 439 | self.padding = None 440 | self.decorations = list() 441 | self.font = wx.FFont(14, wx.FONTFAMILY_SWISS, face="Gill Sans") 442 | self.textColor = None 443 | self.textAlignment = wx.ALIGN_LEFT 444 | self.cellPadding = None 445 | self.gridPen = None 446 | 447 | #---------------------------------------------------------------------------- 448 | # Accessing 449 | 450 | def GetFont(self): 451 | """ 452 | Return the font used by this format 453 | """ 454 | return self.font 455 | 456 | def SetFont(self, font): 457 | """ 458 | Set the font used by this format 459 | """ 460 | self.font = font 461 | 462 | def GetTextAlignment(self): 463 | """ 464 | Return the alignment of text in this format 465 | """ 466 | return self.textAlignment 467 | 468 | def SetTextAlignment(self, alignment): 469 | """ 470 | Set the alignment of text in this format 471 | """ 472 | self.textAlignment = alignment 473 | 474 | def GetTextColor(self): 475 | """ 476 | Return the color of text in this format 477 | """ 478 | return self.textColor 479 | 480 | def SetTextColor(self, color): 481 | """ 482 | Set the color of text in this format 483 | """ 484 | self.textColor = color 485 | 486 | def GetPadding(self): 487 | """ 488 | Get the padding around this format 489 | """ 490 | return self.padding 491 | 492 | def SetPadding(self, padding): 493 | """ 494 | Set the padding around this format 495 | 496 | Padding is either a single numeric (indicating the values on all sides) 497 | or a collection of paddings [left, top, right, bottom] 498 | """ 499 | self.padding = self._MakePadding(padding) 500 | 501 | def GetCellPadding(self): 502 | """ 503 | Get the padding around cells in this format 504 | """ 505 | return self.cellPadding 506 | 507 | def SetCellPadding(self, padding): 508 | """ 509 | Set the padding around cells in this format 510 | 511 | Padding is either a single numeric (indicating the values on all sides) 512 | or a collection of paddings [left, top, right, bottom] 513 | """ 514 | self.cellPadding = self._MakePadding(padding) 515 | 516 | def GetGridPen(self): 517 | """ 518 | Return the pen used to draw a grid in this format 519 | """ 520 | return self.gridPen 521 | 522 | def SetGridPen(self, pen): 523 | """ 524 | Set the pen used to draw a grid in this format 525 | """ 526 | self.gridPen = pen 527 | if self.gridPen: 528 | # Other styles don't produce nice joins 529 | self.gridPen.SetCap(wx.CAP_BUTT) 530 | self.gridPen.SetJoin(wx.JOIN_MITER) 531 | 532 | def _MakePadding(self, padding): 533 | try: 534 | if len(padding) < 4: 535 | return (tuple(padding) + (0, 0, 0, 0))[:4] 536 | else: 537 | return padding 538 | except TypeError: 539 | return (padding,) * 4 540 | 541 | Font = property(GetFont, SetFont) 542 | Padding = property(GetPadding, SetPadding) 543 | TextAlignment = property(GetTextAlignment, SetTextAlignment) 544 | TextColor = property(GetTextColor, SetTextColor) 545 | TextColour = property(GetTextColor, SetTextColor) 546 | CellPadding = property(GetCellPadding, SetCellPadding) 547 | GridPen = property(GetGridPen, SetGridPen) 548 | 549 | #---------------------------------------------------------------------------- 550 | # Decorations 551 | 552 | def Add(self, decoration): 553 | """ 554 | Add the given decoration to those adorning blocks with this format 555 | """ 556 | self.decorations.append(decoration) 557 | 558 | #---------------------------------------------------------------------------- 559 | # Commands 560 | 561 | def SubtractPadding(self, bounds): 562 | """ 563 | Subtract any padding used by this format from the given bounds 564 | """ 565 | if self.padding is None: 566 | return bounds 567 | else: 568 | return RectUtils.InsetRect(bounds, self.padding) 569 | 570 | 571 | def SubtractCellPadding(self, bounds): 572 | """ 573 | Subtract any cell padding used by this format from the given bounds 574 | """ 575 | if self.cellPadding is None: 576 | return bounds 577 | else: 578 | return RectUtils.InsetRect(bounds, self.cellPadding) 579 | 580 | 581 | def SubtractDecorations(self, dc, bounds): 582 | """ 583 | Subtract any space used by our decorations from the given bounds 584 | """ 585 | for x in self.decorations: 586 | bounds = x.SubtractFrom(dc, bounds) 587 | return bounds 588 | 589 | 590 | def DrawDecorations(self, dc, bounds, block): 591 | """ 592 | Draw our decorations on the given block 593 | """ 594 | for x in self.decorations: 595 | x.DrawDecoration(dc, bounds, block) 596 | 597 | 598 | #====================================================================== 599 | 600 | class Block(object): 601 | """ 602 | A Block is a portion of a report that will stack vertically with other 603 | Block. A report consists of several Blocks. 604 | """ 605 | 606 | def __init__(self, engine=None): 607 | self.engine = engine # This is also set when the block is added to a print engine 608 | 609 | #---------------------------------------------------------------------------- 610 | # Accessing 611 | 612 | def GetFont(self): 613 | """ 614 | Return Font that should be used to draw text in this block 615 | """ 616 | return self.GetFormat().GetFont() 617 | 618 | 619 | def GetTextColor(self): 620 | """ 621 | Return Colour that should be used to draw text in this block 622 | """ 623 | return self.GetFormat().GetTextColor() 624 | 625 | 626 | def GetFormat(self): 627 | """ 628 | Return the BlockFormat object that controls the formatting of this block 629 | """ 630 | return self.engine.GetNamedFormat(self.__class__.__name__[:-5]) 631 | 632 | 633 | def GetReducedBlockBounds(self, dc, bounds=None): 634 | """ 635 | Return the bounds of this block once padding and decoration have taken their toll. 636 | """ 637 | bounds = bounds or list(self.GetWorkBounds()) 638 | fmt = self.GetFormat() 639 | if fmt: 640 | bounds = fmt.SubtractPadding(bounds) 641 | bounds = fmt.SubtractDecorations(dc, bounds) 642 | return bounds 643 | 644 | 645 | def GetWorkBounds(self): 646 | """ 647 | Return the boundaries of the work area for this block 648 | """ 649 | return self.engine.workBounds 650 | 651 | 652 | def ChangeWorkBoundsTopBy(self, amt): 653 | """ 654 | Move the top of our work bounds down by the given amount 655 | """ 656 | RectUtils.MoveTopBy(self.engine.workBounds, amt) 657 | 658 | #---------------------------------------------------------------------------- 659 | # Calculating 660 | 661 | def CalculateExtrasHeight(self, dc): 662 | """ 663 | Return the height of the padding and decorations themselves 664 | """ 665 | return 0 - RectUtils.Height(self.GetReducedBlockBounds(dc, [0, 0, 0, 0])) 666 | 667 | 668 | def CalculateHeight(self, dc): 669 | """ 670 | Return the heights of this block in pixels 671 | """ 672 | return -1 673 | 674 | 675 | def CalculateTextHeight(self, dc, txt, bounds=None, font=None): 676 | """ 677 | Return the height of the given txt in pixels 678 | """ 679 | bounds = bounds or self.GetReducedBlockBounds(dc) 680 | font = font or self.GetFont() 681 | dc.SetFont(font) 682 | return WordWrapRenderer.CalculateHeight(dc, txt, RectUtils.Width(bounds)) 683 | 684 | 685 | def CanFit(self, height): 686 | """ 687 | Can this block fit into the remaining work area on the page? 688 | """ 689 | return height <= RectUtils.Height(self.GetWorkBounds()) 690 | 691 | #---------------------------------------------------------------------------- 692 | # Commands 693 | 694 | def Print(self, dc): 695 | """ 696 | Print this Block. 697 | 698 | Return True if the Block has finished printing 699 | """ 700 | # If this block does not have a format, it is simply skipped 701 | if not self.GetFormat(): 702 | return True 703 | 704 | height = self.CalculateHeight(dc) 705 | if not self.CanFit(height): 706 | return False 707 | 708 | bounds = self.GetWorkBounds() 709 | bounds = RectUtils.SetHeight(list(bounds), height) 710 | self.Draw(dc, bounds) 711 | self.ChangeWorkBoundsTopBy(height) 712 | return True 713 | 714 | 715 | def Draw(self, dc, bounds): 716 | """ 717 | Draw this block and its decorations allowing for any padding 718 | """ 719 | fmt = self.GetFormat() 720 | bounds = fmt.SubtractPadding(bounds) 721 | fmt.DrawDecorations(dc, bounds, self) 722 | bounds = fmt.SubtractDecorations(dc, bounds) 723 | self.DrawSelf(dc, bounds) 724 | 725 | 726 | def DrawSelf(self, dc, bounds): 727 | """ 728 | Do the actual work of rendering this block. 729 | """ 730 | pass 731 | 732 | 733 | def DrawText(self, dc, txt, bounds, font=None, alignment=wx.ALIGN_LEFT, image=None, color=None): 734 | """ 735 | """ 736 | dc.SetFont(font or self.GetFont()) 737 | dc.SetTextForeground(color or self.GetTextColor() or wx.BLACK) 738 | WordWrapRenderer.DrawString(dc, txt, bounds, alignment, allowClipping=False) 739 | 740 | 741 | 742 | #====================================================================== 743 | 744 | class TextBlock(Block): 745 | """ 746 | A TextBlock prints a string objects. 747 | """ 748 | 749 | def GetText(self): 750 | return "Missing GetText() in class %s" % self.__class__.__name__ 751 | 752 | def CalculateHeight(self, dc): 753 | """ 754 | Return the heights of this block in pixels 755 | """ 756 | textHeight = self.CalculateTextHeight(dc, self.GetText()) 757 | extras = self.CalculateExtrasHeight(dc) 758 | return textHeight + extras 759 | 760 | def DrawSelf(self, dc, bounds): 761 | """ 762 | Do the actual work of rendering this block. 763 | """ 764 | self.DrawText(dc, self.GetText(), bounds, alignment=self.GetFormat().TextAlignment) 765 | 766 | 767 | #====================================================================== 768 | 769 | class CellBlock(Block): 770 | """ 771 | A CellBlock is a Block whose data is presented in a cell format. 772 | """ 773 | 774 | #---------------------------------------------------------------------------- 775 | # Accessing - Subclasses should override 776 | 777 | def GetCellWidths(self): 778 | """ 779 | Return a list of the widths of the cells in this block 780 | """ 781 | return list() 782 | 783 | def GetTexts(self): 784 | """ 785 | Return a list of the texts that should be drawn with the cells 786 | """ 787 | return list() 788 | 789 | def GetAlignments(self): 790 | """ 791 | Return a list indicating how the text within each cell is aligned. 792 | """ 793 | return list() 794 | 795 | def GetImages(self): 796 | """ 797 | Return a list of the images that should be drawn in each cell 798 | """ 799 | return list() 800 | 801 | 802 | #---------------------------------------------------------------------------- 803 | # Accessing 804 | 805 | def CanCellsWrap(self): 806 | """ 807 | Return True if the text values can wrap within a cell, producing muliline cells 808 | """ 809 | return self.engine.canCellsWrap 810 | 811 | def GetCombinedLists(self): 812 | """ 813 | Return a collection of Buckets that hold all the values of the 814 | subclass-overridable collections above 815 | """ 816 | buckets = [Bucket(cellWidth=x, text="", align=None, image=None) for x in self.GetCellWidths()] 817 | for (i, x) in enumerate(self.GetTexts()): 818 | buckets[i].text = x 819 | for (i, x) in enumerate(self.GetImages()): 820 | buckets[i].image = x 821 | for (i, x) in enumerate(self.GetAlignments()): 822 | buckets[i].align = x 823 | 824 | return buckets 825 | 826 | #---------------------------------------------------------------------------- 827 | # Utiltities 828 | 829 | def GetColumnAlignments(self, olv, left, right): 830 | """ 831 | Return the alignments of the given slice of columns 832 | """ 833 | listAlignments = [olv.GetColumn(i).GetAlign() for i in range(left, right+1)] 834 | mapping = { 835 | wx.LIST_FORMAT_LEFT: wx.ALIGN_LEFT, 836 | wx.LIST_FORMAT_RIGHT: wx.ALIGN_RIGHT, 837 | wx.LIST_FORMAT_CENTRE: wx.ALIGN_CENTRE, 838 | } 839 | return [mapping[x] for x in listAlignments] 840 | 841 | 842 | def GetColumnWidths(self, olv, left, right): 843 | """ 844 | Return a list of the widths of the given slice of columns 845 | """ 846 | return [olv.GetColumnWidth(i) for i in range(left, right+1)] 847 | 848 | #---------------------------------------------------------------------------- 849 | # Calculating 850 | 851 | def CalculateHeight(self, dc): 852 | """ 853 | Return the heights of this block in pixels 854 | """ 855 | # If cells can wrap, figure out the tallest, otherwise we just figure out the height of one line 856 | if self.CanCellsWrap(): 857 | font = self.GetFont() 858 | height = 0 859 | for x in self.GetCombinedLists(): 860 | bounds = [0, 0, x.cellWidth, 99999] 861 | height = max(height, self.CalculateTextHeight(dc, x.text, bounds, font)) 862 | else: 863 | height = self.CalculateTextHeight(dc, "Wy") 864 | 865 | cellPadding = self._CalculateCellPadding(self.GetFormat()) 866 | return height + cellPadding[1] + cellPadding[3] + self.CalculateExtrasHeight(dc) 867 | 868 | def _CalculateCellPadding(self, cellFmt): 869 | if cellFmt.CellPadding: 870 | cellPadding = list(cellFmt.CellPadding) 871 | else: 872 | cellPadding = 0, 0, 0, 0 873 | 874 | if cellFmt.GridPen: 875 | penFactor = int((cellFmt.GridPen.GetWidth()+1)/2) 876 | cellPadding = [x + penFactor for x in cellPadding] 877 | 878 | return cellPadding 879 | 880 | #---------------------------------------------------------------------------- 881 | # Commands 882 | 883 | def DrawSelf(self, dc, bounds): 884 | """ 885 | Do the actual work of rendering this block. 886 | """ 887 | cellFmt = self.GetFormat() 888 | cellPadding = self._CalculateCellPadding(cellFmt) 889 | combined = self.GetCombinedLists() 890 | 891 | # Calculate cell boundaries 892 | cell = list(bounds) 893 | cell[2] = 0 894 | for x in combined: 895 | RectUtils.SetLeft(cell, RectUtils.Right(cell)) 896 | RectUtils.SetWidth(cell, x.cellWidth + cellPadding[0] + cellPadding[2]) 897 | x.cell = list(cell) 898 | #dc.SetPen(wx.BLACK_PEN) 899 | #dc.DrawRectangle(*cell) 900 | 901 | # Draw each cell 902 | font = self.GetFont() 903 | for x in combined: 904 | cellBounds = RectUtils.InsetRect(x.cell, cellPadding) 905 | self.DrawText(dc, x.text, cellBounds, font, x.align, x.image) 906 | #dc.SetPen(wx.RED_PEN) 907 | #dc.DrawRectangle(*cellBounds) 908 | 909 | if cellFmt.GridPen and combined: 910 | dc.SetPen(cellFmt.GridPen) 911 | dc.SetBrush(wx.TRANSPARENT_BRUSH) 912 | 913 | top = RectUtils.Top(combined[0].cell) 914 | bottom = RectUtils.Bottom(combined[0].cell) 915 | 916 | # Draw the interior dividers 917 | for x in combined[:-1]: 918 | right = RectUtils.Right(x.cell) 919 | dc.DrawLine(right, top, right, bottom) 920 | 921 | # Draw the surrounding frame 922 | left = RectUtils.Left(combined[0].cell) 923 | right = RectUtils.Right(combined[-1].cell) 924 | dc.DrawRectangle(left, top, right-left, bottom-top) 925 | 926 | 927 | #====================================================================== 928 | 929 | class ReportBlock(Block): 930 | """ 931 | A ReportBlock is boot strap Block that represents an entire report. 932 | """ 933 | 934 | #---------------------------------------------------------------------------- 935 | # Commands 936 | 937 | def Print(self, dc): 938 | """ 939 | Print this Block. 940 | 941 | Return True if the Block has finished printing 942 | """ 943 | self.engine.AddBlock(ReportHeaderBlock()) 944 | 945 | # Print each ListView. Each list but the first starts on a separate page 946 | first = True 947 | for (olv, title) in self.engine.objectListViews: 948 | if not first: 949 | self.engine.AddBlock(PageBreakBlock()) 950 | self.engine.AddBlock(ListBlock(olv, title)) 951 | first = False 952 | 953 | self.engine.AddBlock(ReportFooterBlock()) 954 | return True 955 | 956 | #====================================================================== 957 | 958 | class ReportHeaderBlock(TextBlock): 959 | """ 960 | A ReportHeader is a text message that appears at the very beginning 961 | of a report. 962 | """ 963 | 964 | def GetText(self): 965 | return self.engine.reportHeaderText 966 | 967 | #====================================================================== 968 | 969 | class ReportFooterBlock(TextBlock): 970 | """ 971 | A ReportFooter is a text message that appears at the very end of a report. 972 | """ 973 | 974 | def GetText(self): 975 | return self.engine.reportFooterText 976 | 977 | 978 | #====================================================================== 979 | 980 | class PageHeaderBlock(TextBlock): 981 | """ 982 | A PageHeaderBlock appears at the top of every page. 983 | """ 984 | 985 | def GetText(self): 986 | return self.engine.pageHeaderText 987 | 988 | 989 | 990 | #====================================================================== 991 | 992 | class PageFooterBlock(TextBlock): 993 | """ 994 | A PageFooterBlock appears at the bottom of every page. 995 | """ 996 | 997 | def GetText(self): 998 | return self.engine.pageFooterText 999 | 1000 | 1001 | #---------------------------------------------------------------------------- 1002 | # Printing 1003 | 1004 | 1005 | def Print(self, dc): 1006 | """ 1007 | Print this block. 1008 | """ 1009 | height = self.CalculateHeight(dc) 1010 | 1011 | # Draw the footer at the bottom of the page 1012 | bounds = self.GetWorkBounds() 1013 | bounds = [RectUtils.Left(bounds), RectUtils.Bottom(bounds) - height, 1014 | RectUtils.Width(bounds), height] 1015 | self.Draw(dc, bounds) 1016 | 1017 | # The footer changes the bottom of the work area 1018 | RectUtils.MoveBottomBy(self.engine.workBounds, height) 1019 | return True 1020 | 1021 | 1022 | #====================================================================== 1023 | 1024 | class PageBreakBlock(Block): 1025 | """ 1026 | A PageBreakBlock acts a page break. 1027 | """ 1028 | 1029 | #---------------------------------------------------------------------------- 1030 | # Commands 1031 | 1032 | def Print(self, dc): 1033 | """ 1034 | Print this Block. 1035 | 1036 | Return True if the Block has finished printing 1037 | """ 1038 | 1039 | # Completely fill the remaining area on the page, forcing a page break 1040 | bounds = self.GetWorkBounds() 1041 | self.ChangeWorkBoundsTopBy(RectUtils.Height(bounds)) 1042 | 1043 | return True 1044 | 1045 | #====================================================================== 1046 | 1047 | class ListBlock(Block): 1048 | """ 1049 | A ListBlock handles the printing of an entire ObjectListView. 1050 | """ 1051 | 1052 | def __init__(self, olv, title): 1053 | self.olv = olv 1054 | self.title = title 1055 | 1056 | #---------------------------------------------------------------------------- 1057 | # Commands 1058 | 1059 | def Print(self, dc): 1060 | """ 1061 | Print this Block. 1062 | 1063 | Return True if the Block has finished printing 1064 | """ 1065 | 1066 | # Break the list into vertical slices. Each one but the first 1067 | # will be placed on a new page. 1068 | first = True 1069 | for (left, right) in self.CalculateListSlices(): 1070 | if not first: 1071 | self.engine.AddBlock(PageBreakBlock()) 1072 | self.engine.AddBlock(ListHeaderBlock(self.olv, self.title)) 1073 | self.engine.AddBlock(ListSliceBlock(self.olv, left, right)) 1074 | self.engine.AddBlock(ListFooterBlock(self.olv, "")) 1075 | first = False 1076 | 1077 | return True 1078 | 1079 | def CalculateListSlices(self): 1080 | """ 1081 | Return a list of integer pairs, where each pair represents 1082 | the left and right columns that can fit into the width of one page 1083 | """ 1084 | boundsWidth = RectUtils.Width(self.GetWorkBounds()) 1085 | columnWidths = [self.olv.GetColumnWidth(i) for i in range(self.olv.GetColumnCount())] 1086 | return self.CalculateSlices(boundsWidth, columnWidths) 1087 | 1088 | def CalculateSlices(self, maxWidth, columnWidths): 1089 | """ 1090 | Return a list of integer pairs, where each pair represents 1091 | the left and right columns that can fit into the width of one page 1092 | """ 1093 | firstColumn = 0 1094 | 1095 | # If a GroupListView has a column just for the expand/collapse, don't include it 1096 | if hasattr(self.olv, "useExpansionColumn") and self.olv.useExpansionColumn: 1097 | firstColumn = 1 1098 | 1099 | # If we are shrinking to fit or all the columns fit, just return all columns 1100 | if self.engine.isShrinkToFit or (sum(columnWidths)) <= maxWidth: 1101 | return [ [firstColumn, len(columnWidths)-1] ] 1102 | 1103 | pairs = list() 1104 | left = firstColumn 1105 | right = firstColumn 1106 | while right < len(columnWidths): 1107 | if (sum(columnWidths[left:right+1])) > maxWidth: 1108 | if left == right: 1109 | pairs.append([left, right]) 1110 | left += 1 1111 | right += 1 1112 | else: 1113 | pairs.append([left, right-1]) 1114 | left = right 1115 | else: 1116 | right += 1 1117 | 1118 | if left < len(columnWidths): 1119 | pairs.append([left, right-1]) 1120 | 1121 | return pairs 1122 | 1123 | 1124 | #====================================================================== 1125 | 1126 | class ListHeaderBlock(TextBlock): 1127 | """ 1128 | A ListHeaderBlock is the title that comes before an ObjectListView. 1129 | """ 1130 | 1131 | def __init__(self, olv, title): 1132 | self.olv = olv 1133 | self.title = title 1134 | 1135 | def GetText(self): 1136 | return self.title 1137 | 1138 | #====================================================================== 1139 | 1140 | class ListFooterBlock(TextBlock): 1141 | """ 1142 | A ListFooterBlock is the text that comes before an ObjectListView. 1143 | """ 1144 | 1145 | def __init__(self, olv, text): 1146 | self.olv = olv 1147 | self.text = text 1148 | 1149 | def GetText(self): 1150 | return self.text 1151 | 1152 | 1153 | #====================================================================== 1154 | 1155 | class GroupTitleBlock(TextBlock): 1156 | """ 1157 | A GroupTitleBlock is the title that comes before a list group. 1158 | """ 1159 | 1160 | def __init__(self, olv, group): 1161 | self.olv = olv 1162 | self.group = group 1163 | 1164 | def GetText(self): 1165 | return self.group.title 1166 | 1167 | #====================================================================== 1168 | 1169 | class ListSliceBlock(Block): 1170 | """ 1171 | A ListSliceBlock prints a vertical slice of an ObjectListView. 1172 | """ 1173 | 1174 | def __init__(self, olv, left, right): 1175 | self.olv = olv 1176 | self.left = left 1177 | self.right = right 1178 | 1179 | 1180 | #---------------------------------------------------------------------------- 1181 | # Commands 1182 | 1183 | def Print(self, dc): 1184 | """ 1185 | Print this Block. 1186 | 1187 | Return True if the Block has finished printing 1188 | """ 1189 | self.engine.AddBlock(ColumnHeaderBlock(self.olv, self.left, self.right)) 1190 | if hasattr(self.olv, "GetShowGroups") and self.olv.GetShowGroups(): 1191 | self.engine.AddBlock(GroupListRowsBlock(self.olv, self.left, self.right)) 1192 | else: 1193 | self.engine.AddBlock(ListRowsBlock(self.olv, self.left, self.right)) 1194 | return True 1195 | 1196 | 1197 | #====================================================================== 1198 | 1199 | class ColumnHeaderBlock(CellBlock): 1200 | """ 1201 | A ColumnHeaderBlock prints a portion of the columns header in 1202 | an ObjectListView. 1203 | """ 1204 | 1205 | def __init__(self, olv, left, right): 1206 | self.olv = olv 1207 | self.left = left 1208 | self.right = right 1209 | 1210 | #---------------------------------------------------------------------------- 1211 | # Accessing - Subclasses should override 1212 | 1213 | def GetCellWidths(self): 1214 | """ 1215 | Return a list of the widths of the cells in this block 1216 | """ 1217 | return self.GetColumnWidths(self.olv, self.left, self.right) 1218 | 1219 | def GetTexts(self): 1220 | """ 1221 | Return a list of the texts that should be drawn with the cells 1222 | """ 1223 | return [self.olv.GetColumn(i).GetText() for i in range(self.left, self.right+1)] 1224 | 1225 | def GetAlignments(self): 1226 | """ 1227 | Return a list indicating how the text within each cell is aligned. 1228 | """ 1229 | if self.engine.alwaysCenterColumnHeader: 1230 | return [wx.ALIGN_CENTRE for i in range(self.left, self.right+1)] 1231 | else: 1232 | return self.GetColumnAlignments(olv, self.left, self.right) 1233 | 1234 | def GetImages(self): 1235 | """ 1236 | Return a list of the images that should be drawn in each cell 1237 | """ 1238 | return [self.olv.GetColumn(i).GetImage() for i in range(self.left, self.right+1)] 1239 | 1240 | 1241 | #====================================================================== 1242 | 1243 | class ListRowsBlock(Block): 1244 | """ 1245 | A ListRowsBlock prints rows of an ObjectListView. 1246 | """ 1247 | 1248 | def __init__(self, olv, left, right): 1249 | """ 1250 | """ 1251 | self.olv = olv 1252 | self.left = left 1253 | self.right = right 1254 | self.currentIndex = 0 1255 | self.totalRows = self.olv.GetItemCount() 1256 | 1257 | #---------------------------------------------------------------------------- 1258 | # Commands 1259 | 1260 | def Print(self, dc): 1261 | """ 1262 | Print this Block. 1263 | 1264 | Return True if the Block has finished printing 1265 | """ 1266 | # This block works by printing a single row and then rescheduling itself 1267 | # to print the remaining rows after the current row has finished. 1268 | 1269 | if self.currentIndex < self.totalRows: 1270 | self.engine.AddBlock(RowBlock(self.olv, self.currentIndex, self.left, self.right)) 1271 | self.currentIndex += 1 1272 | self.engine.AddBlock(self) 1273 | 1274 | return True 1275 | 1276 | #====================================================================== 1277 | 1278 | class GroupListRowsBlock(Block): 1279 | """ 1280 | A GroupListRowsBlock prints rows of an GroupListView. 1281 | """ 1282 | 1283 | def __init__(self, olv, left, right): 1284 | """ 1285 | """ 1286 | self.olv = olv # Must be a GroupListView 1287 | self.left = left 1288 | self.right = right 1289 | 1290 | self.currentIndex = 0 1291 | self.totalRows = self.olv.GetItemCount() 1292 | 1293 | #---------------------------------------------------------------------------- 1294 | # Commands 1295 | 1296 | def Print(self, dc): 1297 | """ 1298 | Print this Block. 1299 | 1300 | Return True if the Block has finished printing 1301 | """ 1302 | # This block works by printing a single row and then rescheduling itself 1303 | # to print the remaining rows after the current row has finished. 1304 | 1305 | if self.currentIndex >= self.totalRows: 1306 | return True 1307 | 1308 | # If GetObjectAt() return an object, then it's a normal row. 1309 | # Otherwise, if the innerList object isn't None, it must be a ListGroup 1310 | # We can't use isinstance(x, GroupListView) because ObjectListView may not be installed 1311 | if self.olv.GetObjectAt(self.currentIndex): 1312 | self.engine.AddBlock(RowBlock(self.olv, self.currentIndex, self.left, self.right)) 1313 | elif self.olv.innerList[self.currentIndex]: 1314 | self.engine.AddBlock(GroupTitleBlock(self.olv, self.olv.innerList[self.currentIndex])) 1315 | 1316 | # Schedule the printing of the remaining rows 1317 | self.currentIndex += 1 1318 | self.engine.AddBlock(self) 1319 | 1320 | return True 1321 | 1322 | 1323 | #====================================================================== 1324 | 1325 | class RowBlock(CellBlock): 1326 | """ 1327 | A RowBlock prints a vertical slice of a single row of an ObjectListView. 1328 | """ 1329 | 1330 | def __init__(self, olv, rowIndex, left, right): 1331 | self.olv = olv 1332 | self.rowIndex = rowIndex 1333 | self.left = left 1334 | self.right = right 1335 | 1336 | 1337 | def GetCellWidths(self): 1338 | """ 1339 | Return a list of the widths of the cells in this block 1340 | """ 1341 | return self.GetColumnWidths(self.olv, self.left, self.right) 1342 | 1343 | def GetTexts(self): 1344 | """ 1345 | Return a list of the texts that should be drawn with the cells 1346 | """ 1347 | return [self.olv.GetItem(self.rowIndex, i).GetText() for i in range(self.left, self.right+1)] 1348 | 1349 | def GetAlignments(self): 1350 | """ 1351 | Return a list indicating how the text within each cell is aligned. 1352 | """ 1353 | return self.GetColumnAlignments(self.olv, self.left, self.right) 1354 | 1355 | def GetImages(self): 1356 | """ 1357 | Return a list of the images that should be drawn in each cell 1358 | """ 1359 | return [self.olv.GetItem(self.rowIndex, i).GetImage() for i in range(self.left, self.right+1)] 1360 | 1361 | #====================================================================== 1362 | 1363 | class Decoration(object): 1364 | """ 1365 | A Decoration add some visual effect to a Block (e.g. borders, 1366 | background image, watermark). They can also reserve a chunk of their Blocks 1367 | space for their own use. 1368 | 1369 | Decorations are added to a BlockFormat which is then applied to a ReportBlock 1370 | """ 1371 | 1372 | LEFT = 0 1373 | RIGHT = 1 1374 | TOP = 2 1375 | BOTTOM = 3 1376 | 1377 | def __init__(self, *args): 1378 | pass 1379 | 1380 | #---------------------------------------------------------------------------- 1381 | # Commands 1382 | 1383 | def SubtractFrom(self, dc, bounds): 1384 | """ 1385 | Subtract the space used by this decoration from the given bounds 1386 | """ 1387 | return bounds 1388 | 1389 | def SubtractInternalFrom(self, dc, bounds): 1390 | """ 1391 | Subtract the space used by this decoration when used within a block. 1392 | This is currently only used for cells within a grid. 1393 | """ 1394 | return bounds 1395 | 1396 | def DrawDecoration(self, dc, bounds, block): 1397 | """ 1398 | Draw this decoration 1399 | """ 1400 | pass 1401 | 1402 | #---------------------------------------------------------------------------- 1403 | 1404 | class BackgroundDecoration(Decoration): 1405 | """ 1406 | A BackgroundDecoration paints the background of a block 1407 | """ 1408 | 1409 | def __init__(self, color=None): 1410 | self.color = color 1411 | 1412 | def DrawDecoration(self, dc, bounds, block): 1413 | """ 1414 | Draw this decoration 1415 | """ 1416 | if self.color is None: 1417 | return 1418 | dc.SetPen(wx.TRANSPARENT_PEN) 1419 | dc.SetBrush(wx.Brush(self.color)) 1420 | dc.DrawRectangle(*bounds) 1421 | 1422 | #---------------------------------------------------------------------------- 1423 | 1424 | class FrameDecoration(Decoration): 1425 | """ 1426 | A FrameDecoration a frame around a block 1427 | """ 1428 | 1429 | def __init__(self, pen=None, space=0, corner=None): 1430 | self.pen = pen 1431 | self.space = space 1432 | self.corner = corner 1433 | 1434 | def SubtractFrom(self, dc, bounds): 1435 | """ 1436 | Subtract the space used by this decoration from the given bounds 1437 | """ 1438 | inset = self.space 1439 | if self.pen is not None: 1440 | inset += self.pen.GetWidth() 1441 | 1442 | return RectUtils.InsetBy(bounds, inset) 1443 | 1444 | def DrawDecoration(self, dc, bounds, block): 1445 | """ 1446 | Draw this decoration 1447 | """ 1448 | if self.pen is None: 1449 | return 1450 | # We want to draw our decoration within the given bounds. Fat pens are drawn half 1451 | # either side of the coords, so we contract our coords so that fat pens don't 1452 | # cause drawing outside our bounds. 1453 | if self.pen.GetWidth() > 1: 1454 | rect = RectUtils.InsetBy(bounds, self.pen.GetWidth()/2) 1455 | else: 1456 | rect = bounds 1457 | dc.SetPen(self.pen) 1458 | dc.SetBrush(wx.TRANSPARENT_BRUSH) 1459 | if self.corner: 1460 | dc.DrawRoundedRectangle(rect[0], rect[1], rect[2], rect[3], self.corner) 1461 | else: 1462 | dc.DrawRectangle(*rect) 1463 | 1464 | #---------------------------------------------------------------------------- 1465 | 1466 | class LineDecoration(Decoration): 1467 | """ 1468 | A LineDecoration draws a line on the side of a decoration. 1469 | """ 1470 | 1471 | def __init__(self, side=Decoration.BOTTOM, pen=None, space=0): 1472 | self.side = side 1473 | self.pen = pen 1474 | self.space = space 1475 | 1476 | #---------------------------------------------------------------------------- 1477 | # Commands 1478 | 1479 | def SubtractFrom(self, dc, bounds): 1480 | """ 1481 | Subtract the space used by this decoration from the given bounds 1482 | """ 1483 | inset = self.space 1484 | if self.pen is not None: 1485 | inset += self.pen.GetWidth() 1486 | 1487 | if self.side == Decoration.LEFT: 1488 | return RectUtils.MoveLeftBy(bounds, inset) 1489 | if self.side == Decoration.RIGHT: 1490 | return RectUtils.MoveRightBy(bounds, inset) 1491 | if self.side == Decoration.TOP: 1492 | return RectUtils.MoveTopBy(bounds, inset) 1493 | if self.side == Decoration.BOTTOM: 1494 | return RectUtils.MoveBottomBy(bounds, inset) 1495 | return bounds 1496 | 1497 | 1498 | def DrawDecoration(self, dc, bounds, block): 1499 | """ 1500 | Draw this decoration 1501 | """ 1502 | if self.pen == None: 1503 | return 1504 | 1505 | if self.side == Decoration.LEFT: 1506 | pt1 = RectUtils.TopLeft(bounds) 1507 | pt2 = RectUtils.BottomLeft(bounds) 1508 | elif self.side == Decoration.RIGHT: 1509 | pt1 = RectUtils.TopRight(bounds) 1510 | pt2 = RectUtils.BottomRight(bounds) 1511 | elif self.side == Decoration.TOP: 1512 | pt1 = RectUtils.TopLeft(bounds) 1513 | pt2 = RectUtils.TopRight(bounds) 1514 | elif self.side == Decoration.BOTTOM: 1515 | pt1 = RectUtils.BottomLeft(bounds) 1516 | pt2 = RectUtils.BottomRight(bounds) 1517 | else: 1518 | return 1519 | 1520 | dc.SetPen(self.pen) 1521 | dc.DrawLine(pt1[0], pt1[1], pt2[0], pt2[1]) 1522 | 1523 | 1524 | #====================================================================== 1525 | 1526 | class Bucket(object): 1527 | """ 1528 | General purpose, hold-all data object 1529 | """ 1530 | 1531 | def __init__(self, **kwargs): 1532 | self.__dict__.update(kwargs) 1533 | 1534 | def __repr__(self): 1535 | strs = ["%s=%r" % kv for kv in self.__dict__.items()] 1536 | return "Bucket(" + ", ".join(strs) + ")" 1537 | 1538 | #====================================================================== 1539 | 1540 | class RectUtils: 1541 | """ 1542 | Static rectangle utilities 1543 | """ 1544 | 1545 | #---------------------------------------------------------------------------- 1546 | # Accessing 1547 | 1548 | @staticmethod 1549 | def Left(r): return r[0] 1550 | 1551 | @staticmethod 1552 | def Top(r): return r[1] 1553 | 1554 | @staticmethod 1555 | def Width(r): return r[2] 1556 | 1557 | @staticmethod 1558 | def Height(r): return r[3] 1559 | 1560 | @staticmethod 1561 | def Right(r): return r[0] + r[2] 1562 | 1563 | @staticmethod 1564 | def Bottom(r): return r[1] + r[3] 1565 | 1566 | @staticmethod 1567 | def TopLeft(r): return [r[0], r[1]] 1568 | 1569 | @staticmethod 1570 | def TopRight(r): return [r[0] + r[2], r[1]] 1571 | 1572 | @staticmethod 1573 | def BottomLeft(r): return [r[0], r[1] + r[3]] 1574 | 1575 | @staticmethod 1576 | def BottomRight(r): return [r[0] + r[2], r[1] + r[3]] 1577 | 1578 | #---------------------------------------------------------------------------- 1579 | # Modifying 1580 | 1581 | @staticmethod 1582 | def SetLeft(r, l): 1583 | r[0] = l 1584 | return r 1585 | 1586 | @staticmethod 1587 | def SetTop(r, t): 1588 | r[1] = t 1589 | return r 1590 | 1591 | @staticmethod 1592 | def SetWidth(r, w): 1593 | r[2] = w 1594 | return r 1595 | 1596 | @staticmethod 1597 | def SetHeight(r, h): 1598 | r[3] = h 1599 | return r 1600 | 1601 | @staticmethod 1602 | def MoveLeftBy(r, delta): 1603 | r[0] += delta 1604 | r[2] -= delta 1605 | return r 1606 | 1607 | @staticmethod 1608 | def MoveTopBy(r, delta): 1609 | r[1] += delta 1610 | r[3] -= delta 1611 | return r 1612 | 1613 | @staticmethod 1614 | def MoveRightBy(r, delta): 1615 | r[2] -= delta 1616 | return r 1617 | 1618 | @staticmethod 1619 | def MoveBottomBy(r, delta): 1620 | r[3] -= delta 1621 | return r 1622 | 1623 | #---------------------------------------------------------------------------- 1624 | # Calculations 1625 | 1626 | @staticmethod 1627 | def InsetBy(r, delta): 1628 | if delta is None: 1629 | return r 1630 | try: 1631 | delta[0] # is it indexable? 1632 | return RectUtils.InsetRect(r, delta) 1633 | except: 1634 | return RectUtils.InsetRect(r, (delta, delta, delta, delta)) 1635 | 1636 | @staticmethod 1637 | def InsetRect(r, r2): 1638 | if r2 is None: 1639 | return r 1640 | else: 1641 | return [r[0] + r2[0], r[1] + r2[1], r[2] - (r2[0] + r2[2]), r[3] - (r2[1] + r2[3])] 1642 | 1643 | #====================================================================== 1644 | # TESTING ONLY 1645 | #====================================================================== 1646 | 1647 | if __name__ == '__main__': 1648 | import wx 1649 | from ObjectListView import ObjectListView, GroupListView, ColumnDefn 1650 | 1651 | # Where can we find the Example module? 1652 | import sys 1653 | sys.path.append("../Examples") 1654 | 1655 | import ExampleModel 1656 | import ExampleImages 1657 | 1658 | class MyFrame(wx.Frame): 1659 | def __init__(self, *args, **kwds): 1660 | kwds["style"] = wx.DEFAULT_FRAME_STYLE 1661 | wx.Frame.__init__(self, *args, **kwds) 1662 | 1663 | self.panel = wx.Panel(self, -1) 1664 | self.olv = ObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) 1665 | self.olv = GroupListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) 1666 | #self.olv = FastObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) 1667 | #self.olv = VirtualObjectListView(self.panel, -1, style=wx.LC_REPORT|wx.SUNKEN_BORDER) 1668 | 1669 | sizer_2 = wx.BoxSizer(wx.VERTICAL) 1670 | sizer_2.Add(self.olv, 1, wx.ALL|wx.EXPAND, 4) 1671 | self.panel.SetSizer(sizer_2) 1672 | self.panel.Layout() 1673 | 1674 | sizer_1 = wx.BoxSizer(wx.VERTICAL) 1675 | sizer_1.Add(self.panel, 1, wx.EXPAND) 1676 | self.SetSizer(sizer_1) 1677 | self.Layout() 1678 | 1679 | groupImage = self.olv.AddImages(ExampleImages.getGroup16Bitmap(), ExampleImages.getGroup32Bitmap()) 1680 | userImage = self.olv.AddImages(ExampleImages.getUser16Bitmap(), ExampleImages.getUser32Bitmap()) 1681 | musicImage = self.olv.AddImages(ExampleImages.getMusic16Bitmap(), ExampleImages.getMusic32Bitmap()) 1682 | 1683 | self.olv.SetColumns([ 1684 | ColumnDefn("Title", "left", 120, "title", imageGetter=musicImage), 1685 | ColumnDefn("Artist", "left", 120, "artist", imageGetter=groupImage), 1686 | ColumnDefn("Last Played", "left", 100, "lastPlayed"), 1687 | ColumnDefn("Size", "center", 100, "sizeInBytes"), 1688 | ColumnDefn("Rating", "center", 100, "rating") 1689 | ]) 1690 | #self.olv.CreateCheckStateColumn() 1691 | self.olv.SetSortColumn(self.olv.columns[2]) 1692 | self.olv.SetObjects(ExampleModel.GetTracks()) 1693 | 1694 | wx.CallLater(50, self.run) 1695 | 1696 | def run(self): 1697 | printer = OLVPrinter(self.olv, "First ObjectListView Report") 1698 | printer.ReportFormat = ReportFormat.Normal() 1699 | 1700 | #fmt.PageHeader.Font = wx.FFont(36, wx.FONTFAMILY_SWISS, face="Gill Sans") 1701 | #fmt.PageHeader.Add(BackgroundDecoration(wx.BLUE)) 1702 | #fmt.PageHeader.Add(LineDecoration(side=Decoration.TOP, pen=wx.Pen(wx.RED, 5), space=0)) 1703 | #fmt.PageHeader.Add(LineDecoration(pen=wx.BLACK_PEN, space=0)) 1704 | # 1705 | #fmt.PageFooter.Font = wx.FFont(12, wx.FONTFAMILY_SWISS, face="Gill Sans") 1706 | #fmt.PageFooter.Add(BackgroundDecoration(wx.GREEN)) 1707 | #fmt.PageFooter.Add(LineDecoration(pen=wx.Pen(wx.BLUE, 5), space=0)) 1708 | #fmt.PageFooter.Add(LineDecoration(side=Decoration.TOP, pen=wx.RED_PEN, space=0)) 1709 | 1710 | printer.PrintPreview(self) 1711 | 1712 | 1713 | app = wx.PySimpleApp(0) 1714 | wx.InitAllImageHandlers() 1715 | frame_1 = MyFrame(None, -1, "") 1716 | app.SetTopWindow(frame_1) 1717 | frame_1.Show() 1718 | app.MainLoop() 1719 | -------------------------------------------------------------------------------- /v1/lib/ObjectListView/WordWrapRenderer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!/usr/bin/env python 3 | #---------------------------------------------------------------------------- 4 | # Name: WordWrapRenderer.py 5 | # Author: Phillip Piper 6 | # Created: 25 July 2008 7 | # SVN-ID: $Id$ 8 | # Copyright: (c) 2008 by Phillip Piper, 2008 9 | # License: wxWindows license 10 | #---------------------------------------------------------------------------- 11 | # Change log: 12 | # 2008/07/25 JPP Initial version 13 | #---------------------------------------------------------------------------- 14 | # To do: 15 | 16 | """ 17 | A WordWrapRenderer encapsulates the ability to draw and measure word wrapped 18 | strings directly to a device context. 19 | 20 | It is meant to be good enough for general use. It is not suitable for typographic layout 21 | -- it does not handle kerning or ligatures. 22 | 23 | The DC passed to these methods cannot be a GraphicContext DC. These methods use 24 | GetPartialTextExtents() which does not work with GCDC's (as of wx 2.8). 25 | 26 | """ 27 | 28 | import wx 29 | import bisect 30 | from wx.lib.wordwrap import wordwrap 31 | 32 | class WordWrapRenderer: 33 | """ 34 | This renderer encapsulates the logic need to draw and measure a word-wrapped 35 | string within a given rectangle. 36 | """ 37 | 38 | #---------------------------------------------------------------------------- 39 | # Calculating 40 | 41 | @staticmethod 42 | def CalculateHeight(dc, text, width): 43 | """ 44 | Calculate the height of the given text when fitted within the given width. 45 | 46 | Remember to set the font on the dc before calling this method. 47 | """ 48 | # There is a bug in the wordwrap routine where a string that needs truncated and 49 | # that ends with a single space causes the method to throw an error (wx 2.8). 50 | # Our simple, but not always accurate, is to remove trailing spaces. 51 | # This won't catch single trailing space imbedded in a multiline string. 52 | text = text.rstrip(' ') 53 | 54 | lines = wordwrap(text, width, dc, True) 55 | (width, height, descent, externalLeading) = dc.GetFullTextExtent("Wy") 56 | return (lines.count("\n")+1) * (height + externalLeading) 57 | 58 | 59 | #---------------------------------------------------------------------------- 60 | # Rendering 61 | 62 | @staticmethod 63 | def DrawString(dc, text, bounds, align=wx.ALIGN_LEFT, valign=wx.ALIGN_TOP, allowClipping=False): 64 | """ 65 | Draw the given text word-wrapped within the given bounds. 66 | 67 | bounds must be a wx.Rect or a 4-element collection: (left, top, width, height). 68 | 69 | If allowClipping is True, this method changes the clipping region so that no 70 | text is drawn outside of the given bounds. 71 | """ 72 | if not text: 73 | return 74 | 75 | if align == wx.ALIGN_CENTER: 76 | align = wx.ALIGN_CENTER_HORIZONTAL 77 | 78 | if valign == wx.ALIGN_CENTER: 79 | valign = wx.ALIGN_CENTER_VERTICAL 80 | 81 | # DrawLabel only accepts a wx.Rect 82 | try: 83 | bounds = wx.Rect(*bounds) 84 | except: 85 | pass 86 | 87 | if allowClipping: 88 | clipper = wx.DCClipper(dc, bounds) 89 | 90 | # There is a bug in the wordwrap routine where a string that needs truncated and 91 | # that ends with a single space causes the method to throw an error (wx 2.8). 92 | # Our simple, but not always accurate, is to remove trailing spaces. 93 | # This won't catch single trailing space imbedded in a multiline string. 94 | text = text.rstrip(' ') 95 | 96 | lines = wordwrap(text, bounds[2], dc, True) 97 | dc.DrawLabel(lines, bounds, align|valign) 98 | 99 | 100 | @staticmethod 101 | def DrawTruncatedString(dc, text, bounds, align=wx.ALIGN_LEFT, valign=wx.ALIGN_TOP, ellipse=wx.RIGHT, ellipseChars="..."): 102 | """ 103 | Draw the given text truncated to the given bounds. 104 | 105 | bounds must be a wx.Rect or a 4-element collection: (left, top, width, height). 106 | 107 | If allowClipping is True, this method changes the clipping region so that no 108 | text is drawn outside of the given bounds. 109 | """ 110 | if not text: 111 | return 112 | 113 | if align == wx.ALIGN_CENTER: 114 | align = wx.ALIGN_CENTER_HORIZONTAL 115 | 116 | if valign == wx.ALIGN_CENTER: 117 | valign = wx.ALIGN_CENTER_VERTICAL 118 | 119 | try: 120 | bounds = wx.Rect(*bounds) 121 | except: 122 | pass 123 | lines = WordWrapRenderer._Truncate(dc, text, bounds[2], ellipse, ellipseChars) 124 | dc.DrawLabel(lines, bounds, align|valign) 125 | 126 | 127 | @staticmethod 128 | def _Truncate(dc, text, maxWidth, ellipse, ellipseChars): 129 | """ 130 | Return a string that will fit within the given width. 131 | """ 132 | line = text.split("\n")[0] # only consider the first line 133 | if not line: 134 | return "" 135 | 136 | pte = dc.GetPartialTextExtents(line) 137 | 138 | # Does the whole thing fit within our given width? 139 | stringWidth = pte[-1] 140 | if stringWidth <= maxWidth: 141 | return line 142 | 143 | # We (probably) have to ellipse the text so allow for ellipse 144 | maxWidthMinusEllipse = maxWidth - dc.GetTextExtent(ellipseChars)[0] 145 | 146 | if ellipse == wx.LEFT: 147 | i = bisect.bisect(pte, stringWidth - maxWidthMinusEllipse) 148 | return ellipseChars + line[i+1:] 149 | 150 | if ellipse == wx.CENTER: 151 | i = bisect.bisect(pte, maxWidthMinusEllipse / 2) 152 | j = bisect.bisect(pte, stringWidth - maxWidthMinusEllipse / 2) 153 | return line[:i] + ellipseChars + line[j+1:] 154 | 155 | if ellipse == wx.RIGHT: 156 | i = bisect.bisect(pte, maxWidthMinusEllipse) 157 | return line[:i] + ellipseChars 158 | 159 | # No ellipsing, just truncating is the default 160 | i = bisect.bisect(pte, maxWidth) 161 | return line[:i] 162 | 163 | #====================================================================== 164 | # TESTING ONLY 165 | #====================================================================== 166 | 167 | if __name__ == '__main__': 168 | 169 | class TestPanel(wx.Panel): 170 | def __init__(self, parent): 171 | wx.Panel.__init__(self, parent, -1, style=wx.FULL_REPAINT_ON_RESIZE) 172 | self.Bind(wx.EVT_PAINT, self.OnPaint) 173 | 174 | self.text = """This is Thisisareallylongwordtoseewhathappens the text to be drawn. It needs to be long to see if wrapping works. to long words. 175 | This is on new line by itself. 176 | 177 | This should have a blank line in front of it but still wrap when we reach the edge. 178 | 179 | The bottom of the red rectangle should be immediately below this.""" 180 | self.font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName="Gill Sans") 181 | 182 | def OnPaint(self, evt): 183 | dc = wx.PaintDC(self) 184 | inset = (20, 20, 20, 20) 185 | rect = [inset[0], inset[1], self.GetSize().width-(inset[0]+inset[2]), self.GetSize().height-(inset[1]+inset[3])] 186 | 187 | # Calculate exactly how high the wrapped is going to be and put a frame around it. 188 | dc.SetFont(self.font) 189 | dc.SetPen(wx.RED_PEN) 190 | rect[3] = WordWrapRenderer.CalculateHeight(dc, self.text, rect[2]) 191 | dc.DrawRectangle(*rect) 192 | WordWrapRenderer.DrawString(dc, self.text, rect, wx.ALIGN_LEFT) 193 | #WordWrapRenderer.DrawTruncatedString(dc, self.text, rect, wx.ALIGN_CENTER_HORIZONTAL,s ellipse=wx.CENTER) 194 | 195 | #bmp = wx.EmptyBitmap(rect[0]+rect[2], rect[1]+rect[3]) 196 | #mdc = wx.MemoryDC(bmp) 197 | #mdc.SetBackground(wx.Brush("white")) 198 | #mdc.Clear() 199 | #mdc.SetFont(self.font) 200 | #mdc.SetPen(wx.RED_PEN) 201 | #rect[3] = WordWrapRenderer.CalculateHeight(mdc, self.text, rect[2]) 202 | #mdc.DrawRectangle(*rect) 203 | #WordWrapRenderer.DrawString(mdc, self.text, rect, wx.ALIGN_LEFT) 204 | #del mdc 205 | #dc = wx.ScreenDC() 206 | #dc.DrawBitmap(bmp, 20, 20) 207 | 208 | class MyFrame(wx.Frame): 209 | def __init__(self, *args, **kwds): 210 | kwds["style"] = wx.DEFAULT_FRAME_STYLE 211 | wx.Frame.__init__(self, *args, **kwds) 212 | 213 | self.panel = wx.Panel(self, -1) 214 | self.testPanel = TestPanel(self.panel) 215 | 216 | sizer_2 = wx.BoxSizer(wx.VERTICAL) 217 | sizer_2.Add(self.testPanel, 1, wx.ALL|wx.EXPAND, 4) 218 | self.panel.SetSizer(sizer_2) 219 | self.panel.Layout() 220 | 221 | sizer_1 = wx.BoxSizer(wx.VERTICAL) 222 | sizer_1.Add(self.panel, 1, wx.EXPAND) 223 | self.SetSizer(sizer_1) 224 | self.Layout() 225 | 226 | 227 | app = wx.PySimpleApp(0) 228 | wx.InitAllImageHandlers() 229 | frame_1 = MyFrame(None, -1, "") 230 | app.SetTopWindow(frame_1) 231 | frame_1.Show() 232 | app.MainLoop() 233 | -------------------------------------------------------------------------------- /v1/lib/ObjectListView/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #---------------------------------------------------------------------------- 3 | # Name: ObjectListView module initialization 4 | # Author: Phillip Piper 5 | # Created: 29 February 2008 6 | # SVN-ID: $Id$ 7 | # Copyright: (c) 2008 by Phillip Piper 8 | # License: wxWindows license 9 | #---------------------------------------------------------------------------- 10 | # Change log: 11 | # 2008/08/02 JPP Added list printing material 12 | # 2008/07/24 JPP Added list group related material 13 | # 2008/06/19 JPP Added sort event related material 14 | # 2008/04/11 JPP Initial Version 15 | 16 | """ 17 | An ObjectListView provides a more convienent and powerful interface to a ListCtrl. 18 | """ 19 | 20 | __version__ = '1.2' 21 | __copyright__ = "Copyright (c) 2008 Phillip Piper (phillip_piper@bigfoot.com)" 22 | 23 | from ObjectListView import ObjectListView, VirtualObjectListView, ColumnDefn, FastObjectListView, GroupListView, ListGroup, BatchedUpdate 24 | from OLVEvent import CellEditFinishedEvent, CellEditFinishingEvent, CellEditStartedEvent, CellEditStartingEvent, SortEvent 25 | from OLVEvent import EVT_CELL_EDIT_STARTING, EVT_CELL_EDIT_STARTED, EVT_CELL_EDIT_FINISHING, EVT_CELL_EDIT_FINISHED, EVT_SORT 26 | from OLVEvent import EVT_COLLAPSING, EVT_COLLAPSED, EVT_EXPANDING, EVT_EXPANDED, EVT_GROUP_CREATING, EVT_GROUP_SORT 27 | from CellEditor import CellEditorRegistry, MakeAutoCompleteTextBox, MakeAutoCompleteComboBox 28 | from ListCtrlPrinter import ListCtrlPrinter, ReportFormat, BlockFormat, LineDecoration, RectangleDecoration, ImageDecoration 29 | 30 | import Filter 31 | __all__ = [ 32 | "BatchedUpdate", 33 | "BlockFormat", 34 | "CellEditFinishedEvent", 35 | "CellEditFinishingEvent", 36 | "CellEditorRegistry", 37 | "CellEditStartedEvent", 38 | "CellEditStartingEvent", 39 | "ColumnDefn", 40 | "EVT_CELL_EDIT_FINISHED", 41 | "EVT_CELL_EDIT_FINISHING", 42 | "EVT_CELL_EDIT_STARTED", 43 | "EVT_CELL_EDIT_STARTING", 44 | "EVT_COLLAPSED", 45 | "EVT_COLLAPSING", 46 | "EVT_EXPANDED", 47 | "EVT_EXPANDING", 48 | "EVT_GROUP_CREATING", 49 | "EVT_GROUP_SORT" 50 | "EVT_SORT", 51 | "Filter", 52 | "FastObjectListView", 53 | "GroupListView", 54 | "ListGroup", 55 | "ImageDecoration", 56 | "MakeAutoCompleteTextBox", 57 | "MakeAutoCompleteComboBox", 58 | "ListGroup", 59 | "ObjectListView", 60 | "ListCtrlPrinter", 61 | "RectangleDecoration", 62 | "ReportFormat", 63 | "SortEvent", 64 | "VirtualObjectListView", 65 | ] 66 | -------------------------------------------------------------------------------- /v1/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackonYang/bookhub/b5c3e42b954af46ab0fa94b5b34337d716c49312/v1/lib/__init__.py -------------------------------------------------------------------------------- /v1/lib/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8-*- 2 | import hashlib 3 | import subprocess 4 | import os 5 | import sys 6 | if sys.platform.startswith('win'): # windows 7 | import win32file 8 | import win32con 9 | 10 | 11 | def md5_for_file(filename, block_size=256*128, hr=True): 12 | """calculate md5 of a file 13 | 14 | Block size directly depends on the block size of your filesystem 15 | to avoid performances issues 16 | Here I have blocks of 4096 octets (Default NTFS) 17 | 18 | """ 19 | md5 = hashlib.md5() 20 | with open(filename, 'rb') as f: 21 | for chunk in iter(lambda: f.read(block_size), b''): 22 | md5.update(chunk) 23 | if hr: 24 | return md5.hexdigest() 25 | return md5.digest() 26 | 27 | 28 | def cmd_open_file(filename): 29 | platform_cmd = { 30 | 'win32': 'start', # win7 32bit, win7 64bit 31 | 'cygwin': 'start', # cygwin 32 | 'linux2': 'xdg-open', # ubuntu 12.04 64bit 33 | 'darwin': 'open', # Mac 34 | } 35 | return "%s '%s'" % (platform_cmd[sys.platform], filename) 36 | 37 | 38 | def is_hiden(filepath): 39 | if sys.platform.startswith('win'): # windows 40 | return win32file.GetFileAttributes(filepath)\ 41 | & win32con.FILE_ATTRIBUTE_HIDDEN 42 | else: # linux 43 | return os.path.basename(filepath).startswith('.') 44 | 45 | 46 | def getSizeInNiceString(sizeInBytes): 47 | """ Convert the given byteCount into a string like: 9.9bytes/KB/MB/GB 48 | 49 | """ 50 | for (cutoff, label) in [(1024*1024*1024, "GB"), 51 | (1024*1024, "MB"), 52 | (1024, "KB"), 53 | ]: 54 | if sizeInBytes >= cutoff: 55 | return "%.1f %s" % (sizeInBytes * 1.0 / cutoff, label) 56 | 57 | if sizeInBytes == 1: 58 | return "1 byte" 59 | else: 60 | bytes = "%.1f" % (sizeInBytes or 0,) 61 | return (bytes[:-2] if bytes.endswith('.0') else bytes) + ' bytes' 62 | -------------------------------------------------------------------------------- /v1/media_repo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8-*- 2 | import os 3 | import shutil 4 | import pymongo 5 | import lib.util as util 6 | import settings 7 | param_template = [('REPO_PATH', None), 8 | ('host', 'localhost'), 9 | ('port', '27017'), 10 | ('db_name', 'bookhub'), 11 | ] 12 | 13 | 14 | class MediaRepo: 15 | 16 | def __init__(self): 17 | params = {k: getattr(settings, k, v) for k, v in param_template} 18 | # running mode detect by media_path 19 | self.repo_path = params['REPO_PATH'] or '' 20 | if self.repo_path is not None and (not os.path.exists(self.repo_path)): 21 | os.makedirs(self.repo_path) 22 | self.hasRepo = os.path.exists(self.repo_path) 23 | 24 | # connect to db 25 | self.conn = pymongo.Connection(params['host'], int(params['port'])) 26 | self.db = self.conn[params['db_name']] 27 | 28 | def __del__(self): 29 | self.conn.disconnect() 30 | self.conn = None 31 | self.db = None 32 | self.repo_path = None 33 | self.hasRepo = None 34 | 35 | def get_booklist(self): 36 | return [BookMeta(meta_info) for meta_info in self.db.book.find()] 37 | 38 | def update_meta(self, md5, setter, upsert=False): 39 | self.db.book.update({'md5': md5}, setter, upsert) 40 | 41 | def update_history(self, md5, setter, upsert=False): 42 | self.db.history.update({'md5': md5}, setter, upsert) 43 | 44 | def getFilePath(self, meta_obj): 45 | if self.hasRepo: 46 | paths = [os.path.join(self.repo_path, meta_obj.get_filename())] 47 | else: 48 | paths = self.db.history.find_one( 49 | {'md5': meta_obj.md5}, 50 | {'path': 1, '_id': 0}).get('path', []) 51 | for bookpath in paths: 52 | if os.path.exists(bookpath): 53 | return bookpath 54 | return None # file not exists 55 | 56 | def add_bookinfo(self, metaInfo): 57 | """add book meta info into database 58 | 59 | """ 60 | rawname = metaInfo.pop("rawname").pop() 61 | setter = {"$set": metaInfo, 62 | "$addToSet": {"rawname": rawname}, 63 | } 64 | self.update_meta(metaInfo['md5'], setter, True) 65 | return 1 66 | 67 | def add_history(self, md5, srcPath): 68 | """write history log to database 69 | 70 | """ 71 | setter = {"$set": {'md5': md5}, 72 | "$addToSet": {"path": srcPath}, 73 | } 74 | self.update_history(md5, setter, True) 75 | return 1 76 | 77 | def add_file(self, srcPath, metaInfo): 78 | filename = metaInfo['md5'] + metaInfo['ext'] 79 | dstfile = os.path.join(self.repo_path, filename) 80 | if self.hasRepo and not os.path.exists(dstfile): 81 | try: 82 | shutil.copy(srcPath, dstfile) 83 | except: 84 | print 'error' 85 | 86 | 87 | class BookMeta: 88 | """Meta info of a single book 89 | 90 | """ 91 | 92 | def __init__(self, meta): 93 | self.meta = meta 94 | self.md5 = meta['md5'] 95 | self.ext = meta['ext'] 96 | 97 | def get_filename(self): 98 | return self.md5+self.ext 99 | 100 | def get_dispname(self): 101 | return self.meta.get('dispname', ','.join(self.meta['rawname'])) 102 | 103 | def set_dispname(self, dispname): 104 | self.meta['dispname'] = dispname 105 | MediaRepo().update_meta(self.md5, {"$set": {"dispname": dispname}}) 106 | 107 | def getSizeString(self): 108 | return util.getSizeInNiceString(self.meta.get('bytes', 0)) 109 | 110 | def get_book_language(self): 111 | return self.meta.get('language', '') 112 | 113 | 114 | if __name__ == '__main__': 115 | repo = MediaRepo() 116 | print repo.get_booklist()[0:2] 117 | -------------------------------------------------------------------------------- /v1/requirement.txt: -------------------------------------------------------------------------------- 1 | wxpython 2 | pymongo 3 | -------------------------------------------------------------------------------- /v1/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8-*- 2 | # connection 3 | REPO_PATH = None 4 | host = "localhost" 5 | port = 27017 6 | db_name = 'bookhub' 7 | 8 | # scan configs 9 | ignore_seq = {'.git', '.svn', 'log', 'logs'} 10 | ext_pool = '.pdf' 11 | ignore_hidden = True 12 | 13 | try: 14 | from local_settings import * 15 | except: 16 | pass 17 | --------------------------------------------------------------------------------