├── .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 |
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 |

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 ? : null}
39 | {props.type === 'add-book' ? fileInput() : }
40 |
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 |
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 |
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 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------