├── .gitignore ├── HOWTO.md ├── LICENSE ├── Makefile ├── README.md ├── Sd.ico ├── Sd.png ├── Secret-Diary.desktop ├── aes.go ├── baidu.jpeg ├── bridge.go ├── categories.go ├── cmd-tools └── to-html │ ├── aes.go │ ├── db.go │ ├── index.tpl │ └── tohtml.go ├── db.go ├── dependences.txt ├── editor.go ├── go.mod ├── go.sum ├── gridlayout_after513.go ├── gridlayout_before513.go ├── gui.go ├── linux_launcher ├── .gitignore └── setup.go ├── locale └── zh_CN │ └── LC_MESSAGES │ ├── sdiary.mo │ └── sdiary.po ├── pay ├── alipay.jpg └── wxpay.png ├── pkg ├── linux.sh ├── mkdeb.sh ├── secret-diary.yml ├── shortcut.vbs └── win32.sh ├── qml ├── icons │ ├── B.png │ ├── I.png │ ├── S.png │ ├── Sd.png │ ├── U.png │ ├── bg.png │ ├── brush.png │ ├── document-new.jpg │ ├── document-save.jpg │ ├── draw-eraser.png │ ├── fg.png │ ├── h1.png │ ├── h2.png │ ├── h3.png │ ├── left.png │ ├── right.png │ ├── std.png │ ├── textcenter.png │ ├── textjustify.png │ ├── textleft.png │ └── textright.png └── pay │ ├── alipay.png │ └── wxpay.png ├── screenshot.png ├── secret-diary.exe.manifest ├── secret-diary.x64.manifest ├── shortcut.vbs ├── shortcut_linux.go ├── shortcut_windows.go ├── storge.go ├── testdata ├── .gitignore ├── README.md ├── aes.go ├── db.go ├── dbgen.go ├── exam.dat └── jinzhi16.go ├── version.go ├── version.json ├── version.sh ├── versionCheck.go └── zip.go /.gitignore: -------------------------------------------------------------------------------- 1 | secret-diary 2 | /deploy 3 | moc* 4 | rcc* 5 | messages.log 6 | linux_launcher 7 | setup 8 | /deb 9 | README.md 10 | 1 11 | 2 12 | 3 13 | *.syso 14 | /vendor 15 | qtbox 16 | -------------------------------------------------------------------------------- /HOWTO.md: -------------------------------------------------------------------------------- 1 | # Linux上如何自行编译和部署本软件(go module) 2 | 3 | ### 一、安装go编译器 4 | 5 | 1. 从`https://go.dev`下载`go`编译器,解压缩到`/usr/local/` 6 | 2. 在`$HOME/.bashrc`中加入一行`export PATH=$PATH:/usr/local/go/bin` 7 | 3. 用命令`. $HOME/.bashrc`导入环境变量 8 | 9 | 然后可以用命令`go version`测试`go`编译器是否能运行。 10 | 11 | 接着要设置`GOPROXY`变量:`go env -w GOPROXY=https://goproxy.cn,direct` 12 | 13 | ### 二、下载本软件的源代码 14 | 运行命令:`git clone https://gitee.com/rocket049/secret-diary.git` 15 | 16 | ### 三、安装必须的共享库(来自 https://github.com/therecipe/qt) 17 | ``` 18 | ubuntu/debian: 19 | sudo apt-get -y install build-essential libglu1-mesa-dev libpulse-dev libglib2.0-dev 20 | sudo apt-get --no-install-recommends install libqt*5-dev qt*5-dev qml-module-qtquick-* qt*5-doc-html 21 | 22 | Fedora/RHEL/CentOS: 23 | sudo yum -y groupinstall "C Development Tools and Libraries" 24 | sudo yum -y install mesa-libGLU-devel gstreamer-plugins-base pulseaudio-libs-devel glib2-devel 25 | sudo yum install qt5-* qt5-*-doc 26 | 27 | openSUSE: 28 | sudo zypper -n install -t pattern devel_basis 29 | sudo zypper install --no-recommends libqt5-qt*-devel 30 | 31 | Arch Linux: 32 | sudo pacman -S base-devel 33 | sudo pacman -S --needed qt5 34 | 35 | Deepin Linux: 36 | sudo apt-get -y install build-essential libglu1-mesa-dev libpulse-dev libglib2.0-dev 37 | sudo apt install qtbase5-dev 38 | 39 | ``` 40 | 41 | ### 四、用go编译器编译软件 42 | 在前面运行`git`命令的目录里,按顺序输入下面的命令: 43 | 44 | ``` 45 | cd secret-diary/ 46 | export QT_PKG_CONFIG=true 47 | go mod tidy 48 | go get -v -tags=no_env github.com/therecipe/qt/cmd/... 49 | go install -v -tags=no_env github.com/therecipe/qt/cmd/... 50 | go mod vendor 51 | ~/go/bin/qtdeploy build desktop 52 | ``` 53 | 54 | 等待上面的命令运行结束,就可以得到编译好的程序,位置是:`deploy/linux/secret-diary` 55 | 56 | ### 五、部署软件 57 | 在`secret-diary`目录中依次运行下面的命令: 58 | 59 | ``` 60 | sudo mkdir /opt/secret-diary 61 | sudo cp deploy/linux/secret-diary /opt/secret-diary/ 62 | sudo cp -r locale /opt/secret-diary/ 63 | sudo cp Sd.png /opt/secret-diary/ 64 | cp Secret-Diary.desktop ~/.local/share/applications/ 65 | ``` 66 | 67 | 这时你已经可以从开始菜单中点击启动`Secret-Diary`软件了。 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | release: 2 | env GO111MODULE=off qtdeploy build 3 | dev: 4 | env GO111MODULE=off go build 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 3 | `secret-diary` (安全日记本)是一个加密日记本,加密强度非常强,只能暴力破解数据,只要密码足够复杂,数据就足够安全。 4 | 编辑器也相当完善,支持富文本格式。 5 | 6 | 适用于场合:办公日记、个人日记。 7 | 8 | 兼容 `linux/windows`。 9 | 支持语言:中文、英语 10 | 11 | ### 支持编译环境 12 | - go >= 1.10 13 | - github.com/therecipe/qt 14 | - qt >= 5.13 15 | - qt < 5.13,必须使用参数 `-tags before513` 16 | 17 | ### 具体编译和部署的方法 18 | [HOWTO](HOWTO.md) 19 | 20 | ### 百度下载 21 | 链接: https://pan.baidu.com/s/14Ltsh1WiuKhHgMA7KgA-dw 提取码: stdc 22 | 23 | ### ZOL下载(win32版) 24 | [ZOL软件下载](http://xiazai.zol.com.cn/detail/48/472109.shtml) 25 | 26 | 二维码下载: 27 | 28 | ![二维码下载](https://github.com/rocket049/secret-diary/raw/master/baidu.jpeg) 29 | 30 | ### ChangeLog 31 | 32 | [At github](https://github.com/rocket049/secret-diary/releases) 33 | 34 | [码云上的](https://gitee.com/rocket049/secret-diary/releases) 35 | 36 | ## 注册用户和删除用户 37 | ### 注册 38 | 在登陆界面上注册用户,方法是: 39 | - 先输入用户名和密码 40 | - 然后点击“注册” 41 | - 接着在弹出的输入框中重复输入密码 42 | - 点击确定 43 | 44 | ### 删除 45 | 1. 如何删除多余的用户?只需到数据存储目录中删除对应名字的目录。How to delete redundant users? Just go to the data storage directory and delete the directory with the corresponding name. 46 | 2. 数据存储目录在哪里?点击“帮助”菜单中的“备份”,会显示该目录路径。Where is the data storage directory? Click Backup on the Help menu to display the directory path. 47 | 48 | # 加密算法说明 49 | 50 | ### 登陆验证算法 51 | `sha256` 52 | 53 | 哈希值计算过程: 54 | 55 | 1. 真实密码 = 用户密码 X 4 56 | 2. sha256( 真实密码 ) 57 | 58 | ### 数据加密算法 59 | `AES256` 60 | 61 | 数据加密密码为创建用户时从系统读取的32字节随机字符串。 62 | 63 | “数据加密密码”被加密后存储在数据库中。 64 | 65 | 用于加密“数据加密密码”的密码形成算法: 66 | 67 | 1. 用户真实密码 = 用户密码 X 40 68 | 2. 真实密码 = sha256( 用户真实密码 ) 69 | 70 | ## 支持作者 Support Author 71 | 全凭您的自愿! Voluntary! 72 | 73 | ![支付宝 alipay](https://github.com/rocket049/secret-diary/raw/master/pay/alipay.jpg) 74 | 75 | ![微信支付 wxpay](https://github.com/rocket049/secret-diary/raw/master/pay/wxpay.png) 76 | -------------------------------------------------------------------------------- /Sd.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/Sd.ico -------------------------------------------------------------------------------- /Sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/Sd.png -------------------------------------------------------------------------------- /Secret-Diary.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Secret-Diary 3 | Comment="Secret-Diary" 4 | Exec=/opt/secret-diary/secret-diary %U 5 | Icon=/opt/secret-diary/Sd.png 6 | Terminal=false 7 | Type=Application 8 | StartupNotify=true 9 | Categories=Office; -------------------------------------------------------------------------------- /aes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "fmt" 10 | "io" 11 | "os" 12 | "path" 13 | ) 14 | 15 | var dataDir string 16 | 17 | func getSha40String(pwd string) string { 18 | bp := []byte(pwd) 19 | buf := bytes.NewBuffer(make([]byte, 0, len(bp)*40)) 20 | for i := 0; i < 40; i++ { 21 | buf.Write(bp) 22 | } 23 | res := sha256.Sum256(buf.Bytes()) 24 | return fmt.Sprintf("%x", res) 25 | } 26 | 27 | func getSha4(pwd string) []byte { 28 | bp := []byte(pwd) 29 | buf := bytes.NewBuffer(make([]byte, 0, len(bp)*4)) 30 | for i := 0; i < 4; i++ { 31 | buf.Write(bp) 32 | } 33 | res := sha256.Sum256(buf.Bytes()) 34 | return res[:] 35 | } 36 | 37 | func getRealKeyString(pwd string) (string, error) { 38 | 39 | rkey := make([]byte, 32) 40 | 41 | io.ReadFull(rand.Reader, rkey) 42 | p1 := getSha4(pwd) 43 | aesBlock, err := aes.NewCipher(p1) 44 | if err != nil { 45 | return "", err 46 | } 47 | iv := make([]byte, aes.BlockSize) 48 | io.ReadFull(rand.Reader, iv) 49 | 50 | aesEncoder := cipher.NewCTR(aesBlock, iv) 51 | var res = make([]byte, 32) 52 | aesEncoder.XORKeyStream(res, rkey) 53 | buf := bytes.NewBuffer(iv) 54 | buf.Write(res) 55 | return fmt.Sprintf("%x", buf.Bytes()), nil 56 | } 57 | 58 | func newRealKeyString(rkey []byte, pwd string) (string, error) { 59 | p1 := getSha4(pwd) 60 | aesBlock, err := aes.NewCipher(p1) 61 | if err != nil { 62 | return "", err 63 | } 64 | iv := make([]byte, aes.BlockSize) 65 | io.ReadFull(rand.Reader, iv) 66 | 67 | aesEncoder := cipher.NewCTR(aesBlock, iv) 68 | var res = make([]byte, 32) 69 | aesEncoder.XORKeyStream(res, rkey) 70 | buf := bytes.NewBuffer(iv) 71 | buf.Write(res) 72 | return fmt.Sprintf("%x", buf.Bytes()), nil 73 | } 74 | 75 | func decodeRealKey(key, data, iv []byte) ([]byte, error) { 76 | aesBlock, err := aes.NewCipher(key) 77 | if err != nil { 78 | return nil, err 79 | } 80 | aesDecoder := cipher.NewCTR(aesBlock, iv) 81 | var res = make([]byte, 32) 82 | aesDecoder.XORKeyStream(res, data) 83 | return res, nil 84 | } 85 | 86 | func encodeToFile(data []byte, filename string, key []byte) error { 87 | dataPath := path.Join(dataDir, filename) 88 | fp, err := os.Create(dataPath) 89 | if err != nil { 90 | return err 91 | } 92 | defer fp.Close() 93 | 94 | aesBlock, err := aes.NewCipher(key) 95 | if err != nil { 96 | return err 97 | } 98 | iv := make([]byte, aesBlock.BlockSize()) 99 | io.ReadFull(rand.Reader, iv) 100 | _, err = fp.Write(iv) 101 | if err != nil { 102 | return err 103 | } 104 | aesEncoder := cipher.NewCTR(aesBlock, iv) 105 | wr := cipher.StreamWriter{S: aesEncoder, W: fp} 106 | _, err = wr.Write(data) 107 | return err 108 | } 109 | 110 | func decodeFromFile(filename string, key []byte) ([]byte, error) { 111 | dataPath := path.Join(dataDir, filename) 112 | fp, err := os.Open(dataPath) 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer fp.Close() 117 | 118 | aesBlock, err := aes.NewCipher(key) 119 | if err != nil { 120 | return nil, err 121 | } 122 | iv := make([]byte, aesBlock.BlockSize()) 123 | _, err = io.ReadFull(fp, iv) 124 | if err != nil { 125 | return nil, err 126 | } 127 | aesStream := cipher.NewCTR(aesBlock, iv) 128 | rd := cipher.StreamReader{S: aesStream, R: fp} 129 | res := bytes.NewBufferString("") 130 | _, err = io.Copy(res, rd) 131 | return res.Bytes(), err 132 | } 133 | 134 | func encodeToPathName(data []byte, filename string, key []byte) error { 135 | dataPath := filename 136 | fp, err := os.Create(dataPath) 137 | if err != nil { 138 | return err 139 | } 140 | defer fp.Close() 141 | 142 | aesBlock, err := aes.NewCipher(key) 143 | if err != nil { 144 | return err 145 | } 146 | iv := make([]byte, aesBlock.BlockSize()) 147 | io.ReadFull(rand.Reader, iv) 148 | _, err = fp.Write(iv) 149 | if err != nil { 150 | return err 151 | } 152 | aesEncoder := cipher.NewCTR(aesBlock, iv) 153 | wr := cipher.StreamWriter{S: aesEncoder, W: fp} 154 | _, err = wr.Write(data) 155 | return err 156 | } 157 | 158 | func decodeFromPathName(filename string, key []byte) ([]byte, error) { 159 | dataPath := filename 160 | fp, err := os.Open(dataPath) 161 | if err != nil { 162 | return nil, err 163 | } 164 | defer fp.Close() 165 | 166 | aesBlock, err := aes.NewCipher(key) 167 | if err != nil { 168 | return nil, err 169 | } 170 | iv := make([]byte, aesBlock.BlockSize()) 171 | _, err = io.ReadFull(fp, iv) 172 | if err != nil { 173 | return nil, err 174 | } 175 | aesStream := cipher.NewCTR(aesBlock, iv) 176 | rd := cipher.StreamReader{S: aesStream, R: fp} 177 | res := bytes.NewBufferString("") 178 | _, err = io.Copy(res, rd) 179 | return res.Bytes(), err 180 | } 181 | 182 | func decodeFromReader(reader io.Reader, key []byte) ([]byte, error) { 183 | aesBlock, err := aes.NewCipher(key) 184 | if err != nil { 185 | return nil, err 186 | } 187 | iv := make([]byte, aesBlock.BlockSize()) 188 | _, err = io.ReadFull(reader, iv) 189 | if err != nil { 190 | return nil, err 191 | } 192 | aesStream := cipher.NewCTR(aesBlock, iv) 193 | rd := cipher.StreamReader{S: aesStream, R: reader} 194 | res := bytes.NewBufferString("") 195 | _, err = io.Copy(res, rd) 196 | return res.Bytes(), err 197 | } 198 | -------------------------------------------------------------------------------- /baidu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/baidu.jpeg -------------------------------------------------------------------------------- /bridge.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/therecipe/qt/core" 5 | ) 6 | 7 | type QmlBridge struct { 8 | core.QObject 9 | 10 | _ func(id, day, title, mtime string, r, c int) `signal:"addDiary"` 11 | _ func(r, c int) `signal:"setMonthFlag"` 12 | _ func(ym string) `signal:"addYearMonth"` 13 | } 14 | -------------------------------------------------------------------------------- /categories.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/therecipe/qt/core" 5 | "github.com/therecipe/qt/widgets" 6 | ) 7 | 8 | func (s *myWindow) showCategores(menubar *widgets.QMenuBar) { 9 | s.categories = menubar.AddMenu2(T("Categories")) 10 | newcategory := s.categories.AddAction(T("New Category")) 11 | newcategory.ConnectTriggered(func(b bool) { 12 | var ok bool 13 | name := widgets.QInputDialog_GetText(s.window, T("New Category Name"), T("Category"), 0, "", &ok, core.Qt__Dialog, 0) 14 | if ok { 15 | id := s.db.AddCategory(name) 16 | s.addCategoryMenuItem(id, name) 17 | } 18 | }) 19 | 20 | renameCategory := s.categories.AddAction(T("Rename Category")) 21 | renameCategory.ConnectTriggered(func(b bool) { 22 | var ok bool 23 | name := widgets.QInputDialog_GetText(s.window, T("Rename Category")+" : "+s.categoryName, T("Rename Category")+" : "+s.categoryName, 0, "", &ok, core.Qt__Dialog, 0) 24 | if ok { 25 | c := s.categoryItem 26 | 27 | id, ok := c.Data().ToInterface().(int) 28 | if ok && id > 0 { 29 | s.db.RenameCategory(id, name) 30 | c.SetText(name) 31 | s.categoryName = name 32 | s.model.SetHorizontalHeaderLabels([]string{T("Diary List") + " - " + s.categoryName}) 33 | s.modelFind.SetHorizontalHeaderLabels([]string{T("Result List") + " - " + s.categoryName}) 34 | } 35 | } 36 | }) 37 | 38 | removeCategory := s.categories.AddAction(T("Remove Category")) 39 | removeCategory.ConnectTriggered(func(b bool) { 40 | s.removeCurCategory() 41 | }) 42 | 43 | s.categories.AddSeparator() 44 | } 45 | 46 | func (s *myWindow) loadUserCategorys() { 47 | c := s.addCategoryMenuItem(0, T("Default")) 48 | 49 | categoryDict := s.db.GetCategories() 50 | for id, name := range categoryDict { 51 | s.addCategoryMenuItem(id, name) 52 | } 53 | 54 | c.Triggered(true) 55 | } 56 | 57 | func (s *myWindow) addCategoryMenuItem(id int, name string) *widgets.QAction { 58 | c := s.categories.AddAction(name) 59 | c.SetCheckable(true) 60 | c.SetData(core.NewQVariant1(id)) 61 | c.ConnectTriggered(func(b bool) { 62 | data, ok := c.Data().ToInterface().(int) 63 | if ok == false { 64 | return 65 | } 66 | 67 | s.saveCurDiary() 68 | s.curDiary.Item = nil 69 | 70 | s.category = data 71 | s.categoryName = c.Text() 72 | //set last selected item to false 73 | if s.categoryItem != nil && b { 74 | s.categoryItem.SetChecked(false) 75 | } 76 | s.categoryItem = c 77 | s.showCurCategory(data) 78 | 79 | c.SetChecked(true) 80 | }) 81 | return c 82 | } 83 | 84 | func (s *myWindow) showCurCategory(c int) { 85 | s.db.setCategory(c) 86 | s.clearModel() 87 | s.clearModelFind() 88 | 89 | s.addYearMonthsFromDb() 90 | } 91 | 92 | func (s *myWindow) removeCurCategory() { 93 | if s.category == 0 { 94 | widgets.QMessageBox_Information(s.window, T("Error"), T("Can not remove default category."), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 95 | return 96 | } 97 | 98 | ret := widgets.QMessageBox_Question(s.window, T("Remove Category")+" : "+s.categoryName, T("Are you sure? All files in this category will move to default category."), widgets.QMessageBox__Yes|widgets.QMessageBox__No, widgets.QMessageBox__Yes) 99 | if ret == widgets.QMessageBox__No { 100 | return 101 | } 102 | s.db.RemoveCategory(s.category) 103 | s.categories.RemoveAction(s.categoryItem) 104 | s.categoryItem = nil 105 | 106 | for _, v := range s.categories.Children() { 107 | item := widgets.NewQActionFromPointer(v.Pointer()) 108 | id, ok := item.Data().ToInterface().(int) 109 | if ok && id == 0 { 110 | item.Triggered(true) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /cmd-tools/to-html/aes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "fmt" 10 | "io" 11 | "os" 12 | "path" 13 | ) 14 | 15 | var dataDir string 16 | 17 | func getSha40String(pwd string) string { 18 | bp := []byte(pwd) 19 | buf := bytes.NewBuffer(make([]byte, 0, len(bp)*40)) 20 | for i := 0; i < 40; i++ { 21 | buf.Write(bp) 22 | } 23 | res := sha256.Sum256(buf.Bytes()) 24 | return fmt.Sprintf("%x", res) 25 | } 26 | 27 | func getSha4(pwd string) []byte { 28 | bp := []byte(pwd) 29 | buf := bytes.NewBuffer(make([]byte, 0, len(bp)*4)) 30 | for i := 0; i < 4; i++ { 31 | buf.Write(bp) 32 | } 33 | res := sha256.Sum256(buf.Bytes()) 34 | return res[:] 35 | } 36 | 37 | func getRealKeyString(pwd string) (string, error) { 38 | 39 | rkey := make([]byte, 32) 40 | 41 | io.ReadFull(rand.Reader, rkey) 42 | p1 := getSha4(pwd) 43 | aesBlock, err := aes.NewCipher(p1) 44 | if err != nil { 45 | return "", err 46 | } 47 | iv := make([]byte, aes.BlockSize) 48 | io.ReadFull(rand.Reader, iv) 49 | 50 | aesEncoder := cipher.NewCTR(aesBlock, iv) 51 | var res = make([]byte, 32) 52 | aesEncoder.XORKeyStream(res, rkey) 53 | buf := bytes.NewBuffer(iv) 54 | buf.Write(res) 55 | return fmt.Sprintf("%x", buf.Bytes()), nil 56 | } 57 | 58 | func newRealKeyString(rkey []byte, pwd string) (string, error) { 59 | p1 := getSha4(pwd) 60 | aesBlock, err := aes.NewCipher(p1) 61 | if err != nil { 62 | return "", err 63 | } 64 | iv := make([]byte, aes.BlockSize) 65 | io.ReadFull(rand.Reader, iv) 66 | 67 | aesEncoder := cipher.NewCTR(aesBlock, iv) 68 | var res = make([]byte, 32) 69 | aesEncoder.XORKeyStream(res, rkey) 70 | buf := bytes.NewBuffer(iv) 71 | buf.Write(res) 72 | return fmt.Sprintf("%x", buf.Bytes()), nil 73 | } 74 | 75 | func decodeRealKey(key, data, iv []byte) ([]byte, error) { 76 | aesBlock, err := aes.NewCipher(key) 77 | if err != nil { 78 | return nil, err 79 | } 80 | aesDecoder := cipher.NewCTR(aesBlock, iv) 81 | var res = make([]byte, 32) 82 | aesDecoder.XORKeyStream(res, data) 83 | return res, nil 84 | } 85 | 86 | func encodeToFile(data []byte, filename string, key []byte) error { 87 | dataPath := path.Join(dataDir, filename) 88 | fp, err := os.Create(dataPath) 89 | if err != nil { 90 | return err 91 | } 92 | defer fp.Close() 93 | 94 | aesBlock, err := aes.NewCipher(key) 95 | if err != nil { 96 | return err 97 | } 98 | iv := make([]byte, aesBlock.BlockSize()) 99 | io.ReadFull(rand.Reader, iv) 100 | _, err = fp.Write(iv) 101 | if err != nil { 102 | return err 103 | } 104 | aesEncoder := cipher.NewCTR(aesBlock, iv) 105 | wr := cipher.StreamWriter{S: aesEncoder, W: fp} 106 | _, err = wr.Write(data) 107 | return err 108 | } 109 | 110 | func decodeFromFile(filename string, key []byte) ([]byte, error) { 111 | dataPath := path.Join(dataDir, filename) 112 | fp, err := os.Open(dataPath) 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer fp.Close() 117 | 118 | aesBlock, err := aes.NewCipher(key) 119 | if err != nil { 120 | return nil, err 121 | } 122 | iv := make([]byte, aesBlock.BlockSize()) 123 | _, err = io.ReadFull(fp, iv) 124 | if err != nil { 125 | return nil, err 126 | } 127 | aesStream := cipher.NewCTR(aesBlock, iv) 128 | rd := cipher.StreamReader{S: aesStream, R: fp} 129 | res := bytes.NewBufferString("") 130 | _, err = io.Copy(res, rd) 131 | return res.Bytes(), err 132 | } 133 | 134 | func encodeToPathName(data []byte, filename string, key []byte) error { 135 | dataPath := filename 136 | fp, err := os.Create(dataPath) 137 | if err != nil { 138 | return err 139 | } 140 | defer fp.Close() 141 | 142 | aesBlock, err := aes.NewCipher(key) 143 | if err != nil { 144 | return err 145 | } 146 | iv := make([]byte, aesBlock.BlockSize()) 147 | io.ReadFull(rand.Reader, iv) 148 | _, err = fp.Write(iv) 149 | if err != nil { 150 | return err 151 | } 152 | aesEncoder := cipher.NewCTR(aesBlock, iv) 153 | wr := cipher.StreamWriter{S: aesEncoder, W: fp} 154 | _, err = wr.Write(data) 155 | return err 156 | } 157 | 158 | func decodeFromPathName(filename string, key []byte) ([]byte, error) { 159 | dataPath := filename 160 | fp, err := os.Open(dataPath) 161 | if err != nil { 162 | return nil, err 163 | } 164 | defer fp.Close() 165 | 166 | aesBlock, err := aes.NewCipher(key) 167 | if err != nil { 168 | return nil, err 169 | } 170 | iv := make([]byte, aesBlock.BlockSize()) 171 | _, err = io.ReadFull(fp, iv) 172 | if err != nil { 173 | return nil, err 174 | } 175 | aesStream := cipher.NewCTR(aesBlock, iv) 176 | rd := cipher.StreamReader{S: aesStream, R: fp} 177 | res := bytes.NewBufferString("") 178 | _, err = io.Copy(res, rd) 179 | return res.Bytes(), err 180 | } 181 | 182 | func decodeFromReader(reader io.Reader, key []byte) ([]byte, error) { 183 | aesBlock, err := aes.NewCipher(key) 184 | if err != nil { 185 | return nil, err 186 | } 187 | iv := make([]byte, aesBlock.BlockSize()) 188 | _, err = io.ReadFull(reader, iv) 189 | if err != nil { 190 | return nil, err 191 | } 192 | aesStream := cipher.NewCTR(aesBlock, iv) 193 | rd := cipher.StreamReader{S: aesStream, R: reader} 194 | res := bytes.NewBufferString("") 195 | _, err = io.Copy(res, rd) 196 | return res.Bytes(), err 197 | } 198 | -------------------------------------------------------------------------------- /cmd-tools/to-html/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/aes" 5 | "database/sql" 6 | "encoding/hex" 7 | "errors" 8 | "log" 9 | "os" 10 | "path" 11 | "time" 12 | 13 | _ "github.com/mattn/go-sqlite3" 14 | ) 15 | 16 | type myDb struct { 17 | db *sql.DB 18 | category int 19 | } 20 | 21 | func openMyDb(name string) (*myDb, error) { 22 | db, err := sql.Open("sqlite3", name) 23 | if err != nil { 24 | return nil, err 25 | } 26 | res := &myDb{db: db} 27 | res.CheckAndUpgrade() 28 | return res, nil 29 | } 30 | 31 | func (s *myDb) setCategory(c int) { 32 | s.category = c 33 | } 34 | 35 | func (s *myDb) CheckAndUpgrade() error { 36 | diaryStruct, err := s.db.Query("PRAGMA table_info(diaries);") 37 | if err != nil { 38 | return err 39 | } 40 | var rows = 0 41 | for diaryStruct.Next() { 42 | rows++ 43 | } 44 | diaryStruct.Close() 45 | if rows == 5 { 46 | s.db.Exec("alter table diaries add column category int default 0;") 47 | s.db.Exec("create table categories(id int,name text);") 48 | } 49 | return nil 50 | } 51 | 52 | func (s *myDb) Close() { 53 | s.db.Close() 54 | } 55 | 56 | func (s *myDb) GetRealKey(pwd string) ([]byte, error) { 57 | sha40 := getSha40String(pwd) 58 | row := s.db.QueryRow("select realkey from user where sha40=?;", sha40) 59 | var realKey string 60 | err := row.Scan(&realKey) 61 | if err != nil { 62 | return nil, err 63 | } 64 | key, err := hex.DecodeString(realKey) 65 | if err != nil { 66 | return nil, err 67 | } 68 | if len(key) != 32+aes.BlockSize { 69 | return nil, errors.New("Data invalid.") 70 | } 71 | iv := key[:aes.BlockSize] 72 | data := key[aes.BlockSize:] 73 | ukey := getSha4(pwd) 74 | return decodeRealKey(ukey, data, iv) 75 | } 76 | 77 | func (s *myDb) UpdateRealKeyAndSha40(pwdOld, pwdNew string) error { 78 | sha40 := getSha40String(pwdOld) 79 | row := s.db.QueryRow("select realkey from user where sha40=?;", sha40) 80 | var realKey string 81 | err := row.Scan(&realKey) 82 | if err != nil { 83 | return err 84 | } 85 | key, err := hex.DecodeString(realKey) 86 | if err != nil { 87 | return err 88 | } 89 | if len(key) != 32+aes.BlockSize { 90 | return errors.New("Data invalid.") 91 | } 92 | iv := key[:aes.BlockSize] 93 | data := key[aes.BlockSize:] 94 | ukey := getSha4(pwdOld) 95 | rkey, err := decodeRealKey(ukey, data, iv) 96 | if err != nil { 97 | return err 98 | } 99 | rkeyStr, err := newRealKeyString(rkey, pwdNew) 100 | if err != nil { 101 | return err 102 | } 103 | _, err = s.db.Exec("update user set realkey=?,sha40=? where id=1;", rkeyStr, getSha40String(pwdNew)) 104 | return err 105 | } 106 | 107 | func (s *myDb) AddDiary(id int, cdate, title, filename string) error { 108 | _, err := s.db.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 109 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), s.category) 110 | if err != nil { 111 | log.Println("UpdateDiaryTitle") 112 | return s.UpdateDiaryTitle(id, title) 113 | } 114 | return nil 115 | } 116 | 117 | func (s *myDb) AddDiary2(id int, cdate, title, filename string, category int) error { 118 | _, err := s.db.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 119 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), category) 120 | if err != nil { 121 | log.Println("UpdateDiaryTitle") 122 | return s.UpdateDiaryTitle(id, title) 123 | } 124 | return nil 125 | } 126 | 127 | func (s *myDb) BeginTx() (*sql.Tx, error) { 128 | return s.db.Begin() 129 | } 130 | 131 | func (s *myDb) AddDiaryTx(tx *sql.Tx, id int, cdate, title, filename string, category int) error { 132 | _, err := tx.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 133 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), category) 134 | return err 135 | } 136 | 137 | func (s *myDb) UpdateDiaryTitle(id int, title string) error { 138 | res, err := s.db.Exec("update diaries set title=?,mtime=? where id=?", title, time.Now().Format("2006-01-02 15:04:05"), id) 139 | if err != nil { 140 | return err 141 | } 142 | if n, _ := res.RowsAffected(); n == 0 { 143 | return errors.New("UpdateDiaryTitle fail.") 144 | } 145 | return nil 146 | } 147 | 148 | func (s *myDb) UpdateDiaryCategory(id, c int) error { 149 | res, err := s.db.Exec("update diaries set category=?,mtime=? where id=?", c, time.Now().Format("2006-01-02 15:04:05"), id) 150 | if err != nil { 151 | return err 152 | } 153 | if n, _ := res.RowsAffected(); n == 0 { 154 | return errors.New("UpdateDiaryCategory fail.") 155 | } 156 | return nil 157 | } 158 | 159 | func (s *myDb) TouchDiary(id int) error { 160 | res, err := s.db.Exec("update diaries set mtime=? where id=?;", time.Now().Format("2006-01-02 15:04:05"), id) 161 | if err != nil { 162 | return err 163 | } 164 | if n, _ := res.RowsAffected(); n == 0 { 165 | return errors.New("AddDiary fail.") 166 | } 167 | return nil 168 | } 169 | 170 | func (s *myDb) NextId() int { 171 | res := s.db.QueryRow("select id from diaries order by id desc limit 1;") 172 | 173 | var id int 174 | err := res.Scan(&id) 175 | if err != nil { 176 | log.Println(err) 177 | return 1 178 | } 179 | return id + 1 180 | } 181 | 182 | func (s *myDb) GetYearMonths() ([]string, error) { 183 | rows, err := s.db.Query("select distinct substr(cdate,0,8) from diaries where category=? order by substr(cdate,0,8) asc;", s.category) 184 | if err != nil { 185 | return nil, err 186 | } 187 | defer rows.Close() 188 | res := []string{} 189 | for rows.Next() { 190 | var v string 191 | rows.Scan(&v) 192 | res = append(res, v) 193 | } 194 | return res, nil 195 | } 196 | 197 | type DiaryItem struct { 198 | Id int 199 | Day string 200 | Title string 201 | MTime string 202 | } 203 | 204 | func (s *myDb) GetListFromYearMonth(ym string) ([]DiaryItem, error) { 205 | rows, err := s.db.Query("select id,substr(cdate,9,11),title,mtime from diaries where substr(cdate,0,8)=? and category=? order by substr(cdate,9,11) desc;", ym, s.category) 206 | if err != nil { 207 | return nil, err 208 | } 209 | defer rows.Close() 210 | res := []DiaryItem{} 211 | for rows.Next() { 212 | var v DiaryItem 213 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 214 | res = append(res, v) 215 | } 216 | return res, nil 217 | } 218 | 219 | func (s *myDb) RemoveDiary(id string) error { 220 | row := s.db.QueryRow("select filename from diaries where id=?;", id) 221 | var filename string 222 | err := row.Scan(&filename) 223 | if err != nil { 224 | return err 225 | } 226 | _, err = s.db.Exec("delete from diaries where id=?;", id) 227 | if err != nil { 228 | return err 229 | } 230 | return os.Remove(path.Join(dataDir, filename)) 231 | } 232 | 233 | func (s *myDb) SearchTitle(kw string) ([]DiaryItem, error) { 234 | rows, err := s.db.Query("select id,cdate,title,mtime from diaries where instr(title,?)>0 and category=? order by cdate desc;", kw, s.category) 235 | if err != nil { 236 | return nil, err 237 | } 238 | defer rows.Close() 239 | res := []DiaryItem{} 240 | for rows.Next() { 241 | var v DiaryItem 242 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 243 | res = append(res, v) 244 | } 245 | return res, nil 246 | } 247 | 248 | func (s *myDb) ListAllDiary() ([]DiaryItem, error) { 249 | rows, err := s.db.Query("select id,cdate,title,mtime from diaries order by cdate desc;") 250 | if err != nil { 251 | return nil, err 252 | } 253 | defer rows.Close() 254 | res := []DiaryItem{} 255 | for rows.Next() { 256 | var v DiaryItem 257 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 258 | res = append(res, v) 259 | } 260 | return res, nil 261 | } 262 | 263 | func (s *myDb) AddCategory(name string) int { 264 | res := s.db.QueryRow("select id from categories order by id desc limit 1;") 265 | 266 | var id int 267 | err := res.Scan(&id) 268 | if err != nil { 269 | id = 1 270 | } else { 271 | id++ 272 | } 273 | 274 | s.db.Exec("insert into categories(id,name) values (?,?);", id, name) 275 | return id 276 | } 277 | 278 | func (s *myDb) RenameCategory(id int, name string) error { 279 | _, err := s.db.Exec("update categories set name=? where id=?;", name, id) 280 | return err 281 | } 282 | 283 | func (s *myDb) RemoveCategory(c int) { 284 | s.db.Exec("delete from categories where id=?;", c) 285 | s.db.Exec("update diaries set category=0 where category=?;", c) 286 | } 287 | 288 | func (s *myDb) GetCategories() map[int]string { 289 | res := make(map[int]string) 290 | 291 | rows, err := s.db.Query("select id,name from categories order by id asc;") 292 | if err != nil { 293 | return nil 294 | } 295 | defer rows.Close() 296 | 297 | for rows.Next() { 298 | var id int 299 | var name string 300 | if rows.Scan(&id, &name) == nil { 301 | res[id] = name 302 | } 303 | } 304 | 305 | return res 306 | } 307 | -------------------------------------------------------------------------------- /cmd-tools/to-html/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 所有记录 6 | 7 | 8 |

所有记录

9 | {{range .}}

{{.Id}}   {{.Title}}   {{.MTime}}

{{end}} 10 | 11 | -------------------------------------------------------------------------------- /cmd-tools/to-html/tohtml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Run program in directory $HOME/.sdiary/$USERNAME/ 4 | // It will generate index.html 5 | 6 | import ( 7 | "bytes" 8 | "encoding/gob" 9 | "fmt" 10 | "html/template" 11 | "io/ioutil" 12 | "os" 13 | "path" 14 | "path/filepath" 15 | 16 | "github.com/PuerkitoBio/goquery" 17 | 18 | _ "embed" 19 | ) 20 | 21 | //go:embed index.tpl 22 | var tplIndex string 23 | 24 | func main() { 25 | fmt.Print("Password:") 26 | var pwd string 27 | n, err := fmt.Scan(&pwd) 28 | if err != nil || n == 0 { 29 | panic(err.Error()) 30 | } 31 | fmt.Println(pwd) 32 | db, err := openMyDb("diary.db") 33 | if err != nil { 34 | panic(err.Error()) 35 | } 36 | defer db.Close() 37 | key, err := db.GetRealKey(pwd) 38 | if err != nil || n == 0 { 39 | panic(err.Error()) 40 | } 41 | fmt.Printf("%x\n", key) 42 | 43 | diaries, err := db.ListAllDiary() 44 | if err != nil { 45 | panic(err) 46 | } 47 | for _, diary := range diaries { 48 | fmt.Printf("%v\n%v\n%v %v\n", diary.Id, diary.Title, diary.Day, diary.MTime) 49 | //转换为html 50 | convertFile(fmt.Sprintf("%v", diary.Id), key) 51 | } 52 | 53 | t := template.New("") 54 | t.Parse(tplIndex) 55 | fp, err := os.Create("index.html") 56 | if err != nil { 57 | panic(err.Error()) 58 | } 59 | defer fp.Close() 60 | err = t.Execute(fp, diaries) 61 | if err != nil { 62 | panic(err.Error()) 63 | } 64 | } 65 | 66 | type attachmentFile struct { 67 | Name string 68 | Data []byte 69 | } 70 | 71 | type qtextFormat struct { 72 | Html string 73 | Images map[string][]byte 74 | Attachments []attachmentFile 75 | } 76 | 77 | func convertFile(name string, key []byte) error { 78 | data, err := decodeFromFile(name+".dat", key) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | buf := bytes.NewReader(data) 84 | 85 | decoder := gob.NewDecoder(buf) 86 | 87 | var document qtextFormat 88 | document.Images = make(map[string][]byte) 89 | document.Attachments = []attachmentFile{} 90 | 91 | err = decoder.Decode(&document) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | buf1 := bytes.NewBufferString(document.Html) 97 | 98 | root, err := goquery.NewDocumentFromReader(buf1) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | os.MkdirAll(name, os.ModePerm) 104 | //htmlImages := "" 105 | for k, img := range document.Images { 106 | fn := path.Base(k) 107 | ioutil.WriteFile(filepath.Join(name, fn), img, 0644) 108 | //htmlImages = htmlImages + fmt.Sprintf("\n

", fn) 109 | 110 | root.Find("img").Each(func(i int, s *goquery.Selection) { 111 | if v, ok := s.Attr("src"); ok { 112 | if v == k { 113 | s.SetAttr("src", fn) 114 | } 115 | } 116 | 117 | }) 118 | 119 | //println(htmlImages) 120 | } 121 | 122 | //htmlAttach := "" 123 | for _, item := range document.Attachments { 124 | ioutil.WriteFile(filepath.Join(name, item.Name), item.Data, 0644) 125 | //htmlAttach = htmlAttach + fmt.Sprintf("\n

%v

", item.Name, item.Name) 126 | //println(htmlAttach) 127 | 128 | root.Find("body").Each(func(i int, s *goquery.Selection) { 129 | s.AppendHtml(fmt.Sprintf("\n

%v

", item.Name, item.Name)) 130 | 131 | }) 132 | 133 | } 134 | 135 | //pos := strings.Index(document.Html, "") 136 | 137 | //html := document.Html[:pos] + htmlImages + htmlAttach + document.Html[pos:] 138 | html, err := root.Html() 139 | 140 | if err != nil { 141 | return err 142 | } 143 | 144 | ioutil.WriteFile(filepath.Join(name, name+".html"), []byte(html), 0644) 145 | return err 146 | } 147 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/aes" 5 | "database/sql" 6 | "encoding/hex" 7 | "errors" 8 | "log" 9 | "os" 10 | "path" 11 | "time" 12 | 13 | _ "github.com/mattn/go-sqlite3" 14 | ) 15 | 16 | type myDb struct { 17 | db *sql.DB 18 | category int 19 | } 20 | 21 | func createUserDb(name string, pwd string) error { 22 | dataDir = path.Join(datDir, name) 23 | os.MkdirAll(dataDir, os.ModePerm) 24 | userDb := path.Join(dataDir, "diary.db") 25 | if _, err := os.Stat(userDb); err == nil { 26 | //exists 27 | return errors.New("Already exists.") 28 | } 29 | db, err := sql.Open("sqlite3", userDb) 30 | if err != nil { 31 | return err 32 | } 33 | defer db.Close() 34 | sqls := []string{"create table user (id integer unique,cdata text,sha40 TEXT not null,realkey text not null,mtime text);", 35 | "create table diaries (id integer unique,cdate text,title text,filename text,mtime text,category int default 0);", 36 | "create index idx1 on diaries(cdate);", 37 | "create table categories(id int,name text);"} 38 | for i := 0; i < len(sqls); i++ { 39 | _, err = db.Exec(sqls[i]) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | id := 1 46 | now := time.Now() 47 | cdate := now.Format("2006-01-02") 48 | sha40 := getSha40String(pwd) 49 | realKey, err := getRealKeyString(pwd) 50 | if err != nil { 51 | return err 52 | } 53 | mtime := now.Format("2006-01-02 15:04:05") 54 | 55 | _, err = db.Exec("insert into user(id,cdata,sha40,realkey,mtime) values(?,?,?,?,?);", id, cdate, sha40, realKey, mtime) 56 | 57 | return err 58 | } 59 | 60 | func getMyDb(name string) (*myDb, error) { 61 | dataDir = path.Join(datDir, name) 62 | os.MkdirAll(dataDir, os.ModePerm) 63 | 64 | userDb := path.Join(dataDir, "diary.db") 65 | if _, err := os.Stat(userDb); err != nil { 66 | return nil, err 67 | } 68 | 69 | db, err := sql.Open("sqlite3", userDb) 70 | if err != nil { 71 | return nil, err 72 | } 73 | res := &myDb{db: db} 74 | res.CheckAndUpgrade() 75 | return res, nil 76 | } 77 | 78 | func (s *myDb) setCategory(c int) { 79 | s.category = c 80 | } 81 | 82 | func (s *myDb) CheckAndUpgrade() error { 83 | diaryStruct, err := s.db.Query("PRAGMA table_info(diaries);") 84 | if err != nil { 85 | return err 86 | } 87 | var rows = 0 88 | for diaryStruct.Next() { 89 | rows++ 90 | } 91 | diaryStruct.Close() 92 | if rows == 5 { 93 | s.db.Exec("alter table diaries add column category int default 0;") 94 | s.db.Exec("create table categories(id int,name text);") 95 | } 96 | return nil 97 | } 98 | 99 | func (s *myDb) Close() { 100 | s.db.Close() 101 | } 102 | 103 | func (s *myDb) GetRealKey(pwd string) ([]byte, error) { 104 | sha40 := getSha40String(pwd) 105 | row := s.db.QueryRow("select realkey from user where sha40=?;", sha40) 106 | var realKey string 107 | err := row.Scan(&realKey) 108 | if err != nil { 109 | return nil, err 110 | } 111 | key, err := hex.DecodeString(realKey) 112 | if err != nil { 113 | return nil, err 114 | } 115 | if len(key) != 32+aes.BlockSize { 116 | return nil, errors.New("Data invalid.") 117 | } 118 | iv := key[:aes.BlockSize] 119 | data := key[aes.BlockSize:] 120 | ukey := getSha4(pwd) 121 | return decodeRealKey(ukey, data, iv) 122 | } 123 | 124 | func (s *myDb) UpdateRealKeyAndSha40(pwdOld, pwdNew string) error { 125 | sha40 := getSha40String(pwdOld) 126 | row := s.db.QueryRow("select realkey from user where sha40=?;", sha40) 127 | var realKey string 128 | err := row.Scan(&realKey) 129 | if err != nil { 130 | return err 131 | } 132 | key, err := hex.DecodeString(realKey) 133 | if err != nil { 134 | return err 135 | } 136 | if len(key) != 32+aes.BlockSize { 137 | return errors.New("Data invalid.") 138 | } 139 | iv := key[:aes.BlockSize] 140 | data := key[aes.BlockSize:] 141 | ukey := getSha4(pwdOld) 142 | rkey, err := decodeRealKey(ukey, data, iv) 143 | if err != nil { 144 | return err 145 | } 146 | rkeyStr, err := newRealKeyString(rkey, pwdNew) 147 | if err != nil { 148 | return err 149 | } 150 | _, err = s.db.Exec("update user set realkey=?,sha40=? where id=1;", rkeyStr, getSha40String(pwdNew)) 151 | return err 152 | } 153 | 154 | func (s *myDb) AddDiary(id int, cdate, title, filename string) error { 155 | _, err := s.db.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 156 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), s.category) 157 | if err != nil { 158 | log.Println("UpdateDiaryTitle") 159 | return s.UpdateDiaryTitle(id, title) 160 | } 161 | return nil 162 | } 163 | 164 | func (s *myDb) AddDiary2(id int, cdate, title, filename string, category int) error { 165 | _, err := s.db.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 166 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), category) 167 | if err != nil { 168 | log.Println("UpdateDiaryTitle") 169 | return s.UpdateDiaryTitle(id, title) 170 | } 171 | return nil 172 | } 173 | 174 | func (s *myDb) BeginTx() (*sql.Tx, error) { 175 | return s.db.Begin() 176 | } 177 | 178 | func (s *myDb) AddDiaryTx(tx *sql.Tx, id int, cdate, title, filename string, category int) error { 179 | _, err := tx.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 180 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), category) 181 | return err 182 | } 183 | 184 | func (s *myDb) UpdateDiaryTitle(id int, title string) error { 185 | res, err := s.db.Exec("update diaries set title=?,mtime=? where id=?", title, time.Now().Format("2006-01-02 15:04:05"), id) 186 | if err != nil { 187 | return err 188 | } 189 | if n, _ := res.RowsAffected(); n == 0 { 190 | return errors.New("UpdateDiaryTitle fail.") 191 | } 192 | return nil 193 | } 194 | 195 | func (s *myDb) UpdateDiaryCategory(id, c int) error { 196 | res, err := s.db.Exec("update diaries set category=?,mtime=? where id=?", c, time.Now().Format("2006-01-02 15:04:05"), id) 197 | if err != nil { 198 | return err 199 | } 200 | if n, _ := res.RowsAffected(); n == 0 { 201 | return errors.New("UpdateDiaryCategory fail.") 202 | } 203 | return nil 204 | } 205 | 206 | func (s *myDb) TouchDiary(id int) error { 207 | res, err := s.db.Exec("update diaries set mtime=? where id=?;", time.Now().Format("2006-01-02 15:04:05"), id) 208 | if err != nil { 209 | return err 210 | } 211 | if n, _ := res.RowsAffected(); n == 0 { 212 | return errors.New("AddDiary fail.") 213 | } 214 | return nil 215 | } 216 | 217 | func (s *myDb) NextId() int { 218 | res := s.db.QueryRow("select id from diaries order by id desc limit 1;") 219 | 220 | var id int 221 | err := res.Scan(&id) 222 | if err != nil { 223 | log.Println(err) 224 | return 1 225 | } 226 | return id + 1 227 | } 228 | 229 | func (s *myDb) GetYearMonths() ([]string, error) { 230 | rows, err := s.db.Query("select distinct substr(cdate,0,8) from diaries where category=? order by substr(cdate,0,8) asc;", s.category) 231 | if err != nil { 232 | return nil, err 233 | } 234 | defer rows.Close() 235 | res := []string{} 236 | for rows.Next() { 237 | var v string 238 | rows.Scan(&v) 239 | res = append(res, v) 240 | } 241 | return res, nil 242 | } 243 | 244 | type diaryItem struct { 245 | Id int 246 | Day string 247 | Title string 248 | MTime string 249 | } 250 | 251 | func (s *myDb) GetListFromYearMonth(ym string) ([]diaryItem, error) { 252 | rows, err := s.db.Query("select id,substr(cdate,9,11),title,mtime from diaries where substr(cdate,0,8)=? and category=? order by substr(cdate,9,11) desc;", ym, s.category) 253 | if err != nil { 254 | return nil, err 255 | } 256 | defer rows.Close() 257 | res := []diaryItem{} 258 | for rows.Next() { 259 | var v diaryItem 260 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 261 | res = append(res, v) 262 | } 263 | return res, nil 264 | } 265 | 266 | func (s *myDb) RemoveDiary(id string) error { 267 | row := s.db.QueryRow("select filename from diaries where id=?;", id) 268 | var filename string 269 | err := row.Scan(&filename) 270 | if err != nil { 271 | return err 272 | } 273 | _, err = s.db.Exec("delete from diaries where id=?;", id) 274 | if err != nil { 275 | return err 276 | } 277 | return os.Remove(path.Join(dataDir, filename)) 278 | } 279 | 280 | func (s *myDb) SearchTitle(kw string) ([]diaryItem, error) { 281 | rows, err := s.db.Query("select id,cdate,title,mtime from diaries where instr(title,?)>0 and category=? order by cdate desc;", kw, s.category) 282 | if err != nil { 283 | return nil, err 284 | } 285 | defer rows.Close() 286 | res := []diaryItem{} 287 | for rows.Next() { 288 | var v diaryItem 289 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 290 | res = append(res, v) 291 | } 292 | return res, nil 293 | } 294 | 295 | func (s *myDb) AddCategory(name string) int { 296 | res := s.db.QueryRow("select id from categories order by id desc limit 1;") 297 | 298 | var id int 299 | err := res.Scan(&id) 300 | if err != nil { 301 | id = 1 302 | } else { 303 | id++ 304 | } 305 | 306 | s.db.Exec("insert into categories(id,name) values (?,?);", id, name) 307 | return id 308 | } 309 | 310 | func (s *myDb) RenameCategory(id int, name string) error { 311 | _, err := s.db.Exec("update categories set name=? where id=?;", name, id) 312 | return err 313 | } 314 | 315 | func (s *myDb) RemoveCategory(c int) { 316 | s.db.Exec("delete from categories where id=?;", c) 317 | s.db.Exec("update diaries set category=0 where category=?;", c) 318 | } 319 | 320 | func (s *myDb) GetCategories() map[int]string { 321 | res := make(map[int]string) 322 | 323 | rows, err := s.db.Query("select id,name from categories order by id asc;") 324 | if err != nil { 325 | return nil 326 | } 327 | defer rows.Close() 328 | 329 | for rows.Next() { 330 | var id int 331 | var name string 332 | if rows.Scan(&id, &name) == nil { 333 | res[id] = name 334 | } 335 | } 336 | 337 | return res 338 | } 339 | -------------------------------------------------------------------------------- /dependences.txt: -------------------------------------------------------------------------------- 1 | libbsd0, libc6, libcap2, libdbus-1-3, libgcc1, libgcrypt20, libglib2.0-0, libgpg-error0, liblzma5, libpcre3, libpulse0, libselinux1, libsystemd0, libuuid1, libwrap0, zlib1g -------------------------------------------------------------------------------- /editor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "path/filepath" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/therecipe/qt/core" 14 | "github.com/therecipe/qt/gui" 15 | "github.com/therecipe/qt/widgets" 16 | ) 17 | 18 | func (s *myWindow) setHeader(level int) { 19 | var cfmt = gui.NewQTextCharFormat() 20 | cfmt.SetFont2(s.app.Font()) 21 | switch level { 22 | case 1: 23 | cfmt.SetFontPointSize(18) 24 | case 2: 25 | cfmt.SetFontPointSize(16) 26 | case 3: 27 | cfmt.SetFontPointSize(14) 28 | default: 29 | cfmt.SetFontPointSize(12) 30 | } 31 | 32 | cfmt.SetForeground(gui.NewQBrush3(gui.NewQColor2(core.Qt__blue), core.Qt__SolidPattern)) 33 | s.mergeFormatOnLineOrSelection(cfmt) 34 | } 35 | 36 | func (s *myWindow) setStandard() { 37 | var cfmt = gui.NewQTextCharFormat() 38 | cfmt.SetFont2(s.app.Font()) 39 | cfmt.SetFontPointSize(14) 40 | cfmt.SetForeground(gui.NewQBrush3(gui.NewQColor2(core.Qt__black), core.Qt__SolidPattern)) 41 | 42 | s.mergeFormatOnLineOrSelection(cfmt) 43 | } 44 | 45 | func (s *myWindow) textStyle(styleIndex int) { 46 | var cursor = s.editor.TextCursor() 47 | 48 | if styleIndex != 0 { 49 | 50 | var style = gui.QTextListFormat__ListDisc 51 | 52 | switch styleIndex { 53 | case 1: 54 | { 55 | style = gui.QTextListFormat__ListDisc 56 | } 57 | 58 | case 2: 59 | { 60 | style = gui.QTextListFormat__ListCircle 61 | } 62 | 63 | case 3: 64 | { 65 | style = gui.QTextListFormat__ListSquare 66 | } 67 | 68 | case 4: 69 | { 70 | style = gui.QTextListFormat__ListDecimal 71 | } 72 | 73 | case 5: 74 | { 75 | style = gui.QTextListFormat__ListLowerAlpha 76 | } 77 | 78 | case 6: 79 | { 80 | style = gui.QTextListFormat__ListUpperAlpha 81 | } 82 | 83 | case 7: 84 | { 85 | style = gui.QTextListFormat__ListLowerRoman 86 | } 87 | 88 | case 8: 89 | { 90 | style = gui.QTextListFormat__ListUpperRoman 91 | } 92 | } 93 | 94 | cursor.BeginEditBlock() 95 | 96 | var ( 97 | blockFmt = cursor.BlockFormat() 98 | listFmt = gui.NewQTextListFormat() 99 | ) 100 | 101 | if cursor.CurrentList().Pointer() != nil { 102 | listFmt = gui.NewQTextListFormatFromPointer(cursor.CurrentList().Format().Pointer()) 103 | } else { 104 | listFmt.SetIndent(blockFmt.Indent() + 1) 105 | blockFmt.SetIndent(0) 106 | cursor.SetBlockFormat(blockFmt) 107 | } 108 | 109 | listFmt.SetStyle(style) 110 | cursor.CreateList(listFmt) 111 | 112 | cursor.EndEditBlock() 113 | 114 | } else { 115 | var bfmt = gui.NewQTextBlockFormat() 116 | bfmt.SetObjectIndex(-1) 117 | cursor.SetBlockFormat(bfmt) 118 | } 119 | } 120 | 121 | // addIndent 增加缩进量,n=1(增加1) or n=-1(减少1) 122 | func (s *myWindow) addIndent(n int) { 123 | cursor := s.editor.TextCursor() 124 | cursor.BeginEditBlock() 125 | 126 | var ( 127 | blockFmt = cursor.BlockFormat() 128 | listFmt = gui.NewQTextListFormat() 129 | ) 130 | 131 | if cursor.CurrentList().Pointer() != nil { 132 | listFmt = gui.NewQTextListFormatFromPointer(cursor.CurrentList().Format().Pointer()) 133 | if listFmt.Indent()+n >= 0 { 134 | listFmt.SetIndent(listFmt.Indent() + n) 135 | cursor.CreateList(listFmt) 136 | } 137 | 138 | } else { 139 | if blockFmt.Indent()+n >= 0 { 140 | blockFmt.SetIndent(blockFmt.Indent() + n) 141 | cursor.SetBlockFormat(blockFmt) 142 | } 143 | 144 | } 145 | 146 | cursor.EndEditBlock() 147 | } 148 | 149 | func (s *myWindow) textColor() { 150 | var col = widgets.QColorDialog_GetColor(s.editor.TextColor(), s.editor, "", 0) 151 | if !col.IsValid() { 152 | return 153 | } 154 | var cfmt = gui.NewQTextCharFormat() 155 | cfmt.SetForeground(gui.NewQBrush3(col, core.Qt__SolidPattern)) 156 | s.mergeFormatOnLineOrSelection(cfmt) 157 | } 158 | 159 | func (s *myWindow) textBgColor() { 160 | var col = widgets.QColorDialog_GetColor(s.editor.TextColor(), s.editor, "", 0) 161 | if !col.IsValid() { 162 | return 163 | } 164 | var cfmt = gui.NewQTextCharFormat() 165 | cfmt.SetBackground(gui.NewQBrush3(col, core.Qt__SolidPattern)) 166 | s.mergeFormatOnLineOrSelection(cfmt) 167 | } 168 | 169 | func (s *myWindow) textBold() { 170 | var afmt = gui.NewQTextCharFormat() 171 | var fw = gui.QFont__Normal 172 | if s.actionTextBold.IsChecked() { 173 | fw = gui.QFont__Bold 174 | } 175 | afmt.SetFontWeight(int(fw)) 176 | s.mergeFormatOnLineOrSelection(afmt) 177 | } 178 | 179 | func (s *myWindow) textUnderline() { 180 | var afmt = gui.NewQTextCharFormat() 181 | afmt.SetFontUnderline(s.actionTextUnderline.IsChecked()) 182 | s.mergeFormatOnLineOrSelection(afmt) 183 | } 184 | 185 | func (s *myWindow) textStrikeOut() { 186 | var afmt = gui.NewQTextCharFormat() 187 | afmt.SetFontStrikeOut(s.actionStrikeOut.IsChecked()) 188 | s.mergeFormatOnLineOrSelection(afmt) 189 | } 190 | 191 | func (s *myWindow) textItalic() { 192 | var afmt = gui.NewQTextCharFormat() 193 | afmt.SetFontItalic(s.actionTextItalic.IsChecked()) 194 | s.mergeFormatOnLineOrSelection(afmt) 195 | } 196 | 197 | func (s *myWindow) insertImage() { 198 | filename := widgets.QFileDialog_GetOpenFileName(s.window, "select a file", ".", "Image (*.png *.jpg)", "Image (*.png *.jpg)", widgets.QFileDialog__ReadOnly) 199 | data, err := ioutil.ReadFile(filename) 200 | if err != nil { 201 | return 202 | } 203 | img := gui.NewQImage() 204 | ok := img.LoadFromData(data, len(data), "") 205 | if !ok { 206 | return 207 | } 208 | 209 | uri := core.NewQUrl3("rc://"+filename, core.QUrl__TolerantMode) 210 | 211 | img = s.scaleImage(img) 212 | 213 | s.editor.Document().AddResource(int(gui.QTextDocument__ImageResource), uri, img.ToVariant()) 214 | url := uri.Url(core.QUrl__None) 215 | cursor := s.editor.TextCursor() 216 | cursor.InsertImage4(img, url) 217 | 218 | ba := core.NewQByteArray() 219 | 220 | iod := core.NewQBuffer2(ba, nil) 221 | 222 | iod.Open(core.QIODevice__WriteOnly) 223 | 224 | ok = img.Save2(iod, filepath.Ext(filename)[1:], -1) 225 | //fmt.Println(filepath.Ext(filename)) 226 | if ok { 227 | s.document.Images[url] = []byte(ba.Data()) 228 | } 229 | 230 | //fmt.Println("save image:", ok) 231 | } 232 | 233 | func (s *myWindow) scaleImage(src *gui.QImage) (res *gui.QImage) { 234 | 235 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 236 | dlg.SetWindowTitle(T("Scale Image Size")) 237 | 238 | grid := newGridLayout(dlg) 239 | 240 | width := widgets.NewQLabel2(fmt.Sprintf("%s : %d =>", T("Width"), src.Width()), dlg, core.Qt__Widget) 241 | grid.AddWidget2(width, 0, 0, 0) 242 | scaledW := src.Width() 243 | scaledH := src.Height() 244 | delta := 30 245 | if scaledW > s.editor.Width()-delta { 246 | scaledW = s.editor.Geometry().Width() - delta 247 | scaledH = int(float64(src.Height()) * float64(scaledW) / float64(src.Width())) 248 | } 249 | wValidor := gui.NewQIntValidator(dlg) 250 | wValidor.SetRange(10, scaledW) 251 | hValidor := gui.NewQIntValidator(dlg) 252 | hValidor.SetRange(10, scaledH) 253 | 254 | widthInput := widgets.NewQLineEdit(dlg) 255 | widthInput.SetText(strconv.Itoa(scaledW)) 256 | widthInput.SetValidator(wValidor) 257 | grid.AddWidget2(widthInput, 0, 1, 0) 258 | 259 | height := widgets.NewQLabel2(fmt.Sprintf("%s : %d =>", T("Height"), src.Height()), dlg, core.Qt__Widget) 260 | 261 | grid.AddWidget2(height, 1, 0, 0) 262 | 263 | heightInput := widgets.NewQLineEdit(dlg) 264 | heightInput.SetText(strconv.Itoa(scaledH)) 265 | heightInput.SetValidator(hValidor) 266 | grid.AddWidget2(heightInput, 1, 1, 0) 267 | 268 | btb := newGridLayout2() 269 | 270 | okBtn := widgets.NewQPushButton2(T("OK"), dlg) 271 | btb.AddWidget2(okBtn, 0, 0, 0) 272 | 273 | cancelBtn := widgets.NewQPushButton2(T("Cancel"), dlg) 274 | btb.AddWidget2(cancelBtn, 0, 1, 0) 275 | 276 | grid.AddLayout2(btb, 2, 0, 1, 2, 0) 277 | 278 | dlg.SetLayout(grid) 279 | 280 | widthInput.ConnectKeyReleaseEvent(func(e *gui.QKeyEvent) { 281 | w, err := strconv.Atoi(widthInput.Text()) 282 | if err != nil { 283 | return 284 | } 285 | w0 := float64(src.Width()) 286 | h0 := float64(src.Height()) 287 | h := float64(w) * h0 / w0 288 | heightInput.SetText(strconv.Itoa(int(h))) 289 | }) 290 | heightInput.ConnectKeyReleaseEvent(func(e *gui.QKeyEvent) { 291 | h, err := strconv.Atoi(heightInput.Text()) 292 | if err != nil { 293 | return 294 | } 295 | w0 := float64(src.Width()) 296 | h0 := float64(src.Height()) 297 | w := float64(h) * w0 / h0 298 | widthInput.SetText(strconv.Itoa(int(w))) 299 | }) 300 | 301 | okBtn.ConnectClicked(func(b bool) { 302 | w, err := strconv.Atoi(widthInput.Text()) 303 | if err != nil { 304 | res = src 305 | } 306 | h, err := strconv.Atoi(heightInput.Text()) 307 | if err != nil { 308 | res = src 309 | } 310 | res = src.Scaled2(w, h, core.Qt__KeepAspectRatioByExpanding, core.Qt__SmoothTransformation) 311 | dlg.Hide() 312 | dlg.Destroy(true, true) 313 | }) 314 | 315 | cancelBtn.ConnectClicked(func(b bool) { 316 | res = src 317 | dlg.Hide() 318 | dlg.Destroy(true, true) 319 | }) 320 | 321 | dlg.Exec() 322 | if res == nil { 323 | res = src 324 | } 325 | return 326 | } 327 | 328 | func (s *myWindow) getImageList(html string) []string { 329 | r := strings.NewReader(html) 330 | bufr := bufio.NewReader(r) 331 | reg1, err := regexp.Compile(`<]+/>`) 332 | if err != nil { 333 | //fmt.Println(err) 334 | return nil 335 | } 336 | reg2, err := regexp.Compile(`src="([^"]+)"`) 337 | if err != nil { 338 | //fmt.Println(err) 339 | return nil 340 | } 341 | imgs := []string{} 342 | for line, _, err := bufr.ReadLine(); err == nil; line, _, err = bufr.ReadLine() { 343 | line1 := string(line) 344 | res1 := reg1.FindAllString(line1, -1) 345 | imgs = append(imgs, res1...) 346 | } 347 | 348 | res := []string{} 349 | for _, img := range imgs { 350 | res2 := reg2.FindStringSubmatch(img) 351 | res = append(res, res2[1]) 352 | } 353 | //fmt.Println(res) 354 | return res 355 | } 356 | 357 | func (s *myWindow) insertTable() { 358 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 359 | dlg.SetWindowTitle(T("Table Rows and Columns")) 360 | dlg.SetFixedWidth(s.charWidth() * 13) 361 | 362 | grid := newGridLayout(dlg) 363 | 364 | row := widgets.NewQLabel2(T("Rows:"), dlg, core.Qt__Widget) 365 | grid.AddWidget2(row, 0, 0, 0) 366 | 367 | rowInput := widgets.NewQLineEdit(dlg) 368 | rowInput.SetText("3") 369 | rowInput.SetValidator(gui.NewQIntValidator(dlg)) 370 | grid.AddWidget2(rowInput, 0, 1, 0) 371 | 372 | col := widgets.NewQLabel2(T("Columns:"), dlg, core.Qt__Widget) 373 | 374 | grid.AddWidget2(col, 1, 0, 0) 375 | 376 | colInput := widgets.NewQLineEdit(dlg) 377 | colInput.SetText("3") 378 | colInput.SetValidator(gui.NewQIntValidator(dlg)) 379 | grid.AddWidget2(colInput, 1, 1, 0) 380 | 381 | btb := newGridLayout2() 382 | 383 | okBtn := widgets.NewQPushButton2(T("OK"), dlg) 384 | btb.AddWidget2(okBtn, 0, 0, 0) 385 | 386 | cancelBtn := widgets.NewQPushButton2(T("Cancel"), dlg) 387 | btb.AddWidget2(cancelBtn, 0, 1, 0) 388 | 389 | grid.AddLayout2(btb, 2, 0, 1, 2, 0) 390 | 391 | dlg.SetLayout(grid) 392 | 393 | okBtn.ConnectClicked(func(b bool) { 394 | cursor := s.editor.TextCursor() 395 | r, err := strconv.Atoi(rowInput.Text()) 396 | if err != nil { 397 | return 398 | } 399 | c, err := strconv.Atoi(colInput.Text()) 400 | if err != nil { 401 | return 402 | } 403 | tbl := cursor.InsertTable2(r, c) 404 | tbl.Format().SetBorderBrush(gui.NewQBrush2(core.Qt__SolidPattern)) 405 | A := 'A' 406 | for i := 0; i < c; i++ { 407 | tbl.CellAt(0, i).FirstCursorPosition().InsertText(fmt.Sprintf(" %c ", A+rune(i))) 408 | } 409 | dlg.Hide() 410 | dlg.Destroy(true, true) 411 | }) 412 | 413 | cancelBtn.ConnectClicked(func(b bool) { 414 | dlg.Hide() 415 | dlg.Destroy(true, true) 416 | }) 417 | 418 | dlg.SetModal(true) 419 | dlg.Show() 420 | } 421 | 422 | func (s *myWindow) addJustifyActions(tb *widgets.QToolBar) { 423 | rsrcPath := ":/qml/icons" 424 | var leftIcon = gui.QIcon_FromTheme2("format-justify-left", gui.NewQIcon5(rsrcPath+"/textleft.png")) 425 | actionAlignLeft := tb.AddAction2(leftIcon, "&Left") 426 | actionAlignLeft.SetPriority(widgets.QAction__LowPriority) 427 | actionAlignLeft.ConnectTriggered(func(b bool) { 428 | s.textAlign(1) 429 | }) 430 | 431 | var centerIcon = gui.QIcon_FromTheme2("format-justify-center", gui.NewQIcon5(rsrcPath+"/textcenter.png")) 432 | actionAlignCenter := tb.AddAction2(centerIcon, "C&enter") 433 | actionAlignCenter.SetPriority(widgets.QAction__LowPriority) 434 | actionAlignCenter.ConnectTriggered(func(b bool) { 435 | s.textAlign(2) 436 | }) 437 | 438 | var rightIcon = gui.QIcon_FromTheme2("format-justify-right", gui.NewQIcon5(rsrcPath+"/textright.png")) 439 | actionAlignRight := tb.AddAction2(rightIcon, "&Right") 440 | actionAlignRight.SetPriority(widgets.QAction__LowPriority) 441 | actionAlignRight.ConnectTriggered(func(b bool) { 442 | s.textAlign(3) 443 | }) 444 | 445 | var fillIcon = gui.QIcon_FromTheme2("format-justify-fill", gui.NewQIcon5(rsrcPath+"/textjustify.png")) 446 | actionAlignJustify := tb.AddAction2(fillIcon, "&Justify") 447 | actionAlignJustify.SetPriority(widgets.QAction__LowPriority) 448 | actionAlignJustify.ConnectTriggered(func(b bool) { 449 | s.textAlign(4) 450 | }) 451 | } 452 | 453 | func (s *myWindow) textAlign(n int) { 454 | switch n { 455 | case 1: 456 | 457 | s.editor.SetAlignment(core.Qt__AlignLeft | core.Qt__AlignAbsolute) 458 | 459 | case 2: 460 | 461 | s.editor.SetAlignment(core.Qt__AlignHCenter) 462 | 463 | case 3: 464 | 465 | s.editor.SetAlignment(core.Qt__AlignRight | core.Qt__AlignAbsolute) 466 | 467 | case 4: 468 | 469 | s.editor.SetAlignment(core.Qt__AlignJustify) 470 | 471 | } 472 | } 473 | 474 | func (s *myWindow) getTable() (t *gui.QTextTable, cell *gui.QTextTableCell) { 475 | cursor := s.editor.TextCursor() 476 | blk := s.editor.Document().FindBlock(cursor.Position()) 477 | for _, frame := range blk.Document().RootFrame().ChildFrames() { 478 | //fmt.Println("table cell:", frame.FrameFormat().IsTableCellFormat(), "table:", frame.FrameFormat().IsTableFormat()) 479 | if frame.FrameFormat().IsTableFormat() { 480 | table := gui.NewQTextTableFromPointer(frame.Pointer()) 481 | cell := table.CellAt2(cursor.Position()) 482 | //fmt.Println(cell.Row(), cell.Column()) 483 | return table, cell 484 | } 485 | } 486 | 487 | return nil, nil 488 | } 489 | 490 | func (s *myWindow) clearFormatAtCursor() { 491 | cursor := s.editor.TextCursor() 492 | 493 | block := cursor.Block() 494 | cfmt := block.CharFormat() 495 | cfmt.ClearBackground() 496 | cfmt.ClearForeground() 497 | cfmt.SetFontUnderline(false) 498 | cfmt.SetFontWeight(int(gui.QFont__Normal)) 499 | cfmt.SetFontPointSize(14) 500 | cfmt.SetFontItalic(false) 501 | cfmt.SetFontStrikeOut(false) 502 | 503 | var bfmt = gui.NewQTextBlockFormat() 504 | bfmt.SetObjectIndex(-1) 505 | 506 | if !cursor.HasSelection() { 507 | cursor.Select(gui.QTextCursor__LineUnderCursor) 508 | } 509 | cursor.SetCharFormat(cfmt) 510 | cursor.SetBlockFormat(bfmt) 511 | s.editor.SetCurrentCharFormat(cfmt) 512 | } 513 | 514 | func OpenDiaryNewWindow(parent *myWindow, id int) *myWindow { 515 | win := new(myWindow) 516 | win.OpenNewWindow(parent, id) 517 | parent.LockEditor() 518 | return win 519 | } 520 | 521 | func (s *myWindow) charWidth() int { 522 | // font := gui.NewQFont() 523 | // font.SetFamily("Serif") 524 | // if runtime.GOOS == "windows" { 525 | // font.SetPointSize(16) 526 | // } else { 527 | // //font.SetFamily("Serif Regular") 528 | // font.SetPointSize(12) 529 | // } 530 | 531 | // fm := gui.NewQFontMetrics(font) 532 | // return fm.BoundingRect2("宽W").Width() 533 | return 24 534 | } 535 | 536 | func (s *myWindow) OpenNewWindow(parent *myWindow, id int) { 537 | s.key = parent.key 538 | s.curDiary = new(diaryPointer) 539 | s.curDiary.Id = id 540 | s.db = parent.db 541 | 542 | s.window = widgets.NewQMainWindow(parent.window, core.Qt__Window) 543 | 544 | s.window.SetWindowTitle(parent.user) 545 | s.window.SetMinimumSize2(800, 600) 546 | s.window.SetWindowIcon(gui.NewQIcon5(":/qml/icons/Sd.png")) 547 | 548 | grid := newGridLayout2() 549 | 550 | frame := widgets.NewQFrame(s.window, core.Qt__Widget) 551 | 552 | s.window.SetCentralWidget(frame) 553 | 554 | charW := s.charWidth() 555 | editor := s.createEditor(charW) 556 | s.window.SetMinimumWidth(s.editor.Width() + 100) 557 | 558 | grid.AddWidget3(editor, 0, 0, 1, 1, 0) 559 | 560 | //grid.SetAlign(core.Qt__AlignTop) 561 | 562 | s.setToolBar() 563 | 564 | s.setMenuBar() 565 | 566 | s.exportEnc.SetEnabled(true) 567 | s.exportPdf.SetEnabled(true) 568 | s.exportOdt.SetEnabled(true) 569 | s.importEnc.SetDisabled(true) 570 | s.newDiary.SetDisabled(true) 571 | s.renDiary.SetDisabled(true) 572 | s.modifyPwd.SetDisabled(true) 573 | 574 | //s.setStandaloneFuncs() 575 | s.setEditorFuncs() 576 | 577 | frame.SetLayout(grid) 578 | 579 | once := sync.Once{} 580 | s.window.ConnectShowEvent(func(e *gui.QShowEvent) { 581 | once.Do(func() { 582 | s.loadDiary() 583 | }) 584 | }) 585 | 586 | s.window.ConnectCloseEvent(func(e *gui.QCloseEvent) { 587 | if s.editor.Document().IsModified() { 588 | ret := widgets.QMessageBox_Question(s.window, T("Close"), T("Do you want to save the document?"), widgets.QMessageBox__Yes|widgets.QMessageBox__No, widgets.QMessageBox__Yes) 589 | if ret == widgets.QMessageBox__Yes { 590 | s.saveCurDiary() 591 | } 592 | } 593 | 594 | }) 595 | s.window.Show() 596 | } 597 | 598 | func (s *myWindow) loadDiary() { 599 | filename := strconv.Itoa(s.curDiary.Id) + ".dat" 600 | data, err := decodeFromFile(filename, s.key) 601 | if err != nil { 602 | s.setStatusBar(err.Error()) 603 | } else { 604 | s.getQText(data) 605 | s.editor.Document().SetHtml(s.document.Html) 606 | 607 | s.showAttachList() 608 | s.editor.Document().SetModified(false) 609 | s.editor.SetReadOnly(false) 610 | } 611 | } 612 | 613 | func (s *myWindow) saveDiaryAlone() { 614 | if s.editor.Document().IsModified() == false { 615 | s.setStatusBar(T("No Diary Saved")) 616 | return 617 | } 618 | filename := strconv.Itoa(s.curDiary.Id) + ".dat" 619 | encodeToFile(s.getRichText(), filename, s.key) 620 | 621 | title := strings.TrimSpace(s.editor.Document().FirstBlock().Text()) 622 | s.db.UpdateDiaryTitle(s.curDiary.Id, title) 623 | 624 | s.setStatusBar(T("Save Diary") + fmt.Sprintf(" %s(%s)", title, filename)) 625 | s.editor.Document().SetModified(false) 626 | } 627 | 628 | func (s *myWindow) searchFromDb(kw string) { 629 | if len(kw) == 0 { 630 | s.setStatusBar(T("Must Input Search Keyword!")) 631 | return 632 | } 633 | s.setStatusBar(T("Loading Diary List...")) 634 | diaryList, err := s.db.SearchTitle(kw) 635 | if err != nil { 636 | s.setStatusBar(err.Error()) 637 | return 638 | } 639 | 640 | s.clearModelFind() 641 | 642 | var ym string 643 | var r, c int 644 | for _, diary := range diaryList { 645 | if diary.Day[:7] != ym { 646 | ym = diary.Day[:7] 647 | item := s.addFindYM(ym) 648 | idx := item.Index() 649 | r, c = idx.Row(), idx.Column() 650 | s.treeFind.Expand(idx) 651 | } 652 | item := s.modelFind.Item(r, c) 653 | child := gui.NewQStandardItem2(fmt.Sprintf("%s-%s", diary.Day[8:], diary.Title)) 654 | child.SetEditable(false) 655 | child.SetAccessibleText(strconv.Itoa(diary.Id)) 656 | child.SetAccessibleDescription("0") 657 | child.SetToolTip(T("Double Click to Open. ") + T("Last Modified:") + diary.MTime) 658 | 659 | item.AppendRow2(child) 660 | } 661 | s.treeFind.ResizeColumnToContents(0) 662 | return 663 | } 664 | 665 | func (s *myWindow) addFindYM(yearMonth string) *gui.QStandardItem { 666 | idx := core.NewQModelIndex() 667 | p := s.modelFind.Parent(idx) 668 | n := s.modelFind.RowCount(p) 669 | for i := 0; i < n; i++ { 670 | item := s.modelFind.Item(i, 0) 671 | if item.Text() == yearMonth { 672 | return item 673 | } 674 | } 675 | item := gui.NewQStandardItem2(yearMonth) 676 | item.SetColumnCount(1) 677 | item.SetEditable(false) 678 | 679 | item.SetAccessibleText("1") 680 | item.SetAccessibleDescription("1") 681 | 682 | s.modelFind.AppendRow2(item) 683 | 684 | return item 685 | } 686 | 687 | func (s *myWindow) setTreeFindFuncs() { 688 | s.treeFind.ConnectActivated(func(idx *core.QModelIndex) { 689 | item := s.modelFind.ItemFromIndex(idx) 690 | if item.AccessibleDescription() == "1" { 691 | return 692 | } 693 | id, err := strconv.Atoi(item.AccessibleText()) 694 | if err != nil { 695 | s.setStatusBar(err.Error()) 696 | } 697 | OpenDiaryNewWindow(s, id) 698 | }) 699 | } 700 | 701 | func (s *myWindow) findText() { 702 | if s.searchInput != nil { 703 | cursor := s.editor.TextCursor() 704 | s.searchInput.SetText(cursor.Selection().ToPlainText()) 705 | return 706 | } 707 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 708 | dlg.SetMinimumWidth(s.editor.Width() / 2) 709 | 710 | dlg.SetWindowTitle(T("Text Search")) 711 | 712 | grid := newGridLayout(dlg) 713 | 714 | word := widgets.NewQLineEdit(dlg) 715 | word.SetPlaceholderText(T("Words to search.")) 716 | grid.AddWidget3(word, 0, 0, 1, 2, 0) 717 | s.searchInput = word 718 | cursor := s.editor.TextCursor() 719 | s.searchInput.SetText(cursor.Selection().ToPlainText()) 720 | 721 | backBtn := widgets.NewQPushButton2(T("Last"), dlg) 722 | grid.AddWidget2(backBtn, 1, 0, 0) 723 | 724 | nextBtn := widgets.NewQPushButton2(T("Next"), dlg) 725 | grid.AddWidget2(nextBtn, 1, 1, 0) 726 | 727 | dlg.SetLayout(grid) 728 | 729 | backBtn.ConnectClicked(func(b bool) { 730 | kw := strings.TrimSpace(word.Text()) 731 | if len(kw) == 0 { 732 | return 733 | } 734 | cursor := s.editor.TextCursor() 735 | if len(cursor.Selection().ToPlainText()) > 0 { 736 | cursor.SetPosition(cursor.SelectionStart(), gui.QTextCursor__MoveAnchor) 737 | s.editor.SetTextCursor(cursor) 738 | } 739 | s.editor.Find(kw, gui.QTextDocument__FindBackward) 740 | }) 741 | 742 | nextBtn.ConnectClicked(func(b bool) { 743 | kw := strings.TrimSpace(word.Text()) 744 | if len(kw) == 0 { 745 | return 746 | } 747 | cursor := s.editor.TextCursor() 748 | if len(cursor.Selection().ToPlainText()) > 0 { 749 | cursor.SetPosition(cursor.SelectionEnd(), gui.QTextCursor__MoveAnchor) 750 | s.editor.SetTextCursor(cursor) 751 | } 752 | s.editor.Find(kw, 0) 753 | }) 754 | 755 | dlg.ConnectCloseEvent(func(e *gui.QCloseEvent) { 756 | dlg.Destroy(true, true) 757 | s.searchInput = nil 758 | }) 759 | 760 | dlg.Show() 761 | } 762 | 763 | func (s *myWindow) replaceText() { 764 | if s.replaceInput != nil { 765 | cursor := s.editor.TextCursor() 766 | s.replaceInput.SetText(cursor.Selection().ToPlainText()) 767 | return 768 | } 769 | 770 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 771 | dlg.SetMinimumWidth(s.editor.Width() / 2) 772 | 773 | dlg.SetWindowTitle(T("Text Replace")) 774 | 775 | grid := newGridLayout(dlg) 776 | 777 | wordOld := widgets.NewQLineEdit(dlg) 778 | wordOld.SetPlaceholderText(T("Old Text.")) 779 | wordOld.SetToolTip(T("Old Text.")) 780 | grid.AddWidget3(wordOld, 0, 0, 1, 3, 0) 781 | s.replaceInput = wordOld 782 | cursor := s.editor.TextCursor() 783 | s.replaceInput.SetText(cursor.Selection().ToPlainText()) 784 | 785 | wordNew := widgets.NewQLineEdit(dlg) 786 | wordNew.SetPlaceholderText(T("New Text.")) 787 | wordNew.SetToolTip(T("New Text.")) 788 | grid.AddWidget3(wordNew, 1, 0, 1, 3, 0) 789 | 790 | backBtn := widgets.NewQPushButton2(T("Last"), dlg) 791 | grid.AddWidget2(backBtn, 2, 0, 0) 792 | 793 | nextBtn := widgets.NewQPushButton2(T("Next"), dlg) 794 | grid.AddWidget2(nextBtn, 2, 1, 0) 795 | 796 | allBtn := widgets.NewQPushButton2(T("All"), dlg) 797 | grid.AddWidget2(allBtn, 2, 2, 0) 798 | 799 | dlg.SetLayout(grid) 800 | 801 | backBtn.ConnectClicked(func(b bool) { 802 | word0 := wordOld.Text() 803 | word1 := wordNew.Text() 804 | if len(word0) == 0 { 805 | return 806 | } 807 | cursor := s.editor.TextCursor() 808 | if cursor.Selection().ToPlainText() == word0 { 809 | cursor.RemoveSelectedText() 810 | cursor.InsertText(word1) 811 | 812 | cursor.SetPosition(cursor.Position()-len(word1), gui.QTextCursor__MoveAnchor) 813 | s.editor.SetTextCursor(cursor) 814 | } 815 | 816 | s.editor.Find(word0, gui.QTextDocument__FindBackward) 817 | }) 818 | 819 | nextBtn.ConnectClicked(func(b bool) { 820 | word0 := wordOld.Text() 821 | word1 := wordNew.Text() 822 | if len(word0) == 0 { 823 | return 824 | } 825 | cursor := s.editor.TextCursor() 826 | 827 | if cursor.Selection().ToPlainText() == word0 { 828 | cursor.RemoveSelectedText() 829 | cursor.InsertText(word1) 830 | } 831 | s.editor.Find(word0, 0) 832 | }) 833 | 834 | allBtn.ConnectClicked(func(b bool) { 835 | word0 := wordOld.Text() 836 | if len(word0) == 0 { 837 | return 838 | } 839 | text := s.editor.ToPlainText() 840 | n := strings.Count(strings.ToLower(text), strings.ToLower(word0)) 841 | 842 | cursor := s.editor.TextCursor() 843 | cursor.SetPosition(0, gui.QTextCursor__MoveAnchor) 844 | s.editor.SetTextCursor(cursor) 845 | 846 | for i := 0; i < n+1; i++ { 847 | nextBtn.Clicked(true) 848 | } 849 | }) 850 | 851 | dlg.ConnectCloseEvent(func(e *gui.QCloseEvent) { 852 | dlg.Destroy(true, true) 853 | s.replaceInput = nil 854 | }) 855 | 856 | dlg.Show() 857 | } 858 | 859 | func (s *myWindow) pasteText() { 860 | board := gui.QGuiApplication_Clipboard() 861 | text := board.Text(gui.QClipboard__Clipboard) 862 | cursor := s.editor.TextCursor() 863 | cursor.InsertText(text) 864 | } 865 | 866 | func (s *myWindow) changeLineMargin() { 867 | cursor := s.editor.TextCursor() 868 | if !cursor.HasSelection() { 869 | cursor.Select(gui.QTextCursor__LineUnderCursor) 870 | } 871 | bfmt := cursor.BlockFormat() 872 | margin := bfmt.TopMargin() 873 | 874 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 875 | dlg.SetWindowTitle(T("Top Margin")) 876 | dlg.SetMinimumWidth(s.charWidth() * 10) 877 | layout := widgets.NewQHBoxLayout() 878 | 879 | label := widgets.NewQLabel2(T("Top Margin")+fmt.Sprintf(":%.0f", margin), dlg, core.Qt__Widget) 880 | layout.AddWidget(label, 1, 0) 881 | 882 | addBtn := widgets.NewQPushButton2("+", dlg) 883 | addBtn.SetFixedWidth(s.charWidth()) 884 | layout.AddWidget(addBtn, 1, 0) 885 | addBtn.ConnectClicked(func(b bool) { 886 | margin += 1.0 887 | bfmt.SetTopMargin(margin) 888 | cursor.SetBlockFormat(bfmt) 889 | label.SetText(T("Top Margin") + fmt.Sprintf(":%.0f", margin)) 890 | }) 891 | 892 | degreeBtn := widgets.NewQPushButton2("-", dlg) 893 | degreeBtn.SetFixedWidth(s.charWidth()) 894 | layout.AddWidget(degreeBtn, 1, 0) 895 | degreeBtn.ConnectClicked(func(b bool) { 896 | margin -= 1.0 897 | bfmt.SetTopMargin(margin) 898 | cursor.SetBlockFormat(bfmt) 899 | label.SetText(T("Top Margin") + fmt.Sprintf(":%.0f", margin)) 900 | }) 901 | 902 | dlg.SetLayout(layout) 903 | dlg.Show() 904 | } 905 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module secret-diary 2 | 3 | go 1.18 4 | 5 | require github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d 6 | 7 | require ( 8 | github.com/andybalholm/cascadia v1.3.2 // indirect 9 | github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect 10 | golang.org/x/net v0.24.0 // indirect 11 | ) 12 | 13 | require ( 14 | github.com/PuerkitoBio/goquery v1.9.2 15 | github.com/mattn/go-sqlite3 v1.14.12 16 | github.com/rocket049/gettext-go v0.0.0-20190410112519-14a7c4752bef 17 | github.com/rocket049/go-locale v0.0.0-20190410112613-f30d0ad89b65 18 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 19 | golang.org/x/text v0.14.0 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= 2 | github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= 3 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 4 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA= 8 | github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 9 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 10 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 11 | github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= 12 | github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/rocket049/gettext-go v0.0.0-20190410112519-14a7c4752bef h1:RmNQgTGRjCn2ZA7mR52Wu8F9mB0lJC5+ayUEbzPIt+Y= 15 | github.com/rocket049/gettext-go v0.0.0-20190410112519-14a7c4752bef/go.mod h1:kwHzDYyH1mBSE4+qy9lA14PHalXGgmolkS7Em8dwNOw= 16 | github.com/rocket049/go-locale v0.0.0-20190410112613-f30d0ad89b65 h1:f/5CdNFwIu2OD5H+HBV2NrHU4+fpRQ+hn7P2n4uxgoQ= 17 | github.com/rocket049/go-locale v0.0.0-20190410112613-f30d0ad89b65/go.mod h1:QYX7uwo3utb8GfO0eRbWV3uaf7NnzhIV/k+e4hnfS5c= 18 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 19 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= 20 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 24 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 25 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 26 | github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o= 27 | github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= 28 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 31 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 32 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 33 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 34 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 35 | golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 37 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 38 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 39 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 40 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 41 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 42 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 57 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 58 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 59 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 60 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 62 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 63 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 64 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 65 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 66 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 67 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 69 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 70 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 71 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 72 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | -------------------------------------------------------------------------------- /gridlayout_after513.go: -------------------------------------------------------------------------------- 1 | //+build !before513 2 | 3 | //兼容 Qt5.13 以后的版本 4 | package main 5 | 6 | import ( 7 | "github.com/therecipe/qt/core" 8 | "github.com/therecipe/qt/gui" 9 | "github.com/therecipe/qt/widgets" 10 | ) 11 | 12 | type gridLayout struct { 13 | widgets.QGridLayout 14 | } 15 | 16 | func newGridLayout(parent widgets.QWidget_ITF) *gridLayout { 17 | return &gridLayout{*widgets.NewQGridLayout(parent)} 18 | } 19 | 20 | func newGridLayout2() *gridLayout { 21 | return &gridLayout{*widgets.NewQGridLayout2()} 22 | } 23 | 24 | func newQPixMap(fn string, format string, flag core.Qt__ImageConversionFlag) *gui.QPixmap { 25 | return gui.NewQPixmap3(fn, format, flag) 26 | } 27 | -------------------------------------------------------------------------------- /gridlayout_before513.go: -------------------------------------------------------------------------------- 1 | //+build before513 2 | 3 | //兼容 Qt5.12 之前的版本 4 | package main 5 | 6 | import ( 7 | "github.com/therecipe/qt/core" 8 | "github.com/therecipe/qt/gui" 9 | "github.com/therecipe/qt/widgets" 10 | ) 11 | 12 | type gridLayout struct { 13 | AddWidget2 func(widgets.QWidget_ITF, int, int, core.Qt__AlignmentFlag) 14 | widgets.QGridLayout 15 | } 16 | 17 | func newGridLayout(parent widgets.QWidget_ITF) *gridLayout { 18 | res := &gridLayout{AddWidget2: nil, QGridLayout: *widgets.NewQGridLayout(parent)} 19 | res.AddWidget2 = res.AddWidget 20 | return res 21 | } 22 | 23 | func newGridLayout2() *gridLayout { 24 | res := &gridLayout{AddWidget2: nil, QGridLayout: *widgets.NewQGridLayout2()} 25 | res.AddWidget2 = res.AddWidget 26 | return res 27 | } 28 | 29 | func newQPixMap(fn string, format string, flag core.Qt__ImageConversionFlag) *gui.QPixmap { 30 | return gui.NewQPixmap5(fn, format, flag) 31 | } 32 | -------------------------------------------------------------------------------- /gui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "regexp" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/skratchdot/open-golang/open" 18 | 19 | "github.com/therecipe/qt/gui" 20 | "github.com/therecipe/qt/printsupport" 21 | 22 | "github.com/rocket049/gettext-go/gettext" 23 | 24 | "github.com/therecipe/qt/core" 25 | "github.com/therecipe/qt/widgets" 26 | ) 27 | 28 | var datDir string 29 | 30 | func init() { 31 | exe1, _ := os.Executable() 32 | dir1 := path.Dir(exe1) 33 | locale1 := path.Join(dir1, "locale") 34 | gettext.BindTextdomain("sdiary", locale1, nil) 35 | gettext.Textdomain("sdiary") 36 | 37 | home1, _ := os.UserHomeDir() 38 | cfg := path.Join(home1, ".sdiary", "datapath.txt") 39 | cfgBytes, err := ioutil.ReadFile(cfg) 40 | if err != nil { 41 | datDir = path.Join(home1, ".sdiary") 42 | } else { 43 | datDir = string(cfgBytes) 44 | } 45 | } 46 | 47 | type diaryPointer struct { 48 | Item *gui.QStandardItem 49 | Id int 50 | Day string 51 | YearMonth string 52 | //Modified bool 53 | } 54 | 55 | func (s *diaryPointer) zero() { 56 | s.Item = nil 57 | s.Id = 0 58 | s.Day = "" 59 | s.YearMonth = "" 60 | } 61 | 62 | //T wrap gettext.T 63 | func T(v string) string { 64 | return gettext.T(v) 65 | } 66 | 67 | type formatData struct { 68 | BF *gui.QTextBlockFormat 69 | CF *gui.QTextCharFormat 70 | } 71 | 72 | type myWindow struct { 73 | app *widgets.QApplication 74 | window *widgets.QMainWindow 75 | tree *widgets.QTreeView 76 | model *gui.QStandardItemModel 77 | treeFind *widgets.QTreeView 78 | modelFind *gui.QStandardItemModel 79 | editor *widgets.QTextEdit 80 | comboAttachs *widgets.QComboBox 81 | actionTextBold *widgets.QAction 82 | actionTextUnderline *widgets.QAction 83 | actionTextItalic *widgets.QAction 84 | actionStrikeOut *widgets.QAction 85 | exportEnc *widgets.QAction 86 | exportPdf *widgets.QAction 87 | exportOdt *widgets.QAction 88 | importEnc *widgets.QAction 89 | changeStorage *widgets.QAction 90 | paste *widgets.QAction 91 | search *widgets.QAction 92 | replace *widgets.QAction 93 | lineMargin *widgets.QAction 94 | searchInput *widgets.QLineEdit 95 | replaceInput *widgets.QLineEdit 96 | newDiary *widgets.QAction 97 | saveDiary *widgets.QAction 98 | renDiary *widgets.QAction 99 | modifyPwd *widgets.QAction 100 | fb *widgets.QAction //格式刷 101 | categories *widgets.QMenu 102 | category int 103 | categoryName string 104 | categoryItem *widgets.QAction 105 | curDiary *diaryPointer 106 | user string 107 | key []byte 108 | db *myDb 109 | 110 | document qtextFormat 111 | format formatData 112 | } 113 | 114 | func (s *myWindow) getNewId() (res int) { 115 | return s.db.NextId() 116 | } 117 | 118 | func (s *myWindow) setMenuBar() { 119 | s.document.Images = make(map[string][]byte) 120 | 121 | menubar := s.window.MenuBar() 122 | menu := menubar.AddMenu2(T("File")) 123 | 124 | s.exportEnc = menu.AddAction(T("Export Encrypted Diary")) 125 | s.exportEnc.SetDisabled(true) 126 | s.exportEnc.ConnectTriggered(func(b bool) { 127 | s.saveCurDiary() 128 | s.exportEncryptedDiary() 129 | }) 130 | 131 | s.exportPdf = menu.AddAction(T("Export PDF...")) 132 | s.exportPdf.SetDisabled(true) 133 | s.exportPdf.ConnectTriggered(func(b bool) { 134 | s.exportAsPdf() 135 | }) 136 | 137 | s.exportOdt = menu.AddAction(T("Export Odt...")) 138 | s.exportOdt.SetDisabled(true) 139 | s.exportOdt.ConnectTriggered(func(b bool) { 140 | s.exportAsOdt() 141 | }) 142 | 143 | s.importEnc = menu.AddAction(T("Import Encrypted Diary")) 144 | s.importEnc.ConnectTriggered(func(b bool) { 145 | s.importEncryptedDiary() 146 | }) 147 | 148 | exportAll := menu.AddAction((T("Export All"))) 149 | exportAll.ConnectTriggered(func(b bool) { 150 | home, _ := os.UserHomeDir() 151 | dir := widgets.QFileDialog_GetExistingDirectory(s.window, T("Select export directory"), home, 0) 152 | if len(dir) == 0 { 153 | return 154 | } 155 | fromDir := filepath.Join(datDir, s.user) 156 | toFile := filepath.Join(home, s.user+".bak") 157 | err := zipData(fromDir, toFile) 158 | if err != nil { 159 | widgets.QMessageBox_About(s.window, T("Error"), err.Error()) 160 | } else { 161 | widgets.QMessageBox_About(s.window, T("Complete"), T("Complete")) 162 | } 163 | }) 164 | 165 | importAll := menu.AddAction(T("Import All")) 166 | importAll.ConnectTriggered(func(b bool) { 167 | home, _ := os.UserHomeDir() 168 | filename := widgets.QFileDialog_GetOpenFileName(s.window, T("Select a bakup file"), home, ".bak (*.bak)", ".bak (*.bak)", 0) 169 | if len(filename) == 0 { 170 | return 171 | } 172 | var ok bool 173 | pwd := widgets.QInputDialog_GetText(s.window, T("The password of the bakup file"), T("Password:"), widgets.QLineEdit__Password, "", 174 | &ok, core.Qt__Dialog, core.Qt__ImhNone) 175 | if len(filename) < 4 || !ok { 176 | return 177 | } 178 | err := s.importFromZip(filename, pwd) 179 | if err != nil { 180 | widgets.QMessageBox_About(s.window, T("Error"), err.Error()) 181 | } else { 182 | widgets.QMessageBox_About(s.window, T("Complete"), T("Complete")) 183 | } 184 | 185 | s.clearModel() 186 | 187 | s.addYearMonthsFromDb() 188 | }) 189 | 190 | s.changeStorage = menu.AddAction(T("Change Storage Dir")) 191 | s.changeStorage.ConnectTriggered(func(b bool) { 192 | dir1 := widgets.QFileDialog_GetExistingDirectory(s.window, T("Select Storage Directory"), datDir, 0) 193 | if len(dir1) > 0 { 194 | home1, _ := os.UserHomeDir() 195 | cfg := path.Join(home1, ".sdiary", "datapath.txt") 196 | ioutil.WriteFile(cfg, []byte(dir1), 0644) 197 | widgets.QMessageBox_Information(s.window, T("Quit program"), 198 | T("The program will terminate now.")+"\n"+T("How to move data?")+"\n"+T("Move the directories to the new directory from ")+datDir, 199 | widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 200 | s.app.Quit() 201 | } 202 | }) 203 | 204 | menu = menubar.AddMenu2(T("Edit")) 205 | s.paste = menu.AddAction(T("Paste Text")) 206 | s.paste.SetShortcut(gui.QKeySequence_FromString("Ctrl+d", gui.QKeySequence__NativeText)) 207 | s.paste.ConnectTriggered(func(b bool) { 208 | s.pasteText() 209 | }) 210 | 211 | s.search = menu.AddAction(T("Search")) 212 | s.search.SetShortcut(gui.QKeySequence_FromString("Ctrl+f", gui.QKeySequence__NativeText)) 213 | s.search.ConnectTriggered(func(b bool) { 214 | s.findText() 215 | }) 216 | 217 | s.replace = menu.AddAction(T("Replace")) 218 | s.replace.SetShortcut(gui.QKeySequence_FromString("Ctrl+r", gui.QKeySequence__NativeText)) 219 | s.replace.ConnectTriggered(func(b bool) { 220 | s.replaceText() 221 | }) 222 | 223 | s.lineMargin = menu.AddAction(T("Top Margin")) 224 | s.lineMargin.SetShortcut(gui.QKeySequence_FromString("Ctrl+l", gui.QKeySequence__NativeText)) 225 | s.lineMargin.ConnectTriggered(func(b bool) { 226 | s.changeLineMargin() 227 | }) 228 | 229 | menu = menubar.AddMenu2(T("Table")) 230 | 231 | newTable := menu.AddAction(T("Insert Table")) 232 | newTable.ConnectTriggered(func(b bool) { 233 | s.insertTable() 234 | }) 235 | 236 | addPreRow := menu.AddAction(T("Insert Row")) 237 | addPreRow.ConnectTriggered(func(b bool) { 238 | t, c := s.getTable() 239 | if t == nil { 240 | return 241 | } 242 | t.InsertRows(c.Row(), 1) 243 | }) 244 | appendRow := menu.AddAction(T("Append Row")) 245 | appendRow.ConnectTriggered(func(b bool) { 246 | t, _ := s.getTable() 247 | if t == nil { 248 | return 249 | } 250 | t.AppendRows(1) 251 | }) 252 | 253 | addPreCol := menu.AddAction(T("Insert Column")) 254 | addPreCol.ConnectTriggered(func(b bool) { 255 | t, c := s.getTable() 256 | if t == nil { 257 | return 258 | } 259 | t.InsertColumns(c.Column(), 1) 260 | }) 261 | appendCol := menu.AddAction(T("Append Column")) 262 | appendCol.ConnectTriggered(func(b bool) { 263 | t, _ := s.getTable() 264 | if t == nil { 265 | return 266 | } 267 | t.AppendColumns(1) 268 | }) 269 | 270 | removeRow := menu.AddAction(T("Remove Row")) 271 | removeRow.ConnectTriggered(func(b bool) { 272 | t, c := s.getTable() 273 | if t == nil { 274 | return 275 | } 276 | t.RemoveRows(c.Row(), 1) 277 | }) 278 | 279 | removeCol := menu.AddAction(T("Remove Column")) 280 | removeCol.ConnectTriggered(func(b bool) { 281 | t, c := s.getTable() 282 | if t == nil { 283 | return 284 | } 285 | t.RemoveColumns(c.Column(), 1) 286 | }) 287 | 288 | s.showCategores(menubar) 289 | 290 | menu = menubar.AddMenu2(T("Manage")) 291 | 292 | s.renDiary = menu.AddAction(T("Rename")) 293 | s.renDiary.ConnectTriggered(func(b bool) { 294 | s.rename() 295 | }) 296 | 297 | s.modifyPwd = menu.AddAction(T("ModifyPassword")) 298 | s.modifyPwd.ConnectTriggered(func(b bool) { 299 | s.updatePwd() 300 | }) 301 | 302 | menu = menubar.AddMenu2(T("Help")) 303 | 304 | about := menu.AddAction(T("About")) 305 | about.ConnectTriggered(func(b bool) { 306 | s.showMsg(T("About"), T("Copy Right: Fu Huizhong \nSecurity Diary ")+version+"\n"+T("Crypto with AES256")+"\nHomepage: https://github.com/rocket049/secret-diary") 307 | }) 308 | 309 | update1 := menu.AddAction(T("Update")) 310 | update1.ConnectTriggered(func(b bool) { 311 | v, newest := versionCheck() 312 | if newest == true { 313 | s.showMsg(T("Update"), T("This is the Newest Version!")) 314 | } else { 315 | s.showMsg(T("Update"), T("Current:")+version+"\n"+T("Newest:")+v+"\n"+T("I will open the page on github website after you click button 'OK'!")) 316 | open.Start("https://github.com/rocket049/secret-diary/releases") 317 | } 318 | }) 319 | 320 | itemName := "Add To Menu" 321 | if runtime.GOOS == "windows" { 322 | itemName = "Create Desktop Shortcut" 323 | } 324 | 325 | add2menu := menu.AddAction(T(itemName)) 326 | add2menu.ConnectTriggered(func(b bool) { 327 | makeShortcut(true) 328 | if runtime.GOOS == "linux" { 329 | s.showMsg(T("Add To Menu"), T("You can start this program from 'Start'->'Office'")) 330 | } 331 | 332 | }) 333 | 334 | bak := menu.AddAction(T("Backup & Delete")) 335 | bak.ConnectTriggered(func(b bool) { 336 | s.showMsg(T("Backup & Delete"), T("Your DATA Storage path is '")+dataDir+"'\n"+T("You can backup and delete the directory yourself.")) 337 | }) 338 | 339 | pwd := menu.AddAction(T("Password")) 340 | pwd.ConnectTriggered(func(b bool) { 341 | s.showMsg(T("Password"), T("There is no way to recover password. \nPlease write it on paper!")) 342 | }) 343 | 344 | support := menu.AddAction(T("Support Author")) 345 | support.ConnectTriggered(func(b bool) { 346 | s.showPay() 347 | }) 348 | 349 | } 350 | 351 | func (s *myWindow) setToolBar() { 352 | bar := widgets.NewQToolBar("File", nil) 353 | var icon *gui.QIcon 354 | 355 | icon = gui.NewQIcon5(":/qml/icons/document-new.jpg") 356 | s.newDiary = bar.AddAction2(icon, T("New")) 357 | s.newDiary.ConnectTriggered(func(b bool) { 358 | s.setStatusBar(T("New Diary")) 359 | now := time.Now() 360 | s.addDiary(now.Format("2006-01"), now.Format("02"), T("New Diary")+now.Format("2006-01-02")) 361 | }) 362 | icon = gui.NewQIcon5(":/qml/icons/document-save.jpg") 363 | s.saveDiary = bar.AddAction2(icon, T("Save")) 364 | s.saveDiary.SetToolTip(T("Save") + " Ctrl-S") 365 | s.saveDiary.SetShortcut(gui.QKeySequence_FromString("CTRL+s", gui.QKeySequence__NativeText)) 366 | 367 | s.window.AddToolBar2(bar) 368 | 369 | bar = widgets.NewQToolBar("Format", nil) 370 | icon = gui.NewQIcon5(":/qml/icons/h1.png") 371 | head1 := bar.AddAction2(icon, "H1") 372 | head1.SetToolTip(T("Header 1")) 373 | head1.ConnectTriggered(func(b bool) { 374 | s.setHeader(1) 375 | }) 376 | icon = gui.NewQIcon5(":/qml/icons/h2.png") 377 | head2 := bar.AddAction2(icon, "H2") 378 | head2.SetToolTip(T("Header 2")) 379 | head2.ConnectTriggered(func(b bool) { 380 | s.setHeader(2) 381 | }) 382 | icon = gui.NewQIcon5(":/qml/icons/h3.png") 383 | head3 := bar.AddAction2(icon, "H3") 384 | head3.SetToolTip(T("Header 3")) 385 | head3.ConnectTriggered(func(b bool) { 386 | s.setHeader(3) 387 | }) 388 | icon = gui.NewQIcon5(":/qml/icons/std.png") 389 | standard := bar.AddAction2(icon, T("Standard")) 390 | standard.ConnectTriggered(func(b bool) { 391 | var cfmt = gui.NewQTextCharFormat() 392 | cfmt.SetFont2(s.app.Font()) 393 | cfmt.SetFontPointSize(14) 394 | cfmt.SetForeground(gui.NewQBrush3(gui.NewQColor2(core.Qt__black), core.Qt__SolidPattern)) 395 | s.mergeFormatOnLineOrSelection(cfmt) 396 | }) 397 | 398 | //var icon *gui.QIcon 399 | 400 | icon = gui.NewQIcon5(":/qml/icons/B.png") 401 | s.actionTextBold = bar.AddAction2(icon, T("Bold")) 402 | s.actionTextBold.SetCheckable(true) 403 | s.actionTextBold.ConnectTriggered(func(checked bool) { 404 | s.textBold() 405 | }) 406 | 407 | icon = gui.NewQIcon5(":/qml/icons/I.png") 408 | s.actionTextItalic = bar.AddAction2(icon, T("Italic")) 409 | s.actionTextItalic.SetCheckable(true) 410 | s.actionTextItalic.ConnectTriggered(func(checked bool) { 411 | s.textItalic() 412 | }) 413 | 414 | icon = gui.NewQIcon5(":/qml/icons/U.png") 415 | s.actionTextUnderline = bar.AddAction2(icon, T("Underline")) 416 | s.actionTextUnderline.SetCheckable(true) 417 | s.actionTextUnderline.ConnectTriggered(func(checked bool) { 418 | s.textUnderline() 419 | }) 420 | 421 | icon = gui.NewQIcon5(":/qml/icons/S.png") 422 | s.actionStrikeOut = bar.AddAction2(icon, T("StrikeOut")) 423 | s.actionStrikeOut.SetCheckable(true) 424 | s.actionStrikeOut.ConnectTriggered(func(checked bool) { 425 | s.textStrikeOut() 426 | }) 427 | 428 | icon = gui.NewQIcon5(":/qml/icons/fg.png") 429 | actionTextColor := bar.AddAction2(icon, T("Text Color...")) 430 | actionTextColor.ConnectTriggered(func(checked bool) { 431 | s.textColor() 432 | }) 433 | 434 | icon = gui.NewQIcon5(":/qml/icons/bg.png") 435 | actionBgColor := bar.AddAction2(icon, T("Background Color...")) 436 | actionBgColor.ConnectTriggered(func(checked bool) { 437 | s.textBgColor() 438 | }) 439 | 440 | icon = gui.NewQIcon5(":/qml/icons/draw-eraser.png") 441 | actionClear := bar.AddAction2(icon, T("Clear Line Format...")) 442 | actionClear.ConnectTriggered(func(checked bool) { 443 | s.clearFormatAtCursor() 444 | }) 445 | 446 | s.addJustifyActions(bar) 447 | 448 | icon = gui.NewQIcon5(":/qml/icons/right.png") 449 | increaseIdent := bar.AddAction2(icon, T("Increase Ident")) 450 | increaseIdent.ConnectTriggered(func(checked bool) { 451 | s.addIndent(1) 452 | }) 453 | 454 | icon = gui.NewQIcon5(":/qml/icons/left.png") 455 | degreeIdent := bar.AddAction2(icon, T("Decrease Ident")) 456 | degreeIdent.ConnectTriggered(func(checked bool) { 457 | s.addIndent(-1) 458 | }) 459 | 460 | icon = gui.NewQIcon5(":/qml/icons/brush.png") 461 | s.fb = bar.AddAction2(icon, T("Format Brush")) 462 | s.fb.SetCheckable(true) 463 | s.fb.ConnectTriggered(func(checked bool) { 464 | cursor := s.editor.TextCursor() 465 | if !cursor.HasSelection() { 466 | cursor.Select(gui.QTextCursor__LineUnderCursor) 467 | } 468 | s.format.BF = cursor.BlockFormat() 469 | s.format.CF = cursor.CharFormat() 470 | s.fb.SetChecked(true) 471 | }) 472 | 473 | comboStyle := widgets.NewQComboBox(bar) 474 | bar.AddWidget(comboStyle) 475 | comboStyle.AddItems([]string{ 476 | T("Standard"), 477 | T("List (Disc)"), 478 | T("List (Circle)"), 479 | T("List (Square)"), 480 | T("List (Decimal)"), 481 | T("List (Alpha lower)"), 482 | T("List (Alpha upper)"), 483 | T("List (Roman lower)"), 484 | T("List (Roman upper)"), 485 | }) 486 | comboStyle.ConnectActivated(s.textStyle) 487 | 488 | s.window.AddToolBar2(bar) 489 | 490 | bar = widgets.NewQToolBar("Insert", nil) 491 | 492 | addImage := bar.AddAction(T("Insert Image")) 493 | addImage.SetToolTip(T("Insert Image")) 494 | addImage.ConnectTriggered(func(b bool) { 495 | s.insertImage() 496 | }) 497 | 498 | addTable := bar.AddAction(T("Insert Table")) 499 | addTable.SetToolTip(T("Insert Table")) 500 | addTable.ConnectTriggered(func(b bool) { 501 | s.insertTable() 502 | }) 503 | 504 | attach := bar.AddAction(T("Attach...")) 505 | attach.SetToolTip(T("Append Attachments")) 506 | attach.ConnectTriggered(func(b bool) { 507 | filename := widgets.QFileDialog_GetOpenFileName(s.window, T("Seclect File"), ".", "All (*)", "All (*)", widgets.QFileDialog__ReadOnly) 508 | if s.addAttachment(filename) { 509 | s.showAttachList() 510 | s.editor.Document().SetModified(true) 511 | //curDiary.Modified = true 512 | } 513 | 514 | }) 515 | 516 | font := bar.AddAction(T("Font")) 517 | font.SetToolTip(T("CurrentFont")) 518 | font.ConnectTriggered(func(b bool) { 519 | var ok bool 520 | f := widgets.QFontDialog_GetFont2(&ok, s.window) 521 | if ok { 522 | cfmt := gui.NewQTextCharFormat() 523 | cfmt.SetFont2(f) 524 | s.mergeFormatOnLineOrSelection(cfmt) 525 | } 526 | }) 527 | 528 | s.window.AddToolBar2(bar) 529 | } 530 | 531 | func (s *myWindow) setStatusBar(msg string) { 532 | bar := s.window.StatusBar() 533 | bar.ShowMessage(msg, 20000) 534 | } 535 | 536 | func (s *myWindow) setupComboAttachs() *widgets.QHBoxLayout { 537 | hbox := widgets.NewQHBoxLayout() 538 | 539 | s.comboAttachs = widgets.NewQComboBox(s.window) 540 | s.comboAttachs.SetSizePolicy2(widgets.QSizePolicy__Expanding, widgets.QSizePolicy__Fixed) 541 | list := []string{fmt.Sprintf("-- %s(%s:0) --", T("Select Attachments"), T("Total"))} 542 | s.comboAttachs.AddItems(list) 543 | expBtn := widgets.NewQPushButton2(T("Export As..."), s.window) 544 | expBtn.SetSizePolicy2(widgets.QSizePolicy__Fixed, widgets.QSizePolicy__Fixed) 545 | hbox.AddWidget(s.comboAttachs, 1, 0) 546 | hbox.AddWidget(expBtn, 1, 0) 547 | 548 | delBtn := widgets.NewQPushButton2(T("Remove"), s.window) 549 | delBtn.SetSizePolicy2(widgets.QSizePolicy__Fixed, widgets.QSizePolicy__Fixed) 550 | hbox.AddWidget(delBtn, 1, 0) 551 | 552 | expBtn.ConnectClicked(func(b bool) { 553 | idx := s.comboAttachs.CurrentIndex() 554 | if idx == 0 { 555 | return 556 | } 557 | idx -= 1 558 | dlg := widgets.NewQFileDialog(s.window, core.Qt__Dialog) 559 | filename := dlg.GetSaveFileName(s.window, T("Save File"), s.document.Attachments[idx].Name, "All (*)", "All (*)", 0) 560 | if len(filename) > 0 { 561 | ioutil.WriteFile(filename, s.document.Attachments[idx].Data, 0644) 562 | } 563 | 564 | }) 565 | 566 | delBtn.ConnectClicked(func(b bool) { 567 | if s.comboAttachs.CurrentIndex() == 0 { 568 | return 569 | } 570 | ret := widgets.QMessageBox_Question(s.window, T("Remove"), T("Are you sure?"), widgets.QMessageBox__Yes|widgets.QMessageBox__No, widgets.QMessageBox__Yes) 571 | if ret == widgets.QMessageBox__Yes { 572 | s.removeCurAttachment() 573 | } 574 | 575 | }) 576 | 577 | return hbox 578 | } 579 | 580 | func (s *myWindow) showAttachList() { 581 | list := []string{fmt.Sprintf("-- %s(%s:%d) --", T("Select Attachments"), T("Total"), len(s.document.Attachments))} 582 | for _, v := range s.document.Attachments { 583 | list = append(list, v.Name) 584 | } 585 | s.comboAttachs.Clear() 586 | s.comboAttachs.AddItems(list) 587 | } 588 | 589 | func (s *myWindow) clearAttachs() { 590 | s.comboAttachs.Clear() 591 | list := []string{fmt.Sprintf("-- %s(%s:0) --", T("Select Attachments"), T("Total"))} 592 | s.comboAttachs.AddItems(list) 593 | } 594 | 595 | func (s *myWindow) addAttachment(filename string) bool { 596 | name := filepath.Base(filename) 597 | data, err := ioutil.ReadFile(filename) 598 | if err != nil { 599 | log.Println(err) 600 | return false 601 | } 602 | s.document.Attachments = append(s.document.Attachments, attachmentFile{Name: name, Data: data}) 603 | return true 604 | } 605 | 606 | func (s *myWindow) Create(app *widgets.QApplication) { 607 | 608 | font := gui.NewQFont() 609 | font.SetPointSize(12) 610 | font.SetFamily("Serif") 611 | font.SetStyleName("Regular") 612 | app.SetFont(font, "serif-regular") 613 | s.app = app 614 | 615 | charW := s.charWidth() 616 | 617 | s.window = widgets.NewQMainWindow(nil, core.Qt__Window) 618 | s.window.SetWindowTitle(T("UserName")) 619 | s.window.SetWindowIcon(gui.NewQIcon5(":/qml/icons/Sd.png")) 620 | 621 | s.curDiary = new(diaryPointer) 622 | 623 | spliter := widgets.NewQSplitter2(core.Qt__Horizontal, s.window) 624 | 625 | s.window.SetCentralWidget(spliter) 626 | 627 | leftArea := s.createLeftArea(charW) 628 | 629 | spliter.AddWidget(leftArea) 630 | 631 | editor := s.createEditor(charW) 632 | 633 | spliter.AddWidget(editor) 634 | 635 | spliter.SetStretchFactor(0, 1) 636 | spliter.SetStretchFactor(1, 2) 637 | 638 | s.setToolBar() 639 | 640 | s.setMenuBar() 641 | 642 | app.SetActiveWindow(s.window) 643 | app.SetApplicationDisplayName(T("Secret Diary")) 644 | app.SetApplicationName("sdiary") 645 | app.SetApplicationVersion(version) 646 | 647 | s.setEditorFuncs() 648 | s.setTreeFuncs() 649 | 650 | once := sync.Once{} 651 | s.window.ConnectShowEvent(func(e *gui.QShowEvent) { 652 | once.Do(func() { 653 | go makeShortcut(false) 654 | s.login() 655 | }) 656 | }) 657 | 658 | s.window.ConnectCloseEvent(func(e *gui.QCloseEvent) { 659 | if s.editor.Document().IsModified() { 660 | ret := widgets.QMessageBox_Question(s.window, T("Close"), T("Do you want to save the document?"), widgets.QMessageBox__Yes|widgets.QMessageBox__No, widgets.QMessageBox__Yes) 661 | if ret == widgets.QMessageBox__Yes { 662 | s.saveCurDiary() 663 | } 664 | } 665 | 666 | s.db.Close() 667 | }) 668 | s.window.ShowMaximized() 669 | } 670 | 671 | func (s *myWindow) createLeftArea(charW int) widgets.QWidget_ITF { 672 | spliter := widgets.NewQSplitter(nil) 673 | spliter.SetOrientation(core.Qt__Vertical) 674 | spliter.SetSizePolicy2(widgets.QSizePolicy__Preferred, widgets.QSizePolicy__Expanding) 675 | 676 | //mwidth := charW * 18 677 | 678 | s.tree = widgets.NewQTreeView(s.window) 679 | //s.tree.SetMaximumWidth(mwidth) 680 | s.tree.SetSizePolicy2(widgets.QSizePolicy__Preferred, widgets.QSizePolicy__Expanding) 681 | s.tree.SetHorizontalScrollBarPolicy(core.Qt__ScrollBarAsNeeded) 682 | s.tree.SetAutoScroll(true) 683 | s.model = gui.NewQStandardItemModel2(0, 1, s.tree) 684 | s.clearModel() 685 | 686 | s.tree.SetModel(s.model) 687 | spliter.AddWidget(s.tree) 688 | 689 | search := widgets.NewQWidget(nil, core.Qt__Widget) 690 | grid := newGridLayout(search) 691 | keyword := widgets.NewQLineEdit(search) 692 | grid.AddWidget2(keyword, 0, 0, 0) 693 | 694 | btn := widgets.NewQPushButton2(T("Search"), search) 695 | grid.AddWidget2(btn, 0, 1, 0) 696 | btn.ConnectClicked(func(b bool) { 697 | s.searchFromDb(strings.TrimSpace(keyword.Text())) 698 | }) 699 | keyword.ConnectEditingFinished(func() { 700 | s.searchFromDb(strings.TrimSpace(keyword.Text())) 701 | }) 702 | 703 | s.treeFind = widgets.NewQTreeView(s.window) 704 | 705 | s.treeFind.SetSizePolicy2(widgets.QSizePolicy__Preferred, widgets.QSizePolicy__Preferred) 706 | s.treeFind.SetHorizontalScrollBarPolicy(core.Qt__ScrollBarAsNeeded) 707 | s.treeFind.SetAutoScroll(true) 708 | s.modelFind = gui.NewQStandardItemModel2(0, 1, s.treeFind) 709 | s.clearModelFind() 710 | s.treeFind.SetModel(s.modelFind) 711 | grid.AddWidget3(s.treeFind, 1, 0, 1, 2, 0) 712 | search.SetLayout(grid) 713 | 714 | spliter.AddWidget(search) 715 | 716 | s.setTreeFindFuncs() 717 | return spliter 718 | } 719 | 720 | func (s *myWindow) createEditor(charW int) widgets.QWidget_ITF { 721 | editorLayout := widgets.NewQVBoxLayout() 722 | 723 | scrollarea := widgets.NewQScrollArea(s.window) 724 | scrollarea.SetHorizontalScrollBarPolicy(core.Qt__ScrollBarAsNeeded) 725 | scrollarea.SetVerticalScrollBarPolicy(core.Qt__ScrollBarAsNeeded) 726 | scrollarea.SetAlignment(core.Qt__AlignCenter) 727 | scrollarea.SetSizePolicy2(widgets.QSizePolicy__Expanding, widgets.QSizePolicy__Expanding) 728 | frame := widgets.NewQFrame(s.window, core.Qt__Widget) 729 | grid := newGridLayout(frame) 730 | s.editor = widgets.NewQTextEdit(s.window) 731 | s.editor.SetSizePolicy2(widgets.QSizePolicy__Fixed, widgets.QSizePolicy__Expanding) 732 | s.editor.SetTabChangesFocus(false) 733 | s.editor.SetReadOnly(true) 734 | s.editor.SetStyleSheet("QTextEdit{background-color:rgba(255,255,255,255);border:1px;border-style:solid;border-color:#000000}") 735 | 736 | width := charW * 30 737 | 738 | s.editor.SetTabStopWidth(40) 739 | s.editor.SetFixedWidth(width) 740 | 741 | scrollarea.SetMinimumWidth(width + 40) 742 | 743 | s.editor.SetSizePolicy2(widgets.QSizePolicy__Fixed, widgets.QSizePolicy__Expanding) 744 | 745 | scrollarea.ConnectResizeEvent(func(e *gui.QResizeEvent) { 746 | frame.SetFixedSize(core.NewQSize2(width+40, scrollarea.Geometry().Height())) 747 | scrollarea.ActivateWindow() 748 | }) 749 | 750 | grid.AddWidget2(s.editor, 0, 0, 0) 751 | 752 | comboBox := s.setupComboAttachs() 753 | //grid.AddLayout(comboBox, 1, 0, 0) 754 | 755 | //grid.SetAlign(core.Qt__AlignHCenter) 756 | frame.SetLayout(grid) 757 | scrollarea.SetWidget(frame) 758 | 759 | editorLayout.AddWidget(scrollarea, 1, 0) 760 | editorLayout.AddLayout(comboBox, 1) 761 | 762 | res := widgets.NewQWidget(s.window, core.Qt__Widget) 763 | res.SetLayout(editorLayout) 764 | 765 | return res 766 | } 767 | 768 | func (s *myWindow) saveCurDiary() { 769 | if s.editor.Document().IsModified() == false { 770 | s.setStatusBar(T("No Diary Saved")) 771 | return 772 | } 773 | //fmt.Println("save", curDiary.Item.AccessibleText(), s.editor.ToHtml()) 774 | var first bool = false 775 | if len(s.curDiary.Item.AccessibleText()) == 0 { 776 | p := s.curDiary.Item.Parent() 777 | pos := s.curDiary.Item.Index() 778 | item := p.TakeChild(pos.Row(), pos.Column()) 779 | item.SetAccessibleText(fmt.Sprintf("%d", s.db.NextId())) 780 | p.SetChild(pos.Row(), pos.Column(), item) 781 | s.curDiary.Item = item 782 | first = true 783 | } 784 | 785 | filename := s.curDiary.Item.AccessibleText() + ".dat" 786 | id, err := strconv.Atoi(s.curDiary.Item.AccessibleText()) 787 | if err != nil { 788 | s.setStatusBar(T("Error ID")) 789 | return 790 | } 791 | vs := strings.SplitN(s.curDiary.Item.Text(), "-", 2) 792 | title := vs[1] 793 | //fmt.Println(filename, title) 794 | if first { 795 | s.db.AddDiary(id, time.Now().Format("2006-01-02"), title, filename) 796 | } else { 797 | s.db.UpdateDiaryTitle(id, title) 798 | } 799 | 800 | encodeToFile(s.getRichText(), filename, s.key) 801 | s.setStatusBar(T("Save Diary") + fmt.Sprintf(" %s(%s)", title, filename)) 802 | 803 | s.editor.Document().SetModified(false) 804 | } 805 | 806 | func (s *myWindow) addDiary(yearMonth, day, title string) { 807 | if s.curDiary.Item != nil { 808 | s.saveCurDiary() 809 | } 810 | 811 | month := s.addYearMonth(yearMonth) 812 | month.SetAccessibleText("1") 813 | month.SetAccessibleDescription("1") 814 | diary := gui.NewQStandardItem2(fmt.Sprintf("%s-%s", day, title)) 815 | diary.SetEditable(false) 816 | diary.SetAccessibleText("") 817 | diary.SetAccessibleDescription("0") 818 | 819 | //month.AppendRow2(diary) 820 | month.InsertRow2(0, diary) 821 | 822 | s.tree.SetCurrentIndex(diary.Index()) 823 | 824 | s.curDiary.Item = diary 825 | s.curDiary.Day = day 826 | s.curDiary.YearMonth = yearMonth 827 | 828 | s.document.Attachments = []attachmentFile{} 829 | s.document.Images = make(map[string][]byte) 830 | 831 | s.editor.SetReadOnly(false) 832 | s.exportEnc.SetEnabled(true) 833 | s.exportPdf.SetEnabled(true) 834 | s.exportOdt.SetEnabled(true) 835 | 836 | s.setTitle(title) 837 | s.clearAttachs() 838 | 839 | s.tree.Expand(month.Index()) 840 | } 841 | 842 | func (s *myWindow) addDiary2(id, day, title, mtime string, r, c int) { 843 | item := s.model.Item(r, c) 844 | diary := gui.NewQStandardItem2(fmt.Sprintf("%s-%s", day, title)) 845 | diary.SetEditable(false) 846 | diary.SetAccessibleText(id) 847 | diary.SetAccessibleDescription("0") 848 | diary.SetToolTip(T("Last Modified:") + mtime) 849 | 850 | item.AppendRow2(diary) 851 | 852 | } 853 | 854 | func (s *myWindow) addYearMonthsFromDb() { 855 | s.setStatusBar(T("Loading Diary List...")) 856 | s.editor.Document().Clear() 857 | s.editor.Document().SetModified(false) 858 | s.editor.SetReadOnly(true) 859 | s.curDiary.zero() 860 | 861 | bridge := NewQmlBridge(s.window) 862 | var wg sync.WaitGroup 863 | bridge.ConnectAddYearMonth(func(ym string) { 864 | s.addYearMonth(ym) 865 | wg.Done() 866 | }) 867 | bridge.ConnectAddDiary(func(id, day, title, mtime string, r, c int) { 868 | item := s.model.Item(r, c) 869 | diary := gui.NewQStandardItem2(fmt.Sprintf("%s-%s", day, title)) 870 | diary.SetEditable(false) 871 | diary.SetAccessibleText(id) 872 | diary.SetAccessibleDescription("0") 873 | diary.SetToolTip(T("Last Modified:") + mtime) 874 | 875 | item.AppendRow2(diary) 876 | wg.Done() 877 | }) 878 | bridge.ConnectSetMonthFlag(func(r, c int) { 879 | item := s.model.TakeItem(r, c) 880 | item.SetAccessibleText("1") 881 | item.SetAccessibleDescription("1") 882 | s.model.SetItem(r, c, item) 883 | s.tree.ResizeColumnToContents(0) 884 | if r < 2 { 885 | s.tree.Expand(item.Index()) 886 | } 887 | }) 888 | go func() { 889 | yms, err := s.db.GetYearMonths() 890 | if err != nil { 891 | return 892 | } 893 | for i := 0; i < len(yms); i++ { 894 | wg.Add(1) 895 | bridge.AddYearMonth(yms[i]) 896 | } 897 | wg.Wait() 898 | pidx := s.tree.RootIndex() 899 | for r := 0; r < s.model.RowCount(pidx); r++ { 900 | if r > 2 { 901 | break 902 | } 903 | c := 0 904 | item := s.model.Item(r, c) 905 | items, err := s.db.GetListFromYearMonth(item.Text()) 906 | if err != nil { 907 | log.Println(err) 908 | return 909 | } 910 | for i := 0; i < len(items); i++ { 911 | wg.Add(1) 912 | bridge.AddDiary(strconv.Itoa(items[i].Id), items[i].Day, items[i].Title, items[i].MTime, r, c) 913 | } 914 | wg.Wait() 915 | bridge.SetMonthFlag(r, c) 916 | 917 | } 918 | }() 919 | 920 | return 921 | } 922 | 923 | func (s *myWindow) setMonthFlag(r, c int) { 924 | item := s.model.TakeItem(r, c) 925 | item.SetAccessibleText("1") 926 | item.SetAccessibleDescription("1") 927 | s.model.SetItem(r, c, item) 928 | s.tree.ResizeColumnToContents(0) 929 | if r < 2 { 930 | s.tree.Expand(item.Index()) 931 | } 932 | } 933 | 934 | func (s *myWindow) addYearMonth(yearMonth string) *gui.QStandardItem { 935 | idx := core.NewQModelIndex() 936 | p := s.model.Parent(idx) 937 | n := s.model.RowCount(p) 938 | for i := 0; i < n; i++ { 939 | item := s.model.Item(i, 0) 940 | if item.Text() == yearMonth { 941 | return item 942 | } 943 | } 944 | item := gui.NewQStandardItem2(yearMonth) 945 | item.SetColumnCount(1) 946 | item.SetEditable(false) 947 | item.SetAccessibleText("") 948 | 949 | s.model.InsertRow2(0, item) 950 | 951 | return item 952 | } 953 | 954 | func (s *myWindow) selectDiary(idx *core.QModelIndex) { 955 | diary := s.model.ItemFromIndex(idx) 956 | if diary.Pointer() == s.curDiary.Item.Pointer() { 957 | return 958 | } 959 | if len(diary.AccessibleText()) == 0 { 960 | return 961 | } 962 | filename := diary.AccessibleText() + ".dat" 963 | data, err := decodeFromFile(filename, s.key) 964 | 965 | if err != nil { 966 | //log.Println(err) 967 | s.getQText(data) 968 | if s.curDiary.Item != nil { 969 | s.saveCurDiary() 970 | } 971 | s.curDiary.Item = diary 972 | //curDiary.Modified = false 973 | s.curDiary.YearMonth = diary.Parent().Text() 974 | vs := strings.Index(diary.Text(), "-") 975 | s.curDiary.Day = diary.Text()[:vs] 976 | s.setTitle(diary.Text()[vs+1:]) 977 | 978 | } else { 979 | if s.curDiary.Item != nil { 980 | s.saveCurDiary() 981 | } 982 | s.getQText(data) 983 | s.curDiary.Item = diary 984 | //curDiary.Modified = false 985 | s.curDiary.YearMonth = diary.Parent().Text() 986 | vs := strings.Index(diary.Text(), "-") 987 | s.curDiary.Day = diary.Text()[:vs] 988 | s.editor.Document().SetHtml(s.document.Html) 989 | 990 | s.showAttachList() 991 | s.editor.Document().SetModified(false) 992 | } 993 | s.tree.SetCurrentIndex(diary.Index()) 994 | s.editor.SetReadOnly(false) 995 | s.exportEnc.SetEnabled(true) 996 | s.exportPdf.SetEnabled(true) 997 | s.exportOdt.SetEnabled(true) 998 | 999 | } 1000 | 1001 | func (s *myWindow) popupOnMonth(item *gui.QStandardItem, point *core.QPoint) { 1002 | s.LockEditor() 1003 | menu := widgets.NewQMenu(s.tree) 1004 | refreshItem := menu.AddAction(T("Refresh")) 1005 | refreshItem.ConnectTriggered(func(checked bool) { 1006 | items, err := s.db.GetListFromYearMonth(item.Text()) 1007 | idx := item.Index() 1008 | item.RemoveRows(0, item.RowCount()) 1009 | r, c := idx.Row(), idx.Column() 1010 | if err != nil { 1011 | log.Println(err) 1012 | return 1013 | } 1014 | for i := 0; i < len(items); i++ { 1015 | id := strconv.Itoa(items[i].Id) 1016 | day := items[i].Day 1017 | title := items[i].Title 1018 | mtime := items[i].MTime 1019 | 1020 | diary := gui.NewQStandardItem2(fmt.Sprintf("%s-%s", day, title)) 1021 | diary.SetEditable(false) 1022 | diary.SetAccessibleText(id) 1023 | diary.SetAccessibleDescription("0") 1024 | diary.SetToolTip(T("Last Modified:") + mtime) 1025 | 1026 | item.AppendRow2(diary) 1027 | } 1028 | 1029 | s.setMonthFlag(r, c) 1030 | s.tree.Expand(idx) 1031 | }) 1032 | menu.Popup(point, nil) 1033 | } 1034 | 1035 | func (s *myWindow) diaryPopup(idx *core.QModelIndex, e *gui.QMouseEvent) { 1036 | //fmt.Println("popup") 1037 | diary := s.model.ItemFromIndex(s.tree.CurrentIndex()) 1038 | switch diary.AccessibleDescription() { 1039 | case "0": 1040 | s.selectDiary(idx) 1041 | case "1": 1042 | s.popupOnMonth(diary, e.GlobalPos()) 1043 | return 1044 | default: 1045 | return 1046 | } 1047 | 1048 | menu := widgets.NewQMenu(s.tree) 1049 | delItem := menu.AddAction(T("Delete")) 1050 | delItem.ConnectTriggered(func(checked bool) { 1051 | dlg := widgets.NewQMessageBox(s.window) 1052 | dlg.SetWindowTitle(T("Confirm Delete")) 1053 | dlg.SetText(T("Are you sure?")) 1054 | dlg.SetIcon(widgets.QMessageBox__Question) 1055 | dlg.SetStandardButtons(widgets.QMessageBox__Yes | widgets.QMessageBox__Cancel) 1056 | ret := dlg.Exec() 1057 | if ret == int(widgets.QMessageBox__Yes) { 1058 | id := diary.AccessibleText() 1059 | if len(id) > 0 { 1060 | s.db.RemoveDiary(id) 1061 | } 1062 | s.curDiary.zero() 1063 | 1064 | //curDiary.Modified = false 1065 | p := diary.Parent() 1066 | p.RemoveRow(diary.Row()) 1067 | 1068 | } 1069 | 1070 | }) 1071 | 1072 | openItem := menu.AddAction(T("Open in New Window")) 1073 | openItem.ConnectTriggered(func(checked bool) { 1074 | id, err := strconv.Atoi(diary.AccessibleText()) 1075 | if err != nil { 1076 | s.setStatusBar(err.Error()) 1077 | return 1078 | } 1079 | OpenDiaryNewWindow(s, id) 1080 | }) 1081 | 1082 | category := menu.AddAction(T("Change Category")) 1083 | category.ConnectTriggered(func(checked bool) { 1084 | id, err := strconv.Atoi(diary.AccessibleText()) 1085 | if err != nil { 1086 | s.setStatusBar(err.Error()) 1087 | return 1088 | } 1089 | dict := s.db.GetCategories() 1090 | dict1 := make(map[string]int) 1091 | dict1[T("Default")] = 0 1092 | items := []string{T("Default")} 1093 | for k, v := range dict { 1094 | dict1[v] = k 1095 | items = append(items, v) 1096 | } 1097 | var ok bool 1098 | ret := widgets.QInputDialog_GetItem(s.window, T("Category"), T("Select a category"), items, 0, false, &ok, core.Qt__Dialog, 0) 1099 | if ok && dict1[ret] != s.category { 1100 | s.db.UpdateDiaryCategory(id, dict1[ret]) 1101 | s.model.RemoveRow(diary.Row(), diary.Index().Parent()) 1102 | } 1103 | 1104 | }) 1105 | 1106 | menu.QWidget.AddAction(s.exportEnc) 1107 | 1108 | menu.QWidget.AddAction(s.exportPdf) 1109 | menu.QWidget.AddAction(s.exportOdt) 1110 | 1111 | menu.Popup(e.GlobalPos(), nil) 1112 | } 1113 | 1114 | func (s *myWindow) LockEditor() { 1115 | s.saveCurDiary() 1116 | s.curDiary.zero() 1117 | s.editor.Clear() 1118 | s.editor.SetReadOnly(true) 1119 | s.editor.Document().SetModified(false) 1120 | } 1121 | 1122 | func (s *myWindow) onSelectItem(idx *core.QModelIndex) { 1123 | item := s.model.ItemFromIndex(idx) 1124 | loadChildren := func() { 1125 | items, err := s.db.GetListFromYearMonth(item.Text()) 1126 | r, c := idx.Row(), idx.Column() 1127 | if err != nil { 1128 | log.Println(err) 1129 | return 1130 | } 1131 | for i := 0; i < len(items); i++ { 1132 | s.addDiary2(strconv.Itoa(items[i].Id), items[i].Day, items[i].Title, items[i].MTime, r, c) 1133 | } 1134 | 1135 | s.setMonthFlag(r, c) 1136 | //s.tree.Expand(idx) 1137 | } 1138 | //fmt.Println("AD:", item.AccessibleDescription()) 1139 | switch item.AccessibleDescription() { 1140 | case "0": 1141 | s.selectDiary(idx) 1142 | case "1": 1143 | s.LockEditor() 1144 | if item.HasChildren() == false { 1145 | loadChildren() 1146 | } else { 1147 | s.tree.ClearSelection() 1148 | } 1149 | 1150 | default: 1151 | loadChildren() 1152 | } 1153 | } 1154 | 1155 | func (s *myWindow) CollapseOrExpand(idx *core.QModelIndex) { 1156 | item := s.model.ItemFromIndex(idx) 1157 | if item.AccessibleDescription() == "1" { 1158 | if s.tree.IsExpanded(idx) { 1159 | s.tree.Collapse(idx) 1160 | } else { 1161 | s.tree.Expand(idx) 1162 | } 1163 | } 1164 | 1165 | } 1166 | 1167 | var expandTime = time.Now() 1168 | 1169 | func (s *myWindow) setTreeFuncs() { 1170 | 1171 | s.tree.SetSelectionMode(widgets.QAbstractItemView__SingleSelection) 1172 | 1173 | s.tree.DisconnectMousePressEvent() 1174 | s.tree.DisconnectMouseDoubleClickEvent() 1175 | s.tree.DisconnectMouseReleaseEvent() 1176 | 1177 | s.tree.ConnectMouseReleaseEvent(func(e *gui.QMouseEvent) { 1178 | idx := s.tree.IndexAt(e.Pos()) 1179 | if !idx.IsValid() { 1180 | return 1181 | } 1182 | s.tree.SetCurrentIndex(idx) 1183 | switch e.Button() { 1184 | case core.Qt__LeftButton: 1185 | if time.Now().After(expandTime.Add(time.Millisecond * 500)) { 1186 | expandTime = time.Now() 1187 | s.CollapseOrExpand(idx) 1188 | } 1189 | 1190 | case core.Qt__RightButton: 1191 | s.diaryPopup(idx, e) 1192 | } 1193 | 1194 | }) 1195 | 1196 | s.tree.ConnectExpanded(func(idx *core.QModelIndex) { 1197 | expandTime = time.Now() 1198 | }) 1199 | 1200 | s.tree.ConnectCollapsed(func(idx *core.QModelIndex) { 1201 | expandTime = time.Now() 1202 | }) 1203 | 1204 | s.tree.ConnectSelectionChanged(func(selected *core.QItemSelection, deselected *core.QItemSelection) { 1205 | idxes := selected.Indexes() 1206 | if len(idxes) == 1 { 1207 | s.onSelectItem(idxes[0]) 1208 | } 1209 | 1210 | }) 1211 | 1212 | } 1213 | 1214 | func (s *myWindow) setTitle(v string) { 1215 | s.editor.Clear() 1216 | s.editor.Document().Clear() 1217 | var cfmt = gui.NewQTextCharFormat() 1218 | cfmt.SetFont2(s.app.Font()) 1219 | cfmt.SetFontPointSize(18) 1220 | cfmt.SetForeground(gui.NewQBrush3(gui.NewQColor2(core.Qt__blue), core.Qt__SolidPattern)) 1221 | 1222 | s.editor.SetFontPointSize(18) 1223 | 1224 | s.editor.SetAlignment(core.Qt__AlignCenter) 1225 | 1226 | cursor := s.editor.TextCursor() 1227 | cursor.SetCharFormat(cfmt) 1228 | cursor.InsertText(v) 1229 | 1230 | cursor.InsertText("\n") 1231 | s.editor.SetTextCursor(cursor) 1232 | 1233 | var afmt = gui.NewQTextCharFormat() 1234 | afmt.SetForeground(gui.NewQBrush3(gui.NewQColor2(core.Qt__black), core.Qt__SolidPattern)) 1235 | afmt.SetFont2(s.app.Font()) 1236 | afmt.SetFontPointSize(14) 1237 | 1238 | s.editor.SetAlignment(core.Qt__AlignLeft | core.Qt__AlignAbsolute) 1239 | 1240 | cursor.InsertText("\n\t") 1241 | s.mergeFormatOnLineOrSelection(afmt) 1242 | s.editor.SetTextCursor(cursor) 1243 | } 1244 | 1245 | func (s *myWindow) setEditorFuncs() { 1246 | 1247 | s.saveDiary.ConnectTriggered(func(b bool) { 1248 | if s.model != nil { 1249 | s.saveCurDiary() 1250 | } else { 1251 | s.saveDiaryAlone() 1252 | } 1253 | 1254 | }) 1255 | if s.model != nil { 1256 | s.editor.ConnectTextChanged(s.OnTextChanged) 1257 | } 1258 | 1259 | s.editor.ConnectContextMenuEvent(func(e *gui.QContextMenuEvent) { 1260 | 1261 | menu := s.editor.CreateStandardContextMenu() 1262 | 1263 | imgUrl := s.getSelectedImage() 1264 | if len(imgUrl) > 0 { 1265 | //addaction export image 1266 | act := menu.AddAction(T("Export Image As...")) 1267 | act.ConnectTriggered(func(b bool) { 1268 | ext := filepath.Ext(imgUrl) 1269 | filter := fmt.Sprintf("%s Image (*%s)", ext, ext) 1270 | filename := widgets.QFileDialog_GetSaveFileName(s.window, T("Export Image As..."), filepath.Base(imgUrl), filter, filter, 0) 1271 | if strings.HasSuffix(filename, ext) == false { 1272 | filename = filename + ext 1273 | } 1274 | 1275 | if len(filename) > 0 { 1276 | ioutil.WriteFile(filename, s.document.Images[imgUrl], 0644) 1277 | } 1278 | }) 1279 | 1280 | actScale := menu.AddAction(T("Scale Image")) 1281 | actScale.ConnectTriggered(func(b bool) { 1282 | v := s.editor.Document().Resource(int(gui.QTextDocument__ImageResource), core.NewQUrl3(imgUrl, core.QUrl__TolerantMode)) 1283 | img := gui.NewQImageFromPointer(v.ToImage()) 1284 | res := s.scaleImage(img) 1285 | 1286 | s.editor.Document().AddResource(int(gui.QTextDocument__ImageResource), core.NewQUrl3(imgUrl, core.QUrl__TolerantMode), res.ToVariant()) 1287 | //save data 1288 | ba := core.NewQByteArray() 1289 | iod := core.NewQBuffer2(ba, nil) 1290 | iod.Open(core.QIODevice__WriteOnly) 1291 | 1292 | ok := res.Save2(iod, filepath.Ext(filepath.Base(imgUrl))[1:], -1) 1293 | //fmt.Println(filepath.Ext(filename)) 1294 | if ok { 1295 | s.document.Images[imgUrl] = []byte(ba.Data()) 1296 | } 1297 | s.editor.Document().SetModified(true) 1298 | }) 1299 | } 1300 | 1301 | //add search 1302 | menu.QWidget.AddAction(s.paste) 1303 | menu.QWidget.AddAction(s.search) 1304 | menu.QWidget.AddAction(s.replace) 1305 | 1306 | menu.QWidget.AddAction(s.lineMargin) 1307 | 1308 | menu.Popup(e.GlobalPos(), nil) 1309 | }) 1310 | 1311 | s.editor.ConnectMouseReleaseEvent(func(e *gui.QMouseEvent) { 1312 | defer e.Accept() 1313 | if s.format.BF == nil { 1314 | return 1315 | } 1316 | cursor := s.editor.TextCursor() 1317 | if !cursor.HasSelection() { 1318 | cursor.Select(gui.QTextCursor__BlockUnderCursor) 1319 | } 1320 | cursor.SetBlockFormat(s.format.BF) 1321 | cursor.SetCharFormat(s.format.CF) 1322 | s.format.BF = nil 1323 | s.format.CF = nil 1324 | s.fb.SetChecked(false) 1325 | }) 1326 | 1327 | onEnterOrReturn := func() { 1328 | cursor := s.editor.TextCursor() 1329 | cursor.Select(gui.QTextCursor__BlockUnderCursor) 1330 | //fmt.Printf("B:%#v\n", cursor.SelectedText()) 1331 | txt := cursor.SelectedText() 1332 | r1 := regexp.MustCompile("^(\t+).*$") 1333 | tabs := r1.FindStringSubmatch(strings.TrimLeft(txt, "\u2029")) 1334 | if len(tabs) > 1 { 1335 | s.editor.InsertPlainTextDefault("\n" + tabs[1]) 1336 | } else { 1337 | s.editor.InsertPlainTextDefault("\n") 1338 | } 1339 | } 1340 | s.editor.ConnectKeyPressEvent(func(e *gui.QKeyEvent) { 1341 | switch core.Qt__Key(e.Key()) { 1342 | case core.Qt__Key_Enter: 1343 | onEnterOrReturn() 1344 | case core.Qt__Key_Return: 1345 | onEnterOrReturn() 1346 | default: 1347 | s.editor.KeyPressEventDefault(e) 1348 | } 1349 | }) 1350 | } 1351 | 1352 | func (s *myWindow) OnTextChanged() { 1353 | if s.curDiary.Item == nil { 1354 | return 1355 | } 1356 | 1357 | disp1 := s.curDiary.Day + "-" + strings.TrimSpace(s.editor.Document().FirstBlock().Text()) 1358 | 1359 | disp0 := s.curDiary.Item.Text() 1360 | if disp0 != disp1 { 1361 | p := s.curDiary.Item.Parent() 1362 | pos := s.curDiary.Item.Index() 1363 | s.curDiary.Item = p.TakeChild(pos.Row(), pos.Column()) 1364 | s.curDiary.Item.SetText(disp1) 1365 | p.SetChild(pos.Row(), pos.Column(), s.curDiary.Item) 1366 | s.tree.SetCurrentIndex(s.curDiary.Item.Index()) 1367 | s.tree.ResizeColumnToContents(0) 1368 | if s.editor.CurrentCharFormat().FontPointSize() != 18 { 1369 | var cfmt = gui.NewQTextCharFormat() 1370 | cfmt.SetFont2(s.app.Font()) 1371 | cfmt.SetFontPointSize(18) 1372 | cfmt.SetForeground(gui.NewQBrush3(gui.NewQColor2(core.Qt__blue), core.Qt__SolidPattern)) 1373 | s.mergeFormatOnLineOrSelection(cfmt) 1374 | } 1375 | 1376 | s.editor.Document().SetModified(true) 1377 | } 1378 | 1379 | } 1380 | 1381 | //return url or "" 1382 | func (s *myWindow) getSelectedImage() (url string) { 1383 | cursor := s.editor.TextCursor() 1384 | if cursor == nil { 1385 | return "" 1386 | } 1387 | 1388 | fragment := cursor.Selection() 1389 | if fragment.IsEmpty() { 1390 | return "" 1391 | } 1392 | 1393 | html := fragment.ToHtml(core.NewQByteArray2("UTF-8", 5)) 1394 | reg1, err := regexp.Compile(`.+`) 1395 | if err != nil { 1396 | log.Println(err) 1397 | return "" 1398 | } 1399 | html = reg1.FindString(html) 1400 | 1401 | reg2, err := regexp.Compile(``) 1402 | if err != nil { 1403 | log.Println(err) 1404 | return "" 1405 | } 1406 | 1407 | urls := reg2.FindAllStringSubmatch(html, -1) 1408 | 1409 | if len(urls) != 1 { 1410 | return "" 1411 | } 1412 | 1413 | return urls[0][1] 1414 | } 1415 | 1416 | func (s *myWindow) clearModel() { 1417 | s.model.Clear() 1418 | s.model.SetHorizontalHeaderLabels([]string{T("Diary List") + " - " + s.categoryName}) 1419 | } 1420 | 1421 | func (s *myWindow) clearModelFind() { 1422 | s.modelFind.Clear() 1423 | s.modelFind.SetHorizontalHeaderLabels([]string{T("Result List") + " - " + s.categoryName}) 1424 | } 1425 | 1426 | func (s *myWindow) lastUser() string { 1427 | v, err := ioutil.ReadFile(path.Join(datDir, "user.txt")) 1428 | if err != nil { 1429 | return "" 1430 | } 1431 | return string(v) 1432 | } 1433 | 1434 | func (s *myWindow) saveLastUser(name string) { 1435 | path1 := path.Join(datDir, "user.txt") 1436 | ioutil.WriteFile(path1, []byte(name), 0644) 1437 | } 1438 | 1439 | func (s *myWindow) mergeFormatOnLineOrSelection(format *gui.QTextCharFormat) *gui.QTextCursor { 1440 | var cursor = s.editor.TextCursor() 1441 | if !cursor.HasSelection() { 1442 | cursor.Select(gui.QTextCursor__LineUnderCursor) 1443 | } 1444 | cursor.MergeCharFormat(format) 1445 | s.editor.MergeCurrentCharFormat(format) 1446 | 1447 | return cursor 1448 | } 1449 | 1450 | func (s *myWindow) rename() { 1451 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 1452 | dlg.SetWindowTitle(T("Rename")) 1453 | 1454 | grid := newGridLayout(dlg) 1455 | 1456 | name := widgets.NewQLabel2(T("New Name:"), dlg, core.Qt__Widget) 1457 | grid.AddWidget2(name, 0, 0, 0) 1458 | 1459 | nameInput := widgets.NewQLineEdit(dlg) 1460 | grid.AddWidget2(nameInput, 0, 1, 0) 1461 | 1462 | okAct := widgets.NewQPushButton2(T("OK"), dlg) 1463 | grid.AddWidget2(okAct, 1, 0, 0) 1464 | cancelAct := widgets.NewQPushButton2(T("Cancel"), dlg) 1465 | grid.AddWidget2(cancelAct, 1, 1, 0) 1466 | 1467 | okAct.ConnectClicked(func(checked bool) { 1468 | ret := widgets.QMessageBox_Question(dlg, T("Confirm"), T("Are you sure?"), widgets.QMessageBox__Yes|widgets.QMessageBox__Cancel, widgets.QMessageBox__Yes) 1469 | //fmt.Println(ret) 1470 | if ret == widgets.QMessageBox__Yes { 1471 | if len(nameInput.Text()) == 0 { 1472 | return 1473 | } 1474 | s.db.Close() 1475 | dstName := path.Join(path.Dir(dataDir), nameInput.Text()) 1476 | err := os.Rename(dataDir, dstName) 1477 | if err == nil { 1478 | widgets.QMessageBox_Information(dlg, T("Information"), T("Success,Please login again."), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1479 | 1480 | } else { 1481 | widgets.QMessageBox_Information(dlg, T("Information"), T("Fail,Please login again."), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1482 | } 1483 | } 1484 | dlg.Hide() 1485 | s.window.Close() 1486 | }) 1487 | 1488 | cancelAct.ConnectClicked(func(c bool) { 1489 | dlg.Hide() 1490 | dlg.Destroy(true, true) 1491 | }) 1492 | 1493 | dlg.SetLayout(grid) 1494 | dlg.SetModal(true) 1495 | dlg.Show() 1496 | } 1497 | 1498 | func (s *myWindow) updatePwd() { 1499 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 1500 | dlg.SetWindowTitle(T("Modify Password")) 1501 | 1502 | grid := newGridLayout(dlg) 1503 | 1504 | pwd1 := widgets.NewQLabel2(T("Old Password:"), dlg, core.Qt__Widget) 1505 | 1506 | grid.AddWidget2(pwd1, 0, 0, 0) 1507 | 1508 | pwdInput1 := widgets.NewQLineEdit(dlg) 1509 | pwdInput1.SetEchoMode(widgets.QLineEdit__Password) 1510 | pwdInput1.SetPlaceholderText(T("Length Must >= 4")) 1511 | grid.AddWidget2(pwdInput1, 0, 1, 0) 1512 | 1513 | pwd2 := widgets.NewQLabel2(T("New Password:"), dlg, core.Qt__Widget) 1514 | 1515 | grid.AddWidget2(pwd2, 1, 0, 0) 1516 | 1517 | pwdInput2 := widgets.NewQLineEdit(dlg) 1518 | pwdInput2.SetEchoMode(widgets.QLineEdit__Password) 1519 | pwdInput2.SetPlaceholderText(T("Length Must >= 4")) 1520 | grid.AddWidget2(pwdInput2, 1, 1, 0) 1521 | 1522 | pwd3 := widgets.NewQLabel2(T("Confirm Password:"), dlg, core.Qt__Widget) 1523 | 1524 | grid.AddWidget2(pwd3, 2, 0, 0) 1525 | 1526 | pwdInput3 := widgets.NewQLineEdit(dlg) 1527 | pwdInput3.SetEchoMode(widgets.QLineEdit__Password) 1528 | pwdInput3.SetPlaceholderText(T("Length Must >= 4")) 1529 | grid.AddWidget2(pwdInput3, 2, 1, 0) 1530 | 1531 | okAct := widgets.NewQPushButton2(T("OK"), dlg) 1532 | grid.AddWidget2(okAct, 3, 0, 0) 1533 | cancelAct := widgets.NewQPushButton2(T("Cancel"), dlg) 1534 | grid.AddWidget2(cancelAct, 3, 1, 0) 1535 | 1536 | okAct.ConnectClicked(func(checked bool) { 1537 | ret := widgets.QMessageBox_Question(dlg, T("Confirm"), T("Are you sure?"), widgets.QMessageBox__Yes|widgets.QMessageBox__Cancel, widgets.QMessageBox__Yes) 1538 | 1539 | if ret == widgets.QMessageBox__Yes { 1540 | if len(pwdInput3.Text()) < 4 { 1541 | return 1542 | } 1543 | if pwdInput2.Text() != pwdInput3.Text() { 1544 | return 1545 | } 1546 | err := s.db.UpdateRealKeyAndSha40(pwdInput1.Text(), pwdInput2.Text()) 1547 | if err == nil { 1548 | widgets.QMessageBox_Information(dlg, T("Information"), T("Success,Please login again."), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1549 | dlg.Hide() 1550 | s.window.Close() 1551 | } else { 1552 | widgets.QMessageBox_Information(dlg, T("Information"), T("Fail")+err.Error(), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1553 | } 1554 | } 1555 | dlg.Hide() 1556 | dlg.Destroy(true, true) 1557 | }) 1558 | 1559 | cancelAct.ConnectClicked(func(c bool) { 1560 | dlg.Hide() 1561 | dlg.Destroy(true, true) 1562 | }) 1563 | 1564 | dlg.SetLayout(grid) 1565 | dlg.SetModal(true) 1566 | dlg.Show() 1567 | } 1568 | 1569 | func (s *myWindow) getUsers() []string { 1570 | dir := datDir 1571 | hdir, err := os.Open(dir) 1572 | if err != nil { 1573 | return []string{} 1574 | } 1575 | dirs, err := hdir.Readdir(-1) 1576 | if err != nil { 1577 | return nil 1578 | } 1579 | res := []string{} 1580 | for _, v := range dirs { 1581 | if v.IsDir() { 1582 | res = append(res, v.Name()) 1583 | } 1584 | } 1585 | return res 1586 | } 1587 | 1588 | func (s *myWindow) login() { 1589 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 1590 | dlg.SetWindowTitle(T("Login")) 1591 | 1592 | grid := newGridLayout(dlg) 1593 | 1594 | name := widgets.NewQLabel2(T("Name:"), dlg, core.Qt__Widget) 1595 | grid.AddWidget2(name, 0, 0, 0) 1596 | 1597 | nameInput := widgets.NewQComboBox(dlg) 1598 | nameInput.SetEditable(true) 1599 | 1600 | nameInput.AddItems(s.getUsers()) 1601 | nameInput.SetCurrentText(s.lastUser()) 1602 | 1603 | grid.AddWidget2(nameInput, 0, 1, 0) 1604 | 1605 | pwd := widgets.NewQLabel2(T("Password:"), dlg, core.Qt__Widget) 1606 | 1607 | grid.AddWidget2(pwd, 1, 0, 0) 1608 | 1609 | pwdInput := widgets.NewQLineEdit(dlg) 1610 | pwdInput.SetEchoMode(widgets.QLineEdit__Password) 1611 | pwdInput.SetPlaceholderText(T("Length Must >= 4")) 1612 | if len(nameInput.CurrentText()) > 0 { 1613 | pwdInput.SetFocus2() 1614 | } 1615 | grid.AddWidget2(pwdInput, 1, 1, 0) 1616 | 1617 | btb := newGridLayout2() 1618 | 1619 | okBtn := widgets.NewQPushButton2(T("OK"), dlg) 1620 | btb.AddWidget2(okBtn, 0, 0, 0) 1621 | 1622 | regBtn := widgets.NewQPushButton2(T("Register"), dlg) 1623 | regBtn.SetToolTip(T("Please input the name and password to register before you click this button!")) 1624 | btb.AddWidget2(regBtn, 0, 1, 0) 1625 | 1626 | cancelBtn := widgets.NewQPushButton2(T("Cancel"), dlg) 1627 | btb.AddWidget2(cancelBtn, 0, 2, 0) 1628 | 1629 | grid.AddLayout2(btb, 2, 0, 1, 2, 0) 1630 | 1631 | dlg.SetLayout(grid) 1632 | 1633 | dlg.SetModal(true) 1634 | 1635 | minLen := 4 1636 | 1637 | okBtn.ConnectClicked(func(b bool) { 1638 | var err error 1639 | if len(pwdInput.Text()) < minLen || len(nameInput.CurrentText()) < 1 { 1640 | return 1641 | } 1642 | s.db, err = getMyDb(nameInput.CurrentText()) 1643 | if err != nil { 1644 | widgets.QMessageBox_Information(dlg, T("Information"), T("Fail"), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1645 | nameInput.SetFocus2() 1646 | return 1647 | } 1648 | s.key, err = s.db.GetRealKey(pwdInput.Text()) 1649 | if err != nil { 1650 | widgets.QMessageBox_Information(dlg, T("Information"), T("Fail"), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1651 | pwdInput.SetFocus2() 1652 | return 1653 | } 1654 | s.saveLastUser(nameInput.CurrentText()) 1655 | s.user = nameInput.CurrentText() 1656 | s.window.SetWindowTitle(nameInput.CurrentText()) 1657 | dlg.Hide() 1658 | 1659 | }) 1660 | 1661 | regBtn.ConnectClicked(func(b bool) { 1662 | if len(pwdInput.Text()) < minLen || len(nameInput.CurrentText()) < 1 { 1663 | widgets.QMessageBox_Warning(s.window, T("Register"), 1664 | T("Please input the name and password to register before you click this button!"), widgets.QMessageBox__Ok, widgets.QMessageBox__Ok) 1665 | return 1666 | } 1667 | var ok bool 1668 | confirm := widgets.QInputDialog_GetText(dlg, T("Confirm"), T("Name:")+nameInput.CurrentText(), widgets.QLineEdit__Password, "", 1669 | &ok, core.Qt__Dialog, core.Qt__ImhNone) 1670 | if !ok { 1671 | return 1672 | } 1673 | if confirm != pwdInput.Text() { 1674 | s.showMsg(T("Fail"), T("Password Not Match")) 1675 | return 1676 | } 1677 | err := createUserDb(nameInput.CurrentText(), pwdInput.Text()) 1678 | if err != nil { 1679 | panic(err) 1680 | } 1681 | s.db, err = getMyDb(nameInput.CurrentText()) 1682 | if err != nil { 1683 | panic(err) 1684 | } 1685 | s.key, err = s.db.GetRealKey(pwdInput.Text()) 1686 | if err != nil { 1687 | panic(err) 1688 | } 1689 | s.saveLastUser(nameInput.CurrentText()) 1690 | s.user = nameInput.CurrentText() 1691 | s.window.SetWindowTitle(nameInput.CurrentText()) 1692 | dlg.Hide() 1693 | 1694 | }) 1695 | 1696 | cancelBtn.ConnectClicked(func(b bool) { 1697 | dlg.Destroy(true, true) 1698 | s.window.Destroy(true, true) 1699 | s.app.Quit() 1700 | }) 1701 | 1702 | dlg.ConnectCloseEvent(func(e *gui.QCloseEvent) { 1703 | dlg.Destroy(true, true) 1704 | s.window.Destroy(true, true) 1705 | s.app.Quit() 1706 | }) 1707 | 1708 | dlg.ConnectHideEvent(func(e *gui.QHideEvent) { 1709 | //log.Println("load list") 1710 | s.loadUserCategorys() 1711 | dlg.Destroy(true, true) 1712 | }) 1713 | 1714 | dlg.Show() 1715 | 1716 | dlg.Move2(0, 0) 1717 | dlg.Move2(s.window.X()+(s.window.Width()-dlg.Width())/2, s.window.Y()+(s.window.Height()-dlg.Width())/2) 1718 | } 1719 | 1720 | func (s *myWindow) exportEncryptedDiary() { 1721 | //export private format .egf (encrypted gob file) 1722 | idx := s.tree.CurrentIndex() 1723 | if idx == nil { 1724 | return 1725 | } 1726 | item := s.model.ItemFromIndex(idx) 1727 | if item == nil { 1728 | return 1729 | } 1730 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 1731 | dlg.SetWindowTitle(T("Export Encrypted Diary") + "...") 1732 | 1733 | grid := newGridLayout2() 1734 | 1735 | name := widgets.NewQLabel2(T("FileName:"), dlg, core.Qt__Widget) 1736 | grid.AddWidget2(name, 0, 0, 0) 1737 | 1738 | nameInput := widgets.NewQLineEdit(dlg) 1739 | grid.AddWidget2(nameInput, 0, 1, 0) 1740 | nameInput.SetPlaceholderText(T("Double click to select...")) 1741 | nameInput.SetMinimumWidth(240) 1742 | 1743 | passwd := widgets.NewQLabel2(T("Password:"), dlg, core.Qt__Widget) 1744 | grid.AddWidget2(passwd, 1, 0, 0) 1745 | 1746 | passwdInput := widgets.NewQLineEdit(dlg) 1747 | grid.AddWidget2(passwdInput, 1, 1, 0) 1748 | passwdInput.SetEchoMode(widgets.QLineEdit__Password) 1749 | passwdInput.SetPlaceholderText(T("Length Must >= 4")) 1750 | 1751 | hbox := widgets.NewQHBoxLayout() 1752 | 1753 | okBtn := widgets.NewQPushButton2(T("OK"), dlg) 1754 | hbox.AddWidget(okBtn, 1, 0) 1755 | 1756 | cancelBtn := widgets.NewQPushButton2(T("Cancel"), dlg) 1757 | hbox.AddWidget(cancelBtn, 1, 0) 1758 | 1759 | cancelBtn.ConnectClicked(func(b bool) { 1760 | dlg.Hide() 1761 | dlg.Destroy(true, true) 1762 | }) 1763 | 1764 | nameInput.ConnectMouseDoubleClickEvent(func(e *gui.QMouseEvent) { 1765 | filename := widgets.QFileDialog_GetSaveFileName(dlg, T("Export To..."), ".", "Encrypted Diary (*.egf)", "Encrypted Diary (*.egf)", 0) 1766 | if strings.HasSuffix(strings.ToLower(filename), ".egf") { 1767 | nameInput.SetText(filename) 1768 | } else { 1769 | nameInput.SetText(filename + ".egf") 1770 | } 1771 | 1772 | }) 1773 | 1774 | okBtn.ConnectClicked(func(b bool) { 1775 | if len(passwdInput.Text()) < 4 { 1776 | return 1777 | } 1778 | 1779 | filename := strings.TrimSpace(nameInput.Text()) 1780 | if len(filename) == 0 { 1781 | return 1782 | } 1783 | key := getSha4(passwdInput.Text()) 1784 | data := s.getRichText() 1785 | err := encodeToPathName(data, filename, key) 1786 | if err != nil { 1787 | s.showMsg(T("Error"), err.Error()) 1788 | } else { 1789 | s.showMsg(T("Success"), filename) 1790 | } 1791 | dlg.Hide() 1792 | dlg.Destroy(true, true) 1793 | }) 1794 | 1795 | grid.AddLayout2(hbox, 2, 0, 1, 2, 0) 1796 | 1797 | dlg.SetLayout(grid) 1798 | dlg.Exec() 1799 | } 1800 | 1801 | func (s *myWindow) importEncryptedDiary() { 1802 | //import private format .egf (encrypted gob file) 1803 | 1804 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 1805 | dlg.SetWindowTitle(T("Import Encrypted Diary") + "...") 1806 | 1807 | grid := newGridLayout2() 1808 | 1809 | name := widgets.NewQLabel2(T("FileName:"), dlg, core.Qt__Widget) 1810 | grid.AddWidget2(name, 0, 0, 0) 1811 | 1812 | nameInput := widgets.NewQLineEdit(dlg) 1813 | grid.AddWidget2(nameInput, 0, 1, 0) 1814 | nameInput.SetPlaceholderText(T("Double click to select...")) 1815 | nameInput.SetMinimumWidth(240) 1816 | 1817 | passwd := widgets.NewQLabel2(T("Password:"), dlg, core.Qt__Widget) 1818 | grid.AddWidget2(passwd, 1, 0, 0) 1819 | 1820 | passwdInput := widgets.NewQLineEdit(dlg) 1821 | grid.AddWidget2(passwdInput, 1, 1, 0) 1822 | passwdInput.SetEchoMode(widgets.QLineEdit__Password) 1823 | 1824 | hbox := widgets.NewQHBoxLayout() 1825 | 1826 | okBtn := widgets.NewQPushButton2(T("OK"), dlg) 1827 | hbox.AddWidget(okBtn, 1, 0) 1828 | 1829 | cancelBtn := widgets.NewQPushButton2(T("Cancel"), dlg) 1830 | hbox.AddWidget(cancelBtn, 1, 0) 1831 | 1832 | cancelBtn.ConnectClicked(func(b bool) { 1833 | dlg.Hide() 1834 | dlg.Destroy(true, true) 1835 | }) 1836 | 1837 | nameInput.ConnectMouseDoubleClickEvent(func(e *gui.QMouseEvent) { 1838 | filename := widgets.QFileDialog_GetOpenFileName(dlg, T("Import File..."), ".", "Encrypted Diary .egf (*.egf)", "Encrypted Diary .egf (*.egf)", widgets.QFileDialog__ReadOnly) 1839 | nameInput.SetText(filename) 1840 | 1841 | }) 1842 | 1843 | okBtn.ConnectClicked(func(b bool) { 1844 | if len(passwdInput.Text()) < 4 { 1845 | return 1846 | } 1847 | 1848 | filename := strings.TrimSpace(nameInput.Text()) 1849 | if len(filename) == 0 { 1850 | return 1851 | } 1852 | key := getSha4(passwdInput.Text()) 1853 | data, err := decodeFromPathName(filename, key) 1854 | if err != nil { 1855 | return 1856 | } 1857 | now := time.Now() 1858 | s.addDiary(now.Format("2006-01"), now.Format("02"), "") 1859 | 1860 | _, err = s.getQText(data) 1861 | 1862 | if err == nil { 1863 | 1864 | s.editor.SetHtml(s.document.Html) 1865 | s.showAttachList() 1866 | //curDiary.Modified = true 1867 | s.editor.Document().SetModified(true) 1868 | 1869 | s.saveCurDiary() 1870 | } else { 1871 | s.showMsg(T("Error"), err.Error()) 1872 | } 1873 | 1874 | dlg.Hide() 1875 | dlg.Destroy(true, true) 1876 | }) 1877 | 1878 | grid.AddLayout2(hbox, 2, 0, 1, 2, 0) 1879 | 1880 | dlg.SetLayout(grid) 1881 | dlg.Exec() 1882 | } 1883 | 1884 | func (s *myWindow) exportAsPdf() { 1885 | defaultName := strings.TrimSpace(s.editor.Document().FirstBlock().Text()) + ".pdf" 1886 | filename := widgets.QFileDialog_GetSaveFileName(s.window, T("Export To..."), defaultName, "PDF (*.pdf)", "Encrypted Diary (*.pdf)", 0) 1887 | if !strings.HasSuffix(strings.ToLower(filename), ".pdf") { 1888 | filename = filename + ".pdf" 1889 | } 1890 | var ( 1891 | fileName = filename 1892 | printer = printsupport.NewQPrinter(printsupport.QPrinter__HighResolution) 1893 | ) 1894 | printer.SetOutputFormat(printsupport.QPrinter__PdfFormat) 1895 | 1896 | printer.SetOutputFileName(fileName) 1897 | s.editor.Document().Print(printer) 1898 | s.setStatusBar(fmt.Sprintf("Exported %v", core.QDir_ToNativeSeparators(fileName))) 1899 | } 1900 | 1901 | func (s *myWindow) exportAsOdt() { 1902 | defaultName := strings.TrimSpace(s.editor.Document().FirstBlock().Text()) + ".odt" 1903 | filename := widgets.QFileDialog_GetSaveFileName(s.window, T("Export To..."), defaultName, "OpenDocument (*.odt)", "OpenDocument (*.odt)", 0) 1904 | if !strings.HasSuffix(strings.ToLower(filename), ".odt") { 1905 | filename = filename + ".odt" 1906 | } 1907 | var ( 1908 | writer = gui.NewQTextDocumentWriter3(filename, core.NewQByteArray()) 1909 | success = writer.Write(s.editor.Document()) 1910 | ) 1911 | if success { 1912 | s.setStatusBar(fmt.Sprintf("Exported %v", core.QDir_ToNativeSeparators(filename))) 1913 | } else { 1914 | s.setStatusBar("Exported Fail.") 1915 | } 1916 | } 1917 | 1918 | func (s *myWindow) showMsg(title, msg string) { 1919 | dlg := widgets.NewQMessageBox(s.window) 1920 | dlg.SetWindowTitle(title) 1921 | dlg.SetText(msg) 1922 | 1923 | dlg.Exec() 1924 | dlg.Destroy(true, true) 1925 | } 1926 | 1927 | func (s *myWindow) showPay() { 1928 | dlg := widgets.NewQDialog(s.window, core.Qt__Dialog) 1929 | dlg.SetWindowTitle(T("Pay Voluntary!")) 1930 | box := widgets.NewQHBoxLayout2(dlg) 1931 | height := 512 1932 | 1933 | labelAli := widgets.NewQLabel(dlg, core.Qt__Widget) 1934 | img1 := newQPixMap(":/qml/pay/alipay.png", "png", core.Qt__NoFormatConversion) 1935 | img1 = img1.ScaledToHeight(height, core.Qt__SmoothTransformation) 1936 | labelAli.SetPixmap(img1) 1937 | box.AddWidget(labelAli, 1, 0) 1938 | 1939 | labelWx := widgets.NewQLabel(dlg, core.Qt__Widget) 1940 | img2 := newQPixMap(":/qml/pay/wxpay.png", "png", core.Qt__NoFormatConversion) 1941 | img2 = img2.ScaledToHeight(height, core.Qt__SmoothTransformation) 1942 | labelWx.SetPixmap(img2) 1943 | box.AddWidget(labelWx, 1, 0) 1944 | 1945 | dlg.SetLayout(box) 1946 | 1947 | dlg.Exec() 1948 | dlg.Destroy(true, true) 1949 | } 1950 | 1951 | func main() { 1952 | //defer gettext.SaveLog() 1953 | app := widgets.NewQApplication(len(os.Args), os.Args) 1954 | window := new(myWindow) 1955 | window.Create(app) 1956 | app.Exec() 1957 | } 1958 | -------------------------------------------------------------------------------- /linux_launcher/.gitignore: -------------------------------------------------------------------------------- 1 | linux_launcher 2 | setup 3 | -------------------------------------------------------------------------------- /linux_launcher/setup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "text/template" 9 | ) 10 | 11 | func copyFile(src, dst string, mode os.FileMode) error { 12 | fp1, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, mode) 13 | if err != nil { 14 | return err 15 | } 16 | defer fp1.Close() 17 | fp2, err := os.Open(src) 18 | if err != nil { 19 | return err 20 | } 21 | defer fp2.Close() 22 | _, err = io.Copy(fp1, fp2) 23 | return err 24 | } 25 | 26 | func makeLauncher(launcherName, fileName, iconName string) { 27 | exe1, _ := os.Executable() 28 | appdir := filepath.Dir(exe1) 29 | appname := filepath.Join(appdir, fileName) 30 | home, _ := os.UserHomeDir() 31 | os.MkdirAll(filepath.Join(home, ".local", "share", "applications"), os.ModePerm) 32 | dst := filepath.Join(home, ".local", "share", "applications", launcherName+".desktop") 33 | iconSrc := filepath.Join(appdir, iconName) 34 | 35 | data := struct { 36 | LauncherName string 37 | Name string 38 | Icon string 39 | }{launcherName, appname, iconSrc} 40 | 41 | tpl := `[Desktop Entry] 42 | Name={{.LauncherName}} 43 | Comment={{.LauncherName}} 44 | Exec="{{.Name}}" %U 45 | Icon={{.Icon}} 46 | Terminal=false 47 | Type=Application 48 | StartupNotify=true 49 | Categories=Office; 50 | 51 | ` 52 | t := template.New("") 53 | t.Parse(tpl) 54 | fp, err := os.Create(dst) 55 | if err != nil { 56 | log.Println(err) 57 | return 58 | } 59 | defer fp.Close() 60 | t.Execute(fp, data) 61 | } 62 | 63 | func main() { 64 | makeLauncher("Secret-Diary", "secret-diary", "Sd.png") 65 | } 66 | -------------------------------------------------------------------------------- /locale/zh_CN/LC_MESSAGES/sdiary.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/locale/zh_CN/LC_MESSAGES/sdiary.mo -------------------------------------------------------------------------------- /locale/zh_CN/LC_MESSAGES/sdiary.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-01-05 19:03+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | msgid "Name:" 21 | msgstr "用户名:" 22 | 23 | msgid "Manage" 24 | msgstr "管理" 25 | 26 | msgid "Help" 27 | msgstr "帮助" 28 | 29 | msgid "" 30 | "Copy Right: Fu Huizhong \n" 31 | "Security Diary " 32 | msgstr "" 33 | "版权所有:Fu Huizhong \n" 34 | "安全日记本 " 35 | 36 | msgid "Crypto with AES256" 37 | msgstr "使用256位AES加密算法" 38 | 39 | msgid "About" 40 | msgstr "关于" 41 | 42 | msgid "Rename" 43 | msgstr "改名" 44 | 45 | msgid "Length Must >= 4" 46 | msgstr "最小长度: 4" 47 | 48 | msgid "Confirm Password:" 49 | msgstr "确认密码:" 50 | 51 | msgid "Your DATA storge path is '" 52 | msgstr "你的数据保存目录:" 53 | 54 | msgid "Password" 55 | msgstr "密码" 56 | 57 | msgid "Password:" 58 | msgstr "密码:" 59 | 60 | msgid "ModifyPassword" 61 | msgstr "修改密码" 62 | 63 | msgid "New Password:" 64 | msgstr "新密码" 65 | 66 | msgid "New" 67 | msgstr "新建" 68 | 69 | msgid "Backup & Delete" 70 | msgstr "备份和删除" 71 | 72 | msgid "Register" 73 | msgstr "注册" 74 | 75 | msgid "Modify Password" 76 | msgstr "修改密码" 77 | 78 | msgid "Old Password:" 79 | msgstr "旧密码" 80 | 81 | msgid "Save" 82 | msgstr "保存" 83 | 84 | msgid "Secret Diary" 85 | msgstr "安全日记本" 86 | 87 | msgid "Loading Diary List..." 88 | msgstr "加载日记..." 89 | 90 | msgid "New Name:" 91 | msgstr "新用户名:" 92 | 93 | msgid "Diary List" 94 | msgstr "日记列表" 95 | 96 | msgid "Last Modified:" 97 | msgstr "最后修改时间:" 98 | 99 | msgid "Standard" 100 | msgstr "正文" 101 | 102 | msgid "" 103 | "There is no way to recover password. \n" 104 | "Please write it on paper!" 105 | msgstr "" 106 | "没有任何捷径恢复密码。\n" 107 | "建议把密码记在纸上。" 108 | 109 | msgid "No Diary Saved" 110 | msgstr "无需保存" 111 | 112 | msgid "OK" 113 | msgstr "确定" 114 | 115 | msgid "UserName" 116 | msgstr "用户名" 117 | 118 | msgid "Cancel" 119 | msgstr "取消" 120 | 121 | msgid "Login" 122 | msgstr "登陆" 123 | 124 | msgid "Confirm" 125 | msgstr "确认密码" 126 | 127 | msgid "Bold" 128 | msgstr "粗体" 129 | 130 | msgid "Italic" 131 | msgstr "斜体" 132 | 133 | msgid "Underline" 134 | msgstr "下划线" 135 | 136 | msgid "StrikeOut" 137 | msgstr "删除线" 138 | 139 | msgid "List (Disc)" 140 | msgstr "列表(实心点)" 141 | 142 | msgid "List (Circle)" 143 | msgstr "列表(空心圆)" 144 | 145 | msgid "List (Square)" 146 | msgstr "列表(方块)" 147 | 148 | msgid "List (Decimal)" 149 | msgstr "列表(数字)" 150 | 151 | msgid "List (Alpha lower)" 152 | msgstr "列表(小写字母)" 153 | 154 | msgid "List (Alpha upper)" 155 | msgstr "列表(大写字母)" 156 | 157 | msgid "List (Roman lower)" 158 | msgstr "列表(小写罗马数字)" 159 | 160 | msgid "List (Roman upper)" 161 | msgstr "列表(大写罗马数字)" 162 | 163 | msgid "Header 2" 164 | msgstr "二级标题" 165 | 166 | msgid "Header 3" 167 | msgstr "三级标题" 168 | 169 | msgid "Image" 170 | msgstr "图片" 171 | 172 | msgid "Insert Image" 173 | msgstr "插入图片" 174 | 175 | msgid "Header 1" 176 | msgstr "一级标题" 177 | 178 | msgid "Table" 179 | msgstr "表格" 180 | 181 | msgid "Insert Table" 182 | msgstr "插入表格" 183 | 184 | msgid "Table Rows and Columns" 185 | msgstr "表格行数和列数" 186 | 187 | msgid "Rows:" 188 | msgstr "行数" 189 | 190 | msgid "Columns:" 191 | msgstr "列数" 192 | 193 | msgid "Text Color..." 194 | msgstr "文字颜色" 195 | 196 | msgid "Background Color..." 197 | msgstr "背景颜色" 198 | 199 | msgid "New Diary" 200 | msgstr "新日记" 201 | 202 | msgid "Font" 203 | msgstr "字体" 204 | 205 | msgid "CurrentFont" 206 | msgstr "当前字体" 207 | 208 | msgid "Insert Row" 209 | msgstr "插入行" 210 | 211 | msgid "Append Row" 212 | msgstr "追加行" 213 | 214 | msgid "Insert Column" 215 | msgstr "插入列" 216 | 217 | msgid "Append Column" 218 | msgstr "追加列" 219 | 220 | msgid "Remove Row" 221 | msgstr "删除行" 222 | 223 | msgid "Remove Column" 224 | msgstr "删除列" 225 | 226 | msgid "Are you sure?" 227 | msgstr "是否确定?" 228 | 229 | msgid "Export Image As..." 230 | msgstr "导出图片..." 231 | 232 | msgid "Delete" 233 | msgstr "删除" 234 | 235 | msgid "Export To..." 236 | msgstr "导出为..." 237 | 238 | msgid "Import File..." 239 | msgstr "导入文件..." 240 | 241 | msgid "Export Encrypted Diary" 242 | msgstr "导出加密日记..." 243 | 244 | msgid "Import Encrypted Diary" 245 | msgstr "导入加密日记..." 246 | 247 | msgid "Export PDF..." 248 | msgstr "导出为PDF..." 249 | 250 | msgid "FileName:" 251 | msgstr "文件:" 252 | 253 | msgid "File" 254 | msgstr "文件" 255 | 256 | msgid "Clear Line Format..." 257 | msgstr "清除本行格式" 258 | 259 | msgid "Save File" 260 | msgstr "保存文件" 261 | 262 | msgid "Select Attachments" 263 | msgstr "选择附件" 264 | 265 | msgid "Total" 266 | msgstr "总数" 267 | 268 | msgid "Export As..." 269 | msgstr "导出" 270 | 271 | msgid "Attach..." 272 | msgstr "插入附件" 273 | 274 | msgid "Remove" 275 | msgstr "删除" 276 | 277 | msgid "Append Attachments" 278 | msgstr "添加附件" 279 | 280 | msgid "Do you want to save the document?" 281 | msgstr "是否保存当前文档?" 282 | 283 | msgid "Please input the name and password to register before you click this button!" 284 | msgstr "点击本按钮前,请先在上面输入你想注册的用户名和密码!" 285 | 286 | msgid "Search" 287 | msgstr "搜索" 288 | 289 | msgid "Open in New Window" 290 | msgstr "在新窗口中打开" 291 | 292 | msgid "Double Click to Open. " 293 | msgstr "双击打开。" 294 | 295 | msgid "Result List" 296 | msgstr "搜索结果" 297 | 298 | msgid "Support Author" 299 | msgstr "支持作者" 300 | 301 | msgid "You can backup and delete the directory yourself." 302 | msgstr "你可以自行备份或删除该目录。" 303 | 304 | msgid "Pay Voluntary!" 305 | msgstr "支付多少全凭自愿!" 306 | 307 | msgid "Confirm Delete" 308 | msgstr "确认删除" 309 | 310 | msgid "Import All" 311 | msgstr "导入备份" 312 | 313 | msgid "Export All" 314 | msgstr "备份全部" 315 | 316 | msgid "Select export directory" 317 | msgstr "选择导出目录" 318 | 319 | msgid "Select a bakup file" 320 | msgstr "选择备份文件" 321 | 322 | msgid "Text Replace" 323 | msgstr "文本替换" 324 | 325 | msgid "Text Search" 326 | msgstr "文本搜索" 327 | 328 | msgid "Last" 329 | msgstr "上一个" 330 | 331 | msgid "Next" 332 | msgstr "下一个" 333 | 334 | msgid "All" 335 | msgstr "全部" 336 | 337 | msgid "Words to search." 338 | msgstr "查找文字" 339 | 340 | msgid "Old Text." 341 | msgstr "旧文字" 342 | 343 | msgid "New Text." 344 | msgstr "新文字" 345 | 346 | msgid "Edit" 347 | msgstr "编辑" 348 | 349 | msgid "Replace" 350 | msgstr "替换" 351 | 352 | msgid "Paste Text" 353 | msgstr "粘帖纯文本" 354 | 355 | msgid "Rename Category" 356 | msgstr "重命名类别" 357 | 358 | msgid "New Category Name" 359 | msgstr "新的类别名称" 360 | 361 | msgid "Error" 362 | msgstr "错误" 363 | 364 | msgid "Are you sure? All files in this category will move to default category." 365 | msgstr "是否确定?本类别的所有文件将转移到默认类别中。" 366 | 367 | msgid "New Category" 368 | msgstr "新建类别" 369 | 370 | msgid "Remove Category" 371 | msgstr "删除类别" 372 | 373 | msgid "Default" 374 | msgstr "默认" 375 | 376 | msgid "Category" 377 | msgstr "分类" 378 | 379 | msgid "Categories" 380 | msgstr "分类" 381 | 382 | msgid "Can not remove default category." 383 | msgstr "不能删除默认类别" 384 | 385 | msgid "Change Category" 386 | msgstr "修改类别" 387 | 388 | msgid "Select a category" 389 | msgstr "选择一个分类" 390 | 391 | msgid "Add To Menu" 392 | msgstr "加入启动菜单" 393 | 394 | msgid "You can start this program from 'Start'->'Office'" 395 | msgstr "你现在可以从 '开始'->'办公' 启动本程序" 396 | 397 | msgid "Increase Ident" 398 | msgstr "增加缩进量" 399 | 400 | msgid "Decrease Ident" 401 | msgstr "减少缩进量" 402 | 403 | msgid "Scale Image" 404 | msgstr "缩放图片" 405 | 406 | msgid "Scale Image Size" 407 | msgstr "缩放图片尺寸" 408 | 409 | msgid "Width" 410 | msgstr "宽" 411 | 412 | msgid "Height" 413 | msgstr "高" 414 | 415 | msgid "Refresh" 416 | msgstr "刷新" 417 | 418 | msgid "Top Margin" 419 | msgstr "段前间距" 420 | 421 | msgid "Format Brush" 422 | msgstr "格式刷" 423 | 424 | msgid "Create Desktop Shortcut" 425 | msgstr "创建桌面快捷方式" 426 | 427 | msgid "Export Odt..." 428 | msgstr "导出为Odt格式..." 429 | 430 | msgid "Update" 431 | msgstr "检查更新" 432 | 433 | msgid "This is the Newest Version!" 434 | msgstr "这是最新版本!" 435 | 436 | msgid "Current:" 437 | msgstr "当前版本:" 438 | 439 | msgid "Newest:" 440 | msgstr "最新版本:" 441 | 442 | msgid "I will open the page on github website after you click button 'OK'!" 443 | msgstr "当你点击'OK'按钮后,我将打开位于github网站上的下载页面!" 444 | 445 | msgid "Change Storage Dir" 446 | msgstr "改变存储目录" 447 | 448 | msgid "Select Storage Directory" 449 | msgstr "选择存储目录" 450 | 451 | msgid "Quit program" 452 | msgstr "即将退出程序" 453 | 454 | msgid "The program will terminate now." 455 | msgstr "本程序即将退出。" 456 | 457 | msgid "How to move data?" 458 | msgstr "如何转移你的数据?" 459 | 460 | msgid "Move the directories to the new directory from " 461 | msgstr "把该目录中的子目录移动到新的数据目录:" 462 | -------------------------------------------------------------------------------- /pay/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/pay/alipay.jpg -------------------------------------------------------------------------------- /pay/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/pay/wxpay.png -------------------------------------------------------------------------------- /pkg/linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ~/go/bin/qtdeploy build 4 | 5 | cd deploy 6 | mkdir -p secret-diary-linux_amd64 7 | cp -ru ../locale secret-diary-linux_amd64/ 8 | cp -u ../Sd.png secret-diary-linux_amd64/ 9 | cp -ru linux/* secret-diary-linux_amd64/ 10 | ln -sf ~/go/bin/qtdeploy/secret-diary-linux_amd64/secret-diary secret-diary 11 | rm secret-diary-linux_amd64.zip 12 | zip -r9 secret-diary-linux_amd64.zip secret-diary-linux_amd64/ 13 | 14 | sh mkdeb.sh 15 | -------------------------------------------------------------------------------- /pkg/mkdeb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | nano secret-diary-deb/DEBIAN/control 3 | cp -ru secret-diary-linux_amd64/* secret-diary-deb/opt/secret-diary/ 4 | VERSION="$(fgrep Version secret-diary-deb/DEBIAN/control |awk '{print $2}')" 5 | DEBNAME="secret-diary-${VERSION}_amd64.deb" 6 | dpkg -b secret-diary-deb $DEBNAME 7 | cp $DEBNAME ~/src/pkg2appimage/debs/secret-diary_amd64.deb 8 | cd ~/src/pkg2appimage/ 9 | ./pkg2appimage recipes/secret-diary.yml 10 | mv out/Secret-Diary-amd64.deb.glibc2.14-x86_64.AppImage ~/go/src/secret-diary/deploy/Secret-Diary-${VERSION}-x86_64.AppImage 11 | -------------------------------------------------------------------------------- /pkg/secret-diary.yml: -------------------------------------------------------------------------------- 1 | app: secret-diary 2 | binpatch: true 3 | 4 | ingredients: 5 | dist: stretch 6 | packages: 7 | - secret-diary 8 | sources: 9 | - deb http://mirrors.163.com/debian/ stretch main contrib 10 | debs: 11 | - /home/fuhz/src/pkg2appimage/debs/secret-diary_amd64.deb 12 | 13 | script: 14 | - cp ./opt/secret-diary/Sd.png . 15 | - cp ./usr/local/share/applications/Secret-Diary.desktop . 16 | - sed -i "s/\/opt\/secret-diary\///g" Secret-Diary.desktop 17 | - mv ./opt/secret-diary/* ./usr/bin/ 18 | -------------------------------------------------------------------------------- /pkg/shortcut.vbs: -------------------------------------------------------------------------------- 1 | Set WshShell = WScript.CreateObject("WScript.Shell") 2 | strDesktop = WshShell.SpecialFolders("Desktop") 3 | 4 | If IsExitAFile(strDesktop & "\secret-diary.lnk") Then 5 | rem "exists" 6 | Else 7 | CreateShortcut(strDesktop & "\secret-diary.lnk") 8 | End If 9 | 10 | Function IsExitAFile(filespec) 11 | Dim fso 12 | Set fso=CreateObject("Scripting.FileSystemObject") 13 | If fso.fileExists(filespec) Then 14 | IsExitAFile=True 15 | Else IsExitAFile=False 16 | End If 17 | End Function 18 | 19 | Sub CreateShortcut(filespec) 20 | Set WshShell = WScript.CreateObject("WScript.Shell") 21 | Set oShellLink = WshShell.CreateShortcut(filespec) 22 | target = Wscript.Arguments.Named("target") 23 | oShellLink.TargetPath =target 24 | oShellLink.WorkingDirectory = mid(target,1,instrrev(target,"\")) 25 | oShellLink.WindowStyle = 3 26 | oShellLink.Save 27 | End Sub 28 | -------------------------------------------------------------------------------- /pkg/win32.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cp deploy/win32.syso . 4 | ~/go/bin/qtdeploy -docker build windows_32_static 5 | rm win32.syso 6 | 7 | cd deploy 8 | mkdir -p secret-diary-win32 9 | cp -ru ../locale secret-diary-win32/ 10 | cp -u ../Sd.ico secret-diary-win32/ 11 | cp -ru windows/* secret-diary-win32/ 12 | rm secret-diary-win32.zip 13 | zip -r9 secret-diary-win32.zip secret-diary-win32/ -------------------------------------------------------------------------------- /qml/icons/B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/B.png -------------------------------------------------------------------------------- /qml/icons/I.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/I.png -------------------------------------------------------------------------------- /qml/icons/S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/S.png -------------------------------------------------------------------------------- /qml/icons/Sd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/Sd.png -------------------------------------------------------------------------------- /qml/icons/U.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/U.png -------------------------------------------------------------------------------- /qml/icons/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/bg.png -------------------------------------------------------------------------------- /qml/icons/brush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/brush.png -------------------------------------------------------------------------------- /qml/icons/document-new.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/document-new.jpg -------------------------------------------------------------------------------- /qml/icons/document-save.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/document-save.jpg -------------------------------------------------------------------------------- /qml/icons/draw-eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/draw-eraser.png -------------------------------------------------------------------------------- /qml/icons/fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/fg.png -------------------------------------------------------------------------------- /qml/icons/h1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/h1.png -------------------------------------------------------------------------------- /qml/icons/h2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/h2.png -------------------------------------------------------------------------------- /qml/icons/h3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/h3.png -------------------------------------------------------------------------------- /qml/icons/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/left.png -------------------------------------------------------------------------------- /qml/icons/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/right.png -------------------------------------------------------------------------------- /qml/icons/std.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/std.png -------------------------------------------------------------------------------- /qml/icons/textcenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/textcenter.png -------------------------------------------------------------------------------- /qml/icons/textjustify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/textjustify.png -------------------------------------------------------------------------------- /qml/icons/textleft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/textleft.png -------------------------------------------------------------------------------- /qml/icons/textright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/icons/textright.png -------------------------------------------------------------------------------- /qml/pay/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/pay/alipay.png -------------------------------------------------------------------------------- /qml/pay/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/qml/pay/wxpay.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/screenshot.png -------------------------------------------------------------------------------- /secret-diary.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /secret-diary.x64.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /shortcut.vbs: -------------------------------------------------------------------------------- 1 | Set WshShell = WScript.CreateObject("WScript.Shell") 2 | strDesktop = WshShell.SpecialFolders("Desktop") 3 | 4 | If IsExitAFile(strDesktop & "\secret-diary.lnk") Then 5 | rem "exists" 6 | Else 7 | CreateShortcut(strDesktop & "\secret-diary.lnk") 8 | End If 9 | 10 | Function IsExitAFile(filespec) 11 | Dim fso 12 | Set fso=CreateObject("Scripting.FileSystemObject") 13 | If fso.fileExists(filespec) Then 14 | IsExitAFile=True 15 | Else IsExitAFile=False 16 | End If 17 | End Function 18 | 19 | Sub CreateShortcut(filespec) 20 | Set WshShell = WScript.CreateObject("WScript.Shell") 21 | Set oShellLink = WshShell.CreateShortcut(filespec) 22 | target = Wscript.Arguments.Named("target") 23 | oShellLink.TargetPath =target 24 | oShellLink.WorkingDirectory = mid(target,1,instrrev(target,"\")) 25 | oShellLink.WindowStyle = 3 26 | oShellLink.Save 27 | End Sub 28 | -------------------------------------------------------------------------------- /shortcut_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "text/template" 12 | ) 13 | 14 | func copyFile(src, dst string) error { 15 | fp1, err := os.Create(dst) 16 | if err != nil { 17 | return err 18 | } 19 | defer fp1.Close() 20 | fp2, err := os.Open(src) 21 | if err != nil { 22 | return err 23 | } 24 | defer fp2.Close() 25 | _, err = io.Copy(fp1, fp2) 26 | log.Println("copy", src, dst) 27 | return err 28 | } 29 | 30 | func makeLauncher(launcherName, fileName, iconName string, force bool) { 31 | _, err := os.Lstat("/usr/local/share/applications/Secret-Diary.desktop") 32 | if err == nil { 33 | //install from deb 34 | return 35 | } 36 | exe1, _ := os.Executable() 37 | appdir := filepath.Dir(exe1) 38 | appname := filepath.Join(appdir, fileName) 39 | home, _ := os.UserHomeDir() 40 | os.MkdirAll(filepath.Join(home, ".local", "share", "applications"), os.ModePerm) 41 | dst := filepath.Join(home, ".local", "share", "applications", launcherName+".desktop") 42 | iconSrc := filepath.Join(appdir, iconName) 43 | 44 | if isNewer(dst, iconSrc) && force == false { 45 | //already exist 46 | return 47 | } 48 | 49 | data := struct { 50 | LauncherName string 51 | Name string 52 | Icon string 53 | }{launcherName, appname, iconSrc} 54 | 55 | tpl := `[Desktop Entry] 56 | Name={{.LauncherName}} 57 | Comment={{.LauncherName}} 58 | Exec="{{.Name}}" %U 59 | Icon={{.Icon}} 60 | Terminal=false 61 | Type=Application 62 | StartupNotify=true 63 | Categories=Office; 64 | 65 | ` 66 | t := template.New("") 67 | t.Parse(tpl) 68 | fp, err := os.Create(dst) 69 | if err != nil { 70 | log.Println(err) 71 | return 72 | } 73 | defer fp.Close() 74 | t.Execute(fp, data) 75 | } 76 | 77 | func isNewer(fileNew, fileOld string) bool { 78 | infoNew, err := os.Lstat(fileNew) 79 | if err != nil { 80 | return false 81 | } 82 | infoOld, err := os.Lstat(fileOld) 83 | if err != nil { 84 | return true 85 | } 86 | if infoNew.ModTime().Unix() > infoOld.ModTime().Unix() { 87 | return true 88 | } 89 | return false 90 | } 91 | 92 | func appimageLauncher(force bool) error { 93 | desktop := "Secret-Diary.desktop" 94 | icon := "Sd.png" 95 | 96 | appimage := os.Getenv("APPIMAGE") 97 | appdir := os.Getenv("APPDIR") 98 | if len(appimage) == 0 || len(appdir) == 0 { 99 | return errors.New("Not Appimage") 100 | } 101 | 102 | home, _ := os.UserHomeDir() 103 | os.MkdirAll(filepath.Join(home, ".local", "share", "applications"), os.ModePerm) 104 | 105 | src := filepath.Join(appdir, desktop) 106 | dst := filepath.Join(home, ".local", "share", "applications", desktop) 107 | if isNewer(dst, appimage) && force == false { 108 | return nil 109 | } 110 | 111 | iconSrc := filepath.Join(appdir, icon) 112 | 113 | iconDir := filepath.Join(home, ".local", "share", "icons") 114 | iconDst := filepath.Join(iconDir, icon) 115 | 116 | os.MkdirAll(iconDir, os.ModePerm) 117 | 118 | err := copyFile(iconSrc, iconDst) 119 | if err != nil { 120 | log.Println(err) 121 | } 122 | 123 | srcFp, err := os.Open(src) 124 | if err != nil { 125 | log.Println(err) 126 | return err 127 | } 128 | defer srcFp.Close() 129 | reader := bufio.NewReader(srcFp) 130 | 131 | fp, err := os.Create(dst) 132 | if err != nil { 133 | log.Println(err) 134 | return err 135 | } 136 | defer fp.Close() 137 | for { 138 | line, _, err := reader.ReadLine() 139 | if err != nil { 140 | break 141 | } 142 | if bytes.HasPrefix(line, []byte("Exec=")) { 143 | fp.WriteString("Exec=" + appimage + "\n") 144 | } else { 145 | fp.Write(line) 146 | fp.WriteString("\n") 147 | } 148 | } 149 | return nil 150 | } 151 | 152 | func makeShortcut(force bool) { 153 | // err := appimageLauncher(force) 154 | // if err != nil { 155 | // makeLauncher("Secret-Diary", "secret-diary", "Sd.png", force) 156 | // } 157 | return 158 | } 159 | -------------------------------------------------------------------------------- /shortcut_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | locale "github.com/rocket049/go-locale" 10 | "github.com/skratchdot/open-golang/open" 11 | "golang.org/x/text/encoding/simplifiedchinese" 12 | ) 13 | 14 | func makeShortcut(force bool) error { 15 | exe1, _ := os.Executable() 16 | appdir := filepath.Dir(exe1) 17 | appname := filepath.Join(appdir, "secret-diary.exe") 18 | script := filepath.Join(appdir, "shortcut.vbs") 19 | bat := filepath.Join(appdir, "shortcut.bat") 20 | _, err := os.Lstat(bat) 21 | if err == nil && force == false { 22 | return err 23 | } 24 | 25 | writeBat(bat, "wscript", script, fmt.Sprintf(`/target:"%s"`, appname), "\r\nexit\r\n") 26 | 27 | open.Start(bat) 28 | 29 | return nil 30 | } 31 | 32 | func writeBat(bat string, msgs ...interface{}) { 33 | var buf = []byte(fmt.Sprintln(msgs...)) 34 | var err error = nil 35 | 36 | loc, _ := locale.DetectLocale() 37 | switch loc { 38 | case "zh_CN": 39 | enc := simplifiedchinese.GB18030.NewEncoder() 40 | buf, err = enc.Bytes(buf) 41 | } 42 | 43 | if err == nil { 44 | ioutil.WriteFile(bat, buf, 0644) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /storge.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "reflect" 7 | 8 | "github.com/therecipe/qt/core" 9 | "github.com/therecipe/qt/gui" 10 | ) 11 | 12 | type attachmentFile struct { 13 | Name string 14 | Data []byte 15 | } 16 | 17 | type qtextFormat struct { 18 | Html string 19 | Images map[string][]byte 20 | Attachments []attachmentFile 21 | } 22 | 23 | func (s *myWindow) getRichText() []byte { 24 | var data qtextFormat 25 | data.Html = s.editor.ToHtml() 26 | data.Images = s.getImagesMap(data.Html) 27 | data.Attachments = s.document.Attachments 28 | 29 | buf := bytes.NewBufferString("") 30 | encoder := gob.NewEncoder(buf) 31 | err := encoder.Encode(data) 32 | if err != nil { 33 | return nil 34 | } 35 | return buf.Bytes() 36 | } 37 | 38 | func (s *myWindow) getQText(data []byte) (*qtextFormat, error) { 39 | buf := bytes.NewReader(data) 40 | decoder := gob.NewDecoder(buf) 41 | s.document.Images = make(map[string][]byte) 42 | s.document.Attachments = []attachmentFile{} 43 | err := decoder.Decode(&s.document) 44 | if err == nil { 45 | s.loadImgResources(s.document.Images) 46 | } 47 | return &s.document, err 48 | } 49 | 50 | func (s *myWindow) getImagesMap(html string) map[string][]byte { 51 | urls := s.getImageList(html) 52 | imgMap := make(map[string][]byte) 53 | for _, url := range urls { 54 | buf, ok := s.document.Images[url] 55 | if ok { 56 | imgMap[url] = buf 57 | } 58 | } 59 | return imgMap 60 | } 61 | 62 | func (s *myWindow) loadImgResources(imgs map[string][]byte) { 63 | for url, data := range imgs { 64 | img := gui.NewQImage() 65 | ok := img.LoadFromData(data, len(data), "") 66 | if !ok { 67 | return 68 | } 69 | uri := core.NewQUrl3(url, core.QUrl__TolerantMode) 70 | 71 | s.editor.Document().AddResource(int(gui.QTextDocument__ImageResource), uri, img.ToVariant()) 72 | } 73 | } 74 | 75 | func (s *myWindow) removeCurAttachment() { 76 | idx := s.comboAttachs.CurrentIndex() 77 | if idx == 0 { 78 | return 79 | } 80 | attachs := sliceValueRemoveIndex(s.document.Attachments, idx-1) 81 | s.document.Attachments = attachs.([]attachmentFile) 82 | s.showAttachList() 83 | s.editor.Document().SetModified(true) 84 | //curDiary.Modified = true 85 | } 86 | 87 | func sliceValueRemoveIndex(v interface{}, idx int) interface{} { 88 | val := reflect.ValueOf(v) 89 | if val.Type().Kind() != reflect.Slice { 90 | return v 91 | } 92 | length := val.Len() 93 | if length == 0 { 94 | return v 95 | } 96 | if idx >= length || idx < 0 { 97 | return v 98 | } 99 | 100 | res := reflect.MakeSlice(val.Type(), length-1, length-1) 101 | for i := 0; i < idx; i++ { 102 | res.Index(i).Set(val.Index(i)) 103 | } 104 | for i := idx + 1; i < length; i++ { 105 | res.Index(i - 1).Set(val.Index(i)) 106 | } 107 | return res.Interface() 108 | } 109 | -------------------------------------------------------------------------------- /testdata/.gitignore: -------------------------------------------------------------------------------- 1 | testdata -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | ## 生成从1979-01-01 到当前日期的所有日志索引 2 | 创建文件 `~/.sdiary/test/diary.db`,生成从1979-01-01 到当前日期的所有日志索,用于测试载入速度。 3 | 4 | 生成的测试用户为:`test`,密码:`1234` 5 | 6 | ## Generate all log indexes from 1979-01-01 to the current date 7 | 8 | Create the file `~/.sdiary/test/diary.db', generate all log cables from 1979-01-01 to the current date, used to test load speed. 9 | 10 | The generated test user is: `test', password: `1234` 11 | -------------------------------------------------------------------------------- /testdata/aes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "fmt" 10 | "io" 11 | "os" 12 | "path" 13 | ) 14 | 15 | var dataDir string 16 | 17 | func getSha40String(pwd string) string { 18 | bp := []byte(pwd) 19 | buf := bytes.NewBuffer(make([]byte, 0, len(bp)*40)) 20 | for i := 0; i < 40; i++ { 21 | buf.Write(bp) 22 | } 23 | res := sha256.Sum256(buf.Bytes()) 24 | return fmt.Sprintf("%x", res) 25 | } 26 | 27 | func getSha4(pwd string) []byte { 28 | bp := []byte(pwd) 29 | buf := bytes.NewBuffer(make([]byte, 0, len(bp)*4)) 30 | for i := 0; i < 4; i++ { 31 | buf.Write(bp) 32 | } 33 | res := sha256.Sum256(buf.Bytes()) 34 | return res[:] 35 | } 36 | 37 | func getRealKeyString(pwd string) (string, error) { 38 | 39 | rkey := make([]byte, 32) 40 | 41 | io.ReadFull(rand.Reader, rkey) 42 | p1 := getSha4(pwd) 43 | aesBlock, err := aes.NewCipher(p1) 44 | if err != nil { 45 | return "", err 46 | } 47 | iv := make([]byte, aes.BlockSize) 48 | io.ReadFull(rand.Reader, iv) 49 | 50 | aesEncoder := cipher.NewCTR(aesBlock, iv) 51 | var res = make([]byte, 32) 52 | aesEncoder.XORKeyStream(res, rkey) 53 | buf := bytes.NewBuffer(iv) 54 | buf.Write(res) 55 | return fmt.Sprintf("%x", buf.Bytes()), nil 56 | } 57 | 58 | func newRealKeyString(rkey []byte, pwd string) (string, error) { 59 | p1 := getSha4(pwd) 60 | aesBlock, err := aes.NewCipher(p1) 61 | if err != nil { 62 | return "", err 63 | } 64 | iv := make([]byte, aes.BlockSize) 65 | io.ReadFull(rand.Reader, iv) 66 | 67 | aesEncoder := cipher.NewCTR(aesBlock, iv) 68 | var res = make([]byte, 32) 69 | aesEncoder.XORKeyStream(res, rkey) 70 | buf := bytes.NewBuffer(iv) 71 | buf.Write(res) 72 | return fmt.Sprintf("%x", buf.Bytes()), nil 73 | } 74 | 75 | func decodeRealKey(key, data, iv []byte) ([]byte, error) { 76 | aesBlock, err := aes.NewCipher(key) 77 | if err != nil { 78 | return nil, err 79 | } 80 | aesDecoder := cipher.NewCTR(aesBlock, iv) 81 | var res = make([]byte, 32) 82 | aesDecoder.XORKeyStream(res, data) 83 | return res, nil 84 | } 85 | 86 | func encodeToFile(data []byte, filename string, key []byte) error { 87 | dataPath := path.Join(dataDir, filename) 88 | fp, err := os.Create(dataPath) 89 | if err != nil { 90 | return err 91 | } 92 | defer fp.Close() 93 | 94 | aesBlock, err := aes.NewCipher(key) 95 | if err != nil { 96 | return err 97 | } 98 | iv := make([]byte, aesBlock.BlockSize()) 99 | io.ReadFull(rand.Reader, iv) 100 | _, err = fp.Write(iv) 101 | if err != nil { 102 | return err 103 | } 104 | aesEncoder := cipher.NewCTR(aesBlock, iv) 105 | wr := cipher.StreamWriter{S: aesEncoder, W: fp} 106 | _, err = wr.Write(data) 107 | return err 108 | } 109 | 110 | func decodeFromFile(filename string, key []byte) ([]byte, error) { 111 | dataPath := path.Join(dataDir, filename) 112 | fp, err := os.Open(dataPath) 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer fp.Close() 117 | 118 | aesBlock, err := aes.NewCipher(key) 119 | if err != nil { 120 | return nil, err 121 | } 122 | iv := make([]byte, aesBlock.BlockSize()) 123 | _, err = io.ReadFull(fp, iv) 124 | if err != nil { 125 | return nil, err 126 | } 127 | aesStream := cipher.NewCTR(aesBlock, iv) 128 | rd := cipher.StreamReader{S: aesStream, R: fp} 129 | res := bytes.NewBufferString("") 130 | _, err = io.Copy(res, rd) 131 | return res.Bytes(), err 132 | } 133 | 134 | func encodeToPathName(data []byte, filename string, key []byte) error { 135 | dataPath := filename 136 | fp, err := os.Create(dataPath) 137 | if err != nil { 138 | return err 139 | } 140 | defer fp.Close() 141 | 142 | aesBlock, err := aes.NewCipher(key) 143 | if err != nil { 144 | return err 145 | } 146 | iv := make([]byte, aesBlock.BlockSize()) 147 | io.ReadFull(rand.Reader, iv) 148 | _, err = fp.Write(iv) 149 | if err != nil { 150 | return err 151 | } 152 | aesEncoder := cipher.NewCTR(aesBlock, iv) 153 | wr := cipher.StreamWriter{S: aesEncoder, W: fp} 154 | _, err = wr.Write(data) 155 | return err 156 | } 157 | 158 | func decodeFromPathName(filename string, key []byte) ([]byte, error) { 159 | dataPath := filename 160 | fp, err := os.Open(dataPath) 161 | if err != nil { 162 | return nil, err 163 | } 164 | defer fp.Close() 165 | 166 | aesBlock, err := aes.NewCipher(key) 167 | if err != nil { 168 | return nil, err 169 | } 170 | iv := make([]byte, aesBlock.BlockSize()) 171 | _, err = io.ReadFull(fp, iv) 172 | if err != nil { 173 | return nil, err 174 | } 175 | aesStream := cipher.NewCTR(aesBlock, iv) 176 | rd := cipher.StreamReader{S: aesStream, R: fp} 177 | res := bytes.NewBufferString("") 178 | _, err = io.Copy(res, rd) 179 | return res.Bytes(), err 180 | } 181 | 182 | func decodeFromReader(reader io.Reader, key []byte) ([]byte, error) { 183 | aesBlock, err := aes.NewCipher(key) 184 | if err != nil { 185 | return nil, err 186 | } 187 | iv := make([]byte, aesBlock.BlockSize()) 188 | _, err = io.ReadFull(reader, iv) 189 | if err != nil { 190 | return nil, err 191 | } 192 | aesStream := cipher.NewCTR(aesBlock, iv) 193 | rd := cipher.StreamReader{S: aesStream, R: reader} 194 | res := bytes.NewBufferString("") 195 | _, err = io.Copy(res, rd) 196 | return res.Bytes(), err 197 | } 198 | -------------------------------------------------------------------------------- /testdata/db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/aes" 5 | "database/sql" 6 | "encoding/hex" 7 | "errors" 8 | "log" 9 | "os" 10 | "path" 11 | "time" 12 | 13 | _ "github.com/mattn/go-sqlite3" 14 | ) 15 | 16 | type myDb struct { 17 | db *sql.DB 18 | category int 19 | } 20 | 21 | func createUserDb(name string, pwd string) error { 22 | home1, _ := os.UserHomeDir() 23 | dataDir = path.Join(home1, ".sdiary", name) 24 | os.MkdirAll(dataDir, os.ModePerm) 25 | userDb := path.Join(dataDir, "diary.db") 26 | if _, err := os.Stat(userDb); err == nil { 27 | //exists 28 | return errors.New("Already exists.") 29 | } 30 | db, err := sql.Open("sqlite3", userDb) 31 | if err != nil { 32 | return err 33 | } 34 | defer db.Close() 35 | sqls := []string{"create table user (id integer unique,cdata text,sha40 TEXT not null,realkey text not null,mtime text);", 36 | "create table diaries (id integer unique,cdate text,title text,filename text,mtime text,category int default 0);", 37 | "create index idx1 on diaries(cdate);", 38 | "create table categories(id int,name text);"} 39 | for i := 0; i < len(sqls); i++ { 40 | _, err = db.Exec(sqls[i]) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | 46 | id := 1 47 | now := time.Now() 48 | cdate := now.Format("2006-01-02") 49 | sha40 := getSha40String(pwd) 50 | realKey, err := getRealKeyString(pwd) 51 | if err != nil { 52 | return err 53 | } 54 | mtime := now.Format("2006-01-02 15:04:05") 55 | 56 | _, err = db.Exec("insert into user(id,cdata,sha40,realkey,mtime) values(?,?,?,?,?);", id, cdate, sha40, realKey, mtime) 57 | 58 | return err 59 | } 60 | 61 | func getMyDb(name string) (*myDb, error) { 62 | home1, _ := os.UserHomeDir() 63 | dataDir = path.Join(home1, ".sdiary", name) 64 | os.MkdirAll(dataDir, os.ModePerm) 65 | 66 | userDb := path.Join(dataDir, "diary.db") 67 | if _, err := os.Stat(userDb); err != nil { 68 | return nil, err 69 | } 70 | 71 | db, err := sql.Open("sqlite3", userDb) 72 | if err != nil { 73 | return nil, err 74 | } 75 | res := &myDb{db: db} 76 | res.CheckAndUpgrade() 77 | return res, nil 78 | } 79 | 80 | func (s *myDb) setCategory(c int) { 81 | s.category = c 82 | } 83 | 84 | func (s *myDb) CheckAndUpgrade() error { 85 | diaryStruct, err := s.db.Query("PRAGMA table_info(diaries);") 86 | if err != nil { 87 | return err 88 | } 89 | var rows = 0 90 | for diaryStruct.Next() { 91 | rows++ 92 | } 93 | diaryStruct.Close() 94 | if rows == 5 { 95 | s.db.Exec("alter table diaries add column category int default 0;") 96 | s.db.Exec("create table categories(id int,name text);") 97 | } 98 | return nil 99 | } 100 | 101 | func (s *myDb) Close() { 102 | s.db.Close() 103 | } 104 | 105 | func (s *myDb) GetRealKey(pwd string) ([]byte, error) { 106 | sha40 := getSha40String(pwd) 107 | row := s.db.QueryRow("select realkey from user where sha40=?;", sha40) 108 | var realKey string 109 | err := row.Scan(&realKey) 110 | if err != nil { 111 | return nil, err 112 | } 113 | key, err := hex.DecodeString(realKey) 114 | if err != nil { 115 | return nil, err 116 | } 117 | if len(key) != 32+aes.BlockSize { 118 | return nil, errors.New("Data invalid.") 119 | } 120 | iv := key[:aes.BlockSize] 121 | data := key[aes.BlockSize:] 122 | ukey := getSha4(pwd) 123 | return decodeRealKey(ukey, data, iv) 124 | } 125 | 126 | func (s *myDb) UpdateRealKeyAndSha40(pwdOld, pwdNew string) error { 127 | sha40 := getSha40String(pwdOld) 128 | row := s.db.QueryRow("select realkey from user where sha40=?;", sha40) 129 | var realKey string 130 | err := row.Scan(&realKey) 131 | if err != nil { 132 | return err 133 | } 134 | key, err := hex.DecodeString(realKey) 135 | if err != nil { 136 | return err 137 | } 138 | if len(key) != 32+aes.BlockSize { 139 | return errors.New("Data invalid.") 140 | } 141 | iv := key[:aes.BlockSize] 142 | data := key[aes.BlockSize:] 143 | ukey := getSha4(pwdOld) 144 | rkey, err := decodeRealKey(ukey, data, iv) 145 | if err != nil { 146 | return err 147 | } 148 | rkeyStr, err := newRealKeyString(rkey, pwdNew) 149 | if err != nil { 150 | return err 151 | } 152 | _, err = s.db.Exec("update user set realkey=?,sha40=? where id=1;", rkeyStr, getSha40String(pwdNew)) 153 | return err 154 | } 155 | 156 | func (s *myDb) AddDiary(id int, cdate, title, filename string) error { 157 | _, err := s.db.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 158 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), s.category) 159 | if err != nil { 160 | log.Println("UpdateDiaryTitle") 161 | return s.UpdateDiaryTitle(id, title) 162 | } 163 | return nil 164 | } 165 | 166 | func (s *myDb) AddDiary2(id int, cdate, title, filename string, category int) error { 167 | _, err := s.db.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 168 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), category) 169 | if err != nil { 170 | log.Println("UpdateDiaryTitle") 171 | return s.UpdateDiaryTitle(id, title) 172 | } 173 | return nil 174 | } 175 | 176 | func (s *myDb) UpdateDiaryTitle(id int, title string) error { 177 | res, err := s.db.Exec("update diaries set title=?,mtime=? where id=?", title, time.Now().Format("2006-01-02 15:04:05"), id) 178 | if err != nil { 179 | return err 180 | } 181 | if n, _ := res.RowsAffected(); n == 0 { 182 | return errors.New("UpdateDiaryTitle fail.") 183 | } 184 | return nil 185 | } 186 | 187 | func (s *myDb) UpdateDiaryCategory(id, c int) error { 188 | res, err := s.db.Exec("update diaries set category=?,mtime=? where id=?", c, time.Now().Format("2006-01-02 15:04:05"), id) 189 | if err != nil { 190 | return err 191 | } 192 | if n, _ := res.RowsAffected(); n == 0 { 193 | return errors.New("UpdateDiaryCategory fail.") 194 | } 195 | return nil 196 | } 197 | 198 | func (s *myDb) TouchDiary(id int) error { 199 | res, err := s.db.Exec("update diaries set mtime=? where id=?;", time.Now().Format("2006-01-02 15:04:05"), id) 200 | if err != nil { 201 | return err 202 | } 203 | if n, _ := res.RowsAffected(); n == 0 { 204 | return errors.New("AddDiary fail.") 205 | } 206 | return nil 207 | } 208 | 209 | func (s *myDb) NextId() int { 210 | res := s.db.QueryRow("select id from diaries order by id desc limit 1;") 211 | 212 | var id int 213 | err := res.Scan(&id) 214 | if err != nil { 215 | log.Println(err.Error() + ": at db 1") 216 | return 1 217 | } 218 | return id + 1 219 | } 220 | 221 | func (s *myDb) GetYearMonths() ([]string, error) { 222 | rows, err := s.db.Query("select distinct substr(cdate,0,8) from diaries where category=? order by substr(cdate,0,8) asc;", s.category) 223 | if err != nil { 224 | return nil, err 225 | } 226 | defer rows.Close() 227 | res := []string{} 228 | for rows.Next() { 229 | var v string 230 | rows.Scan(&v) 231 | res = append(res, v) 232 | } 233 | return res, nil 234 | } 235 | 236 | type diaryItem struct { 237 | Id int 238 | Day string 239 | Title string 240 | MTime string 241 | } 242 | 243 | func (s *myDb) GetListFromYearMonth(ym string) ([]diaryItem, error) { 244 | rows, err := s.db.Query("select id,substr(cdate,9,11),title,mtime from diaries where substr(cdate,0,8)=? and category=? order by substr(cdate,9,11) desc;", ym, s.category) 245 | if err != nil { 246 | return nil, err 247 | } 248 | defer rows.Close() 249 | res := []diaryItem{} 250 | for rows.Next() { 251 | var v diaryItem 252 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 253 | res = append(res, v) 254 | } 255 | return res, nil 256 | } 257 | 258 | func (s *myDb) RemoveDiary(id string) error { 259 | row := s.db.QueryRow("select filename from diaries where id=?;", id) 260 | var filename string 261 | err := row.Scan(&filename) 262 | if err != nil { 263 | return err 264 | } 265 | _, err = s.db.Exec("delete from diaries where id=?;", id) 266 | if err != nil { 267 | return err 268 | } 269 | return os.Remove(path.Join(dataDir, filename)) 270 | } 271 | 272 | func (s *myDb) SearchTitle(kw string) ([]diaryItem, error) { 273 | rows, err := s.db.Query("select id,cdate,title,mtime from diaries where instr(title,?)>0 and category=? order by cdate desc;", kw, s.category) 274 | if err != nil { 275 | return nil, err 276 | } 277 | defer rows.Close() 278 | res := []diaryItem{} 279 | for rows.Next() { 280 | var v diaryItem 281 | rows.Scan(&v.Id, &v.Day, &v.Title, &v.MTime) 282 | res = append(res, v) 283 | } 284 | return res, nil 285 | } 286 | 287 | func (s *myDb) AddCategory(name string) int { 288 | res := s.db.QueryRow("select id from categories order by id desc limit 1;") 289 | 290 | var id int 291 | err := res.Scan(&id) 292 | if err != nil { 293 | id = 1 294 | } else { 295 | id++ 296 | } 297 | 298 | s.db.Exec("insert into categories(id,name) values (?,?);", id, name) 299 | return id 300 | } 301 | 302 | func (s *myDb) RenameCategory(id int, name string) error { 303 | _, err := s.db.Exec("update categories set name=? where id=?;", name, id) 304 | return err 305 | } 306 | 307 | func (s *myDb) RemoveCategory(c int) { 308 | s.db.Exec("delete from categories where id=?;", c) 309 | s.db.Exec("update diaries set category=0 where category=?;", c) 310 | } 311 | 312 | func (s *myDb) GetCategories() map[int]string { 313 | res := make(map[int]string) 314 | 315 | rows, err := s.db.Query("select id,name from categories order by id asc;") 316 | if err != nil { 317 | return nil 318 | } 319 | defer rows.Close() 320 | 321 | for rows.Next() { 322 | var id int 323 | var name string 324 | if rows.Scan(&id, &name) == nil { 325 | res[id] = name 326 | } 327 | } 328 | 329 | return res 330 | } 331 | -------------------------------------------------------------------------------- /testdata/dbgen.go: -------------------------------------------------------------------------------- 1 | //生成从1979-01-01 到当前日期的所有日志索引 `~/.sdiary/test/diary.db`,用于测试载入速度 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/gob" 8 | "fmt" 9 | "log" 10 | "os" 11 | "path" 12 | "time" 13 | ) 14 | 15 | type attachmentFile struct { 16 | Name string 17 | Data []byte 18 | } 19 | type qtextFormat struct { 20 | Html string 21 | Images map[string][]byte 22 | Attachments []attachmentFile 23 | } 24 | 25 | var dat []byte = nil 26 | var key []byte 27 | 28 | func createDat(home string, user string, id int) error { 29 | path1 := path.Join(home, ".sdiary", user, fmt.Sprintf("%d.dat", id)) 30 | if dat == nil { 31 | var err error 32 | var data qtextFormat 33 | data.Html = "Test" 34 | data.Images = nil 35 | data.Attachments = nil 36 | 37 | buf := bytes.NewBufferString("") 38 | encoder := gob.NewEncoder(buf) 39 | err = encoder.Encode(data) 40 | if err != nil { 41 | return err 42 | } 43 | dat = buf.Bytes() 44 | } 45 | return encodeToPathName(dat, path1, key) 46 | } 47 | 48 | func createDiarys() { 49 | home, _ := os.UserHomeDir() 50 | path1 := path.Join(home, ".sdiary", "test", "diary.db") 51 | os.Remove(path1) 52 | err := createUserDb("test", "1234") 53 | if err != nil { 54 | panic(err.Error() + "1") 55 | } 56 | db, err := getMyDb("test") 57 | if err != nil { 58 | panic(err.Error() + "2") 59 | } 60 | defer db.Close() 61 | key, _ = db.GetRealKey("1234") 62 | tt, err := time.Parse("20060102", "19790101") 63 | if err != nil { 64 | panic(err.Error() + "3") 65 | } 66 | log.Println("start", tt.Format("2006-01-02"), tt.Unix()) 67 | stop := time.Now().Unix() 68 | log.Println("end", time.Now().Format("2006-01-02"), stop) 69 | id := db.NextId() 70 | log.Println("start id:", id) 71 | tx, err := db.db.Begin() 72 | if err != nil { 73 | panic(err.Error() + "4") 74 | } 75 | for tt.Unix() < stop { 76 | cdate := tt.Format("2006-01-02") 77 | title := fmt.Sprintf("diary %d", id) 78 | filename := fmt.Sprintf("%d.dat", id) 79 | tx.Exec("insert into diaries(id,cdate,title,filename,mtime,category) values(?,?,?,?,?,?);", 80 | id, cdate, title, filename, time.Now().Format("2006-01-02 15:04:05"), db.category) 81 | err = createDat(home, "test", id) 82 | if err != nil { 83 | log.Println(err, " dat") 84 | } 85 | tt = tt.Add(time.Hour * 24) 86 | id++ 87 | } 88 | err = tx.Commit() 89 | if err != nil { 90 | panic(err.Error() + "5") 91 | } 92 | log.Println("success") 93 | } 94 | 95 | func main() { 96 | createDiarys() 97 | } 98 | -------------------------------------------------------------------------------- /testdata/exam.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocket049/secret-diary/38e841bc3ad155ba813ad9dd8386e40b3421260a/testdata/exam.dat -------------------------------------------------------------------------------- /testdata/jinzhi16.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // import ( 4 | // "bytes" 5 | // "crypto/sha256" 6 | // "fmt" 7 | // ) 8 | 9 | // func getSha40String(pwd string) string { 10 | // bp := []byte(pwd) 11 | // buf := bytes.NewBuffer(make([]byte, 0, len(bp)*40)) 12 | // for i := 0; i < 40; i++ { 13 | // buf.Write(bp) 14 | // } 15 | // res := sha256.Sum256(buf.Bytes()) 16 | // return fmt.Sprintf("%x", res) 17 | // } 18 | 19 | // func main() { 20 | // fmt.Printf("%s\n", getSha40String("abcd")) 21 | // } 22 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const version = "1.2.16" 4 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version":"1.2.16" 3 | } 4 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sed -i s/version\ =\ .*$/version\ =\ \"$1\"/g version.go 4 | sed -i s/\"Version\":.*$/\"Version\":\"$1\"/g version.json 5 | sed -i s/^Version:.*$/Version:\ $1/g ./deploy/secret-diary-deb/DEBIAN/control 6 | -------------------------------------------------------------------------------- /versionCheck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | type VersionOL struct { 12 | Version string 13 | } 14 | 15 | func versionCheck() (v string, newest bool) { 16 | resp, err := http.Get("https://gitee.com/rocket049/secret-diary/raw/master/version.json") 17 | if err != nil { 18 | log.Println(err) 19 | newest = false 20 | return 21 | } 22 | defer resp.Body.Close() 23 | 24 | buf := bytes.NewBufferString("") 25 | _, err = io.Copy(buf, resp.Body) 26 | if err != nil { 27 | log.Println(err) 28 | newest = false 29 | return 30 | } 31 | 32 | var ver VersionOL 33 | err = json.Unmarshal(buf.Bytes(), &ver) 34 | if err != nil { 35 | log.Println(err) 36 | newest = false 37 | return 38 | } 39 | 40 | if version == ver.Version { 41 | v = version 42 | newest = true 43 | return 44 | } 45 | 46 | v = ver.Version 47 | newest = false 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /zip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/zip" 5 | "crypto/aes" 6 | "database/sql" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "os" 12 | "path/filepath" 13 | ) 14 | 15 | func zipData(fromDir, toFile string) error { 16 | dir, err := os.Open(fromDir) 17 | if err != nil { 18 | return err 19 | } 20 | defer dir.Close() 21 | 22 | //open zip 23 | fileToZip, err := os.Create(toFile) 24 | if err != nil { 25 | return err 26 | } 27 | defer fileToZip.Close() 28 | zipFile := zip.NewWriter(fileToZip) 29 | defer zipFile.Close() 30 | 31 | files, err := dir.Readdirnames(-1) 32 | if err != nil { 33 | return err 34 | } 35 | for _, name := range files { 36 | filename := filepath.Join(fromDir, name) 37 | fp, err := os.Open(filename) 38 | if err != nil { 39 | return err 40 | 41 | } 42 | //add to zip 43 | writer, err := zipFile.Create(name) 44 | if err != nil { 45 | return err 46 | } 47 | _, err = io.Copy(writer, fp) 48 | if err != nil { 49 | return err 50 | } 51 | fp.Close() 52 | } 53 | return nil 54 | } 55 | 56 | func (s *myWindow) importFromZip(filename string, pwd string) error { 57 | zipFile, err := zip.OpenReader(filename) 58 | if err != nil { 59 | return err 60 | } 61 | defer zipFile.Close() 62 | //unzip diary.db 63 | var diaryDbName string 64 | for _, zf := range zipFile.File { 65 | if zf.Name == "diary.db" { 66 | diaryDbName = filepath.Join(os.TempDir(), "diary.db") 67 | fp, err := os.Create(diaryDbName) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | reader, err := zf.Open() 73 | if err != nil { 74 | fp.Close() 75 | return err 76 | } 77 | 78 | _, err = io.Copy(fp, reader) 79 | 80 | reader.Close() 81 | if err != nil { 82 | fp.Close() 83 | reader.Close() 84 | return err 85 | } 86 | 87 | fp.Close() 88 | break 89 | } 90 | } 91 | db, err := sql.Open("sqlite3", diaryDbName) 92 | if err != nil { 93 | return err 94 | } 95 | defer os.Remove(diaryDbName) 96 | defer db.Close() 97 | 98 | res, err := db.Query("select cdate,title,filename,category from diaries") 99 | if err != nil { 100 | return err 101 | } 102 | //get filename title dict 103 | type diaryRecod struct { 104 | Cdate string 105 | Title string 106 | Category int 107 | } 108 | var titleDict = make(map[string]diaryRecod) 109 | var cdate, title, fname string 110 | var category int 111 | for res.Next() { 112 | err := res.Scan(&cdate, &title, &fname, &category) 113 | if err != nil { 114 | res.Close() 115 | return err 116 | } 117 | titleDict[fname] = diaryRecod{cdate, title, category} 118 | } 119 | res.Close() 120 | //get real key 121 | key, err := dbGetRealKey(db, pwd) 122 | if err != nil { 123 | return err 124 | } 125 | //import every file from zip 126 | id := s.db.NextId() 127 | tx, err := s.db.BeginTx() 128 | if err != nil { 129 | return err 130 | } 131 | for _, zf := range zipFile.File { 132 | if zf.Name == "diary.db" { 133 | break 134 | } 135 | record, ok := titleDict[zf.Name] 136 | if !ok { 137 | break 138 | } 139 | reader, err := zf.Open() 140 | if err != nil { 141 | return err 142 | } 143 | data, err := decodeFromReader(reader, key) 144 | reader.Close() 145 | if err != nil { 146 | return err 147 | } 148 | 149 | //save diary 150 | datName := fmt.Sprintf("%d.dat", id) 151 | encodeToFile(data, datName, s.key) 152 | s.db.AddDiaryTx(tx, id, record.Cdate, record.Title, datName, record.Category) 153 | s.setStatusBar(datName) 154 | id++ 155 | } 156 | return tx.Commit() 157 | } 158 | 159 | func dbGetRealKey(db *sql.DB, pwd string) ([]byte, error) { 160 | sha40 := getSha40String(pwd) 161 | row := db.QueryRow("select realkey from user where sha40=?;", sha40) 162 | var realKey string 163 | err := row.Scan(&realKey) 164 | if err != nil { 165 | return nil, err 166 | } 167 | key, err := hex.DecodeString(realKey) 168 | if err != nil { 169 | return nil, err 170 | } 171 | if len(key) != 32+aes.BlockSize { 172 | return nil, errors.New("Data invalid.") 173 | } 174 | iv := key[:aes.BlockSize] 175 | data := key[aes.BlockSize:] 176 | ukey := getSha4(pwd) 177 | return decodeRealKey(ukey, data, iv) 178 | } 179 | --------------------------------------------------------------------------------