├── .gitignore ├── Gruntfile.coffee ├── README.md ├── VERSION ├── app ├── app.coffee ├── configs │ ├── darwin │ │ ├── arcade.cfg │ │ ├── gb.cfg │ │ ├── gba.cfg │ │ ├── kart.cfg │ │ ├── megadrive.cfg │ │ ├── n64.cfg │ │ ├── neogeo.cfg │ │ ├── nes.cfg │ │ ├── psx.cfg │ │ └── snes.cfg │ └── win32 │ │ ├── arcade.cfg │ │ ├── gb.cfg │ │ ├── gba.cfg │ │ ├── kart.cfg │ │ ├── megadrive.cfg │ │ ├── n64.cfg │ │ ├── neogeo.cfg │ │ ├── nes.cfg │ │ ├── psx.cfg │ │ └── snes.cfg ├── images │ ├── bg-texture.png │ ├── control-info │ │ ├── a-button.svg │ │ ├── b-button.svg │ │ ├── d-pad.svg │ │ └── start-select.svg │ ├── default-art │ │ └── game-consoles │ │ │ ├── arcade │ │ │ └── gameCard.png │ │ │ ├── gb │ │ │ └── gameCard.png │ │ │ ├── gba │ │ │ └── gameCard.png │ │ │ ├── mac │ │ │ └── gameCard.png │ │ │ ├── megadrive │ │ │ └── gameCard.png │ │ │ ├── n64 │ │ │ └── gameCard.png │ │ │ ├── neogeo │ │ │ └── gameCard.png │ │ │ ├── nes │ │ │ └── gameCard.png │ │ │ ├── pc │ │ │ └── gameCard.png │ │ │ ├── psx │ │ │ └── gameCard.png │ │ │ └── snes │ │ │ └── gameCard.png │ └── fade.png ├── index.html ├── kartMenuTemplate.js ├── main.js ├── package.json ├── src │ ├── application.coffee │ ├── controllers │ │ ├── cards.coffee │ │ ├── collectionPicker.coffee │ │ ├── collections.coffee │ │ ├── favorites.coffee │ │ ├── games.coffee │ │ ├── home.coffee │ │ ├── platforms.coffee │ │ └── settings.coffee │ ├── events.coffee │ ├── index.coffee │ ├── lib │ │ ├── extensions.coffee │ │ ├── fs-utils.coffee │ │ ├── hotkeys.js │ │ ├── jquery-ui.js │ │ ├── jquery.js │ │ ├── jquery.keynav.js │ │ ├── jquery.scrollTo.min.js │ │ ├── jquery.simplemodal.1.4.4.min.js │ │ ├── jquery.visible.min.js │ │ └── spine │ │ │ ├── ajax.coffee │ │ │ ├── list.coffee │ │ │ ├── local.coffee │ │ │ ├── manager.coffee │ │ │ ├── relation.coffee │ │ │ ├── route.coffee │ │ │ └── spine.coffee │ ├── models │ │ ├── collection.coffee │ │ ├── favorites.coffee │ │ ├── game.coffee │ │ ├── gameConsole.coffee │ │ ├── recentlyPlayed.coffee │ │ ├── retroArch.coffee │ │ └── settings.coffee │ ├── views │ │ └── main │ │ │ ├── _card.eco │ │ │ ├── _controlInfo.eco │ │ │ ├── _gameCard.eco │ │ │ ├── cards.eco │ │ │ ├── collectionPicker.eco │ │ │ ├── home.eco │ │ │ └── settings.eco │ └── window │ │ ├── index.coffee │ │ └── stylesheets.coffee └── styles │ └── shared │ ├── 4x3.less │ ├── cards.less │ ├── control-info.less │ ├── font-awesome │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ └── less │ │ ├── bordered-pulled.less │ │ ├── core.less │ │ ├── fixed-width.less │ │ ├── font-awesome.less │ │ ├── icons.less │ │ ├── larger.less │ │ ├── list.less │ │ ├── mixins.less │ │ ├── path.less │ │ ├── rotated-flipped.less │ │ ├── spinning.less │ │ ├── stacked.less │ │ └── variables.less │ ├── home.less │ ├── index.less │ ├── main.less │ ├── modal.less │ ├── press-start │ ├── license.txt │ ├── prstart.ttf │ └── prstartk.ttf │ ├── reset.less │ ├── responsive.less │ ├── retro.less │ ├── settings.less │ └── variables.less ├── art └── buttons.sketch │ ├── Data │ ├── metadata │ └── version ├── package.json ├── script ├── bootstrap ├── bootstrap.ps1 ├── build-windows ├── run └── run.ps1 └── tasks ├── generate-plist.coffee └── task-helpers.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | atom-shell 2 | node_modules 3 | npm-debug.log 4 | app/coffee-cache 5 | app/data 6 | app/lesscache 7 | app/src/lesscache 8 | data-generator/avatars 9 | data-generator/data 10 | data-generator/commits 11 | .DS_Store 12 | build 13 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | os = require 'os' 4 | 5 | packageJson = require './package.json' 6 | 7 | module.exports = (grunt) -> 8 | appName = "#{packageJson.name}.app" 9 | [major, minor, patch] = packageJson.version.split('.') 10 | 11 | if process.platform is 'darwin' 12 | atomAppDir = path.join('atom-shell', 'Atom.app', 'Contents', 'Resources', 'app') 13 | else 14 | atomAppDir = path.join('atom-shell', 'resources', 'app') 15 | 16 | grunt.initConfig 17 | pkg: grunt.file.readJSON('package.json') 18 | symlink: 19 | app: 20 | link: atomAppDir 21 | target: path.join('..', '..', '..', '..', 'app') 22 | options: 23 | type: 'dir' 24 | force: true 25 | overwrite: true 26 | 27 | 'download-electron': 28 | version: packageJson.atomShellVersion 29 | outputDir: path.join('electron') 30 | downloadDir: path.join(os.tmpdir(), 'downloaded-electron') 31 | rebuild: true # rebuild native modules after atom-shell is updated 32 | 33 | shell: 34 | 'app-apm-install': 35 | options: 36 | stdout: true 37 | stderr: true 38 | failOnError: true 39 | execOptions: 40 | cwd: 'app' 41 | command: 'apm install' 42 | 43 | copy: 44 | app: 45 | files: [ 46 | ] 47 | 48 | grunt.loadNpmTasks('grunt-contrib-copy'); 49 | grunt.loadNpmTasks('grunt-symbolic-link') 50 | grunt.loadNpmTasks('grunt-shell') 51 | grunt.loadNpmTasks('grunt-download-electron') 52 | grunt.loadTasks('tasks') 53 | 54 | grunt.registerTask('bootstrap', ['grunt-download-electron', 'symlink:app', 'generate-plist', 'shell:app-apm-install']) 55 | grunt.registerTask('bootstrap-win', ['download-electron', 'shell:app-apm-install']) 56 | grunt.registerTask('build', ['grunt-download-electron', 'shell:app-apm-install', 'copy:app', 'generate-plist']) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kart!! 2 | 3 | :warning: This repo and project is defunct. It is not maintained and was originally created when Electron was named Atom Shell. 4 | 5 | Kart is a frontend to the amazing multi-emulating system 6 | [RetroArch](https://github.com/libretro/RetroArch). 7 | 8 | Kart aspires to be an extremely simple front end that lets you get up and 9 | running fast with a classy way to pick and choose your games. 10 | 11 | Kart is targeted at running on a TV in an HTPC type set up, but can be run from 12 | a desktop window just fine. 13 | 14 | ![kart emulator retroarch frontend](https://cloud.githubusercontent.com/assets/260/2924359/aec71d2e-d731-11e3-8bee-97b6e1b60680.png) 15 | ## Platforms 16 | 17 | Kart is powered by [Electron](https://github.com/atom/electron), a cross 18 | platform application shell. 19 | 20 | While it's being developed in Mac OS X, Electron is multi-platform so Kart 21 | will easily eventually work on Windows and Linux. 22 | 23 | 24 | ## Tech Stack 25 | 26 | From the Electron README: 27 | 28 | > Electron lets you write cross-platform desktop applications using JavaScript, 29 | HTML and CSS. It is based on node.js and Chromium and is used in the Atom 30 | editor. 31 | 32 | Electron wraps up [Chromium](http://www.chromium.org) and integrates it with 33 | Node.js so you have access to the local system. This allows for really fast 34 | iterative development for an application of this nature. 35 | 36 | 37 | Kart is developed using these technologies. 38 | 39 | * HTML5 40 | * CoffeeScript 41 | * Less 42 | * Spine JS 43 | * Node.js 44 | 45 | ## How To Use 46 | 47 | Kart is very simple right now. To use it, click on the settings button and set 48 | your paths. 49 | 50 | ### Settings 51 | 52 | There are only 2 settings for Kart right now. That's all it needs! 53 | 54 | * **RetroArch Path** - The path to your RetroArch bundle. The root directory 55 | where all of your RetroArch things are. 56 | * **Roms Path** - The path to your roms. 57 | 58 | ### Convention over Configuration 59 | 60 | Kart follows a model of Convention over Configuration. Instead of making you 61 | specify a million different things or keeping a library of metadata, Kart makes 62 | certain assumptions. This means as long as you follow some set guidelines, it's 63 | very easy to set up. 64 | 65 | For example, the name of a game is taken from it's rom's filename. The art for 66 | the game should have the same name as the rom. By using this convention, its easy 67 | to load in all of your roms without a complicated scanning process. 68 | 69 | #### tl;dr 70 | 71 | Configuring Kart is actually pretty easy, here's the gist: 72 | 73 | * set up your console and rom directories right 74 | * name your roms the titles you want them to appear in Kart 75 | * add an `/images` directory for each console with `PNG` art that match the rom 76 | filenames 77 | * add an `image.png` image for each console 78 | * set the paths for your roms and RetroArch bundle 79 | 80 | #### Rom Directories 81 | 82 | Your roms should be organized into directories based on the console they are for. 83 | You should have a single rom directory that contains them. Your rom directory 84 | hierarchy should look like this: 85 | 86 | ``` 87 | /roms 88 | /gb 89 | /gba 90 | /megadrive 91 | /nes 92 | /snes 93 | /Super Mario World.smc 94 | ``` 95 | 96 | Your rom names should be named exactly how you want to them appear in Kart. 97 | 98 | ##### Rom Art Directories 99 | 100 | Art for your roms should be inside a directory named `images` within each 101 | console's directory. Art for each rom should have the exact same file name as 102 | the rom it's for. The art should also be a `PNG`. 103 | 104 | ``` 105 | /roms 106 | /snes 107 | /images 108 | /Super Mario World.png 109 | ``` 110 | 111 | Simply add this directory and add the art for all of the roms you want to show 112 | up. 113 | 114 | Kart uses Steam styled art. You can find art for your games all over the 115 | internet, but the easiest place to find it is http://steambanners.booru.org. 116 | 117 | 118 | #### Supported Consoles 119 | 120 | Right now, kart only supports these consoles (directory names are in 121 | parenthesis): 122 | 123 | * Super Nintendo Entertainment System (/snes) 124 | * Nintendo Entertainment System /(nes) 125 | * GameBoy and GameBoy Color (/gb) 126 | * GameBoy Advance (/gba) 127 | * Sega Genesis (/megadrive) 128 | 129 | ##### Console Art 130 | 131 | Add an `image.png` image to a console's directory to set it's art. 132 | 133 | 134 | #### Key Navigation 135 | 136 | Kart supports browsing by the keyboard. 137 | 138 | The keys `up`, `down`, `left`, `rigth`, `enter`, `esc` all do exactly what you'd 139 | think they do. 140 | 141 | In addition, `backspace` is an alias for `esc` to allow you to map controls 142 | better. 143 | 144 | For best results, use a keyboard mapper to map your joystick/controller to these 145 | keys so you can navigate Kart with your controller. 146 | 147 | #### RetroArch Configuration 148 | 149 | In the future, Kart will provide it's own bundled version of RetroArch or the 150 | ability to download a pre-configured one. But for now you need to use your own. 151 | 152 | There are a few assumptions made about your RetroArch setup. 153 | 154 | First, every console needs to have it's own config. Inside that config should 155 | have a setting for the libretro emulator core you want to use. 156 | 157 | For example for : 158 | 159 | ``` 160 | libretro_path = "/Applications/retroarch/libretro/libretro-snes9x-next.dylib" 161 | ``` 162 | 163 | The `libretro_path` is the minimum required setting, but you can add any extra 164 | settings that you want. This may be different key settings, filters, or whatever. 165 | 166 | Your config hierarchy should look like this: 167 | 168 | ``` 169 | /retroarch 170 | /config 171 | /gb 172 | /gba 173 | /megadrive 174 | /nes 175 | /snes 176 | /retroarch.cfg 177 | ``` 178 | 179 | Again, in the future this will be simpler. 180 | 181 | 182 | ## Roadmap 183 | 184 | Kart is in it's early days. It's extremely simple right now, but there are lots 185 | of plans. 186 | 187 | * ~~~Browse by Console~~~ :white_check_mark: 188 | * Browse Recently Played Games 189 | * Set and Browse Favorites 190 | * Bundled RetroArch distribution 191 | * Better full screen support 192 | * Better first run experience 193 | * Everything better, lulz 194 | 195 | Hopefully, by the time Kart is more mature, downloading it and setting it up 196 | will be easy as pie. 197 | 198 | 199 | ## Development 200 | 201 | To get started working on Kart: 202 | 203 | * clone it down 204 | * run `script/bootstrap` 205 | * run `script/run` 206 | 207 | Voila, Kart will be running. 208 | 209 | ## Contributing 210 | 211 | Contributions are welcome and encouraged. Please create pull request from a 212 | feature branch. 213 | 214 | * Fork it 215 | * Create a feature branch 216 | * Push up your branch to your fork 217 | * Create new Pull Request 218 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /app/app.coffee: -------------------------------------------------------------------------------- 1 | require './src/window/index' 2 | 3 | requireStylesheet 'shared/index' 4 | 5 | require './src/index' 6 | -------------------------------------------------------------------------------- /app/configs/darwin/arcade.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-mame2010.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/darwin/gb.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-gambatte.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/darwin/gba.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-vba.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/darwin/kart.cfg: -------------------------------------------------------------------------------- 1 | # These are the default settings that every emulator core will use. You can 2 | # override any setting for each core by editing its individual config file. Find 3 | # these in /configs/$platform/$console.cfg 4 | 5 | # These are the settings you are the most likely to want to edit. 6 | 7 | ######################################################################## 8 | ########################## TYPICAL SETTINGS ############################ 9 | ######################################################################## 10 | 11 | video_cg_shader = "/Applications/retroarch/shaders/Cg/scanline.cg" 12 | 13 | ####### Player 1 ####### 14 | input_player1_b = "backspace" 15 | input_player1_a = "enter" 16 | input_player1_y = "a" 17 | input_player1_x = "s" 18 | input_player1_select = "z" 19 | input_player1_start = "x" 20 | input_player1_up = "up" 21 | input_player1_down = "down" 22 | input_player1_left = "left" 23 | input_player1_right = "right" 24 | input_player1_l = "q" 25 | input_player1_r = "w" 26 | 27 | ####### Player 2 ####### 28 | input_player2_b = "num1" 29 | input_player2_a = "num2" 30 | input_player2_y = "num3" 31 | input_player2_x = "num4" 32 | input_player2_select = "num5" 33 | input_player2_start = "num6" 34 | input_player2_up = "num7" 35 | input_player2_down = "num8" 36 | input_player2_left = "num9" 37 | input_player2_right = "num0" 38 | input_player2_l = "keypad0" 39 | input_player2_r = "keypad1" 40 | 41 | input_exit_emulator = escape 42 | input_hold_fast_forward = l 43 | input_rewind = r 44 | 45 | video_fullscreen = true 46 | 47 | # Rewinding buffer size in megabytes. Bigger rewinding buffer means you can rewind longer. 48 | # The buffer should be approx. 20MB per minute of buffer time. 49 | rewind_enable = true 50 | rewind_buffer_size = 20 51 | rewind_granularity = 2 52 | 53 | 54 | # Automatically saves a savestate at the end of RetroArch's lifetime. 55 | # The path is $SRAM_PATH.auto. 56 | # RetroArch will automatically load any savestate with this path on startup if savestate_auto_load is set. 57 | # savestate_auto_save = false 58 | # savestate_auto_load = true 59 | 60 | system_directory = "/Applications/retroarch/systems/all" 61 | 62 | 63 | ######################################################################## 64 | ########################### OTHER SETTINGS ############################# 65 | ######################################################################## 66 | 67 | #### Video 68 | 69 | # Video driver to use. "gl", "xvideo", "sdl" 70 | video_driver = "gl" 71 | 72 | # If fullscreen, prefer using a windowed fullscreen mode. 73 | # video_windowed_fullscreen = true 74 | 75 | # Which monitor to prefer. 0 (default) means no particular monitor is preferred, 1 and up (1 being first monitor), 76 | # suggests RetroArch to use that particular monitor. 77 | #video_monitor_index = 0 78 | 79 | # Video vsync. 80 | # video_vsync = true 81 | 82 | # Smoothens picture with bilinear filtering. Should be disabled if using pixel shaders. 83 | video_smooth = false 84 | 85 | # Forces rendering area to stay equal to game aspect ratio or as defined in video_aspect_ratio. 86 | # video_force_aspect = true 87 | 88 | # Only scales video in integer steps. 89 | # The base size depends on system-reported geometry and aspect ratio. 90 | # If video_force_aspect is not set, X/Y will be integer scaled independently. 91 | # video_scale_integer = false 92 | 93 | # A floating point value for video aspect ratio (width / height). 94 | # If this is not set, aspect ratio is assumed to be automatic. 95 | # Behavior then is defined by video_aspect_ratio_auto. 96 | # video_aspect_ratio = 97 | 98 | # If this is true and video_aspect_ratio is not set, 99 | # aspect ratio is decided by libretro implementation. 100 | # If this is false, 1:1 PAR will always be assumed if video_aspect_ratio is not set. 101 | video_aspect_ratio_auto = true 102 | 103 | # Forces cropping of overscanned frames. 104 | # Exact behavior of this option is implementation specific. 105 | # video_crop_overscan = false 106 | 107 | # Path to Cg shader. 108 | # video_cg_shader = "/Applications/retroarch/shaders/Cg/scanline.cg" 109 | 110 | # Path to GLSL XML shader. If both Cg shader path and XML shader path are defined, 111 | # Cg shader will take priority unless overridden in video_shader_type. 112 | #video_bsnes_shader = "/Applications/retroarch/shaders/OpenGL/Themaister-scanlines.shader" 113 | 114 | # Which shader type to use. Valid values are "cg", "bsnes", "none" and "auto" 115 | # video_shader_type = auto 116 | 117 | # Defines a directory where XML shaders are kept. 118 | video_shader_dir = "/Applications/retroarch/shaders/OpenGL" 119 | 120 | # Render to texture first. Useful when doing multi-pass shaders or control the output of shaders better. 121 | # video_render_to_texture = false 122 | 123 | # Defines the video scale of render-to-texture. 124 | # The output FBO size is scaled by these amounts against the input size (typically 256 * 224 for SNES). 125 | # video_fbo_scale_x = 2.0 126 | # video_fbo_scale_y = 2.0 127 | 128 | # Define shader to use for second pass (needs render-to-texture). 129 | # video_second_pass_shader = "/path/to/second/shader.{cg,shader}" 130 | 131 | # Defines if bilinear filtering is used during second pass (needs render-to-texture). 132 | # video_second_pass_smooth = true 133 | 134 | # CPU-based filter. Path to a bSNES CPU filter (*.filter) 135 | # video_filter = "/Applications/retroarch/filter/2xSal.filter" 136 | 137 | # Video refresh rate of your monitor. 138 | # Used to calculate a suitable audio input rate. 139 | # video_refresh_rate = 59.95 140 | 141 | #### Audio 142 | 143 | # Enable audio. 144 | audio_enable = true 145 | 146 | # Audio driver backend. Depending on configuration possible candidates are: alsa, pulse, oss, jack, rsound, roar, openal, sdl, xaudio, coreaudio 147 | audio_driver = coreaudio 148 | 149 | # Audio output samplerate. 150 | audio_out_rate = 48000 151 | 152 | # Which resampler to use. "sinc" and "hermite" are currently implemented. 153 | # Default will use "sinc" if compiled in. 154 | # audio_resampler = 155 | 156 | # When altering audio_in_rate on-the-fly, define by how much each time. 157 | # audio_rate_step = 0.25 158 | 159 | # Override the default audio device the audio_driver uses. This is driver dependant. E.g. ALSA wants a PCM device, OSS wants a path (e.g. /dev/dsp), Jack wants portnames (e.g. system:playback1,system:playback_2), and so on ... 160 | # audio_device = 161 | 162 | # External DSP plugin that processes audio before it's sent to the driver. 163 | # audio_dsp_plugin = 164 | 165 | # Will sync (block) on audio. Recommended. 166 | # audio_sync = true 167 | 168 | #### Input 169 | 170 | # Keyboard input. Will recognize normal keypresses and special keys like "left", "right", and so on. 171 | # Keyboard input, Joypad and Joyaxis will all obey the "nul" bind, which disables the bind completely, 172 | # rather than relying on a default. 173 | # input_player1_a = x 174 | # input_player1_b = z 175 | # input_player1_y = a 176 | # input_player1_x = s 177 | # input_player1_start = enter 178 | # input_player1_select = rshift 179 | # input_player1_l = q 180 | # input_player1_r = w 181 | # input_player1_left = left 182 | # input_player1_right = right 183 | # input_player1_up = up 184 | # input_player1_down = down 185 | # input_player1_l2 = 186 | # input_player1_r2 = 187 | # input_player1_l3 = 188 | # input_player1_r3 = 189 | 190 | # Two analog sticks (DualShock-esque). 191 | # Bound as usual, however, if a real analog axis is bound, 192 | # it can be read as a true analog. 193 | # Positive X axis is right, Positive Y axis is down. 194 | # input_player1_l_x_plus = 195 | # input_player1_l_x_minus = 196 | # input_player1_l_y_plus = 197 | # input_player1_l_y_minus = 198 | # input_player1_r_x_plus = 199 | # input_player1_r_x_minus = 200 | # input_player1_r_y_plus = 201 | # input_player1_r_y_minus = 202 | 203 | # If desired, it is possible to override which joypads are being used for player 1 through 5. First joypad available is 0. 204 | # input_player1_joypad_index = 0 205 | # input_player2_joypad_index = 1 206 | # input_player3_joypad_index = 2 207 | # input_player4_joypad_index = 3 208 | # input_player5_joypad_index = 4 209 | # Player 6-8 is not directly expected by libretro API, but we'll futureproof it. 210 | # input_player6_joypad_index = 5 211 | # input_player7_joypad_index = 6 212 | # input_player8_joypad_index = 7 213 | 214 | # Joypad buttons. 215 | # Figure these out by using RetroArch-Phoenix or retroarch-joyconfig. 216 | # You can use joypad hats with hnxx, where n is the hat, and xx is a string representing direction. 217 | # E.g. "h0up" 218 | # input_player1_a_btn = 0 219 | # input_player1_b_btn = 1 220 | # input_player1_y_btn = 3 221 | # input_player1_x_btn = 2 222 | # input_player1_start_btn = 7 223 | # input_player1_select_btn = 6 224 | # input_player1_l_btn = 4 225 | # input_player1_r_btn = 5 226 | # input_player1_left_btn = 227 | # input_player1_right_btn = 228 | # input_player1_up_btn = 229 | # input_player1_down_btn = 230 | # input_player1_l2_btn = 231 | # input_player1_r2_btn = 232 | # input_player1_l3_btn = 233 | # input_player1_r3_btn = 234 | 235 | # Axis for RetroArch D-Pad. 236 | # Needs to be either '+' or '-' in the first character signaling either positive or negative direction of the axis, then the axis number. 237 | # Do note that every other input option has the corresponding _btn and _axis binds as well; they are omitted here for clarity. 238 | # input_player1_left_axis = -0 239 | # input_player1_right_axis = +0 240 | # input_player1_up_axis = -1 241 | # input_player1_down_axis = +1 242 | 243 | # Holding the turbo while pressing another button will let the button enter a turbo mode 244 | # where the button state is modulated with a periodic signal. 245 | # The modulation stops when the button itself (not turbo button) is released. 246 | # input_player1_turbo = 247 | 248 | # Describes the period and how long of that period a turbo-enabled button should behave. 249 | # Numbers are described in frames. 250 | # input_turbo_period = 6 251 | # input_turbo_duty_cycle = 3 252 | 253 | # This goes all the way to player 8 (*_player2_*, *_player3_*, etc), but omitted for clarity. 254 | # All input binds have corresponding binds for keyboard (none), joykeys (_btn) and joyaxes (_axis) as well. 255 | 256 | # Toggles fullscreen. 257 | # input_toggle_fullscreen = f 258 | 259 | # Saves state. 260 | # input_save_state = f2 261 | # Loads state. 262 | # input_load_state = f4 263 | 264 | # State slots. With slot set to 0, save state name is *.state (or whatever defined on commandline). 265 | # When slot is != 0, path will be $path%d, where %d is slot number. 266 | # input_state_slot_increase = f7 267 | # input_state_slot_decrease = f6 268 | 269 | # Toggles between fast-forwarding and normal speed. 270 | # input_toggle_fast_forward = space 271 | 272 | # Applies next and previous XML/Cg shader in directory. 273 | # input_shader_next = m 274 | # input_shader_prev = n 275 | 276 | # Toggle between paused and non-paused state 277 | # input_pause_toggle = p 278 | 279 | # Frame advance when game is paused 280 | # input_frame_advance = k 281 | 282 | # Reset the emulated SNES. 283 | # input_reset = h 284 | 285 | # Configures DSP plugin 286 | # input_dsp_config = c 287 | 288 | # Cheats. 289 | # input_cheat_index_plus = y 290 | # input_cheat_index_minus = t 291 | # input_cheat_toggle = u 292 | 293 | # Mute/unmute audio 294 | # input_audio_mute = f9 295 | 296 | # Take screenshot 297 | # input_screenshot = f8 298 | 299 | # Netplay flip players. 300 | # input_netplay_flip_players = i 301 | 302 | # Hold for slowmotion. 303 | # input_slowmotion = e 304 | 305 | # Enable other hotkeys. 306 | # If this hotkey is bound to either keyboard, joybutton or joyaxis, 307 | # all other hotkeys will be disabled unless this hotkey is also held at the same time. 308 | # This is useful for RETRO_KEYBOARD centric implementations 309 | # which query a large area of the keyboard, where it is not desirable 310 | # that hotkeys get in the way. 311 | 312 | # Alternatively, all hotkeys for keyboard could be disabled by the user. 313 | # input_enable_hotkey = 314 | 315 | # Increases audio volume. 316 | # input_volume_up = kp_plus 317 | # Decreases audio volume. 318 | # input_volume_down = kp_minus 319 | 320 | # Toggles to next overlay. Wraps around. 321 | # input_overlay_next = 322 | 323 | #### Misc 324 | 325 | # Rewinding buffer size in megabytes. Bigger rewinding buffer means you can rewind longer. 326 | # The buffer should be approx. 20MB per minute of buffer time. 327 | rewind_buffer_size = 20 328 | 329 | # Rewind granularity. When rewinding defined number of frames, you can rewind several frames at a time, increasing the rewinding speed. 330 | rewind_granularity = 2 331 | 332 | # Pause gameplay when window focus is lost. 333 | # pause_nonactive = true 334 | 335 | # Autosaves the non-volatile SRAM at a regular interval. This is disabled by default unless set otherwise. 336 | # The interval is measured in seconds. A value of 0 disables autosave. 337 | # autosave_interval = 338 | 339 | # When being client over netplay, use keybinds for player 1. 340 | # netplay_client_swap_input = false 341 | 342 | # Path to XML cheat database (as used by bSNES). 343 | # cheat_database_path = 344 | 345 | # Path to XML cheat config, a file which keeps track of which 346 | # cheat settings are used for individual games. 347 | # If the file does not exist, it will be created. 348 | # cheat_settings_path = 349 | -------------------------------------------------------------------------------- /app/configs/darwin/megadrive.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-genesis-plus-gx.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/darwin/n64.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/mupen64plus_libretro.dylib" 2 | video_cg_shader = "nul" 3 | -------------------------------------------------------------------------------- /app/configs/darwin/neogeo.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-mame2010.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/darwin/nes.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-nestopia.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/darwin/psx.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-mednafen-psx.dylib" 2 | video_cg_shader = "nul" 3 | -------------------------------------------------------------------------------- /app/configs/darwin/snes.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = "/Applications/retroarch/libretro/libretro-snes9x-next.dylib" 2 | -------------------------------------------------------------------------------- /app/configs/win32/arcade.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\mame_libretro.dll" 2 | -------------------------------------------------------------------------------- /app/configs/win32/gb.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\gambatte_libretro.dll" 2 | #video_shader = ":\shaders/handheld/gameboy/gb-shader.cgp" 3 | -------------------------------------------------------------------------------- /app/configs/win32/gba.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\vba_next_libretro.dll" 2 | -------------------------------------------------------------------------------- /app/configs/win32/kart.cfg: -------------------------------------------------------------------------------- 1 | # These are the default settings that every emulator core will use. You can 2 | # override any setting for each core by editing its individual config file. Find 3 | # these in /configs/$platform/$console.cfg 4 | 5 | # These are the settings you are the most likely to want to edit. 6 | 7 | ######################################################################## 8 | ########################## TYPICAL SETTINGS ############################ 9 | ######################################################################## 10 | 11 | video_shader = ":\shaders\scanline.cg" 12 | 13 | ####### Player 1 ####### 14 | input_player1_b = "backspace" 15 | input_player1_a = "enter" 16 | input_player1_y = "a" 17 | input_player1_x = "s" 18 | input_player1_select = "z" 19 | input_player1_start = "x" 20 | input_player1_up = "up" 21 | input_player1_down = "down" 22 | input_player1_left = "left" 23 | input_player1_right = "right" 24 | input_player1_l = "q" 25 | input_player1_r = "w" 26 | 27 | ####### Player 2 ####### 28 | input_player2_b = "num1" 29 | input_player2_a = "num2" 30 | input_player2_y = "num3" 31 | input_player2_x = "num4" 32 | input_player2_select = "num5" 33 | input_player2_start = "num6" 34 | input_player2_up = "num7" 35 | input_player2_down = "num8" 36 | input_player2_left = "num9" 37 | input_player2_right = "num0" 38 | input_player2_l = "keypad0" 39 | input_player2_r = "keypad1" 40 | 41 | input_exit_emulator = escape 42 | input_hold_fast_forward = l 43 | input_rewind = r 44 | 45 | video_fullscreen = "true" 46 | 47 | # Rewinding buffer size in megabytes. Bigger rewinding buffer means you can rewind longer. 48 | # The buffer should be approx. 20MB per minute of buffer time. 49 | rewind_enable = "true" 50 | rewind_buffer_size = "20" 51 | rewind_granularity = "2" 52 | 53 | # Automatically saves a savestate at the end of RetroArch's lifetime. 54 | # The path is $SRAM_PATH.auto. 55 | # RetroArch will automatically load any savestate with this path on startup if savestate_auto_load is set. 56 | savestate_auto_save = false 57 | savestate_auto_load = true 58 | 59 | 60 | ######################################################################## 61 | ########################### OTHER SETTINGS ############################# 62 | ######################################################################## 63 | 64 | libretro_info_path = ":\info/" 65 | system_directory = ":\system/" 66 | rgui_config_directory = ":\configs\" 67 | config_save_on_exit = "false" 68 | video_shader_dir = ":\shaders/" 69 | joypad_autoconfig_dir = ":\autoconfig/" 70 | screenshot_directory = ":\screenshots/" 71 | game_history_path = ":\configs\" 72 | fps_show = "false" 73 | video_aspect_ratio = "-1.000000" 74 | video_xscale = "3.000000" 75 | autosave_interval = "0" 76 | video_yscale = "3.000000" 77 | video_crop_overscan = "true" 78 | video_scale_integer = "false" 79 | video_smooth = "false" 80 | video_threaded = "false" 81 | video_shader_enable = "true" 82 | video_refresh_rate = "59.950001" 83 | video_driver = "d3d" 84 | video_vsync = "true" 85 | video_hard_sync = "true" 86 | video_hard_sync_frames = "0" 87 | video_black_frame_insertion = "false" 88 | video_swap_interval = "1" 89 | video_gpu_screenshot = "true" 90 | video_rotation = "0" 91 | aspect_ratio_index = "0" 92 | audio_rate_control = "true" 93 | audio_rate_control_delta = "0.005000" 94 | audio_driver = "dsound" 95 | audio_out_rate = "48000" 96 | audio_resampler = "sinc" 97 | savefile_directory = "default" 98 | savestate_directory = "default" 99 | content_directory = "default" 100 | rgui_show_start_screen = "false" 101 | game_history_size = "100" 102 | input_autodetect_enable = "false" 103 | overlay_directory = "default" 104 | input_overlay_opacity = "0.700000" 105 | input_overlay_scale = "1.000000" 106 | gamma_correction = "false" 107 | triple_buffering_enable = "true" 108 | soft_filter_enable = "false" 109 | flicker_filter_enable = "false" 110 | flicker_filter_index = "0" 111 | soft_filter_index = "0" 112 | current_resolution_id = "0" 113 | custom_viewport_width = "960" 114 | custom_viewport_height = "720" 115 | custom_viewport_x = "0" 116 | custom_viewport_y = "0" 117 | video_font_size = "32.000000" 118 | block_sram_overwrite = "false" 119 | savestate_auto_index = "false" 120 | sound_mode = "0" 121 | state_slot = "0" 122 | custom_bgm_enable = "false" 123 | input_driver = "dinput" 124 | cheat_database_path = "" 125 | audio_device = "" 126 | input_overlay = "" 127 | input_joypad_driver = "" 128 | input_keyboard_layout = "" 129 | -------------------------------------------------------------------------------- /app/configs/win32/megadrive.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\genesis_plus_gx_libretro.dll" 2 | -------------------------------------------------------------------------------- /app/configs/win32/n64.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\mupen64plus_libretro.dll" 2 | #video_shader = "nul" 3 | video_aspect_ratio = 1.33 4 | 5 | input_device_p1 = "0" 6 | input_player1_joypad_index = "1" 7 | 8 | # Z button - trigger 9 | input_player1_l2_btn = "8" 10 | # analog stick 11 | input_player1_l_x_plus_axis = "+0" 12 | input_player1_l_x_minus_axis = "-0" 13 | input_player1_l_y_plus_axis = "+1" 14 | input_player1_l_y_minus_axis = "-1" 15 | # C buttons 16 | input_player1_r_x_plus_axis = "-5" 17 | input_player1_r_x_minus_axis = "+5" 18 | input_player1_r_y_plus_axis = "+2" 19 | input_player1_r_y_minus_axis = "-2" 20 | -------------------------------------------------------------------------------- /app/configs/win32/neogeo.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\mame_libretro.dll" 2 | -------------------------------------------------------------------------------- /app/configs/win32/nes.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\nestopia_libretro.dll" 2 | -------------------------------------------------------------------------------- /app/configs/win32/psx.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\mednafen_psx_libretro.dll" 2 | video_shader = "nul" 3 | -------------------------------------------------------------------------------- /app/configs/win32/snes.cfg: -------------------------------------------------------------------------------- 1 | libretro_path = ":\cores\snes9x_next_libretro.dll" 2 | -------------------------------------------------------------------------------- /app/images/bg-texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/bg-texture.png -------------------------------------------------------------------------------- /app/images/control-info/a-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | buttons 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/images/control-info/b-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | buttons 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/images/control-info/d-pad.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | buttons 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/images/control-info/start-select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | buttons 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/arcade/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/arcade/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/gb/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/gb/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/gba/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/gba/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/mac/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/mac/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/megadrive/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/megadrive/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/n64/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/n64/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/neogeo/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/neogeo/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/nes/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/nes/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/pc/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/pc/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/psx/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/psx/gameCard.png -------------------------------------------------------------------------------- /app/images/default-art/game-consoles/snes/gameCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/default-art/game-consoles/snes/gameCard.png -------------------------------------------------------------------------------- /app/images/fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/images/fade.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kart 5 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/kartMenuTemplate.js: -------------------------------------------------------------------------------- 1 | exports.getTemplate = function(app, mainWindow) { 2 | return [ 3 | { 4 | label: 'File', 5 | submenu: [ 6 | { 7 | label: 'Quit', 8 | accelerator: 'CommandOrControl+Q', 9 | click: function() { app.quit(); } 10 | } 11 | ] 12 | }, 13 | { 14 | label: 'View', 15 | submenu: [ 16 | { 17 | label: 'Reload', 18 | accelerator: 'CommandOrControl+R', 19 | click: function() { mainWindow.reloadIgnoringCache(); } 20 | }, 21 | { 22 | label: 'Toggle DevTools', 23 | accelerator: 'CommandOrControl+Alt+I', 24 | click: function() { mainWindow.toggleDevTools(); } 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | var app = require('app'); // Module to control application life. 2 | var BrowserWindow = require('browser-window'); // Module to create native browser window. 3 | var kartMenuTemplate = require('./kartMenuTemplate'); 4 | 5 | // Report crashes to our server. 6 | // require('crash-reporter').start(); 7 | 8 | // Keep a global reference of the window object, if you don't, the window will 9 | // be closed automatically when the javascript object is GCed. 10 | var mainWindow = null; 11 | 12 | // Quit when all windows are closed. 13 | app.on('window-all-closed', function() { 14 | if (process.platform != 'darwin') 15 | app.quit(); 16 | }); 17 | 18 | // This method will be called when atom-shell has done everything 19 | // initialization and ready for creating browser windows. 20 | app.on('ready', function() { 21 | // Parse arguments and default devMode to false unless the --dev flag is present 22 | var argv = require('minimist')(process.argv.slice(1)); 23 | var devMode = argv.dev || false; 24 | 25 | // Create the browser window 26 | mainWindow = new BrowserWindow({width: 1280, height: 720, kiosk: true, 'auto-hide-menu-bar': !devMode}); 27 | 28 | if (devMode) { 29 | mainWindow.openDevTools(); 30 | } 31 | 32 | var Menu = require('menu'); 33 | var MenuItem = require('menu-item'); 34 | var menu = Menu.buildFromTemplate(kartMenuTemplate.getTemplate(app, mainWindow)); 35 | Menu.setApplicationMenu(menu); 36 | 37 | // and load the index.html of the app. 38 | mainWindow.loadUrl('file://' + __dirname + '/index.html'); 39 | 40 | // Emitted when the window is closed. 41 | mainWindow.on('closed', function() { 42 | // Dereference the window object, usually you would store windows 43 | // in an array if your app supports multi windows, this is the time 44 | // when you should delete the corresponding element. 45 | mainWindow = null; 46 | }); 47 | 48 | 49 | 50 | 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kart", 3 | "main": "./main.js", 4 | "dependencies": { 5 | "async": "0.2.6", 6 | "coffee-cache": "~0.2.0", 7 | "coffee-script": "~1.6.3", 8 | "eco": "~1.0", 9 | "font-awesome": "4.0.3", 10 | "image-size": "0.3.2", 11 | "less": "git://github.com/nathansobo/less.js.git", 12 | "less-cache": "~0.12", 13 | "minimist": "^1.1.0", 14 | "mkdirp": "0.3.5", 15 | "rimraf": "2.1.4", 16 | "tantamount": "0.3.0", 17 | "underscore": "1.4.4" 18 | }, 19 | "devDependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /app/src/application.coffee: -------------------------------------------------------------------------------- 1 | eco = require "eco" 2 | fs = require "fs" 3 | 4 | Home = require './controllers/home' 5 | Platforms = require './controllers/platforms' 6 | Collections = require './controllers/collections' 7 | Favorites = require './controllers/favorites' 8 | CollectionPicker = require './controllers/collectionPicker' 9 | Games = require './controllers/games' 10 | Settings = require './controllers/settings' 11 | 12 | Spine.Controller.prototype.view = (path, data) -> 13 | template = fs.readFileSync __dirname + "/views/#{path}.eco", "utf-8" 14 | eco.render template, data 15 | 16 | class App extends Spine.Stack 17 | className: 'stack root' 18 | 19 | controllers: 20 | home: Home 21 | platforms: Platforms 22 | collections: Collections 23 | favorites: Favorites 24 | collectionPicker: CollectionPicker 25 | games: Games 26 | settings: Settings 27 | 28 | default: 'home' 29 | 30 | events: 31 | 'click .settings-button': 'toggleSettings' 32 | 33 | constructor: -> 34 | super 35 | 36 | @history = [] 37 | @el.append(@view('main/_controlInfo')) 38 | $('.control-info').show() 39 | 40 | activeController: -> 41 | for controller in @manager.controllers 42 | if controller.isActive() 43 | return controller 44 | break 45 | 46 | goTo: (controller) -> 47 | @history.push(@activeController()) 48 | controller.active() 49 | 50 | back: -> 51 | return if @history.length == 0 52 | 53 | controller = @history.pop() 54 | controller.active() 55 | 56 | showHome: -> 57 | @platforms.update() 58 | @goTo(@platforms) 59 | 60 | showPlatforms: -> 61 | @platforms.update() 62 | @goTo(@platforms) 63 | 64 | showCollections: -> 65 | @collections.update() 66 | @goTo(@collections) 67 | 68 | showFavorites: -> 69 | @favorites.update() 70 | @goTo(@favorites) 71 | 72 | showCollectionPicker: (game) -> 73 | @collectionPicker.show(game) 74 | 75 | toggleSettings: -> 76 | if @settings.isActive() then @back() else @goTo(@settings) 77 | 78 | showGames: (collection) -> 79 | @games.collection = collection 80 | @games.update() 81 | @goTo(@games) 82 | 83 | keydown: (e) -> 84 | switch e.keyCode 85 | when KeyCodes.backspace 86 | @back() 87 | e.preventDefault() 88 | when KeyCodes.esc 89 | @back() 90 | e.preventDefault() 91 | else 92 | @activeController().keyboardNav(e) 93 | 94 | module.exports = App 95 | -------------------------------------------------------------------------------- /app/src/controllers/cards.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | class Cards extends Spine.Controller 6 | className: 'app-cards' 7 | 8 | elements: 9 | '.cards .card': 'cards' 10 | 11 | events: 12 | 'click .card': 'click' 13 | 'mouseover .card': 'mouseover' 14 | 'mouseleave .card': 'mouseleave' 15 | 16 | constructor: -> 17 | super 18 | 19 | @currentlySelectedCard = null 20 | @settings = new App.Settings 21 | 22 | @update() 23 | 24 | build: -> 25 | if @settings.aspect() == "16x9" 26 | @perRow = 4 27 | @rows = 3 28 | else if @settings.aspect() == "4x3" 29 | @perRow = 3 30 | @rows = 3 31 | 32 | @perPage = @rows * @perRow 33 | 34 | @page = 0 35 | @x = -1 36 | @y = -1 37 | 38 | render: -> 39 | @html @view 'main/cards', @ 40 | @setSelected(0,0); 41 | 42 | update: -> 43 | @build() 44 | @render(); 45 | 46 | numberOfPages: -> 47 | number = parseInt(@numberOfItems() / @perPage) 48 | number++ if @numberOfItems() % @perPage 49 | number 50 | 51 | rangeForPage: (page) -> 52 | start = page*@perPage 53 | 54 | if @numberOfItems() <= start + @perPage 55 | length = @numberOfItems() - start 56 | else 57 | length = @perPage 58 | 59 | end = start + length 60 | 61 | [start...end] 62 | 63 | numberOfItems: -> 64 | 1 65 | 66 | selectCard: (card) -> 67 | @currentlySelectedCard.removeClass('selected') if @currentlySelectedCard 68 | $(card).addClass('selected') 69 | 70 | deselectCard: (card) -> 71 | $(card).removeClass('selected') 72 | 73 | setSelected: (i, j) -> 74 | 75 | # check direction for scrolling later 76 | if j > @y 77 | direction = 'right' 78 | else if j < @y 79 | direction = 'left' 80 | 81 | # max up at the top 82 | if i < 0 83 | i = 0 84 | # max down at the bottom 85 | if i >= @rows 86 | i = @rows-1 87 | 88 | # max left on first page 89 | if j < 0 && @page == 0 90 | j = 0 91 | 92 | # go back a page and place on far right column 93 | if j < 0 && @page > 0 94 | j = @perRow-1 95 | @page -= 1 96 | 97 | # advancing a page to the right 98 | if j >= @perRow 99 | # max right to the far right on the last page 100 | if j >= @page+1 >= @numberOfPages() 101 | j = @perRow-1 102 | # advance a page 103 | else 104 | j = 0 105 | @page += 1 106 | 107 | 108 | # check to see if there are contents on that row 109 | adjustedI = @page*@rows + i 110 | index = (@perRow * adjustedI + j) 111 | # no items on that row, pop to the top 112 | if index >= @numberOfItems() 113 | i = 0 114 | 115 | # adjust i for what page it's on 116 | # don't forget, according to the DOM, the pages are 117 | # UNDER each other 118 | adjustedI = @page*@rows + i 119 | index = (@perRow * adjustedI + j) 120 | 121 | if index < @numberOfItems() 122 | # set selected items 123 | @currentlySelectedCard.removeClass('selected') if @currentlySelectedCard 124 | @currentlySelectedCard = $(@cards[index]) 125 | @currentlySelectedCard.addClass('selected') 126 | 127 | 128 | 129 | # check if card is visible, if it isn't scroll to it 130 | if !@currentlySelectedCard.visible() 131 | scrollAmount = @currentlySelectedCard.width() + 50 132 | if direction == 'left' 133 | scrollOption = "-=#{scrollAmount}px" 134 | else 135 | scrollOption = "+=#{scrollAmount}px" 136 | 137 | $.scrollTo(scrollOption, 150, {easing:'swing'}) 138 | 139 | # save these for later 140 | @x = i; 141 | @y = j; 142 | 143 | didPickCardAt: (index) -> 144 | console.log(index) 145 | 146 | pickCardAtIndex: (card) -> 147 | card = $(card) 148 | index = card.index() + (@page*@perPage) 149 | 150 | @didPickCardAt(index) 151 | 152 | cardFor: (index) -> 153 | console.log("overide this to render a card") 154 | data = {"imagePath": "", "title": "Check Console"} 155 | @view 'main/_card', data 156 | 157 | click: (e) -> 158 | @pickCardAtIndex(e.currentTarget) 159 | e.preventDefault() 160 | 161 | mouseover: (e) -> 162 | card = $(e.currentTarget) 163 | @selectCard(card) 164 | 165 | mouseleave: (e) -> 166 | card = $(e.currentTarget) 167 | @deselectCard(card) 168 | 169 | keyboardNav: (e) -> 170 | 171 | switch e.keyCode 172 | when KeyCodes.up 173 | @setSelected(@x-1,@y); 174 | e.preventDefault() 175 | when KeyCodes.down 176 | @setSelected(@x+1,@y); 177 | e.preventDefault() 178 | when KeyCodes.left 179 | @setSelected(@x,@y-1); 180 | e.preventDefault() 181 | when KeyCodes.right 182 | @setSelected(@x,@y+1); 183 | e.preventDefault() 184 | when KeyCodes.enter 185 | @pickCardAtIndex(@currentlySelectedCard) 186 | e.preventDefault() 187 | 188 | module.exports = Cards 189 | -------------------------------------------------------------------------------- /app/src/controllers/collectionPicker.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | 6 | class CollectionPicker extends Spine.Controller 7 | className: 'app-collectionPicker' 8 | 9 | elements: 10 | '.collections-modal': 'collectionsModal' 11 | 12 | constructor: -> 13 | super 14 | 15 | @collections = App.Collection.all() 16 | @render() 17 | 18 | render: -> 19 | @html @view 'main/collectionPicker', @ 20 | 21 | cardFor: (collection) -> 22 | data = {"imagePath": collection.imagePath(), "title": collection.name()} 23 | @view 'main/_card', data 24 | 25 | show: (game) -> 26 | console.log(game) 27 | self = @ 28 | 29 | @collectionsModal.modal({ 30 | overlayClose:true, 31 | minHeight: '60%', 32 | maxHeight: '60%', 33 | minWidth: '60%', 34 | maxWidth: '60%', 35 | onShow: (modal) -> 36 | $(modal.container).find('.card').click (e) -> 37 | card = $(e.currentTarget) 38 | collection = self.collections[card.index()] 39 | collection.addGame(game) 40 | $.modal.close() 41 | }) 42 | 43 | module.exports = CollectionPicker 44 | -------------------------------------------------------------------------------- /app/src/controllers/collections.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | os = require 'os' 6 | 7 | Cards = require './cards' 8 | 9 | class Collections extends Cards 10 | className: 'app-collections' 11 | 12 | elements: 13 | '.header': 'header' 14 | 15 | build: -> 16 | super 17 | @collections = App.Collection.all() 18 | 19 | showGames: (collection) -> 20 | app.showGames(collection) 21 | 22 | numberOfItems: -> 23 | @collections.length 24 | 25 | didPickCardAt: (index) -> 26 | @showGames(@collections[index]) 27 | 28 | cardFor: (index) -> 29 | collection = @collections[index] 30 | data = {"imagePath": collection.imagePath(), "title": collection.name()} 31 | @view 'main/_card', data 32 | 33 | module.exports = Collections 34 | -------------------------------------------------------------------------------- /app/src/controllers/favorites.coffee: -------------------------------------------------------------------------------- 1 | Games = require './games' 2 | 3 | class Favorites extends Games 4 | className: 'app-favorites' 5 | 6 | build: -> 7 | super 8 | 9 | @games = @favorites.games if @favorites 10 | 11 | toggleFavorite: (e) -> 12 | super 13 | 14 | @update() 15 | 16 | module.exports = Favorites 17 | -------------------------------------------------------------------------------- /app/src/controllers/games.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | Cards = require './cards' 6 | 7 | class Games extends Cards 8 | className: 'app-games' 9 | 10 | events: 11 | 'click .card .overlay .game-settings .add-collection': 'addToCollection' 12 | 'click .card .overlay .game-settings .toggle-favorite': 'toggleFavorite' 13 | 14 | constructor: -> 15 | super 16 | 17 | @settings = new App.Settings 18 | @recentlyPlayed = new App.RecentlyPlayed 19 | @favorites = new App.Favorites 20 | @retroArch = new App.RetroArch 21 | 22 | build: -> 23 | super 24 | 25 | @games = @collection.games if @collection 26 | 27 | launchGame: (game) -> 28 | @retroArch.launchGame(game) 29 | 30 | numberOfItems: -> 31 | if @games then @games.length else 0 32 | 33 | didPickCardAt: (index) -> 34 | @launchGame(@games[index]) 35 | 36 | cardFor: (index) -> 37 | game = @games[index] 38 | 39 | data = {"imagePath": game.imagePath(), "title": game.name(), "faved": @favorites.isFaved(game)} 40 | data["centerTitle"] = game.name() if !game.imageExists() 41 | 42 | @view 'main/_gameCard', data 43 | 44 | addToCollection: (e) -> 45 | e.stopPropagation() 46 | card = $(e.currentTarget).parents('.card') 47 | index = card.index() + (@page*@perPage) 48 | 49 | app.showCollectionPicker(@games[index]) 50 | 51 | toggleFavorite: (e) -> 52 | e.stopPropagation() 53 | favButton = $(e.currentTarget) 54 | card = $(e.currentTarget).parents('.card') 55 | index = card.index() + (@page*@perPage) 56 | 57 | if favButton.hasClass('fa-heart-o') 58 | @favorites.addGame(@games[index]) 59 | favButton.removeClass('fa-heart-o').addClass('fa-heart') 60 | else 61 | @favorites.removeGame(@games[index]) 62 | favButton.removeClass('fa-heart').addClass('fa-heart-o') 63 | 64 | module.exports = Games 65 | -------------------------------------------------------------------------------- /app/src/controllers/home.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | fsUtils = require '../lib/fs-utils' 6 | path = require 'path' 7 | querystring = require("querystring"); 8 | sizeOf = require('image-size'); 9 | 10 | class Home extends Spine.Controller 11 | className: 'app-home' 12 | 13 | elements: 14 | '.square': 'squares' 15 | '.card': 'cards' 16 | 'bodys': 'body' 17 | 18 | events: 19 | 'click .card': 'cardClicked' 20 | 'click .square': 'squareClicked' 21 | 'mouseover .card': 'mouseover' 22 | 'mouseleave .card': 'mouseleave' 23 | 'mouseover .square': 'mouseover' 24 | 'mouseleave .square': 'mouseleave' 25 | 26 | 27 | constructor: -> 28 | super 29 | 30 | @active @update 31 | 32 | @settings = new App.Settings 33 | @recentlyPlayed = new App.RecentlyPlayed 34 | @favorites = new App.Favorites 35 | @retroArch = new App.RetroArch 36 | 37 | @currentlySelectedItem = null 38 | 39 | render: -> 40 | @html @view 'main/home', @ 41 | 42 | @selectItem(@squares.first()) 43 | 44 | body = $('body') 45 | 46 | # set the background 47 | custom_background_path = path.join(@settings.romsPath(), 'background.png') 48 | if fsUtils.exists(custom_background_path) 49 | @setBackgroundImage(custom_background_path) 50 | else 51 | @setBackgroundImage(path.join(__dirname, '../../images/bg-texture.png')) 52 | 53 | if @settings.retroMode() 54 | body.addClass('retro') 55 | else 56 | body.removeClass('retro') 57 | 58 | if @settings.aspect() == '16x9' 59 | body.removeClass('fourbythree') 60 | else if @settings.aspect() == '4x3' 61 | body.addClass('fourbythree') 62 | 63 | setBackgroundImage: (imagePath) -> 64 | body = $('body') 65 | imagePath = path.resolve(imagePath) 66 | imagePath = imagePath.replace(/\\/g, '\\\\') if path.sep is '\\' 67 | imageValue = "url('file://#{imagePath.replace(/\s/g, '%20')}')" 68 | 69 | return if body.css('background-image').replace(/'/g, '') == imageValue.replace(/'/g, '') 70 | 71 | dimensions = sizeOf(imagePath); 72 | 73 | if dimensions.width < 600 74 | backgroundRepeat = "repeat" 75 | backgroundSize = "auto" 76 | else 77 | backgroundRepeat = "no-repeat" 78 | backgroundSize = "cover" 79 | 80 | body.css('background-repeat', backgroundSize) 81 | body.css('background-size', backgroundSize) 82 | body.css('background-image', imageValue) 83 | 84 | update: -> 85 | @recentlyPlayed.load() 86 | @render() 87 | 88 | cardFor: (game) -> 89 | data = {"imagePath": game.imagePath(), "title": game.name()} 90 | data["centerTitle"] = game.name() if !game.imageExists() 91 | 92 | @view 'main/_card', data 93 | 94 | numberOfGames: -> 95 | if @settings.aspect() == '16x9' then 4 else 3 96 | 97 | launchGame: (item) -> 98 | @retroArch.launchGame(@recentlyPlayed.games[item.index()]) 99 | 100 | loadPlatforms: -> 101 | app.showPlatforms() 102 | 103 | loadCollections: -> 104 | app.showCollections() 105 | 106 | loadFavorites: -> 107 | app.showFavorites() 108 | 109 | pickItem: (item) -> 110 | if item.hasClass("card") 111 | @launchGame(item) 112 | else 113 | if item.hasClass("platforms") 114 | @loadPlatforms() 115 | else if item.hasClass("collections") 116 | @loadCollections() 117 | else if item.hasClass("favorites") 118 | @loadFavorites() 119 | 120 | 121 | selectItem: (item) -> 122 | @deselectItem(@currentlySelectedItem) if @currentlySelectedItem 123 | item.addClass('selected') 124 | @currentlySelectedItem = item 125 | 126 | deselectItem: (element) -> 127 | $(element).removeClass('selected') 128 | 129 | focusGames: (item) -> 130 | @selectItem($('.card').first()) 131 | 132 | focusSquares: (item) -> 133 | @selectItem($('.square').first()) 134 | 135 | goUp: () -> 136 | @focusSquares() if @currentlySelectedItem.hasClass('card') 137 | 138 | goDown: () -> 139 | @focusGames() if @currentlySelectedItem.hasClass('square') 140 | 141 | goRight: () -> 142 | index = @currentlySelectedItem.index() 143 | 144 | if @currentlySelectedItem.hasClass('card') 145 | nextItem = @cards[index+1] 146 | else 147 | nextItem = @squares[index+1] 148 | 149 | @selectItem($(nextItem)) if nextItem 150 | 151 | goLeft: () -> 152 | index = @currentlySelectedItem.index() 153 | 154 | if @currentlySelectedItem.hasClass('card') 155 | nextItem = @cards[index-1] 156 | else 157 | nextItem = @squares[index-1] 158 | 159 | @selectItem($(nextItem)) if nextItem 160 | 161 | cardClicked: (e) -> 162 | @pickItem($(e.currentTarget)) 163 | 164 | squareClicked: (e) -> 165 | @pickItem($(e.currentTarget)) 166 | 167 | mouseover: (e) -> 168 | @selectItem($(e.currentTarget)) 169 | 170 | mouseleave: (e) -> 171 | @deselectItem($(e.currentTarget)) 172 | 173 | keyboardNav: (e) -> 174 | 175 | switch e.keyCode 176 | when KeyCodes.up 177 | @goUp() 178 | e.preventDefault() 179 | when KeyCodes.down 180 | @goDown() 181 | e.preventDefault() 182 | when KeyCodes.left 183 | @goLeft() 184 | e.preventDefault() 185 | when KeyCodes.right 186 | @goRight() 187 | e.preventDefault() 188 | when KeyCodes.enter 189 | @pickItem(@currentlySelectedItem) 190 | e.preventDefault() 191 | 192 | module.exports = Home 193 | -------------------------------------------------------------------------------- /app/src/controllers/platforms.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | os = require 'os' 6 | 7 | Cards = require './cards' 8 | 9 | class Platforms extends Cards 10 | className: 'app-platforms' 11 | 12 | elements: 13 | '.header': 'header' 14 | 15 | build: -> 16 | super 17 | 18 | @gameConsoles = [] 19 | 20 | if @settings.romsPath() 21 | ## platform dependent sections 22 | switch os.platform() 23 | when "darwin" 24 | @gameConsoles.push new App.GameConsole(prefix: "mac", extensions: ["lnk", "url"], name: "Steam") 25 | when "win32" 26 | @gameConsoles.push new App.GameConsole(prefix: "pc", extensions: ["lnk", "url"], name: "Steam") 27 | 28 | @gameConsoles.push new App.GameConsole(prefix: "arcade", extensions: ["zip"], name: "Arcade") 29 | @gameConsoles.push new App.GameConsole(prefix: "nes", extensions: ["nes", "zip"], name: "Nintendo Entertainment System") 30 | @gameConsoles.push new App.GameConsole(prefix: "snes", extensions: ["smc", "zip"], name: "Super Nintendo") 31 | @gameConsoles.push new App.GameConsole(prefix: "neogeo", extensions: ["zip"], name: "Neo Geo") 32 | @gameConsoles.push new App.GameConsole(prefix: "n64", extensions: ["z64", "zip"], name: "Nintendo 64") 33 | @gameConsoles.push new App.GameConsole(prefix: "gb", extensions: ["gb", "gbc", "zip"], name: "GameBoy") 34 | @gameConsoles.push new App.GameConsole(prefix: "gba", extensions: ["gba", "zip"], name: "GameBoy Advance") 35 | @gameConsoles.push new App.GameConsole(prefix: "megadrive", extensions: ["bin", "zip"], name: "Sega Genesis") 36 | @gameConsoles.push new App.GameConsole(prefix: "psx", extensions: ["cue", "img"], name: "Sony Playstation") 37 | 38 | @gameConsoles = _.filter @gameConsoles, (gameConsole) -> 39 | gameConsole.imageExists() 40 | 41 | showGames: (gameConsole) -> 42 | app.showGames(gameConsole) 43 | 44 | numberOfItems: -> 45 | @gameConsoles.length 46 | 47 | didPickCardAt: (index) -> 48 | @showGames(@gameConsoles[index]) 49 | 50 | cardFor: (index) -> 51 | gameConsole = @gameConsoles[index] 52 | data = {"imagePath": gameConsole.imagePath(), "title": gameConsole.name} 53 | @view 'main/_card', data 54 | 55 | module.exports = Platforms 56 | -------------------------------------------------------------------------------- /app/src/controllers/settings.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | $ = Spine.$ 4 | 5 | fsUtils = require '../lib/fs-utils' 6 | dialog = require('remote').require('dialog') 7 | 8 | class Settings extends Spine.Controller 9 | className: 'app-settings' 10 | 11 | elements: 12 | '#retroarch_path': 'retroarchPathInput' 13 | '#roms_path': 'romsPathInput' 14 | '#retro_mode': 'retroMode' 15 | 16 | events: 17 | 'click #retroarch_path_button': 'browseRetroarchPath' 18 | 'click #roms_path_button': 'browseRomsPath' 19 | 'change #aspect': 'setAspect' 20 | 'change #retro-mode': 'toggleRetroMode' 21 | 22 | constructor: -> 23 | super 24 | 25 | @settings = new App.Settings 26 | 27 | @render() 28 | @build() 29 | 30 | render: -> 31 | @html @view 'main/settings', @ 32 | 33 | build: -> 34 | @retroarchPathInput.html(@settings.retroarchPath()) 35 | @romsPathInput.html(@settings.romsPath()) 36 | 37 | browseRetroarchPath: (e) -> 38 | path = dialog.showOpenDialog({ title: 'Retroarch Path', properties: [ 'openDirectory' ]}) 39 | if path 40 | @settings.setRetroarchPath(path) 41 | @build() 42 | 43 | browseRomsPath: (e) -> 44 | path = dialog.showOpenDialog({ title: 'Roms Path', properties: [ 'openDirectory' ]}) 45 | if path 46 | @settings.setRomsPath(path) 47 | @build() 48 | 49 | setAspect: (e) -> 50 | @settings.setAspect($(e.currentTarget).val()) 51 | 52 | toggleRetroMode: (e) -> 53 | @settings.setRetroMode($(e.currentTarget).is(':checked')) 54 | 55 | keyboardNav: (e) -> 56 | 57 | switch e.keyCode 58 | when KeyCodes.backspace 59 | app.showHome() 60 | e.preventDefault() 61 | when KeyCodes.esc 62 | app.showHome() 63 | e.preventDefault() 64 | 65 | module.exports = Settings 66 | -------------------------------------------------------------------------------- /app/src/events.coffee: -------------------------------------------------------------------------------- 1 | $(document).on 'keydown', (e) -> 2 | app.keydown e 3 | -------------------------------------------------------------------------------- /app/src/index.coffee: -------------------------------------------------------------------------------- 1 | Spine = require './lib/spine/spine' 2 | require './lib/spine/list' 3 | require './lib/spine/local' 4 | require './lib/spine/route' 5 | require './lib/spine/manager' 6 | require './lib/spine/relation' 7 | 8 | window.Spine = Spine 9 | 10 | require './lib/hotkeys' 11 | require './lib/extensions' 12 | 13 | window.App = require './application' 14 | 15 | App.Game = require './models/game' 16 | App.GameConsole = require './models/gameConsole' 17 | App.Collection = require './models/collection' 18 | App.Settings = require './models/settings' 19 | App.RecentlyPlayed = require './models/recentlyPlayed' 20 | App.Favorites = require './models/favorites' 21 | App.RetroArch = require './models/retroArch' 22 | 23 | $ => 24 | $('body').addClass 'loaded' 25 | 26 | 27 | window.app = new App 28 | el: document.body 29 | 30 | require './events' 31 | -------------------------------------------------------------------------------- /app/src/lib/extensions.coffee: -------------------------------------------------------------------------------- 1 | Array::unique = -> 2 | output = {} 3 | output[@[key]] = @[key] for key in [0...@length] 4 | value for key, value of output 5 | -------------------------------------------------------------------------------- /app/src/lib/fs-utils.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | fs = require 'fs' 3 | mkdirp = require 'mkdirp' 4 | Module = require 'module' 5 | async = require 'async' 6 | rimraf = require 'rimraf' 7 | path = require 'path' 8 | 9 | module.exports = 10 | # Make the given path absolute by resolving it against the 11 | # current working directory. 12 | absolute: (relativePath) -> 13 | return null unless relativePath? 14 | 15 | if relativePath is '~' 16 | relativePath = process.env.HOME 17 | else if relativePath.indexOf('~/') is 0 18 | relativePath = "#{process.env.HOME}#{relativePath.substring(1)}" 19 | 20 | try 21 | fs.realpathSync(relativePath) 22 | catch e 23 | relativePath 24 | 25 | # Returns true if a file or folder at the specified path exists. 26 | exists: (pathToCheck) -> 27 | pathToCheck? and fs.existsSync(pathToCheck) 28 | 29 | # Returns true if the specified path is a directory that exists. 30 | isDirectorySync: (directoryPath) -> 31 | return false unless directoryPath?.length > 0 32 | try 33 | fs.statSync(directoryPath).isDirectory() 34 | catch e 35 | false 36 | 37 | isDirectory: (directoryPath, done) -> 38 | return done(false) unless directoryPath?.length > 0 39 | fs.exists directoryPath, (exists) -> 40 | if exists 41 | fs.stat directoryPath, (error, stat) -> 42 | if error? 43 | done(false) 44 | else 45 | done(stat.isDirectory()) 46 | else 47 | done(false) 48 | 49 | # Returns true if the specified path is a regular file that exists. 50 | isFileSync: (filePath) -> 51 | return false unless filePath?.length > 0 52 | try 53 | fs.statSync(filePath).isFile() 54 | catch e 55 | false 56 | 57 | # Returns true if the specified path is executable. 58 | isExecutableSync: (pathToCheck) -> 59 | return false unless pathToCheck?.length > 0 60 | try 61 | (fs.statSync(pathToCheck).mode & 0o777 & 1) isnt 0 62 | catch e 63 | false 64 | 65 | # Returns an array with the paths of the files and folders 66 | # contained in the directory path. 67 | listSync: (rootPath, extensions) -> 68 | return [] unless @isDirectorySync(rootPath) 69 | paths = fs.readdirSync(rootPath) 70 | paths = @filterExtensions(paths, extensions) if extensions 71 | paths = paths.map (childPath) -> path.join(rootPath, childPath) 72 | paths 73 | 74 | list: (rootPath, rest...) -> 75 | extensions = rest.shift() if rest.length > 1 76 | done = rest.shift() 77 | fs.readdir rootPath, (error, paths) => 78 | if error? 79 | done(error) 80 | else 81 | paths = @filterExtensions(paths, extensions) if extensions 82 | paths = paths.map (childPath) -> path.join(rootPath, childPath) 83 | done(null, paths) 84 | 85 | filterExtensions: (paths, extensions) -> 86 | extensions = extensions.map (ext) -> 87 | if ext is '' 88 | ext 89 | else 90 | '.' + ext.replace(/^\./, '') 91 | paths.filter (pathToCheck) -> _.include(extensions, path.extname(pathToCheck)) 92 | 93 | listTreeSync: (rootPath) -> 94 | paths = [] 95 | onPath = (childPath) -> 96 | paths.push(childPath) 97 | true 98 | @traverseTreeSync(rootPath, onPath, onPath) 99 | paths 100 | 101 | move: (source, target) -> 102 | fs.renameSync(source, target) 103 | 104 | # Remove the file or directory at the given path. 105 | remove: (pathToRemove) -> 106 | rimraf.sync(pathToRemove) 107 | 108 | # Open, read, and close a file, returning the file's contents. 109 | read: (filePath) -> 110 | fs.readFileSync(filePath, 'utf8') 111 | 112 | # Open, write, flush, and close a file, writing the given content. 113 | writeSync: (filePath, content) -> 114 | mkdirp.sync(path.dirname(filePath)) 115 | fs.writeFileSync(filePath, content) 116 | 117 | write: (filePath, content, callback) -> 118 | mkdirp path.dirname(filePath), (error) -> 119 | if error? 120 | callback?(error) 121 | else 122 | fs.writeFile(filePath, content, callback) 123 | 124 | copy: (sourcePath, destinationPath, done) -> 125 | mkdirp path.dirname(destinationPath), (error) -> 126 | if error? 127 | done?(error) 128 | return 129 | 130 | sourceStream = fs.createReadStream(sourcePath) 131 | sourceStream.on 'error', (error) -> 132 | done?(error) 133 | done = null 134 | 135 | destinationStream = fs.createWriteStream(destinationPath) 136 | destinationStream.on 'error', (error) -> 137 | done?(error) 138 | done = null 139 | destinationStream.on 'close', -> 140 | done?() 141 | done = null 142 | 143 | sourceStream.pipe(destinationStream) 144 | 145 | # Create a directory at the specified path including any missing parent 146 | # directories. 147 | makeTree: (directoryPath) -> 148 | mkdirp.sync(directoryPath) if directoryPath and not @exists(directoryPath) 149 | 150 | traverseTreeSync: (rootPath, onFile, onDirectory=onFile) -> 151 | return unless @isDirectorySync(rootPath) 152 | 153 | traverse = (directoryPath, onFile, onDirectory) -> 154 | for file in fs.readdirSync(directoryPath) 155 | childPath = path.join(directoryPath, file) 156 | stats = fs.lstatSync(childPath) 157 | if stats.isSymbolicLink() 158 | try 159 | stats = fs.statSync(childPath) 160 | if stats.isDirectory() 161 | traverse(childPath, onFile, onDirectory) if onDirectory(childPath) 162 | else if stats.isFile() 163 | onFile(childPath) 164 | 165 | traverse(rootPath, onFile, onDirectory) 166 | 167 | traverseTree: (rootPath, onFile, onDirectory, onDone) -> 168 | fs.readdir rootPath, (error, files) -> 169 | if error 170 | onDone?() 171 | else 172 | queue = async.queue (childPath, callback) -> 173 | fs.stat childPath, (error, stats) -> 174 | if error 175 | callback(error) 176 | else if stats.isFile() 177 | onFile(childPath) 178 | callback() 179 | else if stats.isDirectory() 180 | if onDirectory(childPath) 181 | fs.readdir childPath, (error, files) -> 182 | if error 183 | callback(error) 184 | else 185 | for file in files 186 | queue.unshift(path.join(childPath, file)) 187 | callback() 188 | else 189 | callback() 190 | queue.concurrency = 1 191 | queue.drain = onDone 192 | queue.push(path.join(rootPath, file)) for file in files 193 | 194 | md5ForPath: (pathToDigest) -> 195 | contents = fs.readFileSync(pathToDigest) 196 | require('crypto').createHash('md5').update(contents).digest('hex') 197 | 198 | resolve: (args...) -> 199 | extensions = args.pop() if _.isArray(_.last(args)) 200 | pathToResolve = args.pop() 201 | loadPaths = args 202 | 203 | if pathToResolve[0] is '/' 204 | if extensions and resolvedPath = @resolveExtension(pathToResolve, extensions) 205 | return resolvedPath 206 | else 207 | return pathToResolve if @exists(pathToResolve) 208 | 209 | for loadPath in loadPaths 210 | candidatePath = path.join(loadPath, pathToResolve) 211 | if extensions 212 | if resolvedPath = @resolveExtension(candidatePath, extensions) 213 | return resolvedPath 214 | else 215 | return @absolute(candidatePath) if @exists(candidatePath) 216 | undefined 217 | 218 | resolveOnLoadPath: (args...) -> 219 | loadPaths = Module.globalPaths.concat(module.paths) 220 | @resolve(loadPaths..., args...) 221 | 222 | resolveExtension: (pathToResolve, extensions) -> 223 | for extension in extensions 224 | if extension == "" 225 | return @absolute(pathToResolve) if @exists(pathToResolve) 226 | else 227 | pathWithExtension = pathToResolve + "." + extension.replace(/^\./, "") 228 | return @absolute(pathWithExtension) if @exists(pathWithExtension) 229 | undefined 230 | 231 | isCompressedExtension: (ext) -> 232 | _.indexOf([ 233 | '.gz' 234 | '.jar' 235 | '.tar' 236 | '.tgz' 237 | '.zip' 238 | ], ext, true) >= 0 239 | 240 | isImageExtension: (ext) -> 241 | _.indexOf([ 242 | '.gif' 243 | '.jpeg' 244 | '.jpg' 245 | '.png' 246 | '.tiff' 247 | ], ext, true) >= 0 248 | 249 | isPdfExtension: (ext) -> 250 | ext is '.pdf' 251 | 252 | isMarkdownExtension: (ext) -> 253 | _.indexOf([ 254 | '.markdown' 255 | '.md' 256 | '.mdown' 257 | '.mkd' 258 | '.mkdown' 259 | '.ron' 260 | ], ext, true) >= 0 261 | 262 | isBinaryExtension: (ext) -> 263 | _.indexOf([ 264 | '.DS_Store' 265 | '.a' 266 | '.o' 267 | '.so' 268 | '.woff' 269 | ], ext, true) >= 0 270 | 271 | isReadmePath: (readmePath) -> 272 | extension = path.extname(readmePath) 273 | base = path.basename(readmePath, extension).toLowerCase() 274 | base is 'readme' and (extension is '' or @isMarkdownExtension(extension)) 275 | 276 | readPlistSync: (plistPath) -> 277 | plist = require 'plist' 278 | plist.parseStringSync(@read(plistPath)) 279 | 280 | readPlist: (plistPath, done) -> 281 | plist = require 'plist' 282 | fs.readFile plistPath, 'utf8', (error, contents) -> 283 | if error? 284 | done(error) 285 | else 286 | try 287 | done(null, plist.parseStringSync(contents)) 288 | catch parseError 289 | done(parseError) 290 | 291 | readObjectSync: (objectPath) -> 292 | CSON = require 'season' 293 | if CSON.isObjectPath(objectPath) 294 | CSON.readFileSync(objectPath) 295 | else 296 | @readPlistSync(objectPath) 297 | 298 | readObject: (objectPath, done) -> 299 | CSON = require 'season' 300 | if CSON.isObjectPath(objectPath) 301 | CSON.readFile(objectPath, done) 302 | else 303 | @readPlist(objectPath, done) 304 | -------------------------------------------------------------------------------- /app/src/lib/hotkeys.js: -------------------------------------------------------------------------------- 1 | KeyCodes = { 2 | enter: 13, 3 | esc: 27, 4 | backspace: 8, 5 | left: 37, 6 | up: 38, 7 | right: 39, 8 | down: 40 9 | } 10 | -------------------------------------------------------------------------------- /app/src/lib/jquery.keynav.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Keynav - jQuery Keyboard Navigation plugin 3 | * 4 | * Copyright (c) 2013 Nick Ostrovsky 5 | * 6 | * Licensed under the MIT license: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * 9 | * Project home: 10 | * http://www.firedev.com/jquery.keynav 11 | * 12 | * Version: 0.1 13 | * 14 | */ 15 | 16 | ;(function($, window, document, undefined) { 17 | 18 | $.fn.keynav = function(checkNav) { 19 | var elements = this; 20 | var matrix; 21 | var x; 22 | var y; 23 | var current = this.filter('.selected'); 24 | var keyNavigationDisabled=false; 25 | if (current.length == 0) current = this.first(); 26 | 27 | current.addClass('selected'); 28 | 29 | function update() { 30 | var i=0; 31 | var row = Array(); 32 | var j = -1; 33 | var oldtop = false; 34 | var m=Array(); 35 | 36 | elements.each(function(){ 37 | if (!oldtop) oldtop = this.offsetTop; 38 | newtop=this.offsetTop; 39 | if (newtop != oldtop) { 40 | oldtop=newtop; 41 | m[i]=row; 42 | row = Array(); 43 | i++; 44 | j=0; 45 | row[j]=this; 46 | } else { 47 | j++; 48 | row[j]=this; 49 | } 50 | }); 51 | m[i]=row; 52 | matrix = m; 53 | coordinates=findCurrent(); 54 | x=coordinates[0]; 55 | y=coordinates[1]; 56 | return matrix; 57 | } 58 | 59 | function findCurrent() { 60 | i=0; j=0; found = false; 61 | try { 62 | for (i=0; i=matrix.length) i=0; 82 | if (j<0) j=(matrix[i].length-1); 83 | if (j>=matrix[i].length) j=0; 84 | current.removeClass('selected'); 85 | current = $(matrix[i][j]); 86 | current.addClass('selected'); 87 | x=i; 88 | y=j; 89 | } 90 | 91 | $(window).bind("resize", function(event) { 92 | update(); 93 | }); 94 | 95 | $(document).ready(function() { 96 | update(); 97 | }); 98 | 99 | 100 | $(document).keydown(function(e){ 101 | if (checkNav && checkNav()) return; 102 | if (e.keyCode == 37) { 103 | // left 104 | setCurrent(x,y-1); 105 | e.preventDefault(); 106 | } else if (e.keyCode == 38) { 107 | // up 108 | setCurrent(x-1,y); 109 | e.preventDefault(); 110 | } else if (e.keyCode == 39) { 111 | // right 112 | setCurrent(x,y+1); 113 | e.preventDefault(); 114 | } else if (e.keyCode == 40) { 115 | // down 116 | setCurrent(x+1,y); 117 | e.preventDefault(); 118 | } else if (e.keyCode == 13) { 119 | e.preventDefault(); 120 | } 121 | }); 122 | 123 | 124 | return this; 125 | } 126 | 127 | })(jQuery, window, document); 128 | -------------------------------------------------------------------------------- /app/src/lib/jquery.scrollTo.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2007-2014 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com 3 | * Licensed under MIT 4 | * @author Ariel Flesler 5 | * @version 1.4.11 6 | */ 7 | ;(function(a){if(typeof define==='function'&&define.amd){define(['jquery'],a)}else{a(jQuery)}}(function($){var j=$.scrollTo=function(a,b,c){return $(window).scrollTo(a,b,c)};j.defaults={axis:'xy',duration:parseFloat($.fn.jquery)>=1.3?0:1,limit:true};j.window=function(a){return $(window)._scrollable()};$.fn._scrollable=function(){return this.map(function(){var a=this,isWin=!a.nodeName||$.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!isWin)return a;var b=(a.contentWindow||a).document||a.ownerDocument||a;return/webkit/i.test(navigator.userAgent)||b.compatMode=='BackCompat'?b.body:b.documentElement})};$.fn.scrollTo=function(f,g,h){if(typeof g=='object'){h=g;g=0}if(typeof h=='function')h={onAfter:h};if(f=='max')f=9e9;h=$.extend({},j.defaults,h);g=g||h.duration;h.queue=h.queue&&h.axis.length>1;if(h.queue)g/=2;h.offset=both(h.offset);h.over=both(h.over);return this._scrollable().each(function(){if(f==null)return;var d=this,$elem=$(d),targ=f,toff,attr={},win=$elem.is('html,body');switch(typeof targ){case'number':case'string':if(/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)){targ=both(targ);break}targ=$(targ,this);if(!targ.length)return;case'object':if(targ.is||targ.style)toff=(targ=$(targ)).offset()}var e=$.isFunction(h.offset)&&h.offset(d,targ)||h.offset;$.each(h.axis.split(''),function(i,a){var b=a=='x'?'Left':'Top',pos=b.toLowerCase(),key='scroll'+b,old=d[key],max=j.max(d,a);if(toff){attr[key]=toff[pos]+(win?0:old-$elem.offset()[pos]);if(h.margin){attr[key]-=parseInt(targ.css('margin'+b))||0;attr[key]-=parseInt(targ.css('border'+b+'Width'))||0}attr[key]+=e[pos]||0;if(h.over[pos])attr[key]+=targ[a=='x'?'width':'height']()*h.over[pos]}else{var c=targ[pos];attr[key]=c.slice&&c.slice(-1)=='%'?parseFloat(c)/100*max:c}if(h.limit&&/^\d+$/.test(attr[key]))attr[key]=attr[key]<=0?0:Math.min(attr[key],max);if(!i&&h.queue){if(old!=attr[key])animate(h.onAfterFirst);delete attr[key]}});animate(h.onAfter);function animate(a){$elem.animate(attr,g,h.easing,a&&function(){a.call(this,targ,h)})}}).end()};j.max=function(a,b){var c=b=='x'?'Width':'Height',scroll='scroll'+c;if(!$(a).is('html,body'))return a[scroll]-$(a)[c.toLowerCase()]();var d='client'+c,html=a.ownerDocument.documentElement,body=a.ownerDocument.body;return Math.max(html[scroll],body[scroll])-Math.min(html[d],body[d])};function both(a){return $.isFunction(a)||typeof a=='object'?a:{top:a,left:a}};return j})); 8 | -------------------------------------------------------------------------------- /app/src/lib/jquery.simplemodal.1.4.4.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SimpleModal 1.4.4 - jQuery Plugin 3 | * http://simplemodal.com/ 4 | * Copyright (c) 2013 Eric Martin 5 | * Licensed under MIT and GPL 6 | * Date: Sun, Jan 20 2013 15:58:56 -0800 7 | */ 8 | (function(b){"function"===typeof define&&define.amd?define(["jquery"],b):b(jQuery)})(function(b){var j=[],n=b(document),k=navigator.userAgent.toLowerCase(),l=b(window),g=[],o=null,p=/msie/.test(k)&&!/opera/.test(k),q=/opera/.test(k),m,r;m=p&&/msie 6./.test(k)&&"object"!==typeof window.XMLHttpRequest;r=p&&/msie 7.0/.test(k);b.modal=function(a,h){return b.modal.impl.init(a,h)};b.modal.close=function(){b.modal.impl.close()};b.modal.focus=function(a){b.modal.impl.focus(a)};b.modal.setContainerDimensions= 9 | function(){b.modal.impl.setContainerDimensions()};b.modal.setPosition=function(){b.modal.impl.setPosition()};b.modal.update=function(a,h){b.modal.impl.update(a,h)};b.fn.modal=function(a){return b.modal.impl.init(this,a)};b.modal.defaults={appendTo:"body",focus:!0,opacity:50,overlayId:"simplemodal-overlay",overlayCss:{},containerId:"simplemodal-container",containerCss:{},dataId:"simplemodal-data",dataCss:{},minHeight:null,minWidth:null,maxHeight:null,maxWidth:null,autoResize:!1,autoPosition:!0,zIndex:1E3, 10 | close:!0,closeHTML:'',closeClass:"simplemodal-close",escClose:!0,overlayClose:!1,fixed:!0,position:null,persist:!1,modal:!0,onOpen:null,onShow:null,onClose:null};b.modal.impl={d:{},init:function(a,h){if(this.d.data)return!1;o=p&&!b.support.boxModel;this.o=b.extend({},b.modal.defaults,h);this.zIndex=this.o.zIndex;this.occb=!1;if("object"===typeof a){if(a=a instanceof b?a:b(a),this.d.placeholder=!1,0").attr("id", 11 | "simplemodal-placeholder").css({display:"none"})),this.d.placeholder=!0,this.display=a.css("display"),!this.o.persist))this.d.orig=a.clone(!0)}else if("string"===typeof a||"number"===typeof a)a=b("
").html(a);else return alert("SimpleModal Error: Unsupported data type: "+typeof a),this;this.create(a);this.open();b.isFunction(this.o.onShow)&&this.o.onShow.apply(this,[this.d]);return this},create:function(a){this.getDimensions();if(this.o.modal&&m)this.d.iframe=b('').css(b.extend(this.o.iframeCss, 12 | {display:"none",opacity:0,position:"fixed",height:g[0],width:g[1],zIndex:this.o.zIndex,top:0,left:0})).appendTo(this.o.appendTo);this.d.overlay=b("
").attr("id",this.o.overlayId).addClass("simplemodal-overlay").css(b.extend(this.o.overlayCss,{display:"none",opacity:this.o.opacity/100,height:this.o.modal?j[0]:0,width:this.o.modal?j[1]:0,position:"fixed",left:0,top:0,zIndex:this.o.zIndex+1})).appendTo(this.o.appendTo);this.d.container=b("
").attr("id",this.o.containerId).addClass("simplemodal-container").css(b.extend({position:this.o.fixed? 13 | "fixed":"absolute"},this.o.containerCss,{display:"none",zIndex:this.o.zIndex+2})).append(this.o.close&&this.o.closeHTML?b(this.o.closeHTML).addClass(this.o.closeClass):"").appendTo(this.o.appendTo);this.d.wrap=b("
").attr("tabIndex",-1).addClass("simplemodal-wrap").css({height:"100%",outline:0,width:"100%"}).appendTo(this.d.container);this.d.data=a.attr("id",a.attr("id")||this.o.dataId).addClass("simplemodal-data").css(b.extend(this.o.dataCss,{display:"none"})).appendTo("body");this.setContainerDimensions(); 14 | this.d.data.appendTo(this.d.wrap);(m||o)&&this.fixIE()},bindEvents:function(){var a=this;b("."+a.o.closeClass).bind("click.simplemodal",function(b){b.preventDefault();a.close()});a.o.modal&&a.o.close&&a.o.overlayClose&&a.d.overlay.bind("click.simplemodal",function(b){b.preventDefault();a.close()});n.bind("keydown.simplemodal",function(b){a.o.modal&&9===b.keyCode?a.watchTab(b):a.o.close&&a.o.escClose&&27===b.keyCode&&(b.preventDefault(),a.close())});l.bind("resize.simplemodal orientationchange.simplemodal", 15 | function(){a.getDimensions();a.o.autoResize?a.setContainerDimensions():a.o.autoPosition&&a.setPosition();m||o?a.fixIE():a.o.modal&&(a.d.iframe&&a.d.iframe.css({height:g[0],width:g[1]}),a.d.overlay.css({height:j[0],width:j[1]}))})},unbindEvents:function(){b("."+this.o.closeClass).unbind("click.simplemodal");n.unbind("keydown.simplemodal");l.unbind(".simplemodal");this.d.overlay.unbind("click.simplemodal")},fixIE:function(){var a=this.o.position;b.each([this.d.iframe||null,!this.o.modal?null:this.d.overlay, 16 | "fixed"===this.d.container.css("position")?this.d.container:null],function(b,e){if(e){var f=e[0].style;f.position="absolute";if(2>b)f.removeExpression("height"),f.removeExpression("width"),f.setExpression("height",'document.body.scrollHeight > document.body.clientHeight ? document.body.scrollHeight : document.body.clientHeight + "px"'),f.setExpression("width",'document.body.scrollWidth > document.body.clientWidth ? document.body.scrollWidth : document.body.clientWidth + "px"');else{var c,d;a&&a.constructor=== 17 | Array?(c=a[0]?"number"===typeof a[0]?a[0].toString():a[0].replace(/px/,""):e.css("top").replace(/px/,""),c=-1===c.indexOf("%")?c+' + (t = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"':parseInt(c.replace(/%/,""))+' * ((document.documentElement.clientHeight || document.body.clientHeight) / 100) + (t = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"',a[1]&&(d="number"===typeof a[1]? 18 | a[1].toString():a[1].replace(/px/,""),d=-1===d.indexOf("%")?d+' + (t = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft) + "px"':parseInt(d.replace(/%/,""))+' * ((document.documentElement.clientWidth || document.body.clientWidth) / 100) + (t = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft) + "px"')):(c='(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (t = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"', 19 | d='(document.documentElement.clientWidth || document.body.clientWidth) / 2 - (this.offsetWidth / 2) + (t = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft) + "px"');f.removeExpression("top");f.removeExpression("left");f.setExpression("top",c);f.setExpression("left",d)}}})},focus:function(a){var h=this,a=a&&-1!==b.inArray(a,["first","last"])?a:"first",e=b(":input:enabled:visible:"+a,h.d.wrap);setTimeout(function(){0c?c:bc?c:this.o.minHeight&&"auto"!==i&&ed?d:ad?d:this.o.minWidth&&"auto"!==c&&fb||f>a?"auto":"visible"});this.o.autoPosition&&this.setPosition()},setPosition:function(){var a,b;a=g[0]/2-this.d.container.outerHeight(!0)/2;b=g[1]/2-this.d.container.outerWidth(!0)/2;var e="fixed"!==this.d.container.css("position")?l.scrollTop():0;this.o.position&&"[object Array]"===Object.prototype.toString.call(this.o.position)?(a=e+(this.o.position[0]||a),b=this.o.position[1]||b): 24 | a=e+a;this.d.container.css({left:b,top:a})},watchTab:function(a){if(0=u&&y<=l&&g>=f;else if(r==="vertical")return!!b&&m<=a&&v>=u;else if(r==="horizontal")return!!b&&y<=l&&g>=f}})(jQuery) 2 | -------------------------------------------------------------------------------- /app/src/lib/spine/ajax.coffee: -------------------------------------------------------------------------------- 1 | Spine = @Spine or require('spine') 2 | $ = Spine.$ 3 | Model = Spine.Model 4 | Queue = $({}) 5 | 6 | Ajax = 7 | getURL: (object) -> 8 | object.url?() or object.url 9 | 10 | getCollectionURL: (object) -> 11 | if object 12 | if typeof object.url is "function" 13 | @generateURL(object) 14 | else 15 | object.url 16 | 17 | getScope: (object) -> 18 | object.scope?() or object.scope 19 | 20 | generateURL: (object, args...) -> 21 | if object.className 22 | collection = object.className.toLowerCase() + 's' 23 | scope = Ajax.getScope(object) 24 | else 25 | if typeof object.constructor.url is 'string' 26 | collection = object.constructor.url 27 | else 28 | collection = object.constructor.className.toLowerCase() + 's' 29 | scope = Ajax.getScope(object) or Ajax.getScope(object.constructor) 30 | args.unshift(collection) 31 | args.unshift(scope) 32 | # construct and clean url 33 | path = args.join('/') 34 | path = path.replace /(\/\/)/g, "/" 35 | path = path.replace /^\/|\/$/g, "" 36 | # handle relative urls vs those that use a host 37 | if path.indexOf("../") isnt 0 38 | Model.host + "/" + path 39 | else 40 | path 41 | 42 | enabled: true 43 | 44 | disable: (callback) -> 45 | if @enabled 46 | @enabled = false 47 | try 48 | do callback 49 | catch e 50 | throw e 51 | finally 52 | @enabled = true 53 | else 54 | do callback 55 | 56 | queue: (request) -> 57 | if request then Queue.queue(request) else Queue.queue() 58 | 59 | clearQueue: -> 60 | @queue [] 61 | 62 | class Base 63 | defaults: 64 | dataType: 'json' 65 | processData: false 66 | headers: {'X-Requested-With': 'XMLHttpRequest'} 67 | 68 | queue: Ajax.queue 69 | 70 | ajax: (params, defaults) -> 71 | $.ajax @ajaxSettings(params, defaults) 72 | 73 | ajaxQueue: (params, defaults, record) -> 74 | jqXHR = null 75 | deferred = $.Deferred() 76 | promise = deferred.promise() 77 | return promise unless Ajax.enabled 78 | settings = @ajaxSettings(params, defaults) 79 | 80 | request = (next) -> 81 | if record?.id? 82 | # for existing singleton, model id may have been updated 83 | # after request has been queued 84 | settings.url ?= Ajax.getURL(record) 85 | settings.data?.id = record.id 86 | 87 | settings.data = JSON.stringify(settings.data) 88 | jqXHR = $.ajax(settings) 89 | .done(deferred.resolve) 90 | .fail(deferred.reject) 91 | .then(next, next) 92 | 93 | promise.abort = (statusText) -> 94 | return jqXHR.abort(statusText) if jqXHR 95 | index = $.inArray(request, @queue()) 96 | @queue().splice(index, 1) if index > -1 97 | deferred.rejectWith( 98 | settings.context or settings, 99 | [promise, statusText, ''] 100 | ) 101 | promise 102 | 103 | @queue request 104 | promise 105 | 106 | ajaxSettings: (params, defaults) -> 107 | $.extend({}, @defaults, defaults, params) 108 | 109 | class Collection extends Base 110 | constructor: (@model) -> 111 | 112 | find: (id, params, options = {}) -> 113 | record = new @model(id: id) 114 | @ajaxQueue( 115 | params, 116 | type: 'GET', 117 | url: options.url or Ajax.getURL(record) 118 | ).done(@recordsResponse) 119 | .fail(@failResponse) 120 | 121 | all: (params, options = {}) -> 122 | @ajaxQueue( 123 | params, 124 | type: 'GET', 125 | url: options.url or Ajax.getURL(@model) 126 | ).done(@recordsResponse) 127 | .fail(@failResponse) 128 | 129 | fetch: (params = {}, options = {}) -> 130 | if id = params.id 131 | delete params.id 132 | @find(id, params, options).done (record) => 133 | @model.refresh(record, options) 134 | else 135 | @all(params, options).done (records) => 136 | @model.refresh(records, options) 137 | 138 | # Private 139 | 140 | recordsResponse: (data, status, xhr) => 141 | @model.trigger('ajaxSuccess', null, status, xhr) 142 | 143 | failResponse: (xhr, statusText, error) => 144 | @model.trigger('ajaxError', null, xhr, statusText, error) 145 | 146 | class Singleton extends Base 147 | constructor: (@record) -> 148 | @model = @record.constructor 149 | 150 | reload: (params, options = {}) -> 151 | @ajaxQueue( 152 | params, { 153 | type: 'GET' 154 | url: options.url 155 | }, @record 156 | ).done(@recordResponse(options)) 157 | .fail(@failResponse(options)) 158 | 159 | create: (params, options = {}) -> 160 | @ajaxQueue( 161 | params, 162 | type: 'POST' 163 | contentType: 'application/json' 164 | data: @record.toJSON() 165 | url: options.url or Ajax.getCollectionURL(@record) 166 | ).done(@recordResponse(options)) 167 | .fail(@failResponse(options)) 168 | 169 | update: (params, options = {}) -> 170 | @ajaxQueue( 171 | params, { 172 | type: 'PUT' 173 | contentType: 'application/json' 174 | data: @record.toJSON() 175 | url: options.url 176 | }, @record 177 | ).done(@recordResponse(options)) 178 | .fail(@failResponse(options)) 179 | 180 | destroy: (params, options = {}) -> 181 | @ajaxQueue( 182 | params, { 183 | type: 'DELETE' 184 | url: options.url 185 | }, @record 186 | ).done(@recordResponse(options)) 187 | .fail(@failResponse(options)) 188 | 189 | # Private 190 | 191 | recordResponse: (options = {}) => 192 | (data, status, xhr) => 193 | 194 | Ajax.disable => 195 | unless Spine.isBlank(data) or @record.destroyed 196 | # ID change, need to do some shifting 197 | if data.id and @record.id isnt data.id 198 | @record.changeID(data.id) 199 | # Update with latest data 200 | @record.refresh(data) 201 | 202 | @record.trigger('ajaxSuccess', data, status, xhr) 203 | options.success?.apply(@record) # Deprecated 204 | options.done?.apply(@record) 205 | 206 | failResponse: (options = {}) => 207 | (xhr, statusText, error) => 208 | @record.trigger('ajaxError', xhr, statusText, error) 209 | options.error?.apply(@record) # Deprecated 210 | options.fail?.apply(@record) 211 | 212 | # Ajax endpoint 213 | Model.host = '' 214 | 215 | Include = 216 | ajax: -> new Singleton(this) 217 | 218 | url: (args...) -> 219 | args.unshift(encodeURIComponent(@id)) 220 | Ajax.generateURL(@, args...) 221 | 222 | Extend = 223 | ajax: -> new Collection(this) 224 | 225 | url: (args...) -> 226 | Ajax.generateURL(@, args...) 227 | 228 | Model.Ajax = 229 | extended: -> 230 | @fetch @ajaxFetch 231 | @change @ajaxChange 232 | @extend Extend 233 | @include Include 234 | 235 | # Private 236 | 237 | ajaxFetch: -> 238 | @ajax().fetch(arguments...) 239 | 240 | ajaxChange: (record, type, options = {}) -> 241 | return if options.ajax is false 242 | record.ajax()[type](options.ajax, options) 243 | 244 | Model.Ajax.Methods = 245 | extended: -> 246 | @extend Extend 247 | @include Include 248 | 249 | # Globals 250 | Ajax.defaults = Base::defaults 251 | Ajax.Base = Base 252 | Ajax.Singleton = Singleton 253 | Ajax.Collection = Collection 254 | Spine.Ajax = Ajax 255 | module?.exports = Ajax 256 | -------------------------------------------------------------------------------- /app/src/lib/spine/list.coffee: -------------------------------------------------------------------------------- 1 | Spine = @Spine or require('./spine') 2 | $ = Spine.$ 3 | 4 | class Spine.List extends Spine.Controller 5 | events: 6 | 'click .item': 'click' 7 | 8 | selectFirst: false 9 | 10 | constructor: -> 11 | super 12 | @bind 'change', @change 13 | 14 | template: -> 15 | throw 'Override template' 16 | 17 | change: (item) => 18 | @current = item 19 | 20 | unless @current 21 | @children().removeClass('active') 22 | return 23 | 24 | @children().removeClass('active') 25 | $(@children().get(@items.indexOf(@current))).addClass('active') 26 | 27 | render: (items) -> 28 | @items = items if items 29 | @html @template(@items) 30 | @change @current 31 | if @selectFirst 32 | unless @children('.active').length 33 | @children(':first').click() 34 | 35 | children: (sel) -> 36 | @el.children(sel) 37 | 38 | click: (e) -> 39 | item = @items[$(e.currentTarget).index()] 40 | @trigger('change', item) 41 | true 42 | 43 | module?.exports = Spine.List 44 | -------------------------------------------------------------------------------- /app/src/lib/spine/local.coffee: -------------------------------------------------------------------------------- 1 | Spine = @Spine or require('./spine') 2 | ipc = require 'ipc' 3 | 4 | Spine.Model.Local = 5 | extended: -> 6 | @change @saveLocal 7 | @fetch @loadLocal 8 | 9 | saveLocal: -> 10 | data = 11 | key: @className 12 | objects: JSON.stringify(@records) 13 | ipc.sendChannel 'save_data', data 14 | 15 | loadLocal: (options = {})-> 16 | options.clear = true unless options.hasOwnProperty('clear') 17 | result = ipc.sendChannelSync 'load_data', @className 18 | result.map (r) -> 19 | r.cid = r.id 20 | r 21 | @refresh(result or [], options) 22 | 23 | module?.exports = Spine.Model.Local 24 | -------------------------------------------------------------------------------- /app/src/lib/spine/manager.coffee: -------------------------------------------------------------------------------- 1 | Spine = @Spine or require('./spine') 2 | $ = Spine.$ 3 | 4 | class Spine.Manager extends Spine.Module 5 | @include Spine.Events 6 | 7 | constructor: -> 8 | @controllers = [] 9 | @bind 'change', @change 10 | @add(arguments...) 11 | 12 | add: (controllers...) -> 13 | @addOne(cont) for cont in controllers 14 | 15 | addOne: (controller) -> 16 | controller.bind 'active', (args...) => 17 | @trigger('change', controller, args...) 18 | controller.bind 'release', => 19 | @controllers.splice(@controllers.indexOf(controller), 1) 20 | 21 | @controllers.push(controller) 22 | 23 | deactivate: -> 24 | @trigger('change', false, arguments...) 25 | 26 | # Private 27 | 28 | change: (current, args...) -> 29 | for cont in @controllers when cont isnt current 30 | cont.deactivate(args...) 31 | 32 | current.activate(args...) if current 33 | 34 | Spine.Controller.include 35 | active: (args...) -> 36 | if typeof args[0] is 'function' 37 | @bind('active', args[0]) 38 | else 39 | args.unshift('active') 40 | @trigger(args...) 41 | @ 42 | 43 | isActive: -> 44 | @el.hasClass('active') 45 | 46 | activate: -> 47 | @el.addClass('active') 48 | @ 49 | 50 | deactivate: -> 51 | @el.removeClass('active') 52 | @ 53 | 54 | class Spine.Stack extends Spine.Controller 55 | controllers: {} 56 | routes: {} 57 | 58 | className: 'spine stack' 59 | 60 | constructor: -> 61 | super 62 | 63 | @manager = new Spine.Manager 64 | 65 | for key, value of @controllers 66 | throw Error "'@#{ key }' already assigned - choose a different name" if @[key]? 67 | @[key] = new value(stack: @) 68 | @add(@[key]) 69 | 70 | for key, value of @routes 71 | do (key, value) => 72 | callback = value if typeof value is 'function' 73 | callback or= => @[value].active(arguments...) 74 | @route(key, callback) 75 | 76 | @[@default].active() if @default 77 | 78 | add: (controller) -> 79 | @manager.add(controller) 80 | @append(controller) 81 | 82 | module?.exports = Spine.Manager 83 | module?.exports.Stack = Spine.Stack 84 | -------------------------------------------------------------------------------- /app/src/lib/spine/relation.coffee: -------------------------------------------------------------------------------- 1 | Spine = @Spine or require('./spine') 2 | isArray = Spine.isArray 3 | # require = @require or ((value) -> eval(value)) 4 | 5 | class Collection extends Spine.Module 6 | constructor: (options = {}) -> 7 | for key, value of options 8 | @[key] = value 9 | 10 | all: -> 11 | @model.select (rec) => @associated(rec) 12 | 13 | first: -> 14 | @all()[0] 15 | 16 | last: -> 17 | values = @all() 18 | values[values.length - 1] 19 | 20 | count: -> 21 | @all().length 22 | 23 | find: (id) -> 24 | records = @select (rec) => 25 | "#{rec.id}" is "#{id}" 26 | throw new Error("\"#{@model.className}\" model could not find a record for the ID \"#{id}\"") unless records[0] 27 | records[0] 28 | 29 | findAllByAttribute: (name, value) -> 30 | @model.select (rec) => 31 | @associated(rec) and rec[name] is value 32 | 33 | findByAttribute: (name, value) -> 34 | @findAllByAttribute(name, value)[0] 35 | 36 | select: (cb) -> 37 | @model.select (rec) => 38 | @associated(rec) and cb(rec) 39 | 40 | refresh: (values) -> 41 | return this unless values? 42 | for record in @all() 43 | delete @model.irecords[record.id] 44 | for match, i in @model.records when match.id is record.id 45 | @model.records.splice(i, 1) 46 | break 47 | values = [values] unless isArray(values) 48 | for record in values 49 | record.newRecord = false 50 | record[@fkey] = @record.id 51 | @model.refresh values 52 | this 53 | 54 | create: (record, options) -> 55 | record[@fkey] = @record.id 56 | @model.create(record, options) 57 | 58 | add: (record, options) -> 59 | record.updateAttribute @fkey, @record.id, options 60 | 61 | remove: (record, options) -> 62 | record.updateAttribute @fkey, null, options 63 | 64 | # Private 65 | 66 | associated: (record) -> 67 | record[@fkey] is @record.id 68 | 69 | class Instance extends Spine.Module 70 | constructor: (options = {}) -> 71 | for key, value of options 72 | @[key] = value 73 | 74 | exists: -> 75 | return if @record[@fkey] then @model.exists(@record[@fkey]) else false 76 | 77 | update: (value) -> 78 | return this unless value? 79 | unless value instanceof @model 80 | value = new @model(value) 81 | value.save() if value.isNew() 82 | @record[@fkey] = value and value.id 83 | this 84 | 85 | class Singleton extends Spine.Module 86 | constructor: (options = {}) -> 87 | for key, value of options 88 | @[key] = value 89 | 90 | find: -> 91 | @record.id and @model.findByAttribute(@fkey, @record.id) 92 | 93 | update: (value) -> 94 | return this unless value? 95 | unless value instanceof @model 96 | value = @model.fromJSON(value) 97 | 98 | value[@fkey] = @record.id 99 | value.save() 100 | this 101 | 102 | singularize = (str) -> 103 | str.replace(/s$/, '') 104 | 105 | underscore = (str) -> 106 | str.replace(/::/g, '/') 107 | .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2') 108 | .replace(/([a-z\d])([A-Z])/g, '$1_$2') 109 | .replace(/-/g, '_') 110 | .toLowerCase() 111 | 112 | association = (name, model, record, fkey, Ctor) -> 113 | model = require(model) if typeof model is 'string' 114 | new Ctor(name: name, model: model, record: record, fkey: fkey) 115 | 116 | Spine.Model.extend 117 | hasMany: (name, model, fkey) -> 118 | fkey ?= "#{underscore(this.className)}_id" 119 | @::[name] = (value) -> 120 | association(name, model, @, fkey, Collection).refresh(value) 121 | 122 | belongsTo: (name, model, fkey) -> 123 | fkey ?= "#{underscore(singularize(name))}_id" 124 | @::[name] = (value) -> 125 | association(name, model, @, fkey, Instance).update(value).exists() 126 | 127 | @attributes.push(fkey) 128 | 129 | hasOne: (name, model, fkey) -> 130 | fkey ?= "#{underscore(@className)}_id" 131 | @::[name] = (value) -> 132 | association(name, model, @, fkey, Singleton).update(value).find() 133 | 134 | Spine.Collection = Collection 135 | Spine.Singleton = Singleton 136 | Spine.Instance = Instance 137 | -------------------------------------------------------------------------------- /app/src/lib/spine/route.coffee: -------------------------------------------------------------------------------- 1 | Spine = @Spine or require('./spine') 2 | $ = Spine.$ 3 | 4 | hashStrip = /^#*/ 5 | namedParam = /:([\w\d]+)/g 6 | splatParam = /\*([\w\d]+)/g 7 | escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g 8 | 9 | class Spine.Route extends Spine.Module 10 | @extend Spine.Events 11 | 12 | @historySupport: window.history?.pushState? 13 | 14 | @routes: [] 15 | 16 | @options: 17 | trigger: true 18 | history: false 19 | shim: false 20 | replace: false 21 | redirect: false 22 | 23 | @add: (path, callback) -> 24 | if (typeof path is 'object' and path not instanceof RegExp) 25 | @add(key, value) for key, value of path 26 | else 27 | @routes.push(new @(path, callback)) 28 | 29 | @setup: (options = {}) -> 30 | @options = $.extend({}, @options, options) 31 | 32 | if (@options.history) 33 | @history = @historySupport and @options.history 34 | 35 | return if @options.shim 36 | 37 | if @history 38 | $(window).bind('popstate', @change) 39 | else 40 | $(window).bind('hashchange', @change) 41 | @change() 42 | 43 | @unbind: -> 44 | return if @options.shim 45 | 46 | if @history 47 | $(window).unbind('popstate', @change) 48 | else 49 | $(window).unbind('hashchange', @change) 50 | 51 | @navigate: (args...) -> 52 | options = {} 53 | 54 | lastArg = args[args.length - 1] 55 | if typeof lastArg is 'object' 56 | options = args.pop() 57 | else if typeof lastArg is 'boolean' 58 | options.trigger = args.pop() 59 | 60 | options = $.extend({}, @options, options) 61 | 62 | path = args.join('/') 63 | return if @path is path 64 | @path = path 65 | 66 | @trigger('navigate', @path) 67 | 68 | route = @matchRoute(@path, options) if options.trigger 69 | 70 | return if options.shim 71 | 72 | if !route 73 | if typeof options.redirect is 'function' 74 | return options.redirect.apply this, [@path, options] 75 | else 76 | if options.redirect is true 77 | @redirect(@path) 78 | 79 | if @history and options.replace 80 | history.replaceState({}, document.title, @path) 81 | else if @history 82 | history.pushState({}, document.title, @path) 83 | else 84 | window.location.hash = @path 85 | 86 | # Private 87 | 88 | @getPath: -> 89 | if @history 90 | path = window.location.pathname 91 | path = '/' + path if path.substr(0,1) isnt '/' 92 | else 93 | path = window.location.hash 94 | path = path.replace(hashStrip, '') 95 | path 96 | 97 | @getHost: -> 98 | "#{window.location.protocol}//#{window.location.host}" 99 | 100 | @change: -> 101 | path = @getPath() 102 | return if path is @path 103 | @path = path 104 | @matchRoute(@path) 105 | 106 | @matchRoute: (path, options) -> 107 | for route in @routes when route.match(path, options) 108 | @trigger('change', route, path) 109 | return route 110 | 111 | @redirect: (path) -> 112 | window.location = path 113 | 114 | constructor: (@path, @callback) -> 115 | @names = [] 116 | 117 | if typeof path is 'string' 118 | namedParam.lastIndex = 0 119 | while (match = namedParam.exec(path)) != null 120 | @names.push(match[1]) 121 | 122 | splatParam.lastIndex = 0 123 | while (match = splatParam.exec(path)) != null 124 | @names.push(match[1]) 125 | 126 | path = path.replace(escapeRegExp, '\\$&') 127 | .replace(namedParam, '([^\/]*)') 128 | .replace(splatParam, '(.*?)') 129 | 130 | @route = new RegExp("^#{path}$") 131 | else 132 | @route = path 133 | 134 | match: (path, options = {}) -> 135 | match = @route.exec(path) 136 | return false unless match 137 | options.match = match 138 | params = match.slice(1) 139 | 140 | if @names.length 141 | for param, i in params 142 | options[@names[i]] = param 143 | 144 | @callback.call(null, options) isnt false 145 | 146 | # Coffee-script bug 147 | Spine.Route.change = Spine.Route.proxy(Spine.Route.change) 148 | 149 | Spine.Controller.include 150 | route: (path, callback) -> 151 | Spine.Route.add(path, @proxy(callback)) 152 | 153 | routes: (routes) -> 154 | @route(key, value) for key, value of routes 155 | 156 | navigate: -> 157 | Spine.Route.navigate.apply(Spine.Route, arguments) 158 | 159 | module?.exports = Spine.Route 160 | -------------------------------------------------------------------------------- /app/src/lib/spine/spine.coffee: -------------------------------------------------------------------------------- 1 | Events = 2 | bind: (ev, callback) -> 3 | evs = ev.split(' ') 4 | calls = @hasOwnProperty('_callbacks') and @_callbacks or= {} 5 | for name in evs 6 | calls[name] or= [] 7 | calls[name].push(callback) 8 | this 9 | 10 | one: (ev, callback) -> 11 | @bind ev, handler = -> 12 | @unbind(ev, handler) 13 | callback.apply(this, arguments) 14 | 15 | trigger: (args...) -> 16 | ev = args.shift() 17 | list = @hasOwnProperty('_callbacks') and @_callbacks?[ev] 18 | return unless list 19 | for callback in list 20 | if callback.apply(this, args) is false 21 | break 22 | true 23 | 24 | listenTo: (obj, ev, callback) -> 25 | obj.bind(ev, callback) 26 | @listeningTo or= [] 27 | @listeningTo.push {obj, ev, callback} 28 | this 29 | 30 | listenToOnce: (obj, ev, callback) -> 31 | listeningToOnce = @listeningToOnce or = [] 32 | obj.bind ev, handler = -> 33 | idx = -1 34 | for lt, i in listeningToOnce when lt.obj is obj 35 | idx = i if lt.ev is ev and lt.callback is callback 36 | obj.unbind(ev, handler) 37 | listeningToOnce.splice(idx, 1) unless idx is -1 38 | callback.apply(this, arguments) 39 | listeningToOnce.push {obj, ev, callback, handler} 40 | this 41 | 42 | stopListening: (obj, events, callback) -> 43 | if arguments.length is 0 44 | for listeningTo in [@listeningTo, @listeningToOnce] 45 | continue unless listeningTo 46 | for lt in listeningTo 47 | lt.obj.unbind(lt.ev, lt.handler or lt.callback) 48 | @listeningTo = undefined 49 | @listeningToOnce = undefined 50 | 51 | else if obj 52 | for listeningTo in [@listeningTo, @listeningToOnce] 53 | continue unless listeningTo 54 | events = if events then events.split(' ') else [undefined] 55 | for ev in events 56 | for idx in [listeningTo.length-1..0] 57 | lt = listeningTo[idx] 58 | if (not ev) or (ev is lt.ev) 59 | lt.obj.unbind(lt.ev, lt.handler or lt.callback) 60 | listeningTo.splice(idx, 1) unless idx is -1 61 | else if ev 62 | evts = lt.ev.split(' ') 63 | if ~(i = evts.indexOf(ev)) 64 | evts.splice(i, 1) 65 | lt.ev = $.trim(evts.join(' ')) 66 | lt.obj.unbind(ev, lt.handler or lt.callback) 67 | 68 | unbind: (ev, callback) -> 69 | if arguments.length is 0 70 | @_callbacks = {} 71 | return this 72 | return this unless ev 73 | evs = ev.split(' ') 74 | for name in evs 75 | list = @_callbacks?[name] 76 | continue unless list 77 | unless callback 78 | delete @_callbacks[name] 79 | continue 80 | for cb, i in list when (cb is callback) 81 | list = list.slice() 82 | list.splice(i, 1) 83 | @_callbacks[name] = list 84 | break 85 | this 86 | 87 | Events.on = Events.bind 88 | Events.off = Events.unbind 89 | 90 | Log = 91 | trace: true 92 | 93 | logPrefix: '(App)' 94 | 95 | log: (args...) -> 96 | return unless @trace 97 | if @logPrefix then args.unshift(@logPrefix) 98 | console?.log?(args...) 99 | this 100 | 101 | moduleKeywords = ['included', 'extended'] 102 | 103 | class Module 104 | @include: (obj) -> 105 | throw new Error('include(obj) requires obj') unless obj 106 | for key, value of obj when key not in moduleKeywords 107 | @::[key] = value 108 | obj.included?.apply(this) 109 | this 110 | 111 | @extend: (obj) -> 112 | throw new Error('extend(obj) requires obj') unless obj 113 | for key, value of obj when key not in moduleKeywords 114 | @[key] = value 115 | obj.extended?.apply(this) 116 | this 117 | 118 | @proxy: (func) -> 119 | => func.apply(this, arguments) 120 | 121 | proxy: (func) -> 122 | => func.apply(this, arguments) 123 | 124 | constructor: -> 125 | @init?(arguments...) 126 | 127 | class Model extends Module 128 | @extend Events 129 | 130 | @records : [] 131 | @irecords : {} 132 | @attributes : [] 133 | 134 | @configure: (name, attributes...) -> 135 | @className = name 136 | @deleteAll() 137 | @attributes = attributes if attributes.length 138 | @attributes and= makeArray(@attributes) 139 | @attributes or= [] 140 | @unbind() 141 | this 142 | 143 | @toString: -> "#{@className}(#{@attributes.join(", ")})" 144 | 145 | @find: (id) -> 146 | record = @exists(id) 147 | throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record 148 | return record 149 | 150 | @exists: (id) -> 151 | @irecords[id]?.clone() 152 | 153 | @addRecord: (record) -> 154 | if record.id and @irecords[record.id] 155 | @irecords[record.id].remove() 156 | 157 | record.id or= record.cid 158 | @records.push(record) 159 | @irecords[record.id] = record 160 | @irecords[record.cid] = record 161 | 162 | @refresh: (values, options = {}) -> 163 | @deleteAll() if options.clear 164 | 165 | records = @fromJSON(values) 166 | records = [records] unless isArray(records) 167 | @addRecord(record) for record in records 168 | @sort() 169 | 170 | result = @cloneArray(records) 171 | @trigger('refresh', result, options) 172 | result 173 | 174 | @select: (callback) -> 175 | (record.clone() for record in @records when callback(record)) 176 | 177 | @findByAttribute: (name, value) -> 178 | for record in @records 179 | if record[name] is value 180 | return record.clone() 181 | null 182 | 183 | @findAllByAttribute: (name, value) -> 184 | @select (item) -> 185 | item[name] is value 186 | 187 | @each: (callback) -> 188 | callback(record.clone()) for record in @records 189 | 190 | @all: -> 191 | @cloneArray(@records) 192 | 193 | @first: -> 194 | @records[0]?.clone() 195 | 196 | @last: -> 197 | @records[@records.length - 1]?.clone() 198 | 199 | @count: -> 200 | @records.length 201 | 202 | @deleteAll: -> 203 | @records = [] 204 | @irecords = {} 205 | 206 | @destroyAll: (options) -> 207 | record.destroy(options) for record in @records 208 | 209 | @update: (id, atts, options) -> 210 | @find(id).updateAttributes(atts, options) 211 | 212 | @create: (atts, options) -> 213 | record = new @(atts) 214 | record.save(options) 215 | 216 | @destroy: (id, options) -> 217 | @find(id).destroy(options) 218 | 219 | @change: (callbackOrParams) -> 220 | if typeof callbackOrParams is 'function' 221 | @bind('change', callbackOrParams) 222 | else 223 | @trigger('change', arguments...) 224 | 225 | @fetch: (callbackOrParams) -> 226 | if typeof callbackOrParams is 'function' 227 | @bind('fetch', callbackOrParams) 228 | else 229 | @trigger('fetch', arguments...) 230 | 231 | @toJSON: -> 232 | @records 233 | 234 | @fromJSON: (objects) -> 235 | return unless objects 236 | if typeof objects is 'string' 237 | objects = JSON.parse(objects) 238 | if isArray(objects) 239 | (new @(value) for value in objects) 240 | else 241 | new @(objects) 242 | 243 | @fromForm: -> 244 | (new this).fromForm(arguments...) 245 | 246 | @sort: -> 247 | if @comparator 248 | @records.sort @comparator 249 | this 250 | 251 | # Private 252 | 253 | @cloneArray: (array) -> 254 | (value.clone() for value in array) 255 | 256 | @idCounter: 0 257 | 258 | @uid: (prefix = '') -> 259 | uid = prefix + @idCounter++ 260 | uid = @uid(prefix) if @exists(uid) 261 | uid 262 | 263 | # Instance 264 | 265 | constructor: (atts) -> 266 | super 267 | @load atts if atts 268 | @cid = atts?.cid or @constructor.uid('c-') 269 | 270 | isNew: -> 271 | not @exists() 272 | 273 | isValid: -> 274 | not @validate() 275 | 276 | validate: -> 277 | 278 | load: (atts) -> 279 | if atts.id then @id = atts.id 280 | for key, value of atts 281 | if atts.hasOwnProperty(key) and typeof @[key] is 'function' 282 | @[key](value) 283 | else 284 | @[key] = value 285 | this 286 | 287 | attributes: -> 288 | result = {} 289 | for key in @constructor.attributes when key of this 290 | if typeof @[key] is 'function' 291 | result[key] = @[key]() 292 | else 293 | result[key] = @[key] 294 | result.id = @id if @id 295 | result 296 | 297 | eql: (rec) -> 298 | !!(rec and rec.constructor is @constructor and 299 | (rec.cid is @cid) or (rec.id and rec.id is @id)) 300 | 301 | save: (options = {}) -> 302 | unless options.validate is false 303 | error = @validate() 304 | if error 305 | @trigger('error', error) 306 | return false 307 | 308 | @trigger('beforeSave', options) 309 | record = if @isNew() then @create(options) else @update(options) 310 | @stripCloneAttrs() 311 | @trigger('save', options) 312 | record 313 | 314 | stripCloneAttrs: -> 315 | return if @hasOwnProperty 'cid' # Make sure it's not the raw object 316 | for own key, value of @ 317 | delete @[key] if @constructor.attributes.indexOf(key) > -1 318 | this 319 | 320 | updateAttribute: (name, value, options) -> 321 | atts = {} 322 | atts[name] = value 323 | @updateAttributes(atts, options) 324 | 325 | updateAttributes: (atts, options) -> 326 | @load(atts) 327 | @save(options) 328 | 329 | changeID: (id) -> 330 | return if id is @id 331 | records = @constructor.irecords 332 | records[id] = records[@id] 333 | delete records[@id] 334 | @id = id 335 | @save() 336 | 337 | remove: -> 338 | # Remove record from model 339 | records = @constructor.records.slice(0) 340 | for record, i in records when @eql(record) 341 | records.splice(i, 1) 342 | break 343 | @constructor.records = records 344 | # Remove the ID and CID 345 | delete @constructor.irecords[@id] 346 | delete @constructor.irecords[@cid] 347 | 348 | destroy: (options = {}) -> 349 | @trigger('beforeDestroy', options) 350 | @remove() 351 | @destroyed = true 352 | # handle events 353 | @trigger('destroy', options) 354 | @trigger('change', 'destroy', options) 355 | if @listeningTo 356 | @stopListening() 357 | @unbind() 358 | this 359 | 360 | dup: (newRecord = true) -> 361 | atts = @attributes() 362 | if newRecord 363 | delete atts.id 364 | else 365 | atts.cid = @cid 366 | new @constructor(atts) 367 | 368 | clone: -> 369 | createObject(this) 370 | 371 | reload: -> 372 | return this if @isNew() 373 | original = @constructor.find(@id) 374 | @load(original.attributes()) 375 | original 376 | 377 | refresh: (data) -> 378 | # go to the source and load attributes 379 | root = @constructor.irecords[@id] 380 | root.load(data) 381 | @trigger('refresh') 382 | @ 383 | 384 | toJSON: -> 385 | @attributes() 386 | 387 | toString: -> 388 | "<#{@constructor.className} (#{JSON.stringify(this)})>" 389 | 390 | fromForm: (form) -> 391 | result = {} 392 | 393 | for checkbox in $(form).find('[type=checkbox]:not([value])') 394 | result[checkbox.name] = $(checkbox).prop('checked') 395 | 396 | for checkbox in $(form).find('[type=checkbox][name$="[]"]') 397 | name = checkbox.name.replace(/\[\]$/, '') 398 | result[name] or= [] 399 | result[name].push checkbox.value if $(checkbox).prop('checked') 400 | 401 | for key in $(form).serializeArray() 402 | result[key.name] or= key.value 403 | 404 | @load(result) 405 | 406 | exists: -> 407 | @constructor.exists(@id) 408 | 409 | # Private 410 | 411 | update: (options) -> 412 | @trigger('beforeUpdate', options) 413 | 414 | records = @constructor.irecords 415 | records[@id].load @attributes() 416 | 417 | @constructor.sort() 418 | 419 | clone = records[@id].clone() 420 | clone.trigger('update', options) 421 | clone.trigger('change', 'update', options) 422 | clone 423 | 424 | create: (options) -> 425 | @trigger('beforeCreate', options) 426 | @id or= @cid 427 | 428 | record = @dup(false) 429 | @constructor.addRecord(record) 430 | @constructor.sort() 431 | 432 | clone = record.clone() 433 | clone.trigger('create', options) 434 | clone.trigger('change', 'create', options) 435 | clone 436 | 437 | bind: (events, callback) -> 438 | @constructor.bind events, binder = (record) => 439 | if record && @eql(record) 440 | callback.apply(this, arguments) 441 | # create a wrapper function to be called with 'unbind' for each event 442 | for singleEvent in events.split(' ') 443 | do (singleEvent) => 444 | @constructor.bind "unbind", unbinder = (record, event, cb) => 445 | if record && @eql(record) 446 | return if event and event isnt singleEvent 447 | return if cb and cb isnt callback 448 | @constructor.unbind(singleEvent, binder) 449 | @constructor.unbind("unbind", unbinder) 450 | this 451 | 452 | one: (events, callback) -> 453 | @bind events, handler = => 454 | @unbind(events, handler) 455 | callback.apply(this, arguments) 456 | 457 | trigger: (args...) -> 458 | args.splice(1, 0, this) 459 | @constructor.trigger(args...) 460 | 461 | listenTo: -> Events.listenTo.apply @, arguments 462 | listenToOnce: -> Events.listenToOnce.apply @, arguments 463 | stopListening: -> Events.stopListening.apply @, arguments 464 | 465 | unbind: (events, callback) -> 466 | if arguments.length is 0 467 | @trigger('unbind') 468 | else if events 469 | for event in events.split(' ') 470 | @trigger('unbind', event, callback) 471 | 472 | Model::on = Model::bind 473 | Model::off = Model::unbind 474 | 475 | class Controller extends Module 476 | @include Events 477 | @include Log 478 | 479 | eventSplitter: /^(\S+)\s*(.*)$/ 480 | tag: 'div' 481 | 482 | constructor: (options) -> 483 | @options = options 484 | 485 | for key, value of @options 486 | @[key] = value 487 | 488 | @el = document.createElement(@tag) unless @el 489 | @el = $(@el) 490 | @$el = @el 491 | 492 | @el.addClass(@className) if @className 493 | @el.attr(@attributes) if @attributes 494 | 495 | @events = @constructor.events unless @events 496 | @elements = @constructor.elements unless @elements 497 | 498 | context = @ 499 | while parent_prototype = context.constructor.__super__ 500 | @events = $.extend({}, parent_prototype.events, @events) if parent_prototype.events 501 | @elements = $.extend({}, parent_prototype.elements, @elements) if parent_prototype.elements 502 | context = parent_prototype 503 | 504 | @delegateEvents(@events) if @events 505 | @refreshElements() if @elements 506 | 507 | super 508 | 509 | release: => 510 | @trigger 'release', this 511 | # no need to unDelegateEvents since remove will end up handling that 512 | @el.remove() 513 | @unbind() 514 | @stopListening() 515 | 516 | $: (selector) -> $(selector, @el) 517 | 518 | delegateEvents: (events) -> 519 | for key, method of events 520 | 521 | if typeof(method) is 'function' 522 | # Always return true from event handlers 523 | method = do (method) => => 524 | method.apply(this, arguments) 525 | true 526 | else 527 | unless @[method] 528 | throw new Error("#{method} doesn't exist") 529 | 530 | method = do (method) => => 531 | @[method].apply(this, arguments) 532 | true 533 | 534 | match = key.match(@eventSplitter) 535 | eventName = match[1] 536 | selector = match[2] 537 | 538 | if selector is '' 539 | @el.bind(eventName, method) 540 | else 541 | @el.on(eventName, selector, method) 542 | 543 | refreshElements: -> 544 | for key, value of @elements 545 | @[value] = @$(key) 546 | 547 | delay: (func, timeout) -> 548 | setTimeout(@proxy(func), timeout || 0) 549 | 550 | # keep controllers elements obj in sync with it contents 551 | 552 | html: (element) -> 553 | @el.html(element.el or element) 554 | @refreshElements() 555 | @el 556 | 557 | append: (elements...) -> 558 | elements = (e.el or e for e in elements) 559 | @el.append(elements...) 560 | @refreshElements() 561 | @el 562 | 563 | appendTo: (element) -> 564 | @el.appendTo(element.el or element) 565 | @refreshElements() 566 | @el 567 | 568 | prepend: (elements...) -> 569 | elements = (e.el or e for e in elements) 570 | @el.prepend(elements...) 571 | @refreshElements() 572 | @el 573 | 574 | replace: (element) -> 575 | element = element.el or element 576 | element = $.trim(element) if typeof element is "string" 577 | # parseHTML is incompatible with Zepto 578 | [previous, @el] = [@el, $($.parseHTML(element)?[0] or element)] 579 | previous.replaceWith(@el) 580 | @delegateEvents(@events) 581 | @refreshElements() 582 | @el 583 | 584 | # Utilities & Shims 585 | 586 | $ = window?.jQuery or window?.Zepto or (element) -> element 587 | 588 | createObject = Object.create or (o) -> 589 | Func = -> 590 | Func.prototype = o 591 | new Func() 592 | 593 | isArray = (value) -> 594 | Object::toString.call(value) is '[object Array]' 595 | 596 | isBlank = (value) -> 597 | return true unless value 598 | return false for key of value 599 | true 600 | 601 | makeArray = (args) -> 602 | Array::slice.call(args, 0) 603 | 604 | # Globals 605 | 606 | Spine = @Spine = {} 607 | module?.exports = Spine 608 | 609 | Spine.version = '1.2.0' 610 | Spine.isArray = isArray 611 | Spine.isBlank = isBlank 612 | Spine.$ = $ 613 | Spine.Events = Events 614 | Spine.Log = Log 615 | Spine.Module = Module 616 | Spine.Controller = Controller 617 | Spine.Model = Model 618 | 619 | # Global events 620 | 621 | Module.extend.call(Spine, Events) 622 | 623 | # JavaScript compatability 624 | 625 | Module.create = Module.sub = 626 | Controller.create = Controller.sub = 627 | Model.sub = (instances, statics) -> 628 | class Result extends this 629 | Result.include(instances) if instances 630 | Result.extend(statics) if statics 631 | Result.unbind?() 632 | Result 633 | 634 | Model.setup = (name, attributes = []) -> 635 | class Instance extends this 636 | Instance.configure(name, attributes...) 637 | Instance 638 | 639 | Spine.Class = Module 640 | -------------------------------------------------------------------------------- /app/src/models/collection.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | 4 | fsUtils = require '../lib/fs-utils' 5 | path = require 'path' 6 | fs = require 'fs' 7 | 8 | class Collection extends Spine.Model 9 | @configure "Collection", "path" 10 | 11 | @path: -> 12 | settings = new App.Settings 13 | return null if !settings.romsPath() 14 | path.join(settings.romsPath(), 'collections') 15 | 16 | @all: -> 17 | return [] if @path() == null 18 | collections = _.map fsUtils.listSync(@path()), (path) -> 19 | new App.Collection(path: path) 20 | 21 | _.filter collections, (collection) -> 22 | collection.isValid() 23 | 24 | constructor: -> 25 | super 26 | @games = [] 27 | @settings = new App.Settings 28 | @loadFromFile() 29 | 30 | isValid: -> 31 | fsUtils.isDirectorySync(@path) && 32 | fsUtils.exists(@imagePath()) 33 | 34 | name: -> 35 | path.basename(@path) 36 | 37 | imagePath: -> 38 | path.join(@path, "image.png") 39 | 40 | filePath: -> 41 | path.join(@path, 'games.json') 42 | 43 | loadFromFile: -> 44 | if fsUtils.exists(@filePath()) 45 | data = JSON.parse(fs.readFileSync(@filePath(), 'utf8')) 46 | @games = [] 47 | 48 | for gameBlob in data['games'] 49 | romPath = path.join(@settings.romsPath(), gameBlob['gameConsole'], gameBlob['filename']) 50 | if fsUtils.exists(romPath) 51 | gameConsole = new App.GameConsole(prefix: gameBlob['gameConsole']) 52 | @games.push(new App.Game(filePath: romPath, gameConsole: gameConsole)) 53 | 54 | _.filter @games, (game) -> 55 | game.imageExists() 56 | 57 | 58 | saveToFile: -> 59 | data = {'games': @games} 60 | fsUtils.writeSync(@filePath(), JSON.stringify(data)) 61 | 62 | addGame: (game) -> 63 | @games.push(game) 64 | @games = @games.unique() 65 | @saveToFile() 66 | 67 | 68 | module.exports = Collection 69 | -------------------------------------------------------------------------------- /app/src/models/favorites.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | 4 | fsUtils = require '../lib/fs-utils' 5 | path = require 'path' 6 | fs = require 'fs' 7 | 8 | class Favorites extends Spine.Model 9 | @configure "Favorites" 10 | 11 | constructor: -> 12 | super 13 | @games = [] 14 | @settings = new App.Settings 15 | @load() 16 | 17 | filePath: -> 18 | path.join(@settings.romsPath(), 'favorites.json') if @settings.romsPath() 19 | 20 | load: -> 21 | if @filePath() && fsUtils.exists(@filePath()) 22 | data = JSON.parse(fs.readFileSync(@filePath(), 'utf8')) 23 | @games = [] 24 | 25 | for gameBlob in data['games'] 26 | romPath = path.join(@settings.romsPath(), gameBlob['gameConsole'], gameBlob['filename']) 27 | if fsUtils.exists(romPath) 28 | gameConsole = new App.GameConsole(prefix: gameBlob['gameConsole']) 29 | @games.push(new App.Game(filePath: romPath, gameConsole: gameConsole)) 30 | 31 | save: -> 32 | data = {'games': @games} 33 | fsUtils.writeSync(@filePath(), JSON.stringify(data)) 34 | 35 | addGame: (game) -> 36 | @games.push(game) 37 | @games = @games.unique() 38 | @games = @games[0..5] 39 | @save() 40 | 41 | removeGame: (game) -> 42 | for foundGame in @games 43 | console.log(foundGame.filePath) 44 | console.log(game.filePath) 45 | if foundGame.filePath == game.filePath 46 | @games.splice(@games.indexOf(foundGame), 1) 47 | break 48 | 49 | @save() 50 | 51 | isFaved: (game) -> 52 | for foundGame in @games 53 | if foundGame.filePath == game.filePath 54 | return true 55 | break 56 | 57 | false 58 | 59 | 60 | module.exports = Favorites 61 | -------------------------------------------------------------------------------- /app/src/models/game.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | 4 | fsUtils = require '../lib/fs-utils' 5 | path = require 'path' 6 | 7 | class Game extends Spine.Model 8 | @configure "Game", "filePath", "gameConsole" 9 | 10 | toJSON: (objects) -> 11 | data = {'gameConsole': @gameConsole.prefix, 'filename': @filename()} 12 | 13 | filename: -> 14 | path.basename(@filePath) 15 | 16 | name: -> 17 | path.basename(@filePath, path.extname(@filePath)) 18 | 19 | imagePath: -> 20 | if @imageExists() 21 | @customImagePath() 22 | else 23 | @gameConsole.gameCardPath() 24 | 25 | customImagePath: -> 26 | path.join(path.dirname(@filePath), 'images', "#{@name()}.png") 27 | 28 | imageExists: -> 29 | fsUtils.exists(@customImagePath()) 30 | 31 | module.exports = Game 32 | -------------------------------------------------------------------------------- /app/src/models/gameConsole.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | 4 | fsUtils = require '../lib/fs-utils' 5 | path = require 'path' 6 | 7 | class GameConsole extends Spine.Model 8 | @configure "GameConsole", "name", "prefix", "extensions" 9 | 10 | constructor: -> 11 | super 12 | @games = [] 13 | @settings = new App.Settings 14 | @loadGames() 15 | @defaultArtPath = path.join(__dirname, '..', '..', 'images', 'default-art', 'game-consoles', @prefix) 16 | 17 | path: -> 18 | path.join(@settings.romsPath(), @prefix) 19 | 20 | romPaths: -> 21 | fsUtils.listSync(@path(), @extensions) 22 | 23 | imagePath: -> 24 | path.join(@path(), 'image.png') 25 | 26 | gameCardPath: -> 27 | path.join(@defaultArtPath, 'gameCard.png') 28 | 29 | imageExists: -> 30 | fsUtils.exists(@imagePath()) 31 | 32 | loadGames: -> 33 | gameConsole = @ 34 | @games = _.map @romPaths(), (path) -> 35 | new App.Game(filePath: path, gameConsole: gameConsole) 36 | 37 | module.exports = GameConsole 38 | -------------------------------------------------------------------------------- /app/src/models/recentlyPlayed.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | 4 | fsUtils = require '../lib/fs-utils' 5 | path = require 'path' 6 | fs = require 'fs' 7 | 8 | class RecentlyPlayed extends Spine.Model 9 | @configure "RecentlyPlayed" 10 | 11 | constructor: -> 12 | super 13 | @games = [] 14 | @settings = new App.Settings 15 | @load() 16 | 17 | filePath: -> 18 | path.join(@settings.romsPath(), 'recently-played.json') if @settings.romsPath() 19 | 20 | load: -> 21 | if @filePath() && fsUtils.exists(@filePath()) 22 | data = JSON.parse(fs.readFileSync(@filePath(), 'utf8')) 23 | @games = [] 24 | 25 | for gameBlob in data['games'] 26 | romPath = path.join(@settings.romsPath(), gameBlob['gameConsole'], gameBlob['filename']) 27 | if fsUtils.exists(romPath) 28 | gameConsole = new App.GameConsole(prefix: gameBlob['gameConsole']) 29 | @games.push(new App.Game(filePath: romPath, gameConsole: gameConsole)) 30 | 31 | _.filter @games, (game) -> 32 | game.imageExists() 33 | 34 | save: -> 35 | data = {'games': @games} 36 | fsUtils.writeSync(@filePath(), JSON.stringify(data)) 37 | 38 | addGame: (game) -> 39 | @games.unshift(game) 40 | @games = @games.unique() 41 | @games = @games[0..5] 42 | @save() 43 | 44 | 45 | module.exports = RecentlyPlayed 46 | -------------------------------------------------------------------------------- /app/src/models/retroArch.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | Spine._ = require 'underscore' 3 | 4 | path = require 'path' 5 | 6 | os = require 'os' 7 | shell = require 'shell' 8 | 9 | class RetroArch extends Spine.Model 10 | @configure "RetroArch" 11 | 12 | constructor: -> 13 | super 14 | @games = [] 15 | @settings = new App.Settings 16 | @recentlyPlayed = new App.RecentlyPlayed 17 | 18 | launchGame: (game) -> 19 | switch os.platform() 20 | when "darwin" 21 | if game.gameConsole.prefix == "mac" 22 | command = game.filePath 23 | else 24 | command = path.join(@settings.retroarchPath(), 'bin', 'retroarch') 25 | when "win32" 26 | if game.gameConsole.prefix == "pc" 27 | command = game.filePath 28 | else 29 | command = path.join(@settings.retroarchPath(), 'retroarch.exe') 30 | else 31 | alert("Sorry, this operating system isn't supported.") 32 | return 33 | 34 | if game.gameConsole.prefix != "pc" && game.gameConsole.prefix != "mac" 35 | configPath = path.join(__dirname, '..', '..', 'configs') 36 | options = ["--config", path.join(configPath, os.platform(), 'kart.cfg'), 37 | "--appendconfig", path.join(configPath, os.platform(), "#{game.gameConsole.prefix}.cfg"), 38 | path.normalize(game.filePath)] 39 | 40 | @recentlyPlayed.addGame(game) 41 | 42 | console.log(command) 43 | console.log(options) 44 | 45 | if options 46 | {spawn} = require 'child_process' 47 | ls = spawn command, options 48 | # receive all output and process 49 | ls.stdout.on 'data', (data) -> console.log data.toString().trim() 50 | # receive error messages and process 51 | ls.stderr.on 'data', (data) -> console.log data.toString().trim() 52 | else 53 | shell.openItem(game.filePath); 54 | 55 | 56 | module.exports = RetroArch 57 | -------------------------------------------------------------------------------- /app/src/models/settings.coffee: -------------------------------------------------------------------------------- 1 | class Settings extends Spine.Model 2 | 3 | constructor: -> 4 | super 5 | @aspects = ['16x9', '4x3'] 6 | 7 | clear: -> 8 | window.localStorage.clear() 9 | 10 | writeSetting: (key, value) -> 11 | window.localStorage.setItem(key, value) 12 | 13 | readSetting: (key) -> 14 | window.localStorage.getItem(key) || '' 15 | 16 | retroarchPath: -> 17 | @readSetting('retroarchPath') 18 | 19 | setRetroarchPath: (path) -> 20 | @writeSetting('retroarchPath', path) 21 | 22 | romsPath: -> 23 | @readSetting('romsPath') 24 | 25 | setRomsPath: (path) -> 26 | @writeSetting('romsPath', path) 27 | 28 | aspect: -> 29 | @readSetting('aspect') || '16x9' 30 | 31 | setAspect: (aspect) -> 32 | @writeSetting('aspect', aspect) 33 | 34 | retroMode: -> 35 | (@readSetting('retroMode') || false) == "true" 36 | 37 | setRetroMode: (retroMode) -> 38 | @writeSetting('retroMode', retroMode) 39 | 40 | module.exports = Settings 41 | -------------------------------------------------------------------------------- /app/src/views/main/_card.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

<%= @centerTitle %>

6 |
7 |

8 | <%= @title %> 9 | 10 |

11 |
12 |
13 | -------------------------------------------------------------------------------- /app/src/views/main/_controlInfo.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 | Back 4 |
5 | 6 |
7 | Select 8 |
9 | 10 |
11 | Choose 12 |
13 | 14 |
15 | Quit Game 16 |
17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /app/src/views/main/_gameCard.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |

<%= @centerTitle %>

6 |
7 |

8 | <%= @title %> 9 | toggle-favorite" title="Toggle Favorite"> 10 | 11 |

12 |
13 |
14 | -------------------------------------------------------------------------------- /app/src/views/main/cards.eco: -------------------------------------------------------------------------------- 1 |
2 | <% for i in [0...@numberOfPages()]: %> 3 |
4 |
5 | <% for j in @rangeForPage(i): %> 6 | <%- @cardFor(j) %> 7 | <% end %> 8 |
9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/src/views/main/collectionPicker.eco: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/src/views/main/home.eco: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | Platforms 7 |
8 | 9 |
10 | Collections 11 |
12 | 13 |
14 | Favorites 15 |
16 | 17 |
18 | 19 |
20 | <% for game in @recentlyPlayed.games[0..@numberOfGames()-1]: %> 21 | <%- @cardFor(game) %> 22 | <% end %> 23 | 24 | -------------------------------------------------------------------------------- /app/src/views/main/settings.eco: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Settings

5 | 6 |
7 |

Retroarch location

8 |

9 | 10 |
11 | 12 |
13 |

Roms location

14 |

15 | 16 |
17 | 18 |
19 |

Aspect Ratio

20 | 25 |
26 | 27 |
28 | > Retro Mode 29 |
30 | 31 |
32 | -------------------------------------------------------------------------------- /app/src/window/index.coffee: -------------------------------------------------------------------------------- 1 | window.$ = window.jQuery = require '../lib/jquery' 2 | 3 | require '../lib/jquery.scrollTo.min' 4 | require '../lib/jquery.visible.min' 5 | require '../lib/jquery.simplemodal.1.4.4.min' 6 | require './stylesheets' 7 | -------------------------------------------------------------------------------- /app/src/window/stylesheets.coffee: -------------------------------------------------------------------------------- 1 | fsUtils = require '../lib/fs-utils' 2 | path = require 'path' 3 | 4 | _ = require 'underscore' 5 | less = require 'less' 6 | LessCache = require 'less-cache' 7 | 8 | cache = new LessCache(cacheDir: path.join(__dirname, '..', 'lesscache')) 9 | 10 | # require '../lib/jquery-extensions' 11 | # require '../lib/underscore-extensions' 12 | 13 | window.stylesheetElementForId = (id) -> 14 | $("""head style[id="#{id}"]""") 15 | 16 | window.resolveStylesheet = (stylesheetPath) -> 17 | stylesheetPath = '../styles/' + stylesheetPath 18 | 19 | if path.extname(stylesheetPath).length > 0 20 | fsUtils.resolveOnLoadPath(stylesheetPath) 21 | else 22 | fsUtils.resolveOnLoadPath(stylesheetPath, ['css', 'less']) 23 | 24 | window.requireStylesheet = (stylesheetPath) -> 25 | if fullPath = window.resolveStylesheet(stylesheetPath) 26 | content = window.loadStylesheet(fullPath) 27 | window.applyStylesheet(fullPath, content) 28 | else 29 | throw new Error("Could not find a file at path '#{stylesheetPath}'") 30 | 31 | window.loadStylesheet = (stylesheetPath) -> 32 | cache.readFileSync stylesheetPath 33 | 34 | window.loadLessStylesheet = (lessStylesheetPath) -> 35 | parser = new less.Parser 36 | syncImport: true 37 | #paths: ['../styles/shared', '../styles/mac'] 38 | filename: lessStylesheetPath 39 | 40 | try 41 | content = null 42 | parser.parse fsUtils.read(lessStylesheetPath), (e, tree) -> 43 | throw e if e? 44 | content = tree.toCSS() 45 | content 46 | catch e 47 | console.error """ 48 | Error compiling less stylesheet: #{lessStylesheetPath} 49 | Line number: #{e.line} 50 | #{e.message} 51 | """ 52 | 53 | window.removeStylesheet = (stylesheetPath) -> 54 | stylesheetPath = '../styles/' + stylesheetPath 55 | unless fullPath = window.resolveStylesheet(stylesheetPath) 56 | throw new Error("Could not find a file at path '#{stylesheetPath}'") 57 | window.stylesheetElementForId(fullPath).remove() 58 | 59 | window.applyStylesheet = (id, text, ttype = 'bundled') -> 60 | unless window.stylesheetElementForId(id).length 61 | if $("head style.#{ttype}").length 62 | $("head style.#{ttype}:last").after "" 63 | else 64 | $("head").append "" 65 | -------------------------------------------------------------------------------- /app/styles/shared/4x3.less: -------------------------------------------------------------------------------- 1 | .fourbythree{ 2 | 3 | .page-container{ 4 | &:first-of-type{ 5 | margin-left: 6%; 6 | } 7 | } 8 | 9 | .card{ 10 | margin: 0 3% 3% 0; 11 | width: 30%; 12 | } 13 | 14 | .square{ 15 | padding: 10% 5% 10% 5%; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/styles/shared/cards.less: -------------------------------------------------------------------------------- 1 | body{ 2 | white-space: nowrap; 3 | } 4 | 5 | .page-container{ 6 | margin-top: 50px; 7 | 8 | display: inline-block; 9 | width: 88%; 10 | vertical-align:top; 11 | 12 | &:nth-of-type(2){ 13 | margin-left: 4%; 14 | } 15 | 16 | &:last-of-type{ 17 | margin-right: 4%; 18 | } 19 | 20 | } 21 | 22 | 23 | .card{ 24 | float: left; 25 | margin: 0 2% 2% 0; 26 | width: 23%; 27 | 28 | .overlay{ 29 | z-index: 1; 30 | position: relative; 31 | border: 8px solid transparent; 32 | transition: 0.05s; 33 | 34 | .marquee{ 35 | position: relative; 36 | opacity: 0; 37 | margin: 0; 38 | padding: 8px 0 0 0; 39 | background-color: white; 40 | font-size: 14px; 41 | color: #313131; 42 | transition: opacity 0.05s; 43 | overflow: hidden; 44 | 45 | .fade{ 46 | position: absolute; 47 | right: 0; 48 | top: 0; 49 | height: 100%; 50 | width: 60px; 51 | background: url(images/fade.png); 52 | } 53 | 54 | .title{ 55 | display: block; 56 | } 57 | 58 | .game-settings{ 59 | display: none; 60 | 61 | .fa:hover{ 62 | color: #717171; 63 | } 64 | 65 | } 66 | 67 | } 68 | 69 | .content{ 70 | img { 71 | display: block; 72 | border-radius: 3px; 73 | width: 100%; 74 | box-shadow: 0 8px 6px -6px black; 75 | } 76 | 77 | .center-title{ 78 | position: absolute; 79 | color: #ffffff; 80 | top: 0; 81 | bottom: 0; 82 | right: 10px; 83 | left: 10px; 84 | overflow: hidden; 85 | text-align: center; 86 | padding-top: 21%; 87 | } 88 | } 89 | 90 | 91 | } 92 | 93 | &.selected { 94 | 95 | .overlay{ 96 | z-index: 1000; 97 | border-color: #ffffff; 98 | background-color: #ffffff; 99 | -webkit-transform:scale(1.3); 100 | box-shadow: 0 8px 6px -6px black; 101 | 102 | .marquee{ 103 | opacity: 1; 104 | height: auto; 105 | display: block; 106 | } 107 | 108 | .content{ 109 | img{ 110 | border-radius: 1px; 111 | box-shadow: none; 112 | } 113 | } 114 | 115 | } 116 | } 117 | 118 | } 119 | 120 | .game-card .overlay .marquee:hover{ 121 | .title{ 122 | display: none; 123 | } 124 | .game-settings{ 125 | display: block; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /app/styles/shared/control-info.less: -------------------------------------------------------------------------------- 1 | .control-info{ 2 | position:fixed; 3 | z-index: 5; 4 | height: 30px; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | 9 | background-color: #adadad; 10 | color: #363636; 11 | font-size: 26px; 12 | 13 | padding: 15px 20px; 14 | 15 | .control-item{ 16 | float: left; 17 | margin-right: 30px; 18 | } 19 | 20 | img{ 21 | height: 30px; 22 | margin-right: 5px; 23 | 24 | &.start-select{ 25 | max-height: 20px; 26 | } 27 | } 28 | 29 | .settings-button{ 30 | display: none; 31 | float: right; 32 | 33 | &:hover{ 34 | color: #626262; 35 | } 36 | } 37 | 38 | &:hover{ 39 | .settings-button{ 40 | display: block; 41 | } 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/styles/shared/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/styles/shared/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/styles/shared/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/styles/shared/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .pull-right { float: right; } 11 | .pull-left { float: left; } 12 | 13 | .@{fa-css-prefix} { 14 | &.pull-left { margin-right: .3em; } 15 | &.pull-right { margin-left: .3em; } 16 | } 17 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font-family: FontAwesome; 7 | font-style: normal; 8 | font-weight: normal; 9 | line-height: 1; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "spinning.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: -@fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon-rotate(@degrees, @rotation) { 5 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); 6 | -webkit-transform: rotate(@degrees); 7 | -moz-transform: rotate(@degrees); 8 | -ms-transform: rotate(@degrees); 9 | -o-transform: rotate(@degrees); 10 | transform: rotate(@degrees); 11 | } 12 | 13 | .fa-icon-flip(@horiz, @vert, @rotation) { 14 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); 15 | -webkit-transform: scale(@horiz, @vert); 16 | -moz-transform: scale(@horiz, @vert); 17 | -ms-transform: scale(@horiz, @vert); 18 | -o-transform: scale(@horiz, @vert); 19 | transform: scale(@horiz, @vert); 20 | } 21 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: ~"url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}')"; 7 | src: ~"url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype')", 8 | ~"url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff')", 9 | ~"url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype')", 10 | ~"url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg')"; 11 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/spinning.less: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: spin 2s infinite linear; 6 | -moz-animation: spin 2s infinite linear; 7 | -o-animation: spin 2s infinite linear; 8 | animation: spin 2s infinite linear; 9 | } 10 | 11 | @-moz-keyframes spin { 12 | 0% { -moz-transform: rotate(0deg); } 13 | 100% { -moz-transform: rotate(359deg); } 14 | } 15 | @-webkit-keyframes spin { 16 | 0% { -webkit-transform: rotate(0deg); } 17 | 100% { -webkit-transform: rotate(359deg); } 18 | } 19 | @-o-keyframes spin { 20 | 0% { -o-transform: rotate(0deg); } 21 | 100% { -o-transform: rotate(359deg); } 22 | } 23 | @keyframes spin { 24 | 0% { 25 | -webkit-transform: rotate(0deg); 26 | transform: rotate(0deg); 27 | } 28 | 100% { 29 | -webkit-transform: rotate(359deg); 30 | transform: rotate(359deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /app/styles/shared/font-awesome/less/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | // -------------------------- 3 | 4 | @fa-font-path: "styles/shared/font-awesome/fonts"; 5 | //@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.1.0/fonts"; // for referencing Bootstrap CDN font files directly 6 | @fa-css-prefix: fa; 7 | @fa-version: "4.1.0"; 8 | @fa-border-color: #eee; 9 | @fa-inverse: #fff; 10 | @fa-li-width: (30em / 14); 11 | 12 | @fa-var-adjust: "\f042"; 13 | @fa-var-adn: "\f170"; 14 | @fa-var-align-center: "\f037"; 15 | @fa-var-align-justify: "\f039"; 16 | @fa-var-align-left: "\f036"; 17 | @fa-var-align-right: "\f038"; 18 | @fa-var-ambulance: "\f0f9"; 19 | @fa-var-anchor: "\f13d"; 20 | @fa-var-android: "\f17b"; 21 | @fa-var-angle-double-down: "\f103"; 22 | @fa-var-angle-double-left: "\f100"; 23 | @fa-var-angle-double-right: "\f101"; 24 | @fa-var-angle-double-up: "\f102"; 25 | @fa-var-angle-down: "\f107"; 26 | @fa-var-angle-left: "\f104"; 27 | @fa-var-angle-right: "\f105"; 28 | @fa-var-angle-up: "\f106"; 29 | @fa-var-apple: "\f179"; 30 | @fa-var-archive: "\f187"; 31 | @fa-var-arrow-circle-down: "\f0ab"; 32 | @fa-var-arrow-circle-left: "\f0a8"; 33 | @fa-var-arrow-circle-o-down: "\f01a"; 34 | @fa-var-arrow-circle-o-left: "\f190"; 35 | @fa-var-arrow-circle-o-right: "\f18e"; 36 | @fa-var-arrow-circle-o-up: "\f01b"; 37 | @fa-var-arrow-circle-right: "\f0a9"; 38 | @fa-var-arrow-circle-up: "\f0aa"; 39 | @fa-var-arrow-down: "\f063"; 40 | @fa-var-arrow-left: "\f060"; 41 | @fa-var-arrow-right: "\f061"; 42 | @fa-var-arrow-up: "\f062"; 43 | @fa-var-arrows: "\f047"; 44 | @fa-var-arrows-alt: "\f0b2"; 45 | @fa-var-arrows-h: "\f07e"; 46 | @fa-var-arrows-v: "\f07d"; 47 | @fa-var-asterisk: "\f069"; 48 | @fa-var-automobile: "\f1b9"; 49 | @fa-var-backward: "\f04a"; 50 | @fa-var-ban: "\f05e"; 51 | @fa-var-bank: "\f19c"; 52 | @fa-var-bar-chart-o: "\f080"; 53 | @fa-var-barcode: "\f02a"; 54 | @fa-var-bars: "\f0c9"; 55 | @fa-var-beer: "\f0fc"; 56 | @fa-var-behance: "\f1b4"; 57 | @fa-var-behance-square: "\f1b5"; 58 | @fa-var-bell: "\f0f3"; 59 | @fa-var-bell-o: "\f0a2"; 60 | @fa-var-bitbucket: "\f171"; 61 | @fa-var-bitbucket-square: "\f172"; 62 | @fa-var-bitcoin: "\f15a"; 63 | @fa-var-bold: "\f032"; 64 | @fa-var-bolt: "\f0e7"; 65 | @fa-var-bomb: "\f1e2"; 66 | @fa-var-book: "\f02d"; 67 | @fa-var-bookmark: "\f02e"; 68 | @fa-var-bookmark-o: "\f097"; 69 | @fa-var-briefcase: "\f0b1"; 70 | @fa-var-btc: "\f15a"; 71 | @fa-var-bug: "\f188"; 72 | @fa-var-building: "\f1ad"; 73 | @fa-var-building-o: "\f0f7"; 74 | @fa-var-bullhorn: "\f0a1"; 75 | @fa-var-bullseye: "\f140"; 76 | @fa-var-cab: "\f1ba"; 77 | @fa-var-calendar: "\f073"; 78 | @fa-var-calendar-o: "\f133"; 79 | @fa-var-camera: "\f030"; 80 | @fa-var-camera-retro: "\f083"; 81 | @fa-var-car: "\f1b9"; 82 | @fa-var-caret-down: "\f0d7"; 83 | @fa-var-caret-left: "\f0d9"; 84 | @fa-var-caret-right: "\f0da"; 85 | @fa-var-caret-square-o-down: "\f150"; 86 | @fa-var-caret-square-o-left: "\f191"; 87 | @fa-var-caret-square-o-right: "\f152"; 88 | @fa-var-caret-square-o-up: "\f151"; 89 | @fa-var-caret-up: "\f0d8"; 90 | @fa-var-certificate: "\f0a3"; 91 | @fa-var-chain: "\f0c1"; 92 | @fa-var-chain-broken: "\f127"; 93 | @fa-var-check: "\f00c"; 94 | @fa-var-check-circle: "\f058"; 95 | @fa-var-check-circle-o: "\f05d"; 96 | @fa-var-check-square: "\f14a"; 97 | @fa-var-check-square-o: "\f046"; 98 | @fa-var-chevron-circle-down: "\f13a"; 99 | @fa-var-chevron-circle-left: "\f137"; 100 | @fa-var-chevron-circle-right: "\f138"; 101 | @fa-var-chevron-circle-up: "\f139"; 102 | @fa-var-chevron-down: "\f078"; 103 | @fa-var-chevron-left: "\f053"; 104 | @fa-var-chevron-right: "\f054"; 105 | @fa-var-chevron-up: "\f077"; 106 | @fa-var-child: "\f1ae"; 107 | @fa-var-circle: "\f111"; 108 | @fa-var-circle-o: "\f10c"; 109 | @fa-var-circle-o-notch: "\f1ce"; 110 | @fa-var-circle-thin: "\f1db"; 111 | @fa-var-clipboard: "\f0ea"; 112 | @fa-var-clock-o: "\f017"; 113 | @fa-var-cloud: "\f0c2"; 114 | @fa-var-cloud-download: "\f0ed"; 115 | @fa-var-cloud-upload: "\f0ee"; 116 | @fa-var-cny: "\f157"; 117 | @fa-var-code: "\f121"; 118 | @fa-var-code-fork: "\f126"; 119 | @fa-var-codepen: "\f1cb"; 120 | @fa-var-coffee: "\f0f4"; 121 | @fa-var-cog: "\f013"; 122 | @fa-var-cogs: "\f085"; 123 | @fa-var-columns: "\f0db"; 124 | @fa-var-comment: "\f075"; 125 | @fa-var-comment-o: "\f0e5"; 126 | @fa-var-comments: "\f086"; 127 | @fa-var-comments-o: "\f0e6"; 128 | @fa-var-compass: "\f14e"; 129 | @fa-var-compress: "\f066"; 130 | @fa-var-copy: "\f0c5"; 131 | @fa-var-credit-card: "\f09d"; 132 | @fa-var-crop: "\f125"; 133 | @fa-var-crosshairs: "\f05b"; 134 | @fa-var-css3: "\f13c"; 135 | @fa-var-cube: "\f1b2"; 136 | @fa-var-cubes: "\f1b3"; 137 | @fa-var-cut: "\f0c4"; 138 | @fa-var-cutlery: "\f0f5"; 139 | @fa-var-dashboard: "\f0e4"; 140 | @fa-var-database: "\f1c0"; 141 | @fa-var-dedent: "\f03b"; 142 | @fa-var-delicious: "\f1a5"; 143 | @fa-var-desktop: "\f108"; 144 | @fa-var-deviantart: "\f1bd"; 145 | @fa-var-digg: "\f1a6"; 146 | @fa-var-dollar: "\f155"; 147 | @fa-var-dot-circle-o: "\f192"; 148 | @fa-var-download: "\f019"; 149 | @fa-var-dribbble: "\f17d"; 150 | @fa-var-dropbox: "\f16b"; 151 | @fa-var-drupal: "\f1a9"; 152 | @fa-var-edit: "\f044"; 153 | @fa-var-eject: "\f052"; 154 | @fa-var-ellipsis-h: "\f141"; 155 | @fa-var-ellipsis-v: "\f142"; 156 | @fa-var-empire: "\f1d1"; 157 | @fa-var-envelope: "\f0e0"; 158 | @fa-var-envelope-o: "\f003"; 159 | @fa-var-envelope-square: "\f199"; 160 | @fa-var-eraser: "\f12d"; 161 | @fa-var-eur: "\f153"; 162 | @fa-var-euro: "\f153"; 163 | @fa-var-exchange: "\f0ec"; 164 | @fa-var-exclamation: "\f12a"; 165 | @fa-var-exclamation-circle: "\f06a"; 166 | @fa-var-exclamation-triangle: "\f071"; 167 | @fa-var-expand: "\f065"; 168 | @fa-var-external-link: "\f08e"; 169 | @fa-var-external-link-square: "\f14c"; 170 | @fa-var-eye: "\f06e"; 171 | @fa-var-eye-slash: "\f070"; 172 | @fa-var-facebook: "\f09a"; 173 | @fa-var-facebook-square: "\f082"; 174 | @fa-var-fast-backward: "\f049"; 175 | @fa-var-fast-forward: "\f050"; 176 | @fa-var-fax: "\f1ac"; 177 | @fa-var-female: "\f182"; 178 | @fa-var-fighter-jet: "\f0fb"; 179 | @fa-var-file: "\f15b"; 180 | @fa-var-file-archive-o: "\f1c6"; 181 | @fa-var-file-audio-o: "\f1c7"; 182 | @fa-var-file-code-o: "\f1c9"; 183 | @fa-var-file-excel-o: "\f1c3"; 184 | @fa-var-file-image-o: "\f1c5"; 185 | @fa-var-file-movie-o: "\f1c8"; 186 | @fa-var-file-o: "\f016"; 187 | @fa-var-file-pdf-o: "\f1c1"; 188 | @fa-var-file-photo-o: "\f1c5"; 189 | @fa-var-file-picture-o: "\f1c5"; 190 | @fa-var-file-powerpoint-o: "\f1c4"; 191 | @fa-var-file-sound-o: "\f1c7"; 192 | @fa-var-file-text: "\f15c"; 193 | @fa-var-file-text-o: "\f0f6"; 194 | @fa-var-file-video-o: "\f1c8"; 195 | @fa-var-file-word-o: "\f1c2"; 196 | @fa-var-file-zip-o: "\f1c6"; 197 | @fa-var-files-o: "\f0c5"; 198 | @fa-var-film: "\f008"; 199 | @fa-var-filter: "\f0b0"; 200 | @fa-var-fire: "\f06d"; 201 | @fa-var-fire-extinguisher: "\f134"; 202 | @fa-var-flag: "\f024"; 203 | @fa-var-flag-checkered: "\f11e"; 204 | @fa-var-flag-o: "\f11d"; 205 | @fa-var-flash: "\f0e7"; 206 | @fa-var-flask: "\f0c3"; 207 | @fa-var-flickr: "\f16e"; 208 | @fa-var-floppy-o: "\f0c7"; 209 | @fa-var-folder: "\f07b"; 210 | @fa-var-folder-o: "\f114"; 211 | @fa-var-folder-open: "\f07c"; 212 | @fa-var-folder-open-o: "\f115"; 213 | @fa-var-font: "\f031"; 214 | @fa-var-forward: "\f04e"; 215 | @fa-var-foursquare: "\f180"; 216 | @fa-var-frown-o: "\f119"; 217 | @fa-var-gamepad: "\f11b"; 218 | @fa-var-gavel: "\f0e3"; 219 | @fa-var-gbp: "\f154"; 220 | @fa-var-ge: "\f1d1"; 221 | @fa-var-gear: "\f013"; 222 | @fa-var-gears: "\f085"; 223 | @fa-var-gift: "\f06b"; 224 | @fa-var-git: "\f1d3"; 225 | @fa-var-git-square: "\f1d2"; 226 | @fa-var-github: "\f09b"; 227 | @fa-var-github-alt: "\f113"; 228 | @fa-var-github-square: "\f092"; 229 | @fa-var-gittip: "\f184"; 230 | @fa-var-glass: "\f000"; 231 | @fa-var-globe: "\f0ac"; 232 | @fa-var-google: "\f1a0"; 233 | @fa-var-google-plus: "\f0d5"; 234 | @fa-var-google-plus-square: "\f0d4"; 235 | @fa-var-graduation-cap: "\f19d"; 236 | @fa-var-group: "\f0c0"; 237 | @fa-var-h-square: "\f0fd"; 238 | @fa-var-hacker-news: "\f1d4"; 239 | @fa-var-hand-o-down: "\f0a7"; 240 | @fa-var-hand-o-left: "\f0a5"; 241 | @fa-var-hand-o-right: "\f0a4"; 242 | @fa-var-hand-o-up: "\f0a6"; 243 | @fa-var-hdd-o: "\f0a0"; 244 | @fa-var-header: "\f1dc"; 245 | @fa-var-headphones: "\f025"; 246 | @fa-var-heart: "\f004"; 247 | @fa-var-heart-o: "\f08a"; 248 | @fa-var-history: "\f1da"; 249 | @fa-var-home: "\f015"; 250 | @fa-var-hospital-o: "\f0f8"; 251 | @fa-var-html5: "\f13b"; 252 | @fa-var-image: "\f03e"; 253 | @fa-var-inbox: "\f01c"; 254 | @fa-var-indent: "\f03c"; 255 | @fa-var-info: "\f129"; 256 | @fa-var-info-circle: "\f05a"; 257 | @fa-var-inr: "\f156"; 258 | @fa-var-instagram: "\f16d"; 259 | @fa-var-institution: "\f19c"; 260 | @fa-var-italic: "\f033"; 261 | @fa-var-joomla: "\f1aa"; 262 | @fa-var-jpy: "\f157"; 263 | @fa-var-jsfiddle: "\f1cc"; 264 | @fa-var-key: "\f084"; 265 | @fa-var-keyboard-o: "\f11c"; 266 | @fa-var-krw: "\f159"; 267 | @fa-var-language: "\f1ab"; 268 | @fa-var-laptop: "\f109"; 269 | @fa-var-leaf: "\f06c"; 270 | @fa-var-legal: "\f0e3"; 271 | @fa-var-lemon-o: "\f094"; 272 | @fa-var-level-down: "\f149"; 273 | @fa-var-level-up: "\f148"; 274 | @fa-var-life-bouy: "\f1cd"; 275 | @fa-var-life-ring: "\f1cd"; 276 | @fa-var-life-saver: "\f1cd"; 277 | @fa-var-lightbulb-o: "\f0eb"; 278 | @fa-var-link: "\f0c1"; 279 | @fa-var-linkedin: "\f0e1"; 280 | @fa-var-linkedin-square: "\f08c"; 281 | @fa-var-linux: "\f17c"; 282 | @fa-var-list: "\f03a"; 283 | @fa-var-list-alt: "\f022"; 284 | @fa-var-list-ol: "\f0cb"; 285 | @fa-var-list-ul: "\f0ca"; 286 | @fa-var-location-arrow: "\f124"; 287 | @fa-var-lock: "\f023"; 288 | @fa-var-long-arrow-down: "\f175"; 289 | @fa-var-long-arrow-left: "\f177"; 290 | @fa-var-long-arrow-right: "\f178"; 291 | @fa-var-long-arrow-up: "\f176"; 292 | @fa-var-magic: "\f0d0"; 293 | @fa-var-magnet: "\f076"; 294 | @fa-var-mail-forward: "\f064"; 295 | @fa-var-mail-reply: "\f112"; 296 | @fa-var-mail-reply-all: "\f122"; 297 | @fa-var-male: "\f183"; 298 | @fa-var-map-marker: "\f041"; 299 | @fa-var-maxcdn: "\f136"; 300 | @fa-var-medkit: "\f0fa"; 301 | @fa-var-meh-o: "\f11a"; 302 | @fa-var-microphone: "\f130"; 303 | @fa-var-microphone-slash: "\f131"; 304 | @fa-var-minus: "\f068"; 305 | @fa-var-minus-circle: "\f056"; 306 | @fa-var-minus-square: "\f146"; 307 | @fa-var-minus-square-o: "\f147"; 308 | @fa-var-mobile: "\f10b"; 309 | @fa-var-mobile-phone: "\f10b"; 310 | @fa-var-money: "\f0d6"; 311 | @fa-var-moon-o: "\f186"; 312 | @fa-var-mortar-board: "\f19d"; 313 | @fa-var-music: "\f001"; 314 | @fa-var-navicon: "\f0c9"; 315 | @fa-var-openid: "\f19b"; 316 | @fa-var-outdent: "\f03b"; 317 | @fa-var-pagelines: "\f18c"; 318 | @fa-var-paper-plane: "\f1d8"; 319 | @fa-var-paper-plane-o: "\f1d9"; 320 | @fa-var-paperclip: "\f0c6"; 321 | @fa-var-paragraph: "\f1dd"; 322 | @fa-var-paste: "\f0ea"; 323 | @fa-var-pause: "\f04c"; 324 | @fa-var-paw: "\f1b0"; 325 | @fa-var-pencil: "\f040"; 326 | @fa-var-pencil-square: "\f14b"; 327 | @fa-var-pencil-square-o: "\f044"; 328 | @fa-var-phone: "\f095"; 329 | @fa-var-phone-square: "\f098"; 330 | @fa-var-photo: "\f03e"; 331 | @fa-var-picture-o: "\f03e"; 332 | @fa-var-pied-piper: "\f1a7"; 333 | @fa-var-pied-piper-alt: "\f1a8"; 334 | @fa-var-pied-piper-square: "\f1a7"; 335 | @fa-var-pinterest: "\f0d2"; 336 | @fa-var-pinterest-square: "\f0d3"; 337 | @fa-var-plane: "\f072"; 338 | @fa-var-play: "\f04b"; 339 | @fa-var-play-circle: "\f144"; 340 | @fa-var-play-circle-o: "\f01d"; 341 | @fa-var-plus: "\f067"; 342 | @fa-var-plus-circle: "\f055"; 343 | @fa-var-plus-square: "\f0fe"; 344 | @fa-var-plus-square-o: "\f196"; 345 | @fa-var-power-off: "\f011"; 346 | @fa-var-print: "\f02f"; 347 | @fa-var-puzzle-piece: "\f12e"; 348 | @fa-var-qq: "\f1d6"; 349 | @fa-var-qrcode: "\f029"; 350 | @fa-var-question: "\f128"; 351 | @fa-var-question-circle: "\f059"; 352 | @fa-var-quote-left: "\f10d"; 353 | @fa-var-quote-right: "\f10e"; 354 | @fa-var-ra: "\f1d0"; 355 | @fa-var-random: "\f074"; 356 | @fa-var-rebel: "\f1d0"; 357 | @fa-var-recycle: "\f1b8"; 358 | @fa-var-reddit: "\f1a1"; 359 | @fa-var-reddit-square: "\f1a2"; 360 | @fa-var-refresh: "\f021"; 361 | @fa-var-renren: "\f18b"; 362 | @fa-var-reorder: "\f0c9"; 363 | @fa-var-repeat: "\f01e"; 364 | @fa-var-reply: "\f112"; 365 | @fa-var-reply-all: "\f122"; 366 | @fa-var-retweet: "\f079"; 367 | @fa-var-rmb: "\f157"; 368 | @fa-var-road: "\f018"; 369 | @fa-var-rocket: "\f135"; 370 | @fa-var-rotate-left: "\f0e2"; 371 | @fa-var-rotate-right: "\f01e"; 372 | @fa-var-rouble: "\f158"; 373 | @fa-var-rss: "\f09e"; 374 | @fa-var-rss-square: "\f143"; 375 | @fa-var-rub: "\f158"; 376 | @fa-var-ruble: "\f158"; 377 | @fa-var-rupee: "\f156"; 378 | @fa-var-save: "\f0c7"; 379 | @fa-var-scissors: "\f0c4"; 380 | @fa-var-search: "\f002"; 381 | @fa-var-search-minus: "\f010"; 382 | @fa-var-search-plus: "\f00e"; 383 | @fa-var-send: "\f1d8"; 384 | @fa-var-send-o: "\f1d9"; 385 | @fa-var-share: "\f064"; 386 | @fa-var-share-alt: "\f1e0"; 387 | @fa-var-share-alt-square: "\f1e1"; 388 | @fa-var-share-square: "\f14d"; 389 | @fa-var-share-square-o: "\f045"; 390 | @fa-var-shield: "\f132"; 391 | @fa-var-shopping-cart: "\f07a"; 392 | @fa-var-sign-in: "\f090"; 393 | @fa-var-sign-out: "\f08b"; 394 | @fa-var-signal: "\f012"; 395 | @fa-var-sitemap: "\f0e8"; 396 | @fa-var-skype: "\f17e"; 397 | @fa-var-slack: "\f198"; 398 | @fa-var-sliders: "\f1de"; 399 | @fa-var-smile-o: "\f118"; 400 | @fa-var-sort: "\f0dc"; 401 | @fa-var-sort-alpha-asc: "\f15d"; 402 | @fa-var-sort-alpha-desc: "\f15e"; 403 | @fa-var-sort-amount-asc: "\f160"; 404 | @fa-var-sort-amount-desc: "\f161"; 405 | @fa-var-sort-asc: "\f0de"; 406 | @fa-var-sort-desc: "\f0dd"; 407 | @fa-var-sort-down: "\f0dd"; 408 | @fa-var-sort-numeric-asc: "\f162"; 409 | @fa-var-sort-numeric-desc: "\f163"; 410 | @fa-var-sort-up: "\f0de"; 411 | @fa-var-soundcloud: "\f1be"; 412 | @fa-var-space-shuttle: "\f197"; 413 | @fa-var-spinner: "\f110"; 414 | @fa-var-spoon: "\f1b1"; 415 | @fa-var-spotify: "\f1bc"; 416 | @fa-var-square: "\f0c8"; 417 | @fa-var-square-o: "\f096"; 418 | @fa-var-stack-exchange: "\f18d"; 419 | @fa-var-stack-overflow: "\f16c"; 420 | @fa-var-star: "\f005"; 421 | @fa-var-star-half: "\f089"; 422 | @fa-var-star-half-empty: "\f123"; 423 | @fa-var-star-half-full: "\f123"; 424 | @fa-var-star-half-o: "\f123"; 425 | @fa-var-star-o: "\f006"; 426 | @fa-var-steam: "\f1b6"; 427 | @fa-var-steam-square: "\f1b7"; 428 | @fa-var-step-backward: "\f048"; 429 | @fa-var-step-forward: "\f051"; 430 | @fa-var-stethoscope: "\f0f1"; 431 | @fa-var-stop: "\f04d"; 432 | @fa-var-strikethrough: "\f0cc"; 433 | @fa-var-stumbleupon: "\f1a4"; 434 | @fa-var-stumbleupon-circle: "\f1a3"; 435 | @fa-var-subscript: "\f12c"; 436 | @fa-var-suitcase: "\f0f2"; 437 | @fa-var-sun-o: "\f185"; 438 | @fa-var-superscript: "\f12b"; 439 | @fa-var-support: "\f1cd"; 440 | @fa-var-table: "\f0ce"; 441 | @fa-var-tablet: "\f10a"; 442 | @fa-var-tachometer: "\f0e4"; 443 | @fa-var-tag: "\f02b"; 444 | @fa-var-tags: "\f02c"; 445 | @fa-var-tasks: "\f0ae"; 446 | @fa-var-taxi: "\f1ba"; 447 | @fa-var-tencent-weibo: "\f1d5"; 448 | @fa-var-terminal: "\f120"; 449 | @fa-var-text-height: "\f034"; 450 | @fa-var-text-width: "\f035"; 451 | @fa-var-th: "\f00a"; 452 | @fa-var-th-large: "\f009"; 453 | @fa-var-th-list: "\f00b"; 454 | @fa-var-thumb-tack: "\f08d"; 455 | @fa-var-thumbs-down: "\f165"; 456 | @fa-var-thumbs-o-down: "\f088"; 457 | @fa-var-thumbs-o-up: "\f087"; 458 | @fa-var-thumbs-up: "\f164"; 459 | @fa-var-ticket: "\f145"; 460 | @fa-var-times: "\f00d"; 461 | @fa-var-times-circle: "\f057"; 462 | @fa-var-times-circle-o: "\f05c"; 463 | @fa-var-tint: "\f043"; 464 | @fa-var-toggle-down: "\f150"; 465 | @fa-var-toggle-left: "\f191"; 466 | @fa-var-toggle-right: "\f152"; 467 | @fa-var-toggle-up: "\f151"; 468 | @fa-var-trash-o: "\f014"; 469 | @fa-var-tree: "\f1bb"; 470 | @fa-var-trello: "\f181"; 471 | @fa-var-trophy: "\f091"; 472 | @fa-var-truck: "\f0d1"; 473 | @fa-var-try: "\f195"; 474 | @fa-var-tumblr: "\f173"; 475 | @fa-var-tumblr-square: "\f174"; 476 | @fa-var-turkish-lira: "\f195"; 477 | @fa-var-twitter: "\f099"; 478 | @fa-var-twitter-square: "\f081"; 479 | @fa-var-umbrella: "\f0e9"; 480 | @fa-var-underline: "\f0cd"; 481 | @fa-var-undo: "\f0e2"; 482 | @fa-var-university: "\f19c"; 483 | @fa-var-unlink: "\f127"; 484 | @fa-var-unlock: "\f09c"; 485 | @fa-var-unlock-alt: "\f13e"; 486 | @fa-var-unsorted: "\f0dc"; 487 | @fa-var-upload: "\f093"; 488 | @fa-var-usd: "\f155"; 489 | @fa-var-user: "\f007"; 490 | @fa-var-user-md: "\f0f0"; 491 | @fa-var-users: "\f0c0"; 492 | @fa-var-video-camera: "\f03d"; 493 | @fa-var-vimeo-square: "\f194"; 494 | @fa-var-vine: "\f1ca"; 495 | @fa-var-vk: "\f189"; 496 | @fa-var-volume-down: "\f027"; 497 | @fa-var-volume-off: "\f026"; 498 | @fa-var-volume-up: "\f028"; 499 | @fa-var-warning: "\f071"; 500 | @fa-var-wechat: "\f1d7"; 501 | @fa-var-weibo: "\f18a"; 502 | @fa-var-weixin: "\f1d7"; 503 | @fa-var-wheelchair: "\f193"; 504 | @fa-var-windows: "\f17a"; 505 | @fa-var-won: "\f159"; 506 | @fa-var-wordpress: "\f19a"; 507 | @fa-var-wrench: "\f0ad"; 508 | @fa-var-xing: "\f168"; 509 | @fa-var-xing-square: "\f169"; 510 | @fa-var-yahoo: "\f19e"; 511 | @fa-var-yen: "\f157"; 512 | @fa-var-youtube: "\f167"; 513 | @fa-var-youtube-play: "\f16a"; 514 | @fa-var-youtube-square: "\f166"; 515 | -------------------------------------------------------------------------------- /app/styles/shared/home.less: -------------------------------------------------------------------------------- 1 | .browse{ 2 | height: 33%; 3 | 4 | margin: 5% 0; 5 | } 6 | 7 | .square{ 8 | float: left; 9 | width: 18%; 10 | margin: 0 0 0 4%; 11 | padding: 5%; 12 | 13 | font-size: 24px; 14 | text-align: center; 15 | color: #e6e6e6; 16 | 17 | border-radius: 3px; 18 | box-shadow: 0 8px 6px -6px black; 19 | 20 | background-color: #393939; 21 | border: 2px #393939 solid; 22 | 23 | 24 | &.selected{ 25 | border-color: #ffffff; 26 | } 27 | } 28 | 29 | .recently-played{ 30 | 31 | padding-left: 6%; 32 | 33 | .card{ 34 | width: 22%; 35 | margin: 0 2% 0 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/styles/shared/index.less: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | @import 'reset'; 4 | @import 'main'; 5 | @import 'cards'; 6 | @import 'settings'; 7 | @import 'modal'; 8 | @import 'home'; 9 | @import '4x3'; 10 | @import 'control-info'; 11 | @import 'retro'; 12 | @import 'responsive'; 13 | @import 'font-awesome/less/font-awesome'; 14 | 15 | 16 | body { 17 | padding:0; 18 | margin:0; 19 | overflow:hidden; 20 | position:absolute; 21 | top:0; 22 | left:0; 23 | right:0; 24 | bottom:0; 25 | font-family: "Helvetica", arial, sans-serif; 26 | font-weight: 300; 27 | } 28 | 29 | strong { 30 | font-weight:bold; 31 | color:#222; 32 | } 33 | 34 | .no-animation { 35 | -webkit-transition:none !important; 36 | } 37 | 38 | .clearfix:after { 39 | content:""; 40 | display:table; 41 | clear:both; 42 | } 43 | 44 | .stack > div { 45 | display:none; 46 | 47 | &.active { 48 | display:block; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/styles/shared/main.less: -------------------------------------------------------------------------------- 1 | 2 | h1{ 3 | font-size: 36px; 4 | margin-bottom: 20px; 5 | } 6 | 7 | .header{ 8 | height: 30px; 9 | padding: 10px; 10 | } 11 | -------------------------------------------------------------------------------- /app/styles/shared/modal.less: -------------------------------------------------------------------------------- 1 | .simplemodal-overlay{ 2 | background-color: black; 3 | } 4 | 5 | .simplemodal-container{ 6 | border: 10px solid #ffffff; 7 | background-color: #4a4a4a; 8 | 9 | } 10 | 11 | .simplemodal-wrap{ 12 | padding: 10px; 13 | 14 | h3{ 15 | color: #ffffff; 16 | font-size: 24px; 17 | margin-bottom: 10px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/styles/shared/press-start/license.txt: -------------------------------------------------------------------------------- 1 | Thanks for downloading one of codeman38's retro video game fonts, as seen on Memepool, BoingBoing, and all around the blogosphere. 2 | 3 | So, you're wondering what the license is for these fonts? Pretty simple; it's based upon that used for Bitstream's Vera font set . 4 | 5 | Basically, here are the key points summarized, in as little legalese as possible; I hate reading license agreements as much as you probably do: 6 | 7 | With one specific exception, you have full permission to bundle these fonts in your own free or commercial projects-- and by projects, I'm referring to not just software but also electronic documents and print publications. 8 | 9 | So what's the exception? Simple: you can't re-sell these fonts in a commercial font collection. I've seen too many font CDs for sale in stores that are just a repackaging of thousands of freeware fonts found on the internet, and in my mind, that's quite a bit like highway robbery. Note that this *only* applies to products that are font collections in and of themselves; you may freely bundle these fonts with an operating system, application program, or the like. 10 | 11 | Feel free to modify these fonts and even to release the modified versions, as long as you change the original font names (to ensure consistency among people with the font installed) and as long as you give credit somewhere in the font file to codeman38 or zone38.net. I may even incorporate these changes into a later version of my fonts if you wish to send me the modifed fonts via e-mail. 12 | 13 | Also, feel free to mirror these fonts on your own site, as long as you make it reasonably clear that these fonts are not your own work. I'm not asking for much; linking to zone38.net or even just mentioning the nickname codeman38 should be enough. 14 | 15 | Well, that pretty much sums it up... so without further ado, install and enjoy these fonts from the golden age of video games. 16 | 17 | [ codeman38 | cody@zone38.net | http://www.zone38.net/ ] -------------------------------------------------------------------------------- /app/styles/shared/press-start/prstart.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/styles/shared/press-start/prstart.ttf -------------------------------------------------------------------------------- /app/styles/shared/press-start/prstartk.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/app/styles/shared/press-start/prstartk.ttf -------------------------------------------------------------------------------- /app/styles/shared/reset.less: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /app/styles/shared/responsive.less: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 300px) { 2 | .square { font-size:0.6em; } 3 | .card .overlay .marquee{ font-size: 0.4em; } 4 | 5 | .retro{ 6 | .square { font-size:0.8em; } 7 | .card .overlay .marquee{ font-size: 0.4em; } 8 | } 9 | 10 | .center-title{ 11 | font-size: 8px; 12 | padding-top: 21%; 13 | } 14 | 15 | .control-info{ 16 | height: 15px; 17 | font-size: 22px; 18 | padding: 10px 15px; 19 | 20 | .control-item{ 21 | margin-right: 20px; 22 | } 23 | 24 | img{ 25 | height: 15px; 26 | 27 | &.start-select{ 28 | max-height: 10px; 29 | } 30 | } 31 | } 32 | 33 | } 34 | @media all and (min-width: 600px) { 35 | .square { font-size:1.2em; } 36 | .card .overlay .marquee{ font-size: 0.8em; } 37 | 38 | .retro{ 39 | .square { font-size:1em; } 40 | .card .overlay .marquee{ font-size: 0.6em; } 41 | } 42 | 43 | .center-title{ 44 | font-size: 12px; 45 | padding-top: 21%; 46 | } 47 | 48 | } 49 | @media all and (min-width: 900px) { 50 | .square { font-size:1.8em; } 51 | .card .overlay .marquee{ font-size: 1em; } 52 | 53 | .retro{ 54 | .square { font-size:1.2em; } 55 | .card .overlay .marquee{ font-size: 0.6em; } 56 | } 57 | 58 | .center-title{ 59 | font-size: 20px; 60 | padding-top: 21%; 61 | } 62 | 63 | } 64 | @media all and (min-width: 1200px) { 65 | .square { font-size:2.4em; } 66 | .card .overlay .marquee{ font-size: 1.1em; } 67 | 68 | .retro{ 69 | .square { font-size:1.4em; } 70 | .card .overlay .marquee{ font-size: 0.8em; } 71 | } 72 | 73 | .control-info{ 74 | height: 20px; 75 | padding: 15px 20px; 76 | font-size: 26px; 77 | 78 | .control-item{ 79 | margin-right: 30px; 80 | } 81 | 82 | img{ 83 | height: 20px; 84 | 85 | &.start-select{ 86 | max-height: 15px; 87 | } 88 | } 89 | } 90 | 91 | .center-title{ 92 | font-size: 24px; 93 | padding-top: 19%; 94 | } 95 | 96 | } 97 | @media all and (min-width: 1500px) { 98 | .square { font-size:3.0em; } 99 | .card .overlay .marquee{ font-size: 1.2em; } 100 | 101 | .retro{ 102 | .square { font-size:1.6em; } 103 | .card .overlay .marquee{ font-size: 1.0em; } 104 | } 105 | 106 | .center-title{ 107 | font-size: 26px; 108 | padding-top: 21%; 109 | } 110 | 111 | } 112 | @media all and (min-width: 1700px) { 113 | .square { font-size:3.6em; } 114 | .card .overlay .marquee{ font-size: 1.4em; } 115 | 116 | .retro{ 117 | .square { font-size:1.8em; } 118 | .card .overlay .marquee{ font-size: 1.2em; } 119 | } 120 | 121 | .control-info{ 122 | height: 25px; 123 | padding: 15px 20px; 124 | font-size: 32px; 125 | 126 | .control-item{ 127 | margin-right: 40px; 128 | } 129 | 130 | img{ 131 | height: 25px; 132 | 133 | &.start-select{ 134 | max-height: 20px; 135 | } 136 | } 137 | } 138 | 139 | } 140 | @media all and (min-width: 1900px) { 141 | .square { font-size:3.6em; } 142 | .card .overlay .marquee{ font-size: 1.4em; } 143 | 144 | .retro{ 145 | .square { font-size:2.1em; } 146 | .card .overlay .marquee{ font-size: 1.4em; } 147 | } 148 | 149 | .center-title{ 150 | font-size: 32px; 151 | padding-top: 16%; 152 | } 153 | 154 | } 155 | @media all and (min-width: 2000px) { 156 | .square { font-size:3.6em; } 157 | .card .overlay .marquee{ font-size: 1.6em; } 158 | 159 | .retro{ 160 | .square { font-size:2.3em; } 161 | .card .overlay .marquee{ font-size: 1.6em; } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/styles/shared/retro.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'PressStart'; 3 | src: url('styles/shared/press-start/prstart.ttf') format('truetype'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | 9 | 10 | .retro { 11 | font-family: 'PressStart'; 12 | -webkit-font-smoothing: none; 13 | 14 | .card{ 15 | .overlay{ 16 | transition: none; 17 | 18 | .marquee{ 19 | font-size: 10px; 20 | transition: none; 21 | } 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/styles/shared/settings.less: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 30px; 3 | color: #ffffff; 4 | 5 | .field { 6 | margin-bottom: 20px; 7 | 8 | .label { 9 | margin-bottom: 5px; 10 | } 11 | 12 | .well { 13 | width: 400px; 14 | padding: 5px; 15 | background-color: gray; 16 | border: 1px solid white; 17 | float: left; 18 | margin-right: 10px; 19 | border-radius: 3px; 20 | box-shadow: inset 0 0 5px #000000; 21 | } 22 | 23 | button { 24 | font-size: 12px; 25 | 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/styles/shared/variables.less: -------------------------------------------------------------------------------- 1 | @selectedColor: #3C77D4; 2 | -------------------------------------------------------------------------------- /art/buttons.sketch/Data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maddox/kart/82f1144aa3783bcd3963f3eef82c8eb93a7b145d/art/buttons.sketch/Data -------------------------------------------------------------------------------- /art/buttons.sketch/metadata: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | app 6 | com.bohemiancoding.sketch3 7 | build 8 | 8054 9 | commit 10 | b2079fe10151ad0eef6cc553b7369ec78d67b9b5 11 | fonts 12 | 13 | Helvetica 14 | 15 | length 16 | 37205 17 | version 18 | 37 19 | 20 | 21 | -------------------------------------------------------------------------------- /art/buttons.sketch/version: -------------------------------------------------------------------------------- 1 | 37 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kart", 3 | "version": "0.1.0", 4 | "application": { 5 | "name": "Kart" 6 | }, 7 | "atomShellVersion": "0.33.0", 8 | "devDependencies": { 9 | "formidable": "~1.0.14", 10 | "fs-plus": "2.x", 11 | "grunt": "~0.4.1", 12 | "grunt-cli": "~0.1.9", 13 | "grunt-contrib-copy": "~0.5", 14 | "grunt-download-atom-shell": "0.7.0", 15 | "grunt-download-electron": "^2.1.2", 16 | "grunt-shell": "~0.3.1", 17 | "grunt-symbolic-link": "~0.3.1", 18 | "request": "~2.27.0", 19 | "walkdir": "0.0.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "${BASH_SOURCE[0]}" )/.." 4 | export APP_VERSION=`cat VERSION` 5 | apm install 6 | npm install grunt-download-electron 7 | rm -r atom-shell 8 | grunt bootstrap 9 | -------------------------------------------------------------------------------- /script/bootstrap.ps1: -------------------------------------------------------------------------------- 1 | $version = Get-Content VERSION 2 | $env:APP_VERSION = $version 3 | & apm install 4 | & npm install grunt-download-electron 5 | & grunt bootstrap-win 6 | -------------------------------------------------------------------------------- /script/build-windows: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Cleaning..." 4 | rm -Rf build/win32/kart/kart.zip 5 | rm -Rf build/win32/kart/resources/app 6 | 7 | echo "Copying files..." 8 | cp -r app build/win32/kart/resources 9 | 10 | echo "Archiving..." 11 | cd build/win32 12 | zip -r kart.zip kart 13 | 14 | echo "Complete!" 15 | -------------------------------------------------------------------------------- /script/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | open "./electron/`ls electron/ | grep app`" --args --dev "$@" 4 | -------------------------------------------------------------------------------- /script/run.ps1: -------------------------------------------------------------------------------- 1 | $appPath = Resolve-Path app 2 | $args = @("--dev", "--", $appPath) 3 | Start-Process -FilePath electron -ArgumentList $args 4 | -------------------------------------------------------------------------------- /tasks/generate-plist.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | rootPath = path.join(__dirname, '..') 5 | packagePath = path.join(rootPath, 'package.json') 6 | shellPath = path.join(rootPath, 'atom-shell') 7 | origAppPath = path.join(shellPath, 'Atom.app') 8 | infoPlistPath = path.join(origAppPath, 'Contents', 'Info.plist') 9 | iconPath = path.join(origAppPath, 'Contents', 'Resources', 'atom.icns') 10 | 11 | module.exports = (grunt) -> 12 | {cp, rm} = require('./task-helpers')(grunt) 13 | 14 | grunt.registerTask 'generate-plist', 'Generates the Info.plist from the package.json', -> 15 | if !fs.existsSync(packagePath) 16 | grunt.log.error('No package.json found') 17 | return false 18 | 19 | packageContents = fs.readFileSync(packagePath) 20 | config = JSON.parse(packageContents) 21 | config = config.application || {} 22 | 23 | name = config.name || 'Kart' 24 | appPath = path.join(shellPath, "#{name}.app") 25 | icon = config.icon 26 | version = process.env.APP_VERSION 27 | version += ".#{process.env.GIT_VERSION}" if process.env.GIT_VERSION 28 | 29 | plist = """ 30 | 31 | 32 | 33 | CFBundleIdentifier 34 | org.kart.desktop 35 | CFBundleExecutable 36 | Atom 37 | CFBundleName 38 | #{name} 39 | CFBundleDisplayName 40 | #{name} 41 | CFBundlePackageType 42 | APPL 43 | CFBundleIconFile 44 | Atom.icns 45 | CFBundleVersion 46 | #{version} 47 | NSMainNibFile 48 | MainMenu 49 | NSPrincipalClass 50 | AtomApplication 51 | NSSupportsAutomaticGraphicsSwitching 52 | 53 | CFBundleDocumentTypes 54 | 55 | 56 | CFBundleTypeRole 57 | Editor 58 | LSItemContentTypes 59 | 60 | public.directory 61 | com.apple.bundle 62 | com.apple.resolvable 63 | 64 | 65 | 66 | 67 | """ 68 | 69 | fs.writeFileSync(infoPlistPath, plist) 70 | grunt.log.writeln("Generated and saved #{infoPlistPath.cyan}") 71 | 72 | if icon 73 | newIconPath = path.resolve(rootPath, icon) 74 | cp(newIconPath, iconPath) 75 | 76 | if name != 'Kart' 77 | rm(appPath) 78 | fs.renameSync(origAppPath, appPath) 79 | grunt.log.writeln("Moved the app to #{appPath.cyan}") 80 | -------------------------------------------------------------------------------- /tasks/task-helpers.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-plus' 2 | path = require 'path' 3 | 4 | module.exports = (grunt) -> 5 | cp: (source, destination, {filter}={}) -> 6 | unless grunt.file.exists(source) 7 | grunt.fatal("Cannot copy non-existent #{source.cyan} to #{destination.cyan}") 8 | 9 | copyFile = (sourcePath, destinationPath) -> 10 | return if filter?.test(sourcePath) 11 | 12 | stats = fs.lstatSync(sourcePath) 13 | if stats.isSymbolicLink() 14 | grunt.file.mkdir(path.dirname(destinationPath)) 15 | fs.symlinkSync(fs.readlinkSync(sourcePath), destinationPath) 16 | else if stats.isFile() 17 | grunt.file.copy(sourcePath, destinationPath) 18 | 19 | if grunt.file.exists(destinationPath) 20 | fs.chmodSync(destinationPath, fs.statSync(sourcePath).mode) 21 | 22 | if grunt.file.isFile(source) 23 | copyFile(source, destination) 24 | else 25 | try 26 | onFile = (sourcePath) -> 27 | destinationPath = path.join(destination, path.relative(source, sourcePath)) 28 | copyFile(sourcePath, destinationPath) 29 | onDirectory = (sourcePath) -> 30 | if fs.isSymbolicLinkSync(sourcePath) 31 | destinationPath = path.join(destination, path.relative(source, sourcePath)) 32 | copyFile(sourcePath, destinationPath) 33 | false 34 | else 35 | true 36 | fs.traverseTreeSync source, onFile, onDirectory 37 | catch error 38 | grunt.fatal(error) 39 | 40 | grunt.verbose.writeln("Copied #{source.cyan} to #{destination.cyan}.") 41 | 42 | mkdir: (args...) -> 43 | grunt.file.mkdir(args...) 44 | 45 | rm: (args...) -> 46 | grunt.file.delete(args..., force: true) if grunt.file.exists(args...) 47 | 48 | spawn: (options, callback) -> 49 | childProcess = require 'child_process' 50 | stdout = [] 51 | stderr = [] 52 | error = null 53 | proc = childProcess.spawn(options.cmd, options.args, options.opts) 54 | proc.stdout.on 'data', (data) -> stdout.push(data.toString()) 55 | proc.stderr.on 'data', (data) -> stderr.push(data.toString()) 56 | proc.on 'close', (exitCode, signal) -> 57 | error = new Error(signal) if exitCode != 0 58 | results = {stderr: stderr.join(''), stdout: stdout.join(''), code: exitCode} 59 | grunt.log.error results.stderr if exitCode != 0 60 | callback(error, results, exitCode) 61 | 62 | isAtomPackage: (packagePath) -> 63 | try 64 | {engines} = grunt.file.readJSON(path.join(packagePath, 'package.json')) 65 | engines?.atom? 66 | catch error 67 | false --------------------------------------------------------------------------------