├── .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 | 
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 |
--------------------------------------------------------------------------------
/app/images/control-info/b-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/images/control-info/d-pad.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/images/control-info/start-select.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
2 |
Which collection would you like to add this game to?
3 |
4 | <% for collection in @collections: %>
5 | <%- @cardFor(collection) %>
6 | <% end %>
7 |
8 |
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
--------------------------------------------------------------------------------