├── dev ├── nwarch.txt ├── nwversion.txt ├── appname.txt ├── appicon.png └── mac │ ├── start.command │ ├── build.command │ └── Info.plist ├── assets ├── cmdr.png ├── terminal1.png ├── terminal2.png ├── screenshot.png └── recoverymode.png ├── app ├── img │ └── icon.png ├── bower.json ├── package.json ├── main.html ├── main.css └── main.js ├── .gitignore ├── LICENSE.md └── README.md /dev/nwarch.txt: -------------------------------------------------------------------------------- 1 | 64 -------------------------------------------------------------------------------- /dev/nwversion.txt: -------------------------------------------------------------------------------- 1 | 0.12.3 -------------------------------------------------------------------------------- /dev/appname.txt: -------------------------------------------------------------------------------- 1 | Bubble Painter -------------------------------------------------------------------------------- /assets/cmdr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/assets/cmdr.png -------------------------------------------------------------------------------- /dev/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/dev/appicon.png -------------------------------------------------------------------------------- /app/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/app/img/icon.png -------------------------------------------------------------------------------- /assets/terminal1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/assets/terminal1.png -------------------------------------------------------------------------------- /assets/terminal2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/assets/terminal2.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /assets/recoverymode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kethinov/BubblePainter/HEAD/assets/recoverymode.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | .DS_Store 16 | node_modules 17 | 18 | .jshintrc 19 | 20 | nwjs-v* 21 | build 22 | 23 | bower_components -------------------------------------------------------------------------------- /app/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nw-app", 3 | "dependencies": { 4 | "nw.js-external-linker.js": "1.0.4", 5 | "page.js": "1.6.1", 6 | "page.js-body-parser.js": "1.0.7", 7 | "page.js-express-mapper.js": "1.0.5", 8 | "teddy": "0.3.4" 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License 2 | === 3 | 4 | All original code in this app is licensed under the [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/). Commercial and noncommercial use is permitted with attribution. 5 | 6 | Thanks goes to [Joaquim Alves Gaspar](http://commons.wikimedia.org/wiki/User:Alvesgaspar) for the dock icon! ([Original source](http://en.wikipedia.org/wiki/File:Reflection_in_a_soap_bubble_edit.jpg)) -------------------------------------------------------------------------------- /dev/mac/start.command: -------------------------------------------------------------------------------- 1 | cd "`dirname "$0"`/../" 2 | 3 | nw=$(cat nwversion.txt) 4 | nwa=$(cat nwarch.txt) 5 | if [ "$nwa" = "64" ]; then 6 | nwa=x64 7 | else 8 | nwa=ia32 9 | fi 10 | 11 | cd .. 12 | 13 | hash npm 2>/dev/null || { 14 | echo >&2 "You must install npm to run this program: http://npmjs.org" 15 | exit 1 16 | } 17 | 18 | if [ ! -d "app/node_modules" ]; then 19 | cd app 20 | npm install 21 | cd .. 22 | fi 23 | 24 | hash bower 2>/dev/null || { 25 | echo >&2 "You must install bower to run this program: http://bower.io" 26 | exit 1 27 | } 28 | 29 | if [ ! -d "app/bower_components" ]; then 30 | cd app 31 | bower install 32 | cd .. 33 | fi 34 | 35 | if [ ! -d "dev/mac/nwjs-v$nw-osx-$nwa" ]; then 36 | echo "Downloading nw.js v$nw development environment..." 37 | curl -sS http://dl.nwjs.io/v$nw/nwjs-v$nw-osx-$nwa.zip > nw.zip 38 | unzip nw.zip -d . 39 | rm nw.zip 40 | mv nwjs-v$nw-osx-$nwa dev/mac/ 41 | fi 42 | 43 | ./dev/mac/nwjs-v$nw-osx-$nwa/nwjs.app/Contents/MacOS/nwjs app/ -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nw-app", 3 | "description": "Customize the message bubbles in Messages.app in OS X 10.10 Yosemite", 4 | "author": "Eric Newport ", 5 | "version": "2.1.1", 6 | "homepage": "https://github.com/kethinov/BubblePainter", 7 | "license": "CC-BY-4.0", 8 | "main": "main.html", 9 | "readmeFilename": "README.md", 10 | "engines": { 11 | "node": ">=0.10.0" 12 | }, 13 | "engineStrict": true, 14 | "window": { 15 | "show": false, 16 | "toolbar": false, 17 | "width": 720, 18 | "min_width": 720, 19 | "max_width": 720, 20 | "height": 720, 21 | "min_height": 720, 22 | "max_height": 720 23 | }, 24 | "dependencies": { 25 | "css": "^1.6.0" 26 | }, 27 | "jshintConfig": { 28 | "camelcase": true, 29 | "curly": true, 30 | "devel": true, 31 | "evil": true, 32 | "indent": 2, 33 | "node": true 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/kethinov/BubblePainter.git" 38 | }, 39 | "private": true 40 | } 41 | -------------------------------------------------------------------------------- /dev/mac/build.command: -------------------------------------------------------------------------------- 1 | cd "`dirname "$0"`/../" 2 | 3 | nw=$(cat nwversion.txt) 4 | nwa=$(cat nwarch.txt) 5 | if [ "$nwa" = "64" ]; then 6 | nwa=x64 7 | else 8 | nwa=ia32 9 | fi 10 | appname=$(cat appname.txt) 11 | 12 | cd .. 13 | 14 | hash npm 2>/dev/null || { 15 | echo >&2 "You must install npm to run this program: http://npmjs.org" 16 | exit 1 17 | } 18 | 19 | if [ ! -d "app/node_modules" ]; then 20 | cd app 21 | npm install 22 | cd .. 23 | fi 24 | 25 | hash bower 2>/dev/null || { 26 | echo >&2 "You must install bower to run this program: http://bower.io" 27 | exit 1 28 | } 29 | 30 | if [ ! -d "app/bower_components" ]; then 31 | cd app 32 | bower install 33 | cd .. 34 | fi 35 | 36 | if [ ! -d "dev/mac/nwjs-v$nw-osx-$nwa" ]; then 37 | echo "Downloading nw.js v$nw development environment..." 38 | curl -sS http://dl.nwjs.io/v$nw/nwjs-v$nw-osx-$nwa.zip > nw.zip 39 | unzip nw.zip -d . 40 | rm nw.zip 41 | mv nwjs-v$nw-osx-$nwa dev/mac/ 42 | fi 43 | 44 | if [ ! -d "build" ]; then 45 | mkdir build 46 | fi 47 | 48 | rm -rf "build/$appname.app" 49 | cp -R dev/mac/nwjs-v$nw-osx-$nwa/nwjs.app build/ 50 | cp -R app build/nwjs.app/Contents/Resources/ 51 | mv build/nwjs.app/Contents/Resources/app build/nwjs.app/Contents/Resources/app.nw 52 | cp dev/mac/Info.plist build/nwjs.app/Contents/ 53 | 54 | mkdir appicon.iconset 55 | sips -z 16 16 dev/appicon.png --out appicon.iconset/icon_16x16.png 56 | sips -z 32 32 dev/appicon.png --out appicon.iconset/icon_16x16@2x.png 57 | sips -z 32 32 dev/appicon.png --out appicon.iconset/icon_32x32.png 58 | sips -z 64 64 dev/appicon.png --out appicon.iconset/icon_32x32@2x.png 59 | sips -z 128 128 dev/appicon.png --out appicon.iconset/icon_128x128.png 60 | sips -z 256 256 dev/appicon.png --out appicon.iconset/icon_128x128@2x.png 61 | sips -z 256 256 dev/appicon.png --out appicon.iconset/icon_256x256.png 62 | sips -z 512 512 dev/appicon.png --out appicon.iconset/icon_256x256@2x.png 63 | sips -z 512 512 dev/appicon.png --out appicon.iconset/icon_512x512.png 64 | cp dev/appicon.png appicon.iconset/icon_512x512@2x.png 65 | iconutil -c icns appicon.iconset 66 | rm -R appicon.iconset 67 | mv appicon.icns build/nwjs.app/Contents/Resources/ 68 | rm -rf appicon.icns build/nwjs.app/Contents/Resources/nw.icns 69 | 70 | mv build/nwjs.app "build/$appname.app" -------------------------------------------------------------------------------- /dev/mac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 12C3006 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleDisplayName 10 | Bubble Painter 11 | CFBundleDocumentTypes 12 | 13 | 14 | CFBundleTypeIconFile 15 | appicon.icns 16 | CFBundleTypeName 17 | Bubble Painter 18 | CFBundleTypeRole 19 | Viewer 20 | LSHandlerRank 21 | Owner 22 | 23 | 24 | CFBundleTypeName 25 | Folder 26 | CFBundleTypeOSTypes 27 | 28 | fold 29 | 30 | CFBundleTypeRole 31 | Viewer 32 | LSHandlerRank 33 | None 34 | 35 | 36 | CFBundleExecutable 37 | nwjs 38 | CFBundleIconFile 39 | appicon.icns 40 | CFBundleInfoDictionaryVersion 41 | 6.0 42 | CFBundleName 43 | Bubble Painter 44 | CFBundlePackageType 45 | APPL 46 | CFBundleShortVersionString 47 | 2.1.1 48 | CFBundleVersion 49 | 1700.107 50 | DTSDKBuild 51 | 11E52 52 | DTSDKName 53 | macosx10.7 54 | DTXcode 55 | 0452 56 | DTXcodeBuild 57 | 4G2008a 58 | LSFileQuarantineEnabled 59 | 60 | LSMinimumSystemVersion 61 | 10.6.0 62 | NSPrincipalClass 63 | NSApplication 64 | NSSupportsAutomaticGraphicsSwitching 65 | 66 | SCMRevision 67 | 239963 68 | UTExportedTypeDeclarations 69 | 70 | 71 | UTTypeConformsTo 72 | 73 | com.pkware.zip-archive 74 | 75 | UTTypeDescription 76 | Bubble Painter 77 | UTTypeIconFile 78 | appicon.icns 79 | UTTypeTagSpecification 80 | 81 | com.apple.ostype 82 | Bubble Painter 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bubble Painter 2 | === 3 | 4 | **NOTE: NO LONGER MAINTAINED.** 5 | 6 | **As of macOS 10.14 Mojave, I have not had time to maintain this project, so it may not work well on recent versions of macOS.** 7 | 8 | Apple removed the ability to customize the colors of the message bubbles in [Messages.app](http://en.wikipedia.org/wiki/Messages_%28application%29#OS_X_version) in Mac OS X 10.10 Yosemite. This app gives you back that ability. 9 | 10 | 11 | 12 | How to install 13 | === 14 | 15 | First [download latest version](https://github.com/kethinov/BubblePainter/releases/latest) 16 | 17 | Then if you're running OS X 10.11 El Capitan or a later version of OS X, you will need to disable System Integrity Protection temporarily in order to use Bubble Painter. 18 | 19 | Disable System Integrity Protection temporarily 20 | --- 21 | 22 | On OS X 10.11 El Capitan or later versions of OS X, you must disable System Integrity Protection temporarily to use this app. 23 | 24 | *Note: none of this is necessary on OS X 10.10 Yosemite. Only OS X 10.11 El Capitan or later versions of OS X.* 25 | 26 | Here's how: 27 | 28 | Restart your Mac and hold down ⌘ Command + R until the Apple logo appears on your screen. 29 | 30 | You should now see this: 31 | 32 | 33 | 34 | Now open the `Utilities` menu and select `Terminal`: 35 | 36 | 37 | 38 | In the Terminal window that opens, enter the following command: `csrutil disable` 39 | 40 | Then press the return key. Afterward you should see the following message: 41 | 42 | 43 | 44 | Then restart your Mac and Bubble Painter should work. 45 | 46 | **It is recommended that you reenable System Integrity Protection after you're done using Bubble Painter.** 47 | 48 | To reenable System Integrity Protection, follow the same steps as above, but enter the following terminal command instead: `csrutil enable` 49 | 50 | Then simply reboot again. 51 | 52 | It's a [known issue](https://github.com/kethinov/BubblePainter/issues/29) that going through this much hassle sucks. If you have an idea for how to avoid all this being necessary, I'd love to hear it! 53 | 54 | How to hack this app's source code 55 | === 56 | 57 | 1. Clone this repo 58 | 2. To run the app from source code, open `dev/mac/start.command` 59 | 3. To do a build, open `dev/mac/build.command` 60 | 61 | .app files will be located in the `build` directory. 62 | -------------------------------------------------------------------------------- /app/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bubble Painter 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 81 | 82 | 90 | 91 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /app/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | user-select: none; 5 | -webkit-user-drag: none; 6 | cursor: default; 7 | margin: 0; 8 | padding: 0; 9 | width: 100%; 10 | height: 100%; 11 | overflow: hidden; 12 | } 13 | 14 | body { 15 | background: radial-gradient(ellipse farthest-corner, #fff 43%, #ddd); 16 | background-repeat: no-repeat; 17 | font-family: 'Helvetica'; 18 | font-weight: 100; 19 | font-size: 18px; 20 | color: #000; 21 | } 22 | 23 | a:link, 24 | a:visited, 25 | a:active, 26 | a:hover { 27 | text-decoration: none; 28 | color: #000; 29 | } 30 | 31 | a:link[rel='external'], 32 | a:visited[rel='external'], 33 | a:active[rel='external'], 34 | a:hover[rel='external'] { 35 | text-decoration: underline; 36 | } 37 | 38 | .disabled label, 39 | [disabled] { 40 | opacity: 0.25; 41 | } 42 | 43 | main { 44 | padding: 25px; 45 | } 46 | 47 | dl { 48 | overflow: hidden; 49 | } 50 | 51 | p { 52 | margin: 10px 0; 53 | text-align: center; 54 | } 55 | 56 | li { 57 | list-style-type: none; 58 | } 59 | 60 | h1, 61 | h2 { 62 | margin: 0; 63 | font-size: 35px; 64 | font-family: 'HelveticaNeue-UltraLight'; 65 | text-align: center; 66 | font-weight: 100; 67 | } 68 | 69 | h2 { 70 | font-size: 32px; 71 | } 72 | 73 | h1.icon { 74 | display: block; 75 | background-image: url('img/icon.png'); 76 | background-position: bottom center; 77 | background-repeat: no-repeat; 78 | background-size: 57px 52px; 79 | padding-bottom: 57px; 80 | } 81 | 82 | ul { 83 | padding: 0; 84 | margin: 0; 85 | } 86 | 87 | dt { 88 | display: block; 89 | width: 165px; 90 | float: left; 91 | clear: both; 92 | } 93 | 94 | .received dt { 95 | width: 125px; 96 | } 97 | 98 | dd { 99 | float: left; 100 | margin: 0; 101 | } 102 | 103 | .sent { 104 | float: right; 105 | } 106 | 107 | .received { 108 | float: left; 109 | } 110 | 111 | .msgwidth dt, 112 | .msgwidth dd { 113 | clear: both; 114 | display: block; 115 | width: 100%; 116 | } 117 | 118 | .msgwidth dd { 119 | text-align: center; 120 | } 121 | 122 | .msgwidth input { 123 | width: 99%; 124 | } 125 | 126 | #sentcolors { 127 | margin-bottom: 20px; 128 | } 129 | 130 | #isentcolors { 131 | margin-top: 5px; 132 | } 133 | 134 | label[for=sameforboth] { 135 | font-size: 80%; 136 | } 137 | 138 | #sameforboth { 139 | margin-top: 3px; 140 | float: right; 141 | } 142 | 143 | .preview { 144 | background-color: #fff; 145 | width: 420px; 146 | clear: left; 147 | border-radius: 10px; 148 | border: 1px #c0c0c0 solid; 149 | padding: 10px; 150 | } 151 | 152 | .preview p { 153 | margin: 0; 154 | font-size: 80%; 155 | } 156 | 157 | .received-preview { 158 | clear: left; 159 | position: relative; 160 | max-width: 450px; 161 | overflow: hidden; 162 | padding: 7px; 163 | } 164 | 165 | .sent-preview, 166 | .isent-preview { 167 | clear: left; 168 | position: relative; 169 | max-width: 450px; 170 | overflow: hidden; 171 | padding: 7px; 172 | } 173 | 174 | .received-preview div, 175 | .sent-preview div, 176 | .isent-preview div { 177 | width: 60%; 178 | word-wrap: break-word; 179 | line-height: 24px; 180 | } 181 | 182 | .received-preview p { 183 | text-align: left; 184 | } 185 | 186 | .sent-preview p, 187 | .isent-preview p { 188 | text-align: right; 189 | } 190 | 191 | .from-me { 192 | position: relative; 193 | padding: 5px 10px; 194 | color: #fff; 195 | background:#0b93f6; 196 | border-radius: 10px; 197 | float: right; 198 | } 199 | 200 | .from-me:before { 201 | content: ''; 202 | position: absolute; 203 | z-index: 1; 204 | bottom: -2px; 205 | right: -7px; 206 | height: 20px; 207 | border-right: 18px solid #0b93f6; 208 | border-bottom-left-radius: 16px 14px; 209 | -webkit-transform: translate(0, -2px); 210 | } 211 | 212 | .from-me:after { 213 | content: ''; 214 | position: absolute; 215 | z-index: 1; 216 | bottom: -2px; 217 | right: -56px; 218 | width: 26px; 219 | height: 20px; 220 | background: #fff; 221 | border-bottom-left-radius: 10px; 222 | -webkit-transform: translate(-30px, -2px); 223 | } 224 | 225 | .from-them { 226 | position: relative; 227 | padding: 5px 10px; 228 | background: #e5e5ea; 229 | border-radius: 10px; 230 | color: #000; 231 | float: left; 232 | } 233 | 234 | .from-them:before { 235 | content: ''; 236 | position: absolute; 237 | z-index: 2; 238 | bottom: -2px; 239 | left: -7px; 240 | height: 20px; 241 | border-left: 18px solid #e5e5ea; 242 | border-bottom-right-radius: 16px 14px; 243 | -webkit-transform: translate(0, -2px); 244 | } 245 | 246 | .from-them:after { 247 | content: ''; 248 | position: absolute; 249 | z-index: 3; 250 | bottom: -2px; 251 | left: 4px; 252 | width: 26px; 253 | height: 20px; 254 | background: #fff; 255 | border-bottom-right-radius: 10px; 256 | -webkit-transform: translate(-30px, -2px); 257 | } 258 | 259 | .actions { 260 | clear: both; 261 | text-align: center; 262 | } 263 | 264 | form[action='/change'] .actions { 265 | position: absolute; 266 | bottom: 20px; 267 | right: 20px; 268 | width: 200px; 269 | display: block; 270 | text-align: right; 271 | } 272 | 273 | .actions li { 274 | display: inline; 275 | margin: 10px; 276 | } 277 | 278 | .actions li:last-child { 279 | margin: 10px 0; 280 | } 281 | 282 | .donate { 283 | position: absolute; 284 | bottom: 10px; 285 | left: 25px; 286 | width: 200px; 287 | display: block; 288 | text-align: left; 289 | } 290 | 291 | .passPrompt dl { 292 | display: block; 293 | width: 100%; 294 | text-align: center; 295 | } 296 | 297 | .passPrompt dt, 298 | .passPrompt dd { 299 | width: auto; 300 | display: inline; 301 | float: none; 302 | } 303 | 304 | .passPrompt dt { 305 | padding-right: 10px; 306 | } -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | util = require('util'), 3 | exec = require('child_process').exec, 4 | css = require('css'), 5 | cssFile = '/System/Library/Messages/PlugIns/Balloons.transcriptstyle/Contents/Resources/balloons-modern.css', 6 | elCapitanFile = '/System/Library/PrivateFrameworks/SocialUI.framework/Versions/A/Resources/balloons-modern.css', 7 | gui = require('nw.gui'), 8 | bubblepainter = gui.Window.get(), 9 | nativeMenuBar = new gui.Menu({type: 'menubar'}), 10 | configFile, 11 | parsedCss, 12 | needPassword = false; 13 | 14 | // activate express-mapper plugin 15 | pageExpressMapper({ 16 | renderMethod: function(template, model) { 17 | if (!teddy.compiledTemplates[template]) { 18 | teddy.compile(document.getElementById(template).innerHTML, template); 19 | } 20 | document.getElementsByTagName('main')[0].innerHTML = teddy.render(template + '.html', model); 21 | }, 22 | expressAppName: 'app' 23 | }); 24 | 25 | /* 26 | * utility methods 27 | */ 28 | 29 | function componentToHex(c) { 30 | var hex = c.toString(16); 31 | return hex.length == 1 ? '0' + hex : hex; 32 | } 33 | 34 | function rgbToHex(r, g, b) { 35 | return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b); 36 | } 37 | 38 | function checkYourPrivilege(req, res, callback) { 39 | fs.open(cssFile, 'r+', function(err, data) { 40 | if (err) { 41 | if (err.code === 'ENOENT') { 42 | if (cssFile !== elCapitanFile) { 43 | cssFile = elCapitanFile; 44 | checkYourPrivilege(req, res, callback); 45 | } 46 | else { 47 | res.render('fileError', {msg: 'Could not locate Messages.app preference files'}); 48 | } 49 | } 50 | else { 51 | fs.open(cssFile, 'r', function(err, data) { 52 | needPassword = true; 53 | callback(req, res); 54 | }); 55 | } 56 | } 57 | else { 58 | callback(req, res); 59 | } 60 | }); 61 | } 62 | 63 | function setColors(skipInputs) { 64 | var previewOverrides = document.getElementById('previewOverrides'), 65 | previewOverrideCss = ''; 66 | 67 | if (!skipInputs) { 68 | document.getElementById('sameforboth').checked = localStorage.getItem('sameforboth') === 'true' ? true : false; 69 | 70 | document.getElementById('msgwidth').value = localStorage.getItem('msgwidth'); 71 | 72 | document.getElementById('received').value = localStorage.getItem('received'); 73 | document.getElementById('receivedText').value = localStorage.getItem('receivedText'); 74 | 75 | document.getElementById('senttop').value = localStorage.getItem('senttop'); 76 | document.getElementById('sentbottom').value = localStorage.getItem('sentbottom'); 77 | document.getElementById('sentText').value = localStorage.getItem('sentText'); 78 | 79 | document.getElementById('isenttop').value = localStorage.getItem('isenttop'); 80 | document.getElementById('isentbottom').value = localStorage.getItem('isentbottom'); 81 | document.getElementById('isentText').value = localStorage.getItem('isentText'); 82 | } 83 | 84 | if (previewOverrides) { 85 | previewOverrides.parentNode.removeChild(previewOverrides); 86 | } 87 | previewOverrideCss += ''; 117 | document.body.insertAdjacentHTML('beforeend', previewOverrideCss); 118 | 119 | document.getElementById('sameforboth').onclick = setColors; 120 | document.getElementById('senttop').onchange = onColorChange; 121 | document.getElementById('sentbottom').onchange = onColorChange; 122 | document.getElementById('sentText').onchange = onColorChange; 123 | document.getElementById('isenttop').onchange = onColorChange; 124 | document.getElementById('isentbottom').onchange = onColorChange; 125 | document.getElementById('isentText').onchange = onColorChange; 126 | document.getElementById('received').onchange = onColorChange; 127 | document.getElementById('receivedText').onchange = onColorChange; 128 | document.getElementById('msgwidth').onchange = onColorChange; 129 | } 130 | 131 | function onColorChange(e) { 132 | localStorage.setItem(e.target.id, e.target.value); 133 | setColors(true); 134 | } 135 | 136 | function getDefaultColors(req, res, callback) { 137 | fs.readFile(cssFile, 'utf8', function(err, data) { 138 | var i, 139 | rules, 140 | rule, 141 | d, 142 | declarations, 143 | prop, 144 | sentRgb, 145 | receivedRgb, 146 | sentHex, 147 | receivedHex, 148 | msgwidth; 149 | 150 | if (err) { 151 | res.render('fileError', {msg: 'Could not locate Messages.app preference files'}); 152 | } 153 | else { 154 | if (data.indexOf('/* begin Bubble Painter code */') > -1) { 155 | req.altered = true; 156 | } 157 | callback(req, res, function() { 158 | if (localStorage.getItem('isenttop')) { 159 | setColors(); 160 | } 161 | else { 162 | 163 | configFile = data; 164 | parsedCss = css.parse(configFile); 165 | rules = parsedCss.stylesheet.rules; 166 | for (i in rules) { 167 | rule = rules[i]; 168 | if (rule.selectors == '[typing-indicator="no"][emote="no"] messagetext') { 169 | declarations = rule.declarations; 170 | for (d in declarations) { 171 | prop = declarations[d]; 172 | if (prop.property == 'max-width' && prop.value.indexOf('!important') == -1) { 173 | msgwidth = prop.value.split('%')[0]; 174 | localStorage.setItem('msgwidth', msgwidth); 175 | break; 176 | } 177 | } 178 | } 179 | else if (rule.selectors == '[from-me="yes"][emote="no"] messagetext') { 180 | declarations = rule.declarations; 181 | for (d in declarations) { 182 | prop = declarations[d]; 183 | if (prop.property == 'background-image' && prop.value.indexOf('!important') == -1) { 184 | sentRgb = prop.value.split('),')[0].split('rgb(')[1].split(','); 185 | sentHex = rgbToHex(parseInt(sentRgb[0]), parseInt(sentRgb[1]), parseInt(sentRgb[2])); 186 | localStorage.setItem('senttop', sentHex); 187 | sentRgb = prop.value.split(', rgb(')[1].split('));')[0].split(','); 188 | sentHex = rgbToHex(parseInt(sentRgb[0]), parseInt(sentRgb[1]), parseInt(sentRgb[2])); 189 | localStorage.setItem('sentbottom', sentHex); 190 | break; 191 | } 192 | } 193 | } 194 | else if (rule.selectors == '[from-me="yes"][emote="no"][service="imessage"][typing-indicator="no"] messagetext') { 195 | declarations = rule.declarations; 196 | for (d in declarations) { 197 | prop = declarations[d]; 198 | if (prop.property == 'background-image' && prop.value.indexOf('!important') == -1) { 199 | sentRgb = prop.value.split('),')[0].split('rgb(')[1].split(','); 200 | sentHex = rgbToHex(parseInt(sentRgb[0]), parseInt(sentRgb[1]), parseInt(sentRgb[2])); 201 | localStorage.setItem('isenttop', sentHex); 202 | sentRgb = prop.value.split(', rgb(')[1].split('));')[0].split(','); 203 | sentHex = rgbToHex(parseInt(sentRgb[0]), parseInt(sentRgb[1]), parseInt(sentRgb[2])); 204 | localStorage.setItem('isentbottom', sentHex); 205 | break; 206 | } 207 | } 208 | } 209 | else if (Array.isArray(rule.selectors) && rule.selectors.length === 3) { 210 | if ( 211 | rule.selectors[0] == '[selected="yes"][item-type="attachment"] [from-me="no"][emote="no"][typing-indicator="no"] messagetext' && 212 | rule.selectors[1] == '[selected="yes"][item-type="audio-message"] [from-me="no"][emote="no"][typing-indicator="no"] messagetext' && 213 | rule.selectors[2] == '[selected="yes"] [from-me="no"][emote="no"][typing-indicator="no"] messagetext' 214 | ) { 215 | declarations = rule.declarations; 216 | for (d in declarations) { 217 | prop = declarations[d]; 218 | if (prop.property == 'background-color' && prop.value.indexOf('!important') == -1) { 219 | receivedRgb = prop.value.split(');')[0].split('rgb(')[1].split(','); 220 | receivedHex = rgbToHex(parseInt(receivedRgb[0]), parseInt(receivedRgb[1]), parseInt(receivedRgb[2])); 221 | localStorage.setItem('received', receivedHex); 222 | break; 223 | } 224 | } 225 | } 226 | } 227 | } 228 | localStorage.setItem('sentText', '#ffffff'); 229 | localStorage.setItem('isentText', '#ffffff'); 230 | localStorage.setItem('receivedText', '#000000'); 231 | setColors(); 232 | } 233 | }); 234 | } 235 | }); 236 | } 237 | 238 | function removeBubblePainterLines(req, res, callback) { 239 | fs.readFile(cssFile, 'utf8', function(err, data) { 240 | var i, 241 | before, 242 | after, 243 | code; 244 | 245 | if (err) { 246 | res.render('fileError', {msg: 'Could not locate Messages.app preference files'}); 247 | } 248 | else { 249 | before = data.split('/* begin Bubble Painter code */')[0]; 250 | after = data.split('/* end Bubble Painter code */')[1]; 251 | code = before; 252 | if (!before) { 253 | res.render('fileError', {msg: 'Could not parse Messages.app preference files'}); 254 | return; 255 | } 256 | if (after) { 257 | code += after; 258 | } 259 | fs.writeFile(cssFile, code, 'utf8', function(err, data) { 260 | req.writeError = err; 261 | callback(req, res); 262 | }); 263 | } 264 | }); 265 | } 266 | 267 | /* 268 | * define routes 269 | */ 270 | 271 | // first page 272 | app.route('/').get(function(req, res) { 273 | checkYourPrivilege(req, res, function(req, res) { 274 | getDefaultColors(req, res, function(req, res, applyColors) { 275 | var model = {}; 276 | if (req.altered) { 277 | model.altered = true; 278 | } 279 | res.render('index', model); 280 | applyColors(); 281 | }); 282 | }); 283 | }); 284 | 285 | // handler for when you change the colors 286 | app.route('/change').post(function(req, res) { 287 | if (needPassword) { 288 | exec("osascript -e \"do shell script \\\"chmod 777 "+cssFile+"\\\" with administrator privileges\"", function(error, stdout, stderr) { 289 | if (!error && !stderr) { 290 | needPassword = false; 291 | change(req, res); 292 | } 293 | else { 294 | // do nothing 295 | } 296 | }); 297 | } 298 | else { 299 | change(req, res); 300 | } 301 | 302 | function change(req, res) { 303 | if (typeof req.body.change != 'undefined') { 304 | // change button was pressed 305 | removeBubblePainterLines(req, res, function(req, res) { 306 | var code = "\n"; 307 | if (req.err) { 308 | res.render('fileError', {msg: 'Could not write to Messages.app preference files'}); 309 | } 310 | else { 311 | localStorage.setItem('sameforboth', req.body.sameforboth); 312 | 313 | code += '/* begin Bubble Painter code */' + "\n"; 314 | 315 | // message width 316 | code += '[typing-indicator="no"][emote="no"] messagetext {max-width: '+(parseInt(req.body.msgwidth) + 3)+'% !important;}' + "\n"; 317 | 318 | // sent gradient (iMessage) 319 | code += '[from-me="yes"][emote="no"][service="imessage"][typing-indicator="no"] messagetext {' + "\n"; 320 | code += 'background-image:-webkit-linear-gradient('+req.body.isenttop+', '+req.body.isentbottom+') !important;' + "\n"; 321 | code += '}' + "\n"; 322 | 323 | // sent when gradients-disabled is flagged (iMessage) 324 | code += '[disable-gradients="yes"] [from-me="yes"][emote="no"][service="imessage"][typing-indicator="no"] messagetext {' + "\n"; 325 | code += 'background-color:rgb('+req.body.isentbottom+') !important;' + "\n"; 326 | code += '}' + "\n"; 327 | 328 | // sent text (iMessage) 329 | code += '[item-type="text"] [emote="no"][from-me="yes"][service="imessage"][typing-indicator="no"] span {' + "\n"; 330 | code += 'color:'+req.body.isentText+' !important;' + "\n"; 331 | code += '}' + "\n"; 332 | 333 | // sent links (iMessage) 334 | code += '[from-me="yes"][emote="no"][service="imessage"][typing-indicator="no"] a:link {' + "\n"; 335 | code += 'color:'+req.body.isentText+' !important;' + "\n"; 336 | code += '}' + "\n"; 337 | 338 | if (!req.body.sameforboth) { 339 | // sent gradient ("green bubble friends") 340 | code += '[from-me="yes"][emote="no"] messagetext {' + "\n"; 341 | code += 'background-image:-webkit-linear-gradient('+req.body.senttop+', '+req.body.sentbottom+') !important;' + "\n"; 342 | code += '}' + "\n"; 343 | 344 | // sent when gradients-disabled is flagged ("green bubble friends") 345 | code += '[disable-gradients="yes"] [from-me="yes"][emote="no"] messagetext {' + "\n"; 346 | code += 'background-color:rgb('+req.body.sentbottom+') !important;' + "\n"; 347 | code += '}' + "\n"; 348 | 349 | // sent text (global default) 350 | code += '[item-type="text"] [emote="no"][from-me="yes"] span {' + "\n"; 351 | code += 'color:'+req.body.sentText+' !important;' + "\n"; 352 | code += '}' + "\n"; 353 | 354 | // sent links (global default) 355 | code += '[from-me="yes"] a:link {' + "\n"; 356 | code += 'color:'+req.body.sentText+' !important;' + "\n"; 357 | code += '}' + "\n"; 358 | } 359 | else { 360 | 361 | // sent gradient ("green bubble friends") 362 | code += '[from-me="yes"][emote="no"] messagetext {' + "\n"; 363 | code += 'background-image:-webkit-linear-gradient('+req.body.isenttop+', '+req.body.isentbottom+') !important;' + "\n"; 364 | code += '}' + "\n"; 365 | 366 | // sent when gradients-disabled is flagged ("green bubble friends") 367 | code += '[disable-gradients="yes"] [from-me="yes"][emote="no"] messagetext {' + "\n"; 368 | code += 'background-color:rgb('+req.body.isentbottom+') !important;' + "\n"; 369 | code += '}' + "\n"; 370 | 371 | // sent text (global default) 372 | code += '[item-type="text"] [emote="no"][from-me="yes"] span {' + "\n"; 373 | code += 'color:'+req.body.isentText+' !important;' + "\n"; 374 | code += '}' + "\n"; 375 | 376 | // sent links (global default) 377 | code += '[from-me="yes"] a:link {' + "\n"; 378 | code += 'color:'+req.body.isentText+' !important;' + "\n"; 379 | code += '}' + "\n"; 380 | } 381 | 382 | // received background color 383 | code += '[item-type="attachment"] [from-me="no"][emote="no"][typing-indicator="no"] messagetext,' + "\n"; 384 | code += '[item-type="audio-message"] [from-me="no"][emote="no"][typing-indicator="no"] messagetext,' + "\n"; 385 | code += '[from-me="no"][emote="no"][typing-indicator="no"] messagetext {' + "\n"; 386 | code += 'background-color:'+req.body.received+' !important;' + "\n"; 387 | code += '}' + "\n"; 388 | 389 | // received text 390 | code += '[item-type="text"] [emote="no"][from-me="no"] span {' + "\n"; 391 | code += 'color:'+req.body.receivedText+' !important;' + "\n"; 392 | code += '}' + "\n"; 393 | 394 | // received links 395 | code += '[from-me="no"] a:link {' + "\n"; 396 | code += 'color:'+req.body.receivedText+' !important;' + "\n"; 397 | code += '}' + "\n"; 398 | 399 | code += '/* end Bubble Painter code */' + "\n"; 400 | fs.appendFile(cssFile, code, 'utf8', function(err, data) { 401 | if (err) { 402 | res.render('fileError', {msg: 'Could not write to Messages.app preference files'}); 403 | } 404 | else { 405 | localStorage.setItem('senttop', req.body.senttop); 406 | localStorage.setItem('sentbottom', req.body.sentbottom); 407 | localStorage.setItem('received', req.body.received); 408 | localStorage.setItem('sentText', req.body.sentText); 409 | localStorage.setItem('receivedText', req.body.receivedText); 410 | res.redirect('/'); 411 | exec('killall Messages && open /Applications/Messages.app', function(error, stdout, stderr) { 412 | // ignore output 413 | }); 414 | } 415 | }); 416 | } 417 | }); 418 | } 419 | 420 | if (typeof req.body.reset != 'undefined') { 421 | // reset button was pressed 422 | removeBubblePainterLines(req, res, function(req, res) { 423 | if (req.err) { 424 | res.render('fileError', {msg: 'Could not write to Messages.app preference files'}); 425 | } 426 | else { 427 | localStorage.removeItem('senttop'); 428 | localStorage.removeItem('sentbottom'); 429 | localStorage.removeItem('sentText'); 430 | localStorage.removeItem('isenttop'); 431 | localStorage.removeItem('isentbottom'); 432 | localStorage.removeItem('isentText'); 433 | localStorage.removeItem('received'); 434 | localStorage.removeItem('receivedText'); 435 | localStorage.setItem('sameforboth', 'false'); 436 | exec("osascript -e \"do shell script \\\"chmod 755 "+cssFile+"\\\" with administrator privileges\"", function(error, stdout, stderr) { 437 | needPassword = true; 438 | res.redirect('/'); 439 | // ignore output 440 | exec('killall Messages && open /Applications/Messages.app', function(error, stdout, stderr) { 441 | // ignore output 442 | }); 443 | }); 444 | } 445 | }); 446 | } 447 | } 448 | }); 449 | 450 | // activate router 451 | page(); 452 | 453 | // activate page.js body-parser plugin 454 | pageBodyParser(); 455 | 456 | /* 457 | * initialize the app 458 | */ 459 | 460 | // render first page 461 | page('/'); 462 | 463 | // render native mac menus 464 | nativeMenuBar.createMacBuiltin('Bubble Painter', { 465 | hideEdit: true 466 | }); 467 | bubblepainter.menu = nativeMenuBar; 468 | 469 | // handles cmd+q on OSX 470 | bubblepainter.on('close', function() { 471 | gui.App.quit(); 472 | }); 473 | 474 | // hide window until fully loaded 475 | window.addEventListener('load', function() { 476 | gui.Window.get().show(); 477 | gui.Window.get().focus(); 478 | }); --------------------------------------------------------------------------------