Repository: pebble-dev/rebble-store
Branch: master
Commit: b4d5ecf61521
Files: 78
Total size: 133.5 KB
Directory structure:
gitextract_8kuhkv4z/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── LICENSE
├── README.md
├── build/
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── sprite_module.conf.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ ├── webpack.prod.conf.js
│ └── webpack.test.conf.js
├── config/
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ └── test.env.js
├── index.html
├── package.json
├── src/
│ ├── 404.html
│ ├── App.vue
│ ├── assets/
│ │ ├── browserconfig.xml
│ │ └── manifest.json
│ ├── components/
│ │ ├── Home.vue
│ │ ├── Navbar.vue
│ │ ├── PageFooter.vue
│ │ ├── SvgContainer.vue
│ │ └── pages/
│ │ ├── AppDetails.vue
│ │ ├── AppVersions.vue
│ │ ├── AppView.vue
│ │ ├── Author.vue
│ │ ├── Category.vue
│ │ ├── Collection.vue
│ │ ├── Error.vue
│ │ ├── Search.vue
│ │ ├── Settings.vue
│ │ └── widgets/
│ │ ├── AppSlider.vue
│ │ ├── AppTitleBar.vue
│ │ ├── CardCollection.vue
│ │ ├── GetAppButton.vue
│ │ ├── HomeSlider.vue
│ │ ├── Pagination.vue
│ │ ├── ScreenshotList.vue
│ │ ├── SingleBanner.vue
│ │ ├── SingleCard.vue
│ │ ├── SingleScreenshot.vue
│ │ ├── TagList.vue
│ │ └── content-loaders/
│ │ ├── SingleBanner.vue
│ │ ├── SingleCard.vue
│ │ ├── SingleScreenshotRound.vue
│ │ └── SingleScreenshotSquare.vue
│ ├── css/
│ │ └── _error.scss
│ ├── main.js
│ ├── mixin/
│ │ └── index.js
│ ├── router/
│ │ ├── index.js
│ │ └── search-router.js
│ ├── services/
│ │ └── index.js
│ └── store/
│ ├── config.js
│ ├── index.js
│ ├── secure.js
│ └── userParameters.js
├── static/
│ ├── .gitkeep
│ ├── browserconfig.xml
│ ├── css/
│ │ └── _variables.scss
│ └── manifest.json
└── test/
├── e2e/
│ ├── custom-assertions/
│ │ └── elementCount.js
│ ├── nightwatch.conf.js
│ ├── runner.js
│ └── specs/
│ └── test.js
└── unit/
├── .eslintrc
├── index.js
└── karma.conf.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"],
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": ["transform-vue-jsx", "istanbul"]
}
}
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .eslintignore
================================================
/build/
/config/
/dist/
/*.js
/test/unit/coverage/
================================================
FILE: .eslintrc.js
================================================
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test/unit/coverage/
/test/e2e/reports/
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
================================================
FILE: .postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Rebble (pebble-dev)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Rebble Store for pebble
The Rebble Store is a Pebble Appstore replacement.
If you want to contribute join us on the [Pebble Dev Discord server](http://discord.gg/aRUAYFN), then head to `#appstore`.
This is the Rebble replacement for the Pebble app store. This project is under active development, though the eventual goal is to reach feature parity with the current Pebble smartwatch app store.
This project is built with [VueJS 2](https://vuejs.org/), with webpack scripts included for debugging, hot-reload, and production builds. More information on the Vue webpack build scripts can be found [here](https://github.com/vuejs-templates/webpack).
## Backend/API
This project has a separate backend/api that's currently written in Python. It can be found [here](https://github.com/pebble-dev/rebble-appstore-api).
It's not necessary to run the API locally unless also developing for the API. The frontend points to the production API by default.
## Installing
If you want to run a local version you will also need to run the backend.
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8081
npm run dev
# build for production with minification
npm run build
# run unit tests
npm run unit
# run e2e tests
npm run e2e
# run all tests
npm test
```
================================================
FILE: build/build.js
================================================
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
================================================
FILE: build/check-versions.js
================================================
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
================================================
FILE: build/dev-client.js
================================================
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})
================================================
FILE: build/dev-server.js
================================================
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var opn = require('opn')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
stats: {
colors: true,
chunks: false
}
})
var hotMiddleware = require('webpack-hot-middleware')(compiler)
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
var uri = 'http://localhost:' + port
console.log('Listening at ' + uri + '\n')
// when env is testing, don't need open it
if (process.env.NODE_ENV !== 'testing') {
opn(uri)
}
})
================================================
FILE: build/sprite_module.conf.js
================================================
import BrowserSprite from 'svg-baker-runtime/browser-sprite';
import domready from 'domready'
const sprite = new BrowserSprite();
domready(() => sprite.mount('#svgContainer'))
export default sprite;
================================================
FILE: build/utils.js
================================================
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass').concat(
{
loader: 'sass-resources-loader',
options: {
resources: [path.resolve(__dirname, '../static/css/_variables.scss')]
}
}
),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
================================================
FILE: build/vue-loader.conf.js
================================================
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
================================================
FILE: build/webpack.base.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.scss$/,
use: [
{
loader: "style-loader" // creates style nodes from JS strings
},
{
loader: "css-loader" // translates CSS into CommonJS
},
{
loader: "sass-loader" // compiles Sass to CSS
}
]
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(svg)$/,
loader: 'svg-sprite-loader',
include: [resolve('src/assets/svg')],
options: {
symbolId: 'icon[name]',
spriteModule: resolve('build/sprite_module.conf.js')
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
exclude: [resolve('src/assets/svg')],
use:[
'file-loader',
]
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
================================================
FILE: build/webpack.dev.conf.js
================================================
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
================================================
FILE: build/webpack.prod.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing'
? 'index.html'
: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
================================================
FILE: build/webpack.test.conf.js
================================================
'use strict'
// This is the webpack config used for unit tests.
const utils = require('./utils')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const webpackConfig = merge(baseWebpackConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
rules: utils.styleLoaders()
},
devtool: '#inline-source-map',
resolveLoader: {
alias: {
// necessary to to make lang="scss" work in test when using vue-loader's ?inject option
// see discussion at https://github.com/vuejs/vue-loader/issues/724
'scss-loader': 'sass-loader'
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/test.env')
})
]
})
// no need for app entry during tests
delete webpackConfig.entry
module.exports = webpackConfig
================================================
FILE: config/dev.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
================================================
FILE: config/index.js
================================================
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: '0.0.0.0', // can be overwritten by process.env.HOST
port: 8081, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
================================================
FILE: config/prod.env.js
================================================
'use strict'
module.exports = {
NODE_ENV: '"production"'
}
================================================
FILE: config/test.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Rebble App Store</title>
<!-- Site icons and WebApp metadata -->
<link rel="apple-touch-icon" sizes="180x180" href="static/apple-touch-icon.png">
<link rel="icon" type="image/png" href="static/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="static/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="static/manifest.json">
<link rel="mask-icon" href="static/safari-pinned-tab.svg" color="#373a3c">
<link rel="shortcut icon" href="static/favicon.ico">
<meta name="apple-mobile-web-app-title" content="Rebble Store">
<meta name="application-name" content="Rebble Store">
<meta name="msapplication-config" content="static/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "rebble-app-store",
"version": "1.0.0-beta.3",
"description": "Rebble Store, a pebble app store replacement.",
"author": "Rebble Web Services Team",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
"build": "node build/build.js"
},
"dependencies": {
"algoliasearch": "^3.33.0",
"bootstrap-vue": "^2.0.0-rc.27",
"es6-promise": "^4.2.5",
"vue": "^2.6.10",
"vue-cookie": "^1.1.4",
"vue-dragscroll": "^1.10.0",
"vue-instantsearch": "^2.3.0",
"vue-router": "^3.0.7",
"vuex": "^3.1.1",
"vuex-pathify": "^1.2.4"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chai": "^4.2.0",
"chalk": "^2.4.1",
"chromedriver": "^2.44.1",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.2.0",
"cross-spawn": "^5.0.1",
"css-loader": "^0.28.0",
"domready": "^1.0.8",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^2.30.1",
"inject-loader": "^3.0.0",
"karma": "^1.4.1",
"karma-coverage": "^1.1.2",
"karma-mocha": "^1.3.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-phantomjs-shim": "^1.4.0",
"karma-sinon-chai": "^1.3.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.31",
"karma-webpack": "^2.0.2",
"mocha": "^3.2.0",
"nightwatch": "^0.9.12",
"node-notifier": "^5.3.0",
"node-sass": "^4.11.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"phantomjs-prebuilt": "^2.1.14",
"portfinder": "^1.0.20",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"sass-loader": "^6.0.6",
"sass-resources-loader": "^1.3.2",
"selenium-server": "^3.141.59",
"semver": "^5.6.0",
"shelljs": "^0.7.6",
"sinon": "^4.0.0",
"sinon-chai": "^2.8.0",
"svg-sprite-loader": "^3.9.0",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-content-loading": "^1.5.3",
"vue-images-loaded": "^1.1.2",
"vue-loader": "^13.3.0",
"vue-resource": "^1.5.1",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.21",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.5"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
================================================
FILE: src/404.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags always come first -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Rebble Store</title>
<!-- Site icons -->
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
<link rel="icon" type="image/png" href="assets/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="assets/favicon-16x16.png" sizes="16x16">
<link rel="manifest" href="assets/manifest.json">
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#373a3c">
<link rel="shortcut icon" href="assets/favicon.ico">
<meta name="apple-mobile-web-app-title" content="Rebble Store">
<meta name="application-name" content="Rebble Store">
<meta name="msapplication-config" content="assets/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="css/bootstrap.css">
<link rel="stylesheet" href="css/font-awesome.min.css">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="flex-content">
<nav class="navbar navbar-fixed-top navbar-dark bg-inverse text-sm-center translucent">
<div class="navbar-container">
<a class="navbar-brand" href="index.html">Rebble Store <small>for <div class="pebble">pebble</div></small></a>
<div class="navbar__items right">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#categorySelector" aria-controls="categorySelector" aria-expanded="false" aria-label="Toggle navigation"></button>
<a class="search" href="search.html">
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Search" stroke="#a5a6a7">
<path id="Shape" stroke-width="1.5" class="st0" fill="none" d="M13.9,16.6l1.2-0.4c0,0,0.8-1.5,0.8-1.6c2.3,2.4,4.7,4.7,7,7.1l-2,2
C18.5,21.2,16.2,18.9,13.9,16.6z"/>
<polygon id="Shape_1_" stroke-width="2" fill="none" class="st1" points="2.2,10.2 4.6,5 10.3,3 15.4,5.4 17.5,11 15.1,16.1 9.4,18.2 4.3,15.8 "/>
<path id="Line" fill="none" class="st2" d="M11.3,6.6l2,1l1,3" stroke-linecap="square"/>
</g>
</g>
</svg>
</a>
</div>
</div>
<div class="collapse text-xs-center" id="categorySelector">
<div class="text-muted p-1">
<div class="btn-group btn-group-lg" role="group">
<a href="index.html" class="btn btn-outline-secondary" role="button">
<svg class="watchface" width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Clock" stroke-width="2" stroke="rgba(204,204,204,.5)">
<polygon id="Shape" fill="none" points="2 8.00285097 8 2 17 2 23 8.00285097 23 17.0255772 17 23 8 23 2 17.0255772"></polygon>
<polyline id="Line" stroke-linecap="square" points="15.5630383 14.7579965 12.040745 12.1503281 12 6"></polyline>
</g>
</g>
</svg> Watchfaces
</a>
<a href="apps.html" class="btn btn-outline-secondary active" role="button">
<svg class="app" width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<polygon id="path-1" points="9 4 19 4 21 6 21 16 19 18 9 18 7 15.9960225 7 6"></polygon>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="-2" y="-2" width="18" height="18">
<rect x="5" y="2" width="18" height="18" fill="white"></rect>
<use xlink:href="#path-1" fill="black"></use>
</mask>
</defs>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="App">
<g id="Rectangle-2">
<use fill="none" fill-rule="evenodd" xlink:href="#path-1"></use>
<use id="use2" stroke="rgba(204,204,204,.5)" mask="url(#mask-2)" stroke-width="4" xlink:href="#path-1"></use>
</g>
<polyline id="Line" stroke="rgba(204,204,204,.5)" stroke-width="2" stroke-linecap="square" points="17 22.0005626 7 22.0005626 3 18 3 8"></polyline>
</g>
</g>
</svg>
Apps
</a>
</div>
</div>
</div>
</nav>
<main class="container text-xs-center">
<section>
<div class="page-error page-error--404">
<h2>Our pet rock has lost your page, sorry about that</h2>
<svg class="pet-rock-pebble"xmlns="http://www.w3.org/2000/svg" width="406" height="199" viewBox="0 0 406 199" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<polygon id="a" points="160.865 162.763 243.499 144.619 257.327 109.929 242.053 72.586 209.66 56.935 180.398 34.881 138.125 29.331 89.961 41.943 58.829 43.073 31.846 60.054 13.107 99.878 36.862 138.39 100.393 158.597"/>
<mask id="d" width="244.22" height="133.432" x="0" y="0" fill="white">
<use xlink:href="#a"/>
</mask>
<circle id="b" cx="80.5" cy="114.5" r="9.5"/>
<mask id="e" width="19" height="19" x="0" y="0" fill="white">
<use xlink:href="#b"/>
</mask>
<circle id="c" cx="29.5" cy="101.5" r="9.5"/>
<mask id="f" width="19" height="19" x="0" y="0" fill="white">
<use xlink:href="#c"/>
</mask>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(-14 3)">
<g transform="matrix(-1 0 0 1 271 30)">
<use fill="#D8D8D8" stroke="#373A3C" stroke-width="10" mask="url(#d)" transform="rotate(-15 135.217 96.047)" xlink:href="#a"/>
<polyline stroke="#373A3C" stroke-width="2" points="21 129.5 60.5 150 118.5 146.5 175 137 236.5 103 253.5 75.5"/>
<circle cx="31.5" cy="98.5" r="13.5" fill="#FFFFFF" stroke="#373A3C" stroke-width="2"/>
<circle cx="83.5" cy="110.5" r="13.5" fill="#FFFFFF" stroke="#373A3C" stroke-width="2"/>
<use fill="#373A3C" stroke="#373A3C" stroke-width="2" mask="url(#e)" xlink:href="#b"/>
<use fill="#373A3C" stroke="#373A3C" stroke-width="2" mask="url(#f)" xlink:href="#c"/>
<circle cx="86" cy="113" r="2" fill="#FFFFFF"/>
<circle cx="34" cy="100" r="2" fill="#FFFFFF"/>
</g>
<polygon fill="#FFFFFF" stroke="#373A3C" stroke-width="5" points="296.576 0 391.886 0 417 22.464 417 91.623 391.886 111.304 308.5 111.304 282.089 128 282.089 100.5 271 91.623 271 22.464"/>
<text fill="#000000" font-family="OpenSans-Light, Open Sans" font-size="44" font-weight="300">
<tspan x="307" y="71">404</tspan>
</text>
</g>
</svg>
<h4>We're getting the following message {{ 404 }}</h4>
<div class="page-error_buttons">
<a href="javascript:history.go(0)" class="btn btn-outline-secondary">Try again</a>
<a href="/" class="btn btn-outline-pebble">Back to home</a>
</div>
</div>
</section>
</main>
</div>
<footer>
<p>© 2016 Rebble · <a href="#">Contact Us</a> · <a href="#">Terms</a>
</p>
<a class="pull-right" href="https://rebble.io/submit/">Developer Portal</a>
</footer>
<!-- jQuery first, then Tether, then Bootstrap JS. -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha384-3ceskX3iaEnIogmQchP8opvBy3Mi7Ce34nWjpBIwVTHfGYWQS9jwHDVRnpKKHJg7" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js" integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js" integrity="sha384-BLiI7JTZm+JWlgKa0M0kGRpJbF2J8q+qreVrKBC47e3K6BW78kGLrCkeRX6I9RoK" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/App.vue
================================================
<template>
<div id="app">
<div v-bind:class="$store.state.userParameters.inApp ? 'flex-content main-in-app' : 'flex-content'">
<svg-container></svg-container>
<navbar v-if="!$store.state.userParameters.inApp"></navbar>
<router-view ></router-view>
</div>
<page-footer v-bind:brand="$store.state.userParameters.inApp"></page-footer>
</div>
</template>
<script>
import SvgContainer from './components/SvgContainer'
import Home from './components/Home'
import Navbar from './components/Navbar'
import PageFooter from './components/PageFooter'
import { platformEnum, hardwareEnum } from './store/userParameters'
export default {
name: 'app',
components: {
SvgContainer,
Navbar,
Home,
PageFooter
},
data: function () {
return {
}
},
beforeMount () {
// PARAM LIST
// platform = 'ios' || 'android
// inApp: boolean
// devMode: boolean
// hardware = 'aplite' || 'basalt' || 'chalk' ...
// accessToken: string = rebble access token
// appVersion
let routeParameters = this.$route.query
// Platform refers to phone. Android or iOS.
if (routeParameters.platform && Object.values(platformEnum).includes(routeParameters.platform.toLowerCase())) {
this.$store.set('userParameters/platform', routeParameters.platform.toLowerCase())
}
if (routeParameters.inApp != null) {
this.$store.set('userParameters/inApp', routeParameters.inApp !== 'false' && routeParameters.inApp !== '0')
}
if (routeParameters.devMode != null) {
this.$store.set('userParameters/devMode', routeParameters.devMode !== 'false' && routeParameters.devMode !== '0')
}
// hardware refers to the watch. basalt, chalk, aplite, etc.
if (routeParameters.hardware && Object.values(hardwareEnum).includes(routeParameters.hardware.toLowerCase())) {
this.$store.set('userParameters/hardware', routeParameters.hardware.toLowerCase())
}
if (routeParameters.appVersion) {
this.$store.set('userParameters/appVersion', routeParameters.appVersion)
}
// Bearer token provided by the mobile app, needed to fetch and set app hearts
if (routeParameters.accessToken) {
this.$store.set('secure/accessToken', routeParameters.accessToken)
}
}
}
</script>
<style lang="scss">
@import './static/css/_variables.scss';
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
min-height: 100vh;
flex-direction: column;
}
.main-in-app {
margin-top: -43px;
}
// _fonts.scss
// Set font-family and font-weight in here
// Open Sans 300 for titles and 400 for small titles
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,400i');
//Raleway 400, 400i, 700, 700i for all of the other text
@import url('https://fonts.googleapis.com/css?family=Raleway:400,400i,700,700i');
body {
font-family: 'Raleway', sans-serif;
}
h1, h2, h3, h4, h5, h6, small {
font-family: 'Open Sans', sans-serif;
font-weight: 400;
}
p {
font-family: 'Raleway', sans-serif;
font-weight: 400;
}
b {
font-weight: 700;
}
.btn {
// Remove transitions from .btn
transition: none;
&:focus {
box-shadow: none;
}
}
body {
background-color: $main-bg-color;
// Make footer sticky
display: flex;
min-height: 100vh;
flex-direction: column;
.flex-content {
flex: 1;
}
// App columns container
.apps{
margin-top: 40px;
}
}
.pull-right {
float: right;
}
// Pebble color helper class
.pebble {
color: $pebble-color !important;
display: inline;
}
// Pebble colored badge
.badge-pebble {
background-color: $pebble-color;
color: #fff;
}
// Pebble color outline button
.btn-outline-pebble {
margin-left: 10px;
color: $pebble-color;
border-color: $pebble-color;
background-image: none;
background-color: transparent;
// Pebble button button hover styles
&:hover, &.active, &:hover:focus {
background-color: $pebble-color;
color: #333;
}
&:focus {
// Override some bootstrap styles
color: $pebble-color;
}
}
// Add new card style called subsection with inverse color (reusable component)
.card.subsection {
max-width: 720px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
border-radius: 0;
margin-bottom: 40px;
display: block;
}
.card.subsection-inverse {
@extend .card.subsection;
background-color: #333;
border-color: #333;
}
.card.subsection-extra {
@extend .card.subsection;
margin-top: -40px;
background-color: #ccc;
border-color: #ccc;
}
// Modify default pagination styles, mostly color (not inside section because it may get reused)
ul.pagination {
display: inline-flex;
margin-bottom: 0;
.page-item {
&.active .page-link {
// Button is active
background: $pebble-color;
border-color: $pebble-color;
color: #fff;
}
&.disabled .page-link {
// Button is disabled
color: #818a91;
}
.page-link {
// Change text colot
color: $pebble-color;
cursor: pointer;
&:hover, &:focus {
// Overwrite hover and focus states
text-decoration: none;
}
}
}
}
header.main {
padding-top: 58px;
background: linear-gradient(to bottom, rgba(55, 58, 60, 1) 0%, rgba(55, 58, 60, 1) 65%, rgba(55, 58, 60, 0) 65%, rgba(55, 58, 60, 0) 100%);
.title-card {
width: 90vw;
padding: 20px;
background-color: #fff;
max-width: 720px;
max-height: 320px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
h3 {
margin: 0;
}
}
}
</style>
================================================
FILE: src/assets/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/assets/mstile-150x150.png"/>
<TileColor>#373a3c</TileColor>
</tile>
</msapplication>
</browserconfig>
================================================
FILE: src/assets/manifest.json
================================================
{
"name": "Rebble Store",
"icons": [
{
"src": "\/assets\/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image\/png"
},
{
"src": "\/assets\/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image\/png"
}
],
"theme_color": "#ffffff",
"display": "standalone"
}
================================================
FILE: src/components/Home.vue
================================================
<template>
<div>
<slider v-bind:banners="page.banners"></slider>
<main class="home apps container">
<tag-list v-if="type != 'faces' && page.categories !== undefined" v-bind:tags="page.categories"></tag-list>
<card-collection v-for="(collection, index) in page.collections" v-bind:key="index" v-bind:elTitle="collection.name" v-bind:cards="collection.data" v-bind:allUrl="`/${type}/${collection.slug}`"></card-collection>
</main>
</div>
</template>
<script>
import CardCollection from './pages/widgets/CardCollection'
import TagList from './pages/widgets/TagList'
import Slider from './pages/widgets/HomeSlider'
const MAX_HOME_COLLECTION_SIZE = 6
// mostRecentCards and freshPicksCards are placeholders. Todo: actually fetch those from the server.
export default {
name: 'Home',
components: {
CardCollection,
Slider,
TagList
},
data: function () {
return {
page: {},
type: ''
}
},
watch: {
'$route' (to, from) {
if (to.fullPath !== from.fullPath) {
this.page = {}
this.get_data(to.params)
}
}
},
methods: {
get_data: function (routeParams) {
var that = this
this.type = routeParams.type
this.setTitle(this.type === 'faces' || this.type === 'watchfaces' ? 'Watchfaces' : 'Apps')
this.$http.get(this.buildResourceUrl(`home/${this.type}`)).then(response => {
that.page = response.body
this.build_collections()
}, response => {
console.error(response)
})
},
build_collections: function () {
for (let collection of this.page.collections) {
collection.data = []
for (let id of collection.application_ids) {
if (collection.data.length >= MAX_HOME_COLLECTION_SIZE) {
// Limit collection size
break
}
collection.data.push(this.page.applications.find(function (app) {
return id === app.id
}))
}
}
}
},
beforeMount: function () {
this.get_data(this.$route.params)
}
}
</script>
<style lang="scss">
main.home {
@media screen and (min-width: 992px) {
section .card-columns {
column-count: 3 !important;
}
}
}
</style>
================================================
FILE: src/components/Navbar.vue
================================================
<template>
<b-navbar toggleable="true" type="dark" variant="dark" class="translucent" fixed="top">
<div class="navbar__items left" v-show="showBackButton">
<b-nav-item class="back" v-on:click="goBack()">
❮
</b-nav-item>
</div>
<b-navbar-brand to="/">Rebble Store
<small>
for
<div class="pebble">pebble</div>
</small>
</b-navbar-brand>
<div class="navbar__items right">
<b-navbar-toggle target="categorySelector"></b-navbar-toggle>
<b-nav-item class="search" to="/faces/search">
<svg class="icon-search" width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="#iconSearch"></use>
</svg>
</b-nav-item>
<b-nav-item v-if="$store.state.userParameters.devMode" class="settings" to="/settings">
<svg class="icon-settings" width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="#iconSettings"></use>
</svg>
</b-nav-item>
</div>
<b-collapse id="categorySelector" class="text-center" v-model="showCollapse" is-nav>
<div class="text-muted category-container">
<div class="btn-group btn-group-lg" role="group">
<router-link to="/" v-bind:class="{ active: currentRoute == '/faces'}" class="btn btn-outline-secondary btn-watchface" role="button">
<svg class="icon-watchface" width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="#iconWatchface"></use>
</svg>
Watchfaces
</router-link>
<router-link to="/apps" v-bind:class="{ active: currentRoute == '/apps'}" class="btn btn-outline-secondary btn-app" role="button">
<svg class="icon-app" width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<use xlink:href="#iconApp"></use>
</svg>
Apps
</router-link>
</div>
</div>
</b-collapse>
</b-navbar>
</template>
<script>
export default {
name: 'navbar',
data () {
return {
showBackButton: false,
currentRoute: '/',
goBack () {
this.$router.go(-1)
},
showCollapse: false
}
},
mounted () {
// Update the route state on page load, in case we didn't start at home
this.currentRoute = (this.$route.path === '' ? '/' : this.$route.path)
this.updateBackButton()
},
watch: {
'$route' (to, from) {
// Ternary operation makes sure the path is never an empty string when we go home
// This is for the class binding on the watchfaces button,
// since that is currently the home route
this.currentRoute = (this.$route.path === '' ? '/' : this.$route.path)
this.updateBackButton()
this.showCollapse = false
}
},
methods: {
updateBackButton () {
this.showBackButton = (this.currentRoute !== '/faces' && this.currentRoute !== '/apps')
}
}
}
</script>
<style lang="scss" scoped>
// Select the dark translucent navbar
.navbar-dark.bg-dark.translucent {
// Fix navbar being smaller than 58px
min-height: 42px;
width: 100%;
// Navbar brand (app logo or title) styles
.navbar-brand {
display: initial;
margin: 0;
position: relative;
top: 2px;
@media screen and (min-width: 768px) {
margin-right: auto;
margin-left: auto;
}
// Make text smaller on small screens
@media screen and (max-width: 430px) {
margin-left: -10px;
top: 2px;
font-size: 1rem;
}
}
li.nav-item {
a {
padding: 0 2px;
}
}
// Navbar items right container
.navbar__items {
// Items in the right
&.right {
@media screen and (max-width: 767px) {
margin-left: auto;
}
// Search Icon
.search a {
padding-top: 2px; // Move it down
}
// Hamburger icon
.navbar-toggler {
cursor: pointer;
padding: .25rem .35rem;
margin-right: .25rem;
}
}
// Items in the left
&.left {
left: 16px;
// Prevent brand from going on top of this
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
position: static;
}
// Back arrow button
li.back a {
position: relative;
top: 2px;
font-size: 27px;
color: rgba(255, 255, 255, 0.5);
background: none;
border: none;
cursor: pointer;
// Add margin on breakpoint to prevent brand from being really close
margin-right: 20px;
}
}
// Hamburger icon
.navbar-toggler, .navbar-toggler:focus {
border: 0;
border-radius: 0;
outline: none;
}
> * {
display: inline-block;
vertical-align: middle;
}
}
@supports (not (backdrop-filter: blur(10px))) and (not (-webkit-backdrop-filter: blur(10px))) {
// Only if doesn't supports backdrop filters
background-color: rgb(55, 58, 60) !important;
}
@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
// Styles that are in here will only apply if backdrop filters are supported by the browser
background-color: rgba(55, 58, 60, 0.96)!important;
// Make the navbar background blurry
backdrop-filter: blur(10px);
}
}
// Collapsable menu (this may change in the future) in which you select to browse watchfaces or apps
#categorySelector {
width: 100vw;
margin-left: -16px;
margin-right: -16px;
.category-container {
padding: 0.25rem;
padding-top: calc(0.25rem + 10px);
}
// Change default button group styles to ones that match the style
.btn-group {
.btn-outline-secondary {
border-color: rgba(204, 204, 204, 0.50);
color: rgba(204, 204, 204, 0.50);
&.active, &:active, &:hover {
color: #fff;
background-color: rgba(204, 204, 204, 0.50);
// Overwrite bootstrap style
border-color: rgba(204, 204, 204, 0.50);
}
&:focus, &.focus {
// Overwrite bootstrap style
border-color: rgba(204, 204, 204, 0.50);
}
}
}
}
</style>
================================================
FILE: src/components/PageFooter.vue
================================================
<template>
<footer v-bind:class="brand ? 'inApp' : 'main'">
<div class="brand" to="/">
Rebble Store <small>for pebble</small>
</div>
<div class="main">
<p>© {{ new Date().getFullYear() }} Rebble · <a v-on:click="openExternal($store.state.config.contactLink)">Contact Us</a> · <a v-on:click="openExternal($store.state.config.tosLink)">Terms</a></p>
<a class="pull-right" v-on:click="openExternal('$store.state.config.devPortalLink')">Developer Portal</a>
</div>
</footer>
</template>
<script>
export default {
name: 'page-footer',
props: {
brand: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="scss" scoped>
// _footer.scss
// Footer styles
footer.main {
.main {
display: block;
}
.brand {
display: none;
}
margin-top: 40px;
background-color: $pebble-color;
color: #fff;
padding: 5px 10px;
// Footer text styles
p {
display: inline;
}
// Prevent bootstrap default link color and text-decoration
a, a:hover, a:focus {
color: #ebebeb;
text-decoration: none;
}
}
footer.inApp {
user-select: none;
text-align: center;
.main {
display: none;
}
.brand {
display: block;
margin-left: auto;
margin-right: auto;
font-weight: 700;
font-size: 20px;
color: rgb(148, 148, 148);
small {
font-weight: 700;
}
margin: 20px 10px;
}
}
</style>
================================================
FILE: src/components/SvgContainer.vue
================================================
<template>
<div id="svgContainer">
<!-- <svg id="petRock" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<polygon id="a" points="160.865 162.763 243.499 144.619 257.327 109.929 242.053 72.586 209.66 56.935 180.398 34.881 138.125 29.331 89.961 41.943 58.829 43.073 31.846 60.054 13.107 99.878 36.862 138.39 100.393 158.597"/>
<mask id="d" width="244.22" height="133.432" x="0" y="0" fill="white">
<use xlink:href="#a"/>
</mask>
<circle id="b" cx="80.5" cy="114.5" r="9.5"/>
<mask id="e" width="19" height="19" x="0" y="0" fill="white">
<use xlink:href="#b"/>
</mask>
<circle id="c" cx="29.5" cy="101.5" r="9.5"/>
<mask id="f" width="19" height="19" x="0" y="0" fill="white">
<use xlink:href="#c"/>
</mask>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(-14 3)">
<g transform="matrix(-1 0 0 1 271 30)">
<use fill="#D8D8D8" stroke="#373A3C" stroke-width="10" mask="url(#d)" transform="rotate(-15 135.217 96.047)" xlink:href="#a"/>
<polyline stroke="#373A3C" stroke-width="2" points="21 129.5 60.5 150 118.5 146.5 175 137 236.5 103 253.5 75.5"/>
<circle cx="31.5" cy="98.5" r="13.5" fill="#FFFFFF" stroke="#373A3C" stroke-width="2"/>
<circle cx="83.5" cy="110.5" r="13.5" fill="#FFFFFF" stroke="#373A3C" stroke-width="2"/>
<use fill="#373A3C" stroke="#373A3C" stroke-width="2" mask="url(#e)" xlink:href="#b"/>
<use fill="#373A3C" stroke="#373A3C" stroke-width="2" mask="url(#f)" xlink:href="#c"/>
<circle cx="86" cy="113" r="2" fill="#FFFFFF"/>
<circle cx="34" cy="100" r="2" fill="#FFFFFF"/>
</g>
<polygon fill="#FFFFFF" stroke="#373A3C" stroke-width="5" points="296.576 0 391.886 0 417 22.464 417 91.623 391.886 111.304 308.5 111.304 282.089 128 282.089 100.5 271 91.623 271 22.464"/>
<text fill="#000000" font-family="OpenSans-Light, Open Sans" font-size="44" font-weight="300">
<tspan x="307" y="71">404</tspan>
</text>
</g>
</svg> -->
</div>
</template>
<script>
const iconList = require.context('@/assets/svg/', true, /\.svg$/)
iconList.keys().forEach(key => iconList(key))
export default {
name: 'svg-container'
}
</script>
<style lang="scss">
// Remember that font-awesome is included in the projects
#svgContainer {
display: none;
}
#svgContainer * {
stroke: inherit;
fill: inherit;
}
.icon-search {
fill: transparent;
stroke: #9b9d9e;
// This should be the color but the overlapping paths make it not ideal because it is 50% translucent
// stroke: rgba(255, 255, 255, 0.5);
}
.icon-settings {
fill: transparent;
stroke: #9b9d9e;
}
.btn-watchface, .btn-app {
svg.icon-watchface, svg.icon-app {
fill: transparent;
stroke: #828682;
use {
fill: transparent;
color: #828682;
}
}
}
.icon-inverted-thumbs-up {
fill: #FFF;
stroke: #889097;
}
// Select the dark translucent navbar
.navbar-dark.bg-dark.translucent {
// Search, magnifier icon
a.search {
svg {
height: 22px;
margin-top: 3px;
}
}
#categorySelector {
// Watchface/App Selecctor icon styles
.btn-group {
.btn-outline-secondary {
&.active, &:active, &:hover {
//Hover and active styles
svg.icon-watchface, svg.icon-app {
fill: transparent;
stroke: #FFF;
// Watchface Icon
use {
color: #fff;
}
}
}
}
}
}
}
// App columns container
.apps {
.card-columns {
// Modify bootstrap's default .card-columns stylse
// This isn't inside the "a" to avoid issue if it doesn't ends up being inside a "a"
.card {
// Make it smaller on small screens
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
.card-text {
svg.thumbs-up {
height: 14px;
top: 3px;
}
}
}
.card-text {
// Style thumbs up svg inside of app card
svg.thumbs-up {
height: 16px;
margin-right: -6px;
}
}
}
}
}
// Change the font-size of the arrow in the buttons that are located at the bottom of the app-details container
.app-details{
a.app-button {
div {
i {
font-size: 25px
}
}
}
}
// Set styles of SVG in the app-button-container (app-details and app-versions page)
.app-button-container{
.btn {
@media screen and (max-width: 430px) {
// Set SVG size and margin when screen smaller than 430px to avoid breaking all styles
svg {
height: 16px !important;
margin-right: -5px;
}
}
// Set SVG size and margin
svg {
height: 20px;
position: relative;
}
// Set thumbs up SVG styles
&.btn-thumbs-up {
// Default styles are set in the svg
// Thumbs up svg ivon hover styles
.icon-thumbs-up {
fill: #333;
stroke: #ccc;
}
/*
We're triggering the hover on the parent button instead of the SVG.
*/
&.active {
svg {
fill: #ccc;
stroke: #333;
}
}
&.disabled {
// Don't change any style if disabled
&:hover, &.active {
svg {
fill: #333;
stroke: #ccc;
}
}
}
}
// Download button styles
&.btn-download {
// Default styles are set in the svg
// Download svg icon hover styles
.icon-download {
fill: #333;
stroke: #ff4700;
}
&:hover, &.active {
svg {
fill: #ff4700;
stroke: #333;
}
}
}
}
}
</style>
================================================
FILE: src/components/pages/AppDetails.vue
================================================
<template>
<main v-if="Object.entries(app).length !== 0" class="text-center">
<screenshot-list v-bind:screenshots="app.screenshot_images"></screenshot-list>
<div class="card subsection text-left p-3 app-details">
<h1>Description</h1> <hr>
<pre class="description">{{ app.description }}</pre>
<table class="extra-table">
<tr>
<td>Developer</td>
<td>{{ app.author }}</td>
</tr>
<tr>
<td>Category</td>
<td>
<router-link v-if="app.type !== 'watchapp'" v-bind:to="'/collection/' + app.category_id"><span class="badge badge-pill badge-pebble">{{ app.category }}</span></router-link>
<span v-if="app.type === 'watchapp'" class="badge badge-pill badge-pebble">{{ app.category }}</span>
</td>
</tr>
<tr v-if="app.latest_release">
<td>Updated</td>
<td>{{ app.latest_release.published_date | formatDate }}</td>
</tr>
<tr v-if="app.latest_release">
<td>Version</td>
<td>{{ app.latest_release.version }}</td>
</tr>
</table>
<router-link v-if="app.changelog.length > 0" v-bind:to="'/app/' + $route.params.id + '/versions/'" class="app-button">
<div>
Version Information <i class="fa fa-angle-right float-right" aria-hidden="true"></i>
</div>
</router-link>
<a v-if="app.website != null" v-on:click="openExternal(app.website)" class="app-button">
<div>
Website Link <i class="fa fa-angle-right float-right" aria-hidden="true"></i>
</div>
</a>
<!--a v-if="app.appInfo.supportUrl != ''" v-bind:href="app.appInfo.supportUrl" class="app-button">
<div>Support <i class="fa fa-angle-right float-right" aria-hidden="true"></i>
</div>
</a-->
<a v-if="app.source != null" v-on:click="openExternal(app.source)" class="app-button" >
<div>Source code <i class="fa fa-angle-right float-right" aria-hidden="true"></i></div>
</a>
<router-link v-bind:to="'/author/' + app.developer_id" class="app-button">
<div>More From This Developer<i class="fa fa-angle-right float-right" aria-hidden="true"></i>
</div>
</router-link>
<a v-if="app.latest_release && app.latest_release.pbw_file != '' && $store.state.userParameters.devMode" v-on:click="openExternal(app.latest_release.pbw_file)" class="app-button">
<div>Download .pbw<i class="fa fa-angle-right float-right" aria-hidden="true"></i>
</div>
</a>
</div>
</main>
</template>
<script>
import ScreenshotList from './widgets/ScreenshotList'
export default {
name: 'app-details',
components: {
ScreenshotList
},
props: {
app: {
default: null
},
clientWatchPlatform: {
default: null
}
},
watch: {
'app' (to, from) {
this.setTitle(this.app.title)
}
},
beforeMount: function () {
if (this.app.title !== undefined) {
this.setTitle(this.app.title)
}
}
}
</script>
<style lang="scss" scoped>
// App details container (below screenshots)
.app-details {
margin-bottom: 0;
}
</style>
================================================
FILE: src/components/pages/AppVersions.vue
================================================
<template>
<main class="text-center">
<div class="card subsection text-left p-3 app-details" v-for="(changelog, index) in app.changelog" v-bind:key="index">
<h2>Version {{ changelog.version }}</h2> <h3 class="float-right">{{ changelog.published_date | formatDate }}</h3><hr>
<pre class="description">{{ changelog.release_notes }}</pre>
</div>
</main>
</template>
<script>
import AppTitleBar from './widgets/AppTitleBar'
export default {
name: 'app-version',
components: {
AppTitleBar
},
props: {
app: {}
},
data: function () {
return {
versions: {
'versions': []
}
}
},
beforeMount: function () {
if (this.app.title !== undefined) {
this.setTitle(`${this.app.title} Versions`)
}
},
watch: {
'app' (to, from) {
this.setTitle(`${this.app.title} Versions`)
}
}
}
</script>
<style lang="scss" scoped>
// App details container (below screenshots)
.app-details {
&:last-of-type {
margin-bottom: 0;
}
}
</style>
================================================
FILE: src/components/pages/AppView.vue
================================================
<template>
<section v-bind:class="app.type" >
<header class="main" v-bind:class="($store.state.userParameters.inApp && !app.header_images) ? 'inApp no-banner': ''">
<slider v-if="app.header_images != ''" v-bind:banners="app.header_images"></slider>
</header>
<app-title-bar v-bind:app="app" v-bind:class="($store.state.userParameters.inApp && !app.header_images) ? 'title-bar extra-margin': ''"></app-title-bar>
<router-view v-bind:app="app" ></router-view>
</section>
</template>
<script>
import AppTitleBar from './widgets/AppTitleBar'
import Slider from './widgets/AppSlider'
import ScreenshotList from './widgets/ScreenshotList'
import { Native } from '../../services'
export default {
name: 'app-view',
components: {
AppTitleBar,
ScreenshotList,
Slider
},
data: function () {
return {
app: {}
}
},
methods: {
get_app: function (id) {
this.$http.get(this.buildResourceUrl(`apps/id/${id}`)).then(response => {
this.app = response.body.data[0]
if (this.$store.state.userParameters.inApp) {
Native.send('setVisibleApp', this.app)
}
}, response => {
console.error(response)
})
}
},
beforeMount: function () {
this.get_app(this.$route.params.id)
}
}
</script>
<style lang="scss">
// _app-details.scss
// App details page styles
// Similar to carousel but only used when displaying only one image
header.main {
&.inApp.no-banner {
padding: 0;
display: none;
min-height: 0;
}
min-height: 90px;
.app-banner {
margin-bottom: -15px;
max-width: 720px;
max-height: 320px;
margin-left: auto;
margin-right: auto;
img {
width: 100%;
max-width: 720px;
}
}
}
.title-bar.extra-margin {
margin-top: 43px;
}
// App details container (below screenshots)
.app-details {
margin-bottom: 0;
// Main title
h1 {
font-size: 20px;
margin: none;
}
// Separator
hr {
margin-top: 4px;
}
// H2 and H3 used in app-versions
h2 {
font-size: 16px;
display: inline;
}
h3 {
display: inline;
font-size: 14px;
margin-bottom: 0;
margin-top: 4px;
}
// App Description container
pre.description {
// Make sentences break and prevent scrollbars
word-wrap: break-word;
white-space: pre-wrap;
// Change app description font and weight
font-family: 'Raleway', sans-serif;
font-weight: 400;
}
// App extra-info table (Author, Version, Release date, etc...)
table {
margin-top: 30px;
margin-bottom: 30px;
tr td {
padding-right: 10px;
font-family: 'Open Sans', sans-serif;
}
tr td:last-child {
padding-right: 0;
// A second font to make it look different from the first column
font-family: 'Raleway', sans-serif;
}
}
// Buttons at the bottom of the app-details container
a.app-button {
div {
width: calc(100% + 2rem);
padding: 1rem;
margin-left: -16px;
margin-right: -16px;
border-top: 1px solid #e1e1e1;
// Change the fonts from the buttons at the bottom of the app-details container
font-family: 'Open Sans', sans-serif;
}
color: $pebble-color;
&:last-child {
div {
margin-bottom: -1rem;
}
}
&:hover, &:focus {
text-decoration: none;
outline: none;
}
}
}
</style>
================================================
FILE: src/components/pages/Author.vue
================================================
<template>
<div>
<header class="main">
<div v-if="page.data !== undefined" class="title-card">
<h3>Apps by: {{ page.data[0].author }}</h3>
</div>
</header>
<main class="apps container text-center">
<card-collection :showTop="false" v-bind:cards="page.data"></card-collection>
<pagination v-bind:links="page.links" v-bind:baseUrl="`/author/${this.$route.params.id}`"></pagination>
</main>
</div>
</template>
<script>
import CardCollection from './widgets/CardCollection'
import Pagination from './widgets/Pagination'
export default {
name: 'author',
components: {
CardCollection,
Pagination
},
data: function () {
return {
page: {},
pageLimit: 6
}
},
watch: {
'$route' (to, from) {
this.get_data()
}
},
methods: {
get_author: function (id, offsetPage) {
var offset = this.pageLimit * (offsetPage - 1)
this.$http.get(`${this.buildResourceUrl(`apps/dev/${id}`)}&offset=${offset}&limit=${this.pageLimit}`).then(response => {
this.page = response.body
this.setTitle(this.page.data[0].author)
}, response => {
console.error(response)
})
},
get_data: function () {
this.get_author(this.$route.params.id, this.$route.params.page)
}
},
beforeMount: function () {
this.get_data()
}
}
</script>
<style lang="scss" scoped>
</style>
================================================
FILE: src/components/pages/Category.vue
================================================
<template>
<div>
<header class="main">
<div class="title-card">
<h3>{{id | readable-name}}</h3>
</div>
</header>
<main class="apps container text-center">
<div class="text-center header-tool">
<div class="btn-group btn-group-sm" role="group">
<router-link v-bind:to="'/category/' + id + '/hearts/1'" v-bind:class="sort == 'hearts' ? 'btn btn-outline-secondary active': 'btn btn-outline-secondary'" role="button">Most Liked</router-link>
<router-link v-bind:to="'/category/' + id + '/updated/1'" v-bind:class="sort == 'updated' ? 'btn btn-outline-secondary active': 'btn btn-outline-secondary'" role="button">Recently Added</router-link>
</div>
</div>
<card-collection :showTop="false" v-bind:cards="page.data"></card-collection>
<pagination v-bind:links="page.links" v-bind:baseUrl="`/category/${this.id}/${this.sort}`"></pagination>
</main>
</div>
</template>
<script>
import CardCollection from './widgets/CardCollection'
import Pagination from './widgets/Pagination'
export default {
name: 'category',
components: {
CardCollection,
Pagination
},
data: function () {
return {
page: {},
id: '',
sort: '',
offsetPage: 1,
pageLimit: 24
}
},
watch: {
'$route' (to, from) {
this.get_data(to.params)
}
},
methods: {
get_category: function () {
var that = this
var offset = this.pageLimit * (this.offsetPage - 1)
this.$http.get(`${this.buildResourceUrl(`apps/category/${this.id}`)}&sort=${this.sort}&offset=${offset}&limit=${this.pageLimit}`).then(response => {
that.page = response.body
}, response => {
console.error(response)
})
},
get_data: function (routeParams) {
this.id = routeParams.id
this.sort = routeParams.sort
this.offsetPage = routeParams.page
this.setTitle(this.$options.filters['readable-name'](this.id))
this.get_category()
}
},
beforeMount: function () {
this.get_data(this.$route.params)
}
}
</script>
<style lang="scss" scoped>
.header-tool {
margin-bottom: 40px;
}
</style>
================================================
FILE: src/components/pages/Collection.vue
================================================
<template>
<div>
<header class="main">
<div class="title-card">
<h3>Collection: {{slug | readable-name}}</h3>
</div>
</header>
<main class="apps container text-center">
<card-collection :showTop="false" v-bind:cards="page.data"></card-collection>
<pagination v-bind:links="page.links" v-bind:baseUrl="`/${this.type}/${this.slug}`"></pagination>
</main>
</div>
</template>
<script>
import CardCollection from './widgets/CardCollection'
import Pagination from './widgets/Pagination'
export default {
name: 'collection',
components: {
CardCollection,
Pagination
},
data: function () {
return {
page: {},
slug: '',
sort: '',
offsetPage: 1,
pageLimit: 24,
type: ''
}
},
watch: {
'$route' (to, from) {
this.get_data(to.params)
}
},
methods: {
get_collection: function () {
var offset = this.pageLimit * (this.offsetPage - 1)
this.$http.get(`${this.buildResourceUrl(`apps/collection/${this.slug}/${this.type}`)}&offset=${offset}&limit=${this.pageLimit}`).then(response => {
this.page = response.body
}, response => {
console.error(response)
})
},
get_data: function (routeParams) {
this.slug = routeParams.slug
this.offsetPage = routeParams.page
this.type = routeParams.type
this.setTitle(this.$options.filters['readable-name'](this.slug))
this.get_collection()
}
},
beforeMount: function () {
this.get_data(this.$route.params)
}
}
</script>
<style lang="scss" scoped>
</style>
================================================
FILE: src/components/pages/Error.vue
================================================
<template>
<div>
<main class="container text-xs-center">
<section>
<div class="page-error page-error--404">
<h2>Our pet rock has lost your page, sorry about that</h2>
<svg class="pet-rock-pebble" width="406" height="201" viewBox="0 0 406 201">
<g fill="none" fill-rule="evenodd" transform="translate(2 2)">
<polygon fill="#D8D8D8" stroke="#373A3C" stroke-width="4" points="131.999 196.638 216.6 196.071 237.338 165.014 230.162 125.311 201.731 103.267 177.694 75.611 137.498 61.394 87.765 63.716 57.078 58.349 27.154 69.348 .545 104.406 15.774 147.016 73.715 179.99"/>
<polyline stroke="#373A3C" stroke-width="2" points="237.049 166.139 208.87 183.73 134.959 181.099 79.012 166.139 27.964 139.016 2.519 105.431"/>
<g transform="translate(159 127)">
<circle cx="13.5" cy="13.5" r="13.5" fill="#FFFFFF" stroke="#373A3C" stroke-width="2" transform="matrix(-1 0 0 1 27 0)"/>
<circle cx="16.5" cy="17.5" r="9.5" fill="#373A3C" stroke="#373A3C" stroke-width="2" transform="matrix(-1 0 0 1 33 0)"/>
<circle cx="11" cy="16" r="2" fill="#FFFFFF" transform="matrix(-1 0 0 1 22 0)"/>
</g>
<g transform="translate(211 115)">
<circle cx="13.5" cy="13.5" r="13.5" fill="#FFFFFF" stroke="#373A3C" stroke-width="2" transform="matrix(-1 0 0 1 27 0)"/>
<circle cx="15.5" cy="16.5" r="9.5" fill="#373A3C" stroke="#373A3C" stroke-width="2" transform="matrix(-1 0 0 1 31 0)"/>
<circle cx="11" cy="15" r="2" fill="#FFFFFF" transform="matrix(-1 0 0 1 22 0)"/>
</g>
<g transform="translate(256)">
<polygon fill="#FFFFFF" stroke="#373A3C" stroke-width="4" points="25.576 0 120.886 0 146 22.464 146 91.623 120.886 111.304 37.5 111.304 11.089 128 11.089 100.5 0 91.623 0 22.464"/>
<text fill="#000000" transform="translate(36 24)" font-size="44" font-family="OpenSans-Light, Open Sans" font-weight="300">
<tspan x="0" y="47">404</tspan>
</text>
</g>
</g>
</svg>
<h4>We're getting the following message {{ 404 }}</h4>
<div class="page-error_buttons">
<button v-on:click="reload()" class="btn btn-outline-secondary">Try again</button>
<router-link v-bind:to="'/'" class="btn btn-outline-pebble">Back to home</router-link>
</div>
</div>
</section>
</main>
</div>
</template>
<script>
export default {
name: 'error',
data: function () {
return {
reload () {
this.$router.go(0)
}
}
},
beforeMount: function () {
this.setTitle('404 Page not found')
}
}
</script>
<style lang="scss" scoped>
// _error.scss
// Error page styles
.page-error {
// Calculate top margin 58 px ar of the navbar
margin-top: 40px + 58px;
min-height: 400px;
height: 80vh;
display: flex;
flex-direction: column;
max-width: 36rem;
margin-left: auto;
margin-right: auto;
align-items: center;
justify-content: center;
h4 {
// Change font to make this page more interesting
font-family: 'Raleway', sans-serif;
font-weight: 400;
}
.pet-rock-pebble {
// Mascot svg
margin-top: 3rem;
margin-bottom: 3rem;
}
.page-error_buttons {
// Minimal style changes to buttons
margin-top: 3rem;
.btn-outline-pebble {
&:hover {
color: #f4f3f4;
}
}
}
}
</style>
================================================
FILE: src/components/pages/Search.vue
================================================
<template>
<ais-instant-search :search-client="rebbleSearch" index-name="rebble-appstore-production" :routing="routing">
<ais-configure :hits-per-page.camel="24" :tag-filters.camel="build_filter_list()" />
<div>
<header class="main">
<div class="title-card search">
<ais-search-box>
<input autofocus placeholder="Search" type="search" autocorrect="off" autocapitalize="off" autocomplete="off" spellcheck="false" slot-scope="{ currentRefinement, refine }" :value="currentRefinement" @input="refine($event.currentTarget.value)">
</ais-search-box>
</div>
</header>
<main class="apps container text-center">
<div class="text-center header-tool">
<div class="btn-group btn-group-sm" role="group">
<router-link v-bind:to="'/faces/search'" v-bind:class="type == 'faces' ? 'btn btn-outline-secondary active': 'btn btn-outline-secondary'" role="button">Watchfaces</router-link>
<router-link v-bind:to="'/apps/search'" v-bind:class="type == 'apps' ? 'btn btn-outline-secondary active': 'btn btn-outline-secondary'" role="button">Apps</router-link>
</div>
</div>
<ais-state-results>
<template slot-scope="{ hits }">
<ais-hits v-if="hits.length > 0">
<card-collection slot-scope="{ items }" :showTop="false" v-bind:cards="items" v-bind:searchData="true"></card-collection>
</ais-hits>
<nav v-if="hits.length > 0">
<ais-pagination :classNames="{
'ais-Pagination-list': 'pagination',
'ais-Pagination-item': 'page-item',
'ais-Pagination-link': 'page-link',
'ais-Pagination-item--selected': 'active',
'ais-Pagination-item--disabled': 'disabled'
}" />
</nav>
<div v-if="hits.length <= 0">
No results were found.
</div>
</template>
</ais-state-results>
</main>
</div>
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite'
import { searchRouting } from '../../router/search-router'
import { simple as simpleMapping } from 'instantsearch.js/es/lib/stateMappings'
import { platformEnum, hardwareEnum } from '../../store/userParameters'
export default {
name: 'search',
props: {
type: {
type: String,
default: 'faces'
}
},
data () {
return {
rebbleSearch: algoliasearch(
'7683OW76EQ',
'252f4938082b8693a8a9fc0157d1d24f'
),
routing: {
router: searchRouting.router,
stateMapping: simpleMapping()
}
}
},
methods: {
build_filter_list: function () {
var filterList = []
if (this.$store.state.userParameters.platform !== platformEnum.all) {
filterList.push(this.$store.state.userParameters.platform)
}
if (this.$store.state.userParameters.hardware !== hardwareEnum.all) {
filterList.push(this.$store.state.userParameters.hardware)
}
if (this.type === 'faces' || this.type === 'watchfaces') {
filterList.push('(watchface)')
} else if (this.type === 'apps' || this.type === 'watchapps') {
filterList.push('(watchapp,companion-app)')
}
return filterList.join(',')
}
},
beforeMount: function () {
this.setTitle('Search')
}
}
</script>
<style lang="scss" scoped>
.title-card {
padding: 0;
}
.search {
height: 70px;
input {
margin: -20px;
font-family: 'Open Sans', sans-serif;
font-weight: 400;
width: calc(100% + 40px);
height: 70px;
font-size: 1.75rem;
padding: 20px;
border: 0;
background: none;
color: #373a3c;
&::-webkit-search-decoration,
&::-webkit-search-cancel-button,
&::-webkit-search-results-button,
&::-webkit-search-results-decoration { display: none; }
&:focus {
outline: none;
}
}
}
.header-tool {
margin-bottom: 40px;
}
.ais-Pagination {
margin-top: 40px
}
</style>
================================================
FILE: src/components/pages/Settings.vue
================================================
<template>
<div>
<header class="main">
<div class="title-card">
<h3>Pebble Type</h3>
</div>
</header>
<main class="text-center">
<form>
<input type="radio" id="aplite" value="aplite" v-model="hardware"> <label for="aplite">Pebble
Original/Steel (aplite)</label><br/>
<input type="radio" id="basalt" value="basalt" v-model="hardware"> <label for="basalt">Pebble Time and Time Steel
(basalt)</label><br/>
<input type="radio" id="chalk" value="chalk" v-model="hardware"> <label for="chalk">Pebble Time Round
(chalk)</label><br/>
<input type="radio" id="diorite" value="diorite" v-model="hardware"> <label for="diorite">Pebble 2
(diorite)</label><br/>
</form>
</main>
</div>
</template>
<script>
export default {
name: 'settings',
data: function () {
return {
hardware: this.$store.state.userParameters.hardware
}
},
watch: {
hardware: function (p) {
this.hardware = p
this.$store.state.userParameters.hardware = p
window.localStorage.setItem('hardware', p)
}
},
beforeMount: function () {
this.setTitle('Settings')
}
}
</script>
<style lang="scss" scoped>
main {
margin-top: 40px;
}
</style>
================================================
FILE: src/components/pages/widgets/AppSlider.vue
================================================
<template>
<div v-if="banners != null && banners[0] != null" id="banner-carousel" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators" v-if="banners[1] != null">
<li v-for="(banner, index) in banners" v-bind:key="index" data-target="#banner-carousel" v-bind:data-slide-to="index" v-bind:class="index == 0 ? 'active' : ''"></li>
</ol>
<div class="carousel-inner" role="listbox">
<div v-bind:class="index == 0 ? 'carousel-item active' : 'carousel-item'" v-for="(banner, index) in banners" v-bind:key="index"><single-banner v-bind:bannerSrc="banner['720x320']"></single-banner></div>
</div>
<a v-if="banners[1] != null" class="carousel-control carousel-control-prev" href="#banner-carousel" role="button" data-slide="prev">
<i class="fa fa-angle-left" aria-hidden="true"></i>
<span class="sr-only">Previous</span>
</a>
<a v-if="banners[1] != null" class="carousel-control carousel-control-next" href="#banner-carousel" role="button" data-slide="next">
<i class="fa fa-angle-right" aria-hidden="true"></i>
<span class="sr-only">Next</span>
</a>
</div>
</template>
<script>
import SingleBanner from './SingleBanner'
export default {
name: 'slider',
components: {
SingleBanner
},
props: {
banners: null
}
}
</script>
<style lang="scss" scoped>
.carousel {
max-width: 720px;
max-height: 320px;
margin-left: auto;
margin-right: auto;
background-color: #000;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
.carousel-indicators {
// Slide indicators, make them smaller with browser resize
margin-bottom: 0;
margin-left: 30%;
margin-right: 30%;
}
.carousel-control {
i {
// carousel control < > icons
font-size: 30px;
}
}
.carousel-item.active {
display: block;
}
}
</style>
================================================
FILE: src/components/pages/widgets/AppTitleBar.vue
================================================
<template>
<!-- Fix url args -->
<div v-if="Object.entries(app).length !== 0" v-bind:class="(this.$store.state.userParameters.inApp) ? 'app-title-bar-cont sticky-top': 'app-title-bar-cont'">
<div class="card subsection-inverse card-inverse text-left p-3 app-title-bar">
<img class="app-icon" v-if="app.icon_image != null && app.icon_image['48x48'] != ''" v-bind:src="app.icon_image['48x48']">
<div v-bind:class="app.icon_image ? 'title-author app' : 'title-author face'">
<h1 class="tile">{{ app.title }}</h1>
<h2 class="author">{{ app.author }}</h2>
</div>
<div class="app-button-container float-right">
<button type="button" v-bind:class="heartClass" v-on:click="toggle_heart_button_state" ref="heartsButton">
<svg class="svg-icon icon-thumbs-up" width="25px" height="25px" viewBox="0 0 25 25">
<use xlink:href="#iconThumbsUp"></use>
</svg>
{{ hearts }}
</button>
<get-app-button v-bind:app="app" v-bind:state="added" ref="addButton"></get-app-button>
</div>
</div>
<div class="card subsection-extra card-inverse text-left p-2" v-if=" app.companions != undefined && (app.companions.ios != null || app.companions.android != null) && app.type != 'watchface'">
<h2 v-if="app.type === 'companion-app'">Requires Companion</h2>
<h2 v-if="app.type !== 'companion-app'">Companion</h2>
<div class="ml-auto">
<a v-if="app.companions.ios" v-bind:href="app.companions.ios.url" target="_blank">
<svg class="app-icon" width="22px" height="22px">
<use xlink:href="#iconApple"></use>
</svg>
</a>
<h2 v-if="app.companions.ios && app.companions.android">
+
</h2>
<a v-if="app.companions.android" v-bind:href="app.companions.android.url" target="_blank">
<svg class="app-icon" width="22px" height="22px">
<use xlink:href="#iconAndroid"></use>
</svg>
</a>
</div>
</div>
</div>
</template>
<script>
import GetAppButton from './GetAppButton'
export default {
name: 'ScreenshotList',
components: {
GetAppButton
},
props: ['app', 'urlArguments'],
data: function () {
return {
heartClass: 'btn btn-outline-secondary btn-thumbs-up disabled',
hearts: 0,
hearted: false,
added: false,
flagged: false
}
},
methods: {
get_user_data: function (id) {
if (this.$store.state.secure.accessToken !== '' && this.$store.state.secure.accessToken != null) {
this.$http.get(this.$store.state.config.devPortalBackendUrl + '/users/me', {headers: {Authorization: 'Bearer ' + this.$store.state.secure.accessToken}}).then(response => {
let userInfo = response.body.users[0]
this.added = !(!userInfo || !~userInfo.added_ids.indexOf(id))
this.hearted = !(!userInfo || !~userInfo.voted_ids.indexOf(id))
this.flagged = !(!userInfo || !~userInfo.flagged_ids.indexOf(id))
console.log(this.added)
this.build_hearts_class()
}, response => {
console.error(response)
})
}
},
change_heart: function (operation) {
this.$http.post(this.$store.state.config.devPortalBackendUrl + '/applications/' + this.app.id + '/' + operation + '_heart', null, {headers: {Authorization: 'Bearer ' + this.$store.state.secure.accessToken}}).then(response => {
if (operation === 'add') {
this.hearts++
this.hearted = true
} else {
this.hearts--
this.hearted = false
}
this.build_hearts_class()
}, response => {
console.error(response)
if (operation === 'add') {
this.hearted = false
} else {
this.hearted = true
}
this.build_hearts_class()
})
},
toggle_heart_button_state: function () {
if (this.$store.state.secure.accessToken !== null) {
if (this.hearted) {
this.change_heart('remove')
} else {
this.change_heart('add')
}
this.build_hearts_class()
}
},
build_hearts_class: function () {
if (this.$store.state.secure.accessToken !== '' && this.$store.state.secure.accessToken != null) {
if (this.hearted) {
this.heartClass = 'btn btn-outline-secondary btn-thumbs-up active'
} else {
this.heartClass = 'btn btn-outline-secondary btn-thumbs-up'
}
} else {
this.heartClass = 'btn btn-outline-secondary btn-thumbs-up disabled'
}
}
},
watch: {
'app' (to, from) {
this.hearts = this.app.hearts
}
},
beforeMount: function () {
if (this.app.hearts !== undefined) {
this.hearts = this.app.hearts
}
this.get_user_data(this.$route.params.id)
}
}
</script>
<style lang="scss">
// Title bar displayed below app banner
.app-title-bar {
display: flex !important;
flex-direction: row;
img {
border-radius: 4px;
width: 42px;
min-width: 42px;
height: 42px;
margin-right: 5px;
}
// Author name and app title text container
.title-author {
height: 45px;
margin-top: -3px;
margin-bottom: 0;
display: inline-block;
max-width: 493px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
/*@media screen and (max-width: 430px) {
width: calc(100% - 160px);
}*/
//width: calc(100% - 220px);
width: 100%;
h1 {
font-size: 19px;
line-height: 26px;
display: inline-block;
color: #fff;
margin-bottom: 0;
}
h2 {
line-height: 16px;
font-size: 16px;
color: #c3c3c3;
margin: 0;
}
// Styles for small screens
@media screen and (max-width: 430px) {
h1 {
font-size: 15px;
}
h2 {
font-size: 12px;
}
}
@media screen and (max-width: 320px) {
h1 {
font-size: 12px;
}
h2 {
font-size: 10px;
}
}
}
// Set styles of buttons in the app-button-container (app-details and app-versions page)
.app-button-container {
margin-left: auto;
padding-left: 5px;
margin-top: 2px;
margin-bottom: 2px;
min-width: 206px;
text-align: right;
@media screen and (max-width: 430px) {
min-width: 156px;
}
.btn {
@media screen and (max-width: 430px) {
// styles for when screen smaller than 430px to avoid breaking all styles (they make things smaller)
margin-top: 3px;
font-size: 0.7rem;
padding: .5rem .5rem;
}
&.btn-download {
margin-left: 2px;
}
// Set thumbs up button styles
&.btn-thumbs-up {
color: #ccc;
border-color: #ccc;
cursor: hand;
// Styles for when it is in focus, hovered, or active
&:hover, &:active {
color: #ccc;
border-color: #ccc;
background: none;
}
&.active {
color: #333;
outline: none;
background: #ccc;
}
&.disabled:hover {
// Don't change any style if disabled
color: #ccc;
background: none;
}
}
}
}
}
.card.subsection-extra {
padding-left: 30px !important;
padding-right: 30px !important;
display: flex;
flex-direction: row;
h2 {
font-size: 16px;
line-height: 26px;
display: inline-block;
margin: 0;
}
.app-icon {
margin-top: -4px;
width: 22px;
vertical-align: middle;
color: #333;
fill: currentColor
}
}
</style>
================================================
FILE: src/components/pages/widgets/CardCollection.vue
================================================
<template>
<section class="text-center">
<div class="header" v-if="showTop">
<h6 class="text-left">
<div class="pebble">{{ elTitle }}</div>
</h6>
<small><router-link v-bind:to="allUrl" class="text-right">See All ></router-link></small>
</div>
<div class="card-columns">
<single-card v-for="(card, index) in cards" v-bind:card="card" v-bind:key="index" v-bind:searchData="searchData"></single-card>
</div>
</section>
</template>
<script>
import SingleCard from './SingleCard'
export default {
name: 'card-collection',
props: {
elTitle: {
type: String,
default: 'Collection Title'
},
showTop: {
type: Boolean,
default: true
},
cards: {
cards: []
},
allUrl: {
type: String,
default: ''
},
searchData: {
type: Boolean,
default: false
}
},
components: {
SingleCard
}
}
</script>
<style lang="scss" scoped>
// Each group of cards is supposed to be a section
section {
// Add a margin to the bottom of each section
margin-bottom: 40px;
max-width: 850px;
margin-left: auto;
margin-right: auto;
&:last-child {
// Remove unnecesary margin from the last one
margin-bottom: 0;
}
// The header is the title of each section
.header {
height: 30px;
margin-bottom: 10px;
h6 {
float: left;
display: inline;
margin-right: 10px;
margin-bottom: 0;
}
a {
color: #696969;
float: right;
&:hover, &:focus {
text-decoration: none;
outline: none;
}
}
}
.card-columns {
// Modify bootstrap's default .card-columns styles
max-width: 800px;
margin-left: auto;
margin-right: auto;
margin-bottom: -12px;
// Add more columns on large screens
@media screen and (min-width: map-get($grid-breakpoints, lg)) {
column-count: 4;
}
// Remove gaps on smaller screens
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
column-gap: 5px;
}
column-count: 3;
column-gap: calc(1.25rem + 5px);
}
}
</style>
================================================
FILE: src/components/pages/widgets/GetAppButton.vue
================================================
<template>
<span>
<a v-bind:href="'pebble://appstore/' + app.id" class="btn btn-outline-pebble btn-download" v-if="$store.state.userParameters.inApp !== true && ($store.state.userParameters.platform === 'all' || app.compatibility[$store.state.userParameters.platform].supported === true)">
<svg class="svg-icon icon-download" width="25px" height="25px" viewBox="0 0 25 25">
<use xlink:href="#iconDownload"></use>
</svg>
GET
</a>
<button v-on:click="check_app" class="btn btn-outline-pebble btn-download" :class="added || loading ? 'active': ''|| (added === null || !hardwareSupported || !platformSupported)?'disabled':''" v-if="$store.state.userParameters.inApp === true">
<svg class="svg-icon icon-download" width="25px" height="25px" viewBox="0 0 25 25">
<use xlink:href="#iconDownload"></use>
</svg>
{{(loading)?'...':'GET'}}
</button>
<b-modal id="modal-permissions" centered title="Pebble Permissions" ok-title="Accept" cancel-title="Reject" @ok="add_app()">
<p>"{{app.title}}" is requesting access to the following services.</p>
<b-list-group class="my-4">
<b-list-group-item v-for="(item, index) in permissions" :key="index">{{ item }}</b-list-group-item>
</b-list-group>
<p>If you tap on accept you will grant "{{app.title}}" access to read and use your data.</p>
</b-modal>
<b-modal id="modal-companion" centered title="Companion Required" ok-title="Get" cancel-title="Cancel" @ok="get_companion()">
<p class="my-4">"{{app.title}}" requires a companion a to be used.</p>
</b-modal>
</span>
</template>
<script>
import { Native } from '../../../services'
export default {
name: 'get-app-button',
components: {
},
props: {
app: {
default: null
},
state: {
default: null
}
},
data: function () {
return {
loading: false,
added: false,
hardwareSupported: false,
platformSupported: false,
permissions: []
}
},
methods: {
add_app () {
this.loading = true
Native.send('loadAppToDeviceAndLocker', {
id: this.app.id,
uuid: this.app.uuid,
title: this.app.title,
list_image: this.app.list_image && this.app.list_image['144x144'],
icon_image: this.app.icon_image && this.app.icon_image['48x48'],
screenshot_image: this.app.screenshot_images && this.app.screenshot_images[0] && this.app.screenshot_images[0][Object.keys(this.app.screenshot_images[0])[0]],
type: this.app.type,
pbw_file: this.app.latest_release.pbw_file,
links: {
add: this.$store.state.backendUrl + '/applications/' + this.app.id + '/add',
remove: this.$store.state.backendUrl + '/applications/' + this.app.id + '/remove',
share: 'http://apps.rebble.io/app/' + this.app.id
}
}, (res) => {
if (res.added_to_locker) {
this.loading = false
this.added = true
}
})
},
get_companion () {
this.openExternal(this.app.companions[this.$store.state.userParameters.platform].url)
},
check_app () {
if (!this.hardwareSupported || !this.platformSupported || this.added === true || this.loading) return
if (this.permissions.length > 0) {
this.$bvModal.show('modal-permissions')
return
}
if (this.app.type === 'companion-app') {
this.$bvModal.show('modal-companion')
return
}
this.add_app()
},
check_supported () {
this.hardwareSupported = this.$store.state.userParameters.hardware === 'all' || (this.app.compatibility && this.app.compatibility[this.$store.state.userParameters.hardware] !== undefined && this.app.compatibility[this.$store.state.userParameters.hardware].supported === true)
this.platformSupported = this.$store.state.userParameters.platform === 'all' || this.app.compatibility[this.$store.state.userParameters.platform].supported
},
create_permissions () {
if (this.app.capabilities != null) {
this.permissions = this.app.capabilities
this.permissions.splice(this.permissions.indexOf('configurable'))
}
}
},
watch: {
'state' (to, from) {
this.added = this.state
},
'app' (to, from) {
this.check_supported()
this.create_permissions()
}
},
beforeMount: function () {
this.added = this.state
if (this.app !== null) {
this.check_supported()
this.create_permissions()
}
}
}
</script>
<style lang='scss' scoped>
</style>
================================================
FILE: src/components/pages/widgets/HomeSlider.vue
================================================
<template>
<header class="main">
<div id="banner-carousel" class="carousel slide" data-ride="carousel" v-if="banners != null && banners[0] != null">
<ol class="carousel-indicators">
<li v-for="(banner, index) in banners" v-bind:key="index" data-target="#banner-carousel" v-bind:data-slide-to="index" v-bind:class="index == 0 ? 'active' : ''"></li>
</ol>
<div class="carousel-inner" role="listbox">
<div v-bind:class="index == 0 ? 'carousel-item active' : 'carousel-item'" v-for="(banner, index) in banners" v-bind:key="index"><router-link v-bind:alt="banner.title" v-bind:to="'/app/' + banner.application_id"><single-banner v-bind:bannerSrc="banner.image['720x320']"></single-banner></router-link></div>
</div>
<a class="carousel-control carousel-control-prev" href="#banner-carousel" role="button" data-slide="prev">
<i class="fa fa-angle-left" aria-hidden="true"></i>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control carousel-control-next" href="#banner-carousel" role="button" data-slide="next">
<i class="fa fa-angle-right" aria-hidden="true"></i>
<span class="sr-only">Next</span>
</a>
</div>
</header>
</template>
<script>
import SingleBanner from './SingleBanner'
export default {
name: 'slider',
components: {
SingleBanner
},
props: {
banners: null
}
}
</script>
<style lang="scss" scoped>
.carousel {
max-width: 720px;
max-height: 320px;
margin-left: auto;
margin-right: auto;
background-color: #000;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
.carousel-indicators {
// Slide indicators, make them smaller with browser resize
margin-bottom: 0;
margin-left: 30%;
margin-right: 30%;
}
.carousel-control {
i {
// carousel control < > icons
font-size: 30px;
}
}
.carousel-item.active {
display: block;
}
}
</style>
================================================
FILE: src/components/pages/widgets/Pagination.vue
================================================
<template>
<nav v-if="links != undefined">
<ul class="pagination">
<li v-bind:class="this.$route.params.page > 1 ? 'page-item': 'page-item disabled'">
<router-link class="page-link" v-bind:to="`${baseUrl}/${+(this.$route.params.page) - 1}`" tabindex="-1" aria-label="Previous">
<span aria-hidden="true"><i class="fa fa-angle-left" aria-hidden="true"></i></span> Previous
<span class="sr-only">Previous</span>
</router-link>
</li>
<li class="page-item" v-bind:class="links.nextPage != null ? 'page-item': 'page-item disabled'">
<router-link class="page-link" v-bind:to="`${baseUrl}/${+(this.$route.params.page) + 1}`" aria-label="Next">
Next <span aria-hidden="true"><i class="fa fa-angle-right" aria-hidden="true"></i></span>
<span class="sr-only">Next</span>
</router-link>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: 'pagination',
components: {
},
props: {
links: {
default: undefined
},
baseUrl: {
default: ''
}
}
}
</script>
<style lang="scss" scoped>
</style>
================================================
FILE: src/components/pages/widgets/ScreenshotList.vue
================================================
<template>
<div v-dragscroll.x="!$store.state.userParameters.inApp" class="screenshots">
<div id="scrollbar" v-bind:style="scrollStyle">
<single-screenshot v-for="(screenshot, index) in screenshots" v-bind:key="index" v-bind:screenshotSrc="screenshot"></single-screenshot>
</div>
</div>
</template>
<script>
import {dragscroll} from 'vue-dragscroll'
import SingleScreenshot from './SingleScreenshot'
export default {
name: 'ScreenshotList',
directives: {
dragscroll
},
components: {
SingleScreenshot
},
props: ['screenshots'],
computed: {
scrollStyle: function () {
var screenshotsCount = this.screenshots.length
return {'width': 'calc(100% + (' + screenshotsCount + ' - 1) * 184px)'}
}
}
}
</script>
<style lang="scss">
// Screenshots slider
.screenshots {
width: 100%;
margin-bottom: 30px;
padding-bottom: 10px;
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
background: transparent;
width: 0 !important;
}
}
#scrollbar {
display: inline-block;
padding-left: calc(50% - 92px);
}
#scrollbar .screenshot {
float: left;
}
</style>
================================================
FILE: src/components/pages/widgets/SingleBanner.vue
================================================
<template>
<div class="app-banner" v-images-loaded:on.done="loaded">
<img v-show="bannerSrc && imageLoaded" v-bind:src="bannerSrc" alt="Banner" />
<vcl-banner v-show="!bannerSrc || !imageLoaded" class="loader"></vcl-banner>
</div>
</template>
<script>
import VclBanner from './content-loaders/SingleBanner'
import imagesLoaded from 'vue-images-loaded'
export default {
name: 'SingleBanner',
directives: {
imagesLoaded
},
components: {
VclBanner
},
props: [
'bannerSrc'
],
watch: {
'bannerSrc': function () {
this.imageLoaded = false
}
},
data: function () {
return {
'imageLoaded': false
}
},
methods: {
loaded: function (instance) {
this.imageLoaded = true
}
}
}
</script>
<style lang="scss" scoped>
.loader {
width: 720px;
height: 320px;
}
img {
user-drag: none;
user-select: none;
-moz-user-select: none;
-webkit-user-drag: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
================================================
FILE: src/components/pages/widgets/SingleCard.vue
================================================
<template>
<div v-bind:class="imageLoaded ? 'loaded' : 'loading'">
<vcl-card class="loader"></vcl-card>
<router-link class="real-card" v-bind:to="'/app/' + card.id" v-images-loaded="loaded">
<div class="card" :class="$store.state.userParameters.hardware == 'chalk' ? 'round' : ''">
<img class="card-img-top" v-bind:src="card.screenshot_images[0][Object.keys(card.screenshot_images[0])[0]]" alt="App Icon">
<div class="card-block text-xs-center">
<h6 class="card-title">{{ card.title }}</h6>
<p class="card-text">
<small class="text-muted">
<svg class="svg-icon icon-inverted-thumbs-up" width="16px" height="16px" viewBox="0 0 25 25">
<use xlink:href="#iconThumbsUp"></use>
</svg>
{{ card.hearts }}
</small>
</p>
</div>
</div>
</router-link>
</div>
</template>
<script>
import VclCard from './content-loaders/SingleCard'
import imagesLoaded from 'vue-images-loaded'
export default {
name: 'single-card',
directives: {
imagesLoaded
},
components: {
VclCard
},
props: {
card: {
id: '',
title: '',
type: '',
screenshot_images: [],
thumbs_up: 0
},
searchData: false
},
watch: {
card: function () {
this.imageLoaded = false
if (this.searchData) {
this.build_from_search()
}
}
},
data: function () {
return {
'imageLoaded': false
}
},
methods: {
loaded: function (instance) {
this.imageLoaded = true
},
build_from_search: function () {
// Identify platform and assign one screenshot in the right format
let hardware = this.$store.state.userParameters.hardware
let thisAssetCollection = this.card.asset_collections.find(function (assetCollection) {
return assetCollection.hardware_platform === hardware
})
if (thisAssetCollection == null) {
thisAssetCollection = this.card.asset_collections[0]
}
this.card.screenshot_images = [{'144x168': thisAssetCollection.screenshots[0]}]
}
},
beforeMount: function () {
if (this.searchData) {
this.build_from_search()
}
}
}
</script>
<style lang="scss" scoped>
div.loading {
.loader {
display: block;
}
.real-card {
display: none;
}
}
div.loaded {
.loader {
display: none;
}
.real-card {
display: block;
}
}
.loader {
max-width: 170px;
max-height: 253px;
margin-bottom: .75rem;
margin-left: auto;
margin-right: auto;
width: 100%;
}
a {
// Remove text decoration that comes from having the .card inside a "a"
color: #373a3c;
text-decoration: none;
&:hover, &:focus {
color: #373a3c;
outline: none;
text-decoration: none;
}
@media screen and (max-width: map-get($grid-breakpoints, lg)) {
// Remove 2 extra cards
&:last-child, &:nth-last-child(2) {
display: none;
}
}
.card {
max-width: 170px;
&.round {
border-top-left-radius: 50%;
border-top-right-radius: 50%;
.card-img-top {
border-radius: 50%;
}
}
.card-title {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin: 7px 6px 5px 6px;
}
// Make it smaller on small screens
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
max-width: 32vw;
display: inline-block;
margin-bottom: .75rem;
width: 100%;
.card-block {
padding-left: 0.2rem;
padding-right: 0.2rem;
}
.card-title {
font-size: 16px;
}
}
// Make the app-screenshot take full-width
// TODO: decide what to do with app icons
img {
width:100%;
height:auto;
}
}
}
</style>
================================================
FILE: src/components/pages/widgets/SingleScreenshot.vue
================================================
<template>
<div v-if="imageSize !== ''" class="screenshot" :class="$store.state.userParameters.hardware === 'chalk' ? 'round' : ''" v-images-loaded:on.done="loaded">
<img v-show="screenshotSrc[imageSize] && imageLoaded" v-bind:src="screenshotSrc[imageSize]" alt="Screenshot" />
<vcl-screenshot-square v-if="$store.state.userParameters.hardware !== 'chalk'" v-show="!screenshotSrc || !imageLoaded" class="loader square"></vcl-screenshot-square>
<vcl-screenshot-round v-if="$store.state.userParameters.hardware === 'chalk'" v-show="!screenshotSrc || !imageLoaded" class="loader round"></vcl-screenshot-round>
</div>
</template>
<script>
import VclScreenshotSquare from './content-loaders/SingleScreenshotSquare'
import VclScreenshotRound from './content-loaders/SingleScreenshotRound'
import imagesLoaded from 'vue-images-loaded'
export default {
name: 'SingleScreenshot',
directives: {
imagesLoaded
},
components: {
VclScreenshotSquare,
VclScreenshotRound
},
props: [
'screenshotSrc'
],
data: function () {
return {
'imageLoaded': false,
'imageSize': ''
}
},
methods: {
loaded: function (instance) {
this.imageLoaded = true
}
},
beforeMount: function () {
this.imageSize = Object.keys(this.screenshotSrc)[0]
}
}
</script>
<style lang="scss" scoped>
.loader {
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
margin-left: 20px;
margin-right: 20px;
&.square {
width: 144px;
height: 168px;
}
&.round {
width: 180px;
height: 180px;
border-radius: 50%
}
}
.round {
img {
border-radius: 50%;
}
}
img {
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
margin-left: 20px;
margin-right: 20px;
user-drag: none;
user-select: none;
-moz-user-select: none;
-webkit-user-drag: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
================================================
FILE: src/components/pages/widgets/TagList.vue
================================================
<template>
<div class="row tag-container">
<div v-for="(tag, index) in tags" v-bind:key="index" class="col-6"><router-link v-bind:to="'category/' + tag.slug" class="card text-white bg-dark text-center">{{tag.name}}</router-link></div>
</div>
</template>
<script>
export default {
name: 'tag-list',
props: {
tags: {}
}
}
</script>
<style lang="scss" scoped>
.tag-container {
max-width: 850px;
margin-left: auto;
margin-right: auto;
.col-6 {
padding-left: 5px;
padding-right: 5px;
padding-top: 5px;
a:hover {
text-decoration: none;
}
.card {
font-family: 'Open Sans', sans-serif;
font-weight: 400;
border: 0;
padding: 6px;
border-radius: 4px;
box-shadow: 0 1px 1px 0 rgba(60,64,67,.08), 0 1px 3px 1px rgba(60,64,67,.16);
}
}
margin-bottom: 30px;
}
</style>
================================================
FILE: src/components/pages/widgets/content-loaders/SingleBanner.vue
================================================
<script>
import VueContentLoading from 'vue-content-loading'
export default {
components: {
VueContentLoading
}
}
</script>
<template>
<vue-content-loading :width="720" :height="320" primary="#dddddd" secondary="#bcbcbc">
<rect x="0" y="0" width="720" height="320"/>
</vue-content-loading>
</template>
================================================
FILE: src/components/pages/widgets/content-loaders/SingleCard.vue
================================================
<script>
import VueContentLoading from 'vue-content-loading'
export default {
components: {
VueContentLoading
}
}
</script>
<template>
<vue-content-loading :width="170" :height="253" primary="#dddddd" secondary="#bcbcbc">
<path d="M170,196H0V4A4,4,0,0,1,4,0H166a4,4,0,0,1,4,4Z"/>
<path d="M169,196v53a3,3,0,0,1-3,3H4a3,3,0,0,1-3-3V196H169m1-1H0v54a4,4,0,0,0,4,4H166a4,4,0,0,0,4-4V195Z"/>
<rect x="29" y="203" width="106" height="19" rx="4" ry="4"/>
<rect x="65" y="229" width="34" height="18" rx="4" ry="4"/>
</vue-content-loading>
</template>
================================================
FILE: src/components/pages/widgets/content-loaders/SingleScreenshotRound.vue
================================================
<script>
import VueContentLoading from 'vue-content-loading'
export default {
components: {
VueContentLoading
}
}
</script>
<template>
<vue-content-loading :width="180" :height="180" primary="#dddddd" secondary="#bcbcbc">
<circle cx="90" cy="90" r="90"/>
</vue-content-loading>
</template>
================================================
FILE: src/components/pages/widgets/content-loaders/SingleScreenshotSquare.vue
================================================
<script>
import VueContentLoading from 'vue-content-loading'
export default {
components: {
VueContentLoading
}
}
</script>
<template>
<vue-content-loading :width="144" :height="168" primary="#dddddd" secondary="#bcbcbc">
<rect x="0" y="0" width="144" height="168"/>
</vue-content-loading>
</template>
================================================
FILE: src/css/_error.scss
================================================
// _error.scss
// Error page styles
.page-error {
// Calculate top margin 58 px ar of the navbar
margin-top: 40px + 58px;
min-height: 400px;
height: 80vh;
display: flex;
flex-direction: column;
max-width: 36rem;
margin-left: auto;
margin-right: auto;
align-items: center;
justify-content: center;
.pet-rock-pebble {
// Mascot svg
margin-top: 3rem;
margin-bottom: 3rem;
}
.page-error_buttons {
// Minimal style changes to buttons
margin-top: 3rem;
.btn-outline-pebble {
&:hover {
color: #f4f3f4;
}
}
}
}
================================================
FILE: src/main.js
================================================
import Vue from 'vue'
import VueResource from 'vue-resource'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import App from './App'
import router from './router'
import store from './store'
import mixin from './mixin'
import InstantSearch from 'vue-instantsearch'
import VueCookie from 'vue-cookie'
import CardCollection from './components/pages/widgets/CardCollection'
Vue.filter('formatDate', function (d) {
let date = new Date(d)
if (date) {
return date.getFullYear() + '-' + ((date.getMonth() + 1) >= 10 ? (date.getMonth() + 1) : ('0' + (date.getMonth() + 1))) + '-' + (date.getDate() >= 10 ? date.getDate() : ('0' + date.getDate()))
}
})
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
Vue.filter('readable-name', function (value) {
if (!value) return ''
value = value.toString()
return value[0].toUpperCase() + value.replace(new RegExp('-', 'g'), ' ').substring(1)
})
Vue.mixin({
methods: {
buildResourceUrl: mixin.buildResourceUrl,
setTitle: mixin.setTitle,
openExternal: mixin.openExternal
}
})
Vue.use(VueResource)
Vue.use(BootstrapVue)
Vue.use(InstantSearch)
Vue.use(VueCookie)
Vue.component('card-collection', CardCollection)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
beforeCreate () {
this.$store.commit('userParameters/INIT', null, { root: true })
this.$store.commit('secure/INIT', null, { root: true })
store.subscribe((mutation, state) => {
if (mutation.type.substr(0, 15) === 'userParameters/') {
// Only save user parameters when needed
localStorage.setItem('rebbleUserParameters', JSON.stringify(state.userParameters))
}
if (mutation.type.substr(0, 23) === 'secure/SET_ACCESS_TOKEN') {
// Only save user parameters when needed
this.$cookie.set('access_token', state.secure.accessToken)
}
})
},
render: h => h(App)
})
================================================
FILE: src/mixin/index.js
================================================
import { Native } from '../services'
import { hardwareEnum } from '../store/userParameters'
const mixins = {
buildResourceUrl (resource) {
return `${this.$store.state.config.backendUrl}/${resource}?platform=${this.$store.state.userParameters.platform}${this.$store.state.userParameters.hardware !== hardwareEnum.all ? `&hardware=${this.$store.state.userParameters.hardware}&filter_hardware=true` : ''}`
},
setTitle (title = '') {
document.title = title === '' ? 'Rebble Store' : `${title} | Rebble Store`
if (this.$store.state.userParameters.inApp === true) {
Native.send('setNavBarTitle', { title: title })
}
},
openExternal (url) {
if (this.$store.state.userParameters.inApp === true) {
Native.send('openURL', {
url: url
})
} else {
window.open(url, '_blank')
}
}
}
export default mixins
================================================
FILE: src/router/index.js
================================================
import qs from 'qs'
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Category from '@/components/pages/Category'
import AppView from '@/components/pages/AppView'
import AppDetails from '@/components/pages/AppDetails'
import AppVersions from '@/components/pages/AppVersions'
import Author from '@/components/pages/Author'
import Search from '@/components/pages/Search'
import Collection from '@/components/pages/Collection'
import Settings from '@/components/pages/Settings'
import Error from '@/components/pages/Error'
Vue.use(Router)
const routes = [
{path: '', redirect: '/faces'},
{
path: '/category/:id/:sort/:page',
component: Category
},
{
path: '/category/:id',
redirect: '/category/:id/hearts/1'
},
{
path: '/category/:id/:sort',
redirect: '/category/:id/:sort/1'
},
{
path: '/app/:id',
component: AppView,
children: [
{
path: '',
component: AppDetails
},
{
path: 'versions',
component: AppVersions
}
]
},
{path: '/author/:id', redirect: '/author/:id/1'},
{path: '/author/:id/:page', component: Author},
{path: '/settings', component: Settings},
{path: '/:type', component: Home},
{
path: '/:type/search',
component: Search,
props: true
},
{path: '/:type/:slug', redirect: '/:type/:slug/1'},
{path: '/:type/:slug/:page', component: Collection},
{path: '*', component: Error}
]
export default new Router({
mode: 'history',
routes: routes,
parseQuery (query) {
return qs.parse(query)
},
stringifyQuery (query) {
const result = qs.stringify(query)
return result ? '?' + result : ''
}
})
================================================
FILE: src/router/search-router.js
================================================
import vueRouter from './index'
export const searchRouting = {
router: {
read () {
return vueRouter.currentRoute.query
},
write (routeState) {
vueRouter.push({
query: routeState
})
},
createURL (routeState) {
return vueRouter.resolve({
query: routeState
}).href
},
onUpdate (cb) {
this._onPopState = ({state}) => {
const routeState = state
// at initial load, the state is read from the URL without
// update. Therefore the state object is not there. In this
// case we fallback and read the URL.
if (!routeState) {
cb(this.read())
} else {
cb(routeState)
}
}
window.addEventListener('popstate', this._onPopState)
},
dispose () {
window.removeEventListener('popstate', this._onPopState)
this.write()
}
}
}
================================================
FILE: src/services/index.js
================================================
import store from '../store'
import router from '../router'
class NativeService {
constructor () {
this.callbacks = []
this.callbackId = 0
this.methods = ['setNavBarTitle', 'openURL', 'addToLocker', 'loadAppToDeviceAndLocker', 'promptUserForAddToLockerOrLoad', 'getAppsFromLocker', 'removeFromLocker', 'isAppInLocker', 'unloadAppFromPebble', 'getLoadedAppsFromPebble', 'tryWatchface', 'isConnected', 'closeScreen', 'skipStep', 'bulkLoadAndClose', 'setVisibleApp', 'refreshAccessToken']
window.PebbleBridge = this
}
send (methodName, args, responseCallback, sendCallback) {
window.setTimeout(() => {
if (typeof methodName !== 'string') return this._sendError('Native: methodName is not an object', sendCallback)
if (!~this.methods.indexOf(methodName)) return this._sendError(`Native: ${methodName} is not in list of known methods`, sendCallback)
if (typeof args !== 'object') return this._sendError('Native: args is not an object', sendCallback)
// if (config.IS_BROWSER) return void $log.debug('Native: ' + methodName + ' not available in browser');
var _callbackId = -1
if (typeof responseCallback === 'function') {
this.callbacks.push(responseCallback)
_callbackId = this.callbackId
this.callbackId = this.callbackId + 1
} else {
responseCallback && this._sendError('Native: callback is not a function')
}
var uri = this._buildURI(methodName, _callbackId, args)
this._executeSend(uri)
if (typeof sendCallback === 'function') {
sendCallback(null, uri)
}
})
}
_executeSend (uri) {
let iframe = document.createElement('iframe')
iframe.setAttribute('src', uri)
iframe.setAttribute('height', '1px')
iframe.setAttribute('width', '1px')
document.documentElement.appendChild(iframe)
iframe.parentNode.removeChild(iframe)
iframe = null
}
_sendError (err, callback) {
console.error(err)
callback(err)
}
_buildURI (methodName, callbackId, args) {
let msg = this._encodeMsg(methodName, callbackId, args)
let protocol = 'pebble-method-call-js-frame://'
let queryCharacter = store.state.userParameters.platform === 'ios' ? '?' : ''
let uri = protocol + queryCharacter + 'method=' + methodName + '&args=' + msg
return uri
}
_encodeMsg (methodName, callbackId, args) {
let msgStringified
let msg = {
methodName: methodName,
callbackId: callbackId,
data: args
}
try {
msgStringified = JSON.stringify(msg)
} catch (e) {
return void console.error('Native: msg cannot be JSON encoded', e)
}
let msgURIEncoded
try {
msgURIEncoded = encodeURIComponent(msgStringified)
} catch (e) {
return void console.error('Native: msg cannot be URI encoded', e)
}
return msgURIEncoded
}
handleResponse (args) {
if (typeof args !== 'object' && args !== null) return void console.error('Native: args.methodName is not an object')
if (typeof args.data !== 'object') return void console.error('Native: args.data is not an object')
if (typeof args.callbackId !== 'number') return void console.error('Native: args.callbackId is not a number')
if (args.callbackId < 0) return
let callback = this.callbacks[args.callbackId]
delete this.callbacks[args.callbackId]
if (callback && typeof callback === 'function') {
callback(args.data)
} else {
console.error('Native: callback is not a function', callback)
}
}
_reload () {
window.location.reload(true)
}
handleRequest (args) {
if (typeof args !== 'object') return void console.error('Native: args.methodName is not an object')
if (typeof args.methodName !== 'string') return void console.error('Native: args.methodName is not an object')
switch (args.methodName) {
case 'search':
// let section = args.section || Storage.get('activeSection') || 'watchapps'
// let query = args.query || (Storage.get('searchData-' + section) || {}).query || ''
let section = args.section || 'apps'
let query = args.query || ''
let isNative = !(!args.query && !args.section)
let url = `/${section}/search?page=1&query=${encodeURIComponent(query)}${(isNative ? '&inApp=true' : '')}`
router.push(url)
break
case 'navigate':
router.push(args.url || '/')
break
case 'refresh':
this._reload()
}
}
}
export const Native = new NativeService()
================================================
FILE: src/store/config.js
================================================
export default {
namespaced: true,
state: {
backendUrl: 'https://appstore-api.rebble.io/api/v1',
devPortalBackendUrl: 'https://appstore-api.rebble.io/api/v0',
tosLink: 'https://rebble.io/tos/',
devPortalLink: 'https://rebble.io/submit/',
contactLink: '',
accessToken: null
}
}
================================================
FILE: src/store/index.js
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import pathify from 'vuex-pathify'
import userParameters from './userParameters'
import config from './config'
import secure from './secure'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
userParameters: userParameters,
config: config,
secure: secure
},
plugins: [pathify.plugin]
})
================================================
FILE: src/store/secure.js
================================================
import VueCookie from 'vue-cookie'
import { make } from 'vuex-pathify'
const state = {
accessToken: null
}
export default {
namespaced: true,
state: state,
mutations: {
...make.mutations(state),
INIT (state) {
if (VueCookie.get('access_token')) {
state.accessToken = VueCookie.get('access_token')
}
}
}
}
================================================
FILE: src/store/userParameters.js
================================================
import { make } from 'vuex-pathify'
import { version } from '../../package.json'
export const platformEnum = {
all: 'all',
ios: 'ios',
android: 'android'
}
export const hardwareEnum = {
all: 'all',
aplite: 'aplite', // OG Pebble and Pebble Steel
basalt: 'basalt', // Pebble Time and Pebble Time Steel
chalk: 'chalk', // Pebble Round
diorite: 'diorite' // Pebble 2
}
const state = {
version: '',
platform: platformEnum.all, // either 'android', 'ios', or 'all'
hardware: hardwareEnum.all,
appVersion: '',
inApp: false,
devMode: false
}
export default {
namespaced: true,
state: state,
mutations: {
...make.mutations(state),
INIT (state) {
if (localStorage.getItem('rebbleUserParameters')) {
let cacheState = JSON.parse(localStorage.getItem('rebbleUserParameters'))
if (cacheState.version === version) {
Object.assign(state, cacheState)
} else {
state.version = version
}
} else {
state.version = version
}
}
}
}
================================================
FILE: static/.gitkeep
================================================
================================================
FILE: static/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/static/mstile-150x150.png"/>
<TileColor>#373a3c</TileColor>
</tile>
</msapplication>
</browserconfig>
================================================
FILE: static/css/_variables.scss
================================================
// _varaibles.scss
// Store all the variables in here
// Bootstrap's grid breakpoints
// Use them this way: map-get($grid-breakpoints, sm)
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px
);
// Pebble orange color
$pebble-color: #ff4700;
// Main Background color (originally a really light gray)
$main-bg-color: #f4f3f4;
================================================
FILE: static/manifest.json
================================================
{
"name": "Rebble Store",
"icons": [
{
"src": "\/static\/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image\/png"
},
{
"src": "\/static\/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image\/png"
}
],
"theme_color": "#ffffff",
"display": "standalone"
}
================================================
FILE: test/e2e/custom-assertions/elementCount.js
================================================
// A custom Nightwatch assertion.
// The assertion name is the filename.
// Example usage:
//
// browser.assert.elementCount(selector, count)
//
// For more information on custom assertions see:
// http://nightwatchjs.org/guide#writing-custom-assertions
exports.assertion = function (selector, count) {
this.message = 'Testing if element <' + selector + '> has count: ' + count
this.expected = count
this.pass = function (val) {
return val === this.expected
}
this.value = function (res) {
return res.value
}
this.command = function (cb) {
var self = this
return this.api.execute(function (selector) {
return document.querySelectorAll(selector).length
}, [selector], function (res) {
cb.call(self, res)
})
}
}
================================================
FILE: test/e2e/nightwatch.conf.js
================================================
require('babel-register')
var config = require('../../config')
// http://nightwatchjs.org/gettingstarted#settings-file
module.exports = {
src_folders: ['test/e2e/specs'],
output_folder: 'test/e2e/reports',
custom_assertions_path: ['test/e2e/custom-assertions'],
selenium: {
start_process: true,
server_path: require('selenium-server').path,
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': require('chromedriver').path
}
},
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
}
},
chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true
}
},
firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
}
}
}
}
================================================
FILE: test/e2e/runner.js
================================================
// 1. start the dev server using production config
process.env.NODE_ENV = 'testing'
const webpack = require('webpack')
const DevServer = require('webpack-dev-server')
const webpackConfig = require('../../build/webpack.prod.conf')
const devConfigPromise = require('../../build/webpack.dev.conf')
let server
devConfigPromise.then(devConfig => {
const devServerOptions = devConfig.devServer
const compiler = webpack(webpackConfig)
server = new DevServer(compiler, devServerOptions)
const port = devServerOptions.port
const host = devServerOptions.host
return server.listen(port, host)
})
.then(() => {
// 2. run the nightwatch test suite against it
// to run in additional browsers:
// 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings"
// 2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
let opts = process.argv.slice(2)
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
}
if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome'])
}
const spawn = require('cross-spawn')
const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })
runner.on('exit', function (code) {
server.close()
process.exit(code)
})
runner.on('error', function (err) {
server.close()
throw err
})
})
================================================
FILE: test/e2e/specs/test.js
================================================
// For authoring Nightwatch tests, see
// http://nightwatchjs.org/guide#usage
module.exports = {
'default e2e tests': function (browser) {
// automatically uses dev Server port from /config.index.js
// default: http://localhost:8080
// see nightwatch.conf.js
const devServer = browser.globals.devServerURL
browser
.url(devServer)
.waitForElementVisible('#app', 5000)
.end()
}
}
================================================
FILE: test/unit/.eslintrc
================================================
{
"env": {
"mocha": true
},
"globals": {
"expect": true,
"sinon": true
}
}
================================================
FILE: test/unit/index.js
================================================
import Vue from 'vue'
Vue.config.productionTip = false
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
================================================
FILE: test/unit/karma.conf.js
================================================
// This is a karma config file. For more details see
// http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var webpackConfig = require('../../build/webpack.test.conf')
module.exports = function karmaConfig (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
browsers: ['PhantomJS'],
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
reporters: ['spec', 'coverage'],
files: ['./index.js'],
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}
gitextract_8kuhkv4z/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── LICENSE
├── README.md
├── build/
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── sprite_module.conf.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ ├── webpack.prod.conf.js
│ └── webpack.test.conf.js
├── config/
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ └── test.env.js
├── index.html
├── package.json
├── src/
│ ├── 404.html
│ ├── App.vue
│ ├── assets/
│ │ ├── browserconfig.xml
│ │ └── manifest.json
│ ├── components/
│ │ ├── Home.vue
│ │ ├── Navbar.vue
│ │ ├── PageFooter.vue
│ │ ├── SvgContainer.vue
│ │ └── pages/
│ │ ├── AppDetails.vue
│ │ ├── AppVersions.vue
│ │ ├── AppView.vue
│ │ ├── Author.vue
│ │ ├── Category.vue
│ │ ├── Collection.vue
│ │ ├── Error.vue
│ │ ├── Search.vue
│ │ ├── Settings.vue
│ │ └── widgets/
│ │ ├── AppSlider.vue
│ │ ├── AppTitleBar.vue
│ │ ├── CardCollection.vue
│ │ ├── GetAppButton.vue
│ │ ├── HomeSlider.vue
│ │ ├── Pagination.vue
│ │ ├── ScreenshotList.vue
│ │ ├── SingleBanner.vue
│ │ ├── SingleCard.vue
│ │ ├── SingleScreenshot.vue
│ │ ├── TagList.vue
│ │ └── content-loaders/
│ │ ├── SingleBanner.vue
│ │ ├── SingleCard.vue
│ │ ├── SingleScreenshotRound.vue
│ │ └── SingleScreenshotSquare.vue
│ ├── css/
│ │ └── _error.scss
│ ├── main.js
│ ├── mixin/
│ │ └── index.js
│ ├── router/
│ │ ├── index.js
│ │ └── search-router.js
│ ├── services/
│ │ └── index.js
│ └── store/
│ ├── config.js
│ ├── index.js
│ ├── secure.js
│ └── userParameters.js
├── static/
│ ├── .gitkeep
│ ├── browserconfig.xml
│ ├── css/
│ │ └── _variables.scss
│ └── manifest.json
└── test/
├── e2e/
│ ├── custom-assertions/
│ │ └── elementCount.js
│ ├── nightwatch.conf.js
│ ├── runner.js
│ └── specs/
│ └── test.js
└── unit/
├── .eslintrc
├── index.js
└── karma.conf.js
SYMBOL INDEX (29 symbols across 12 files)
FILE: build/check-versions.js
function exec (line 7) | function exec (cmd) {
FILE: build/utils.js
function generateLoaders (line 33) | function generateLoaders (loader, loaderOptions) {
FILE: build/webpack.base.conf.js
function resolve (line 8) | function resolve (dir) {
FILE: build/webpack.dev.conf.js
constant HOST (line 13) | const HOST = process.env.HOST
constant PORT (line 14) | const PORT = process.env.PORT && Number(process.env.PORT)
FILE: build/webpack.prod.conf.js
method minChunks (line 88) | minChunks (module) {
FILE: src/main.js
method beforeCreate (line 58) | beforeCreate () {
FILE: src/mixin/index.js
method buildResourceUrl (line 5) | buildResourceUrl (resource) {
method setTitle (line 8) | setTitle (title = '') {
method openExternal (line 14) | openExternal (url) {
FILE: src/router/index.js
method parseQuery (line 62) | parseQuery (query) {
method stringifyQuery (line 65) | stringifyQuery (query) {
FILE: src/router/search-router.js
method read (line 5) | read () {
method write (line 8) | write (routeState) {
method createURL (line 13) | createURL (routeState) {
method onUpdate (line 18) | onUpdate (cb) {
method dispose (line 32) | dispose () {
FILE: src/services/index.js
class NativeService (line 4) | class NativeService {
method constructor (line 5) | constructor () {
method send (line 12) | send (methodName, args, responseCallback, sendCallback) {
method _executeSend (line 34) | _executeSend (uri) {
method _sendError (line 44) | _sendError (err, callback) {
method _buildURI (line 49) | _buildURI (methodName, callbackId, args) {
method _encodeMsg (line 57) | _encodeMsg (methodName, callbackId, args) {
method handleResponse (line 78) | handleResponse (args) {
method _reload (line 91) | _reload () {
method handleRequest (line 95) | handleRequest (args) {
FILE: src/store/secure.js
method INIT (line 13) | INIT (state) {
FILE: src/store/userParameters.js
method INIT (line 32) | INIT (state) {
Condensed preview — 78 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (148K chars).
[
{
"path": ".babelrc",
"chars": 354,
"preview": "{\n \"presets\": [\n [\"env\", {\n \"modules\": false,\n \"targets\": {\n \"browsers\": [\"> 1%\", \"last 2 versions\""
},
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
},
{
"path": ".eslintignore",
"chars": 51,
"preview": "/build/\n/config/\n/dist/\n/*.js\n/test/unit/coverage/\n"
},
{
"path": ".eslintrc.js",
"chars": 791,
"preview": "// https://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n root: true,\n parserOptions: {\n parser: 'babe"
},
{
"path": ".gitignore",
"chars": 212,
"preview": ".DS_Store\nnode_modules/\ndist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n/test/unit/coverage/\n/test/e2e/reports/\nsel"
},
{
"path": ".postcssrc.js",
"chars": 246,
"preview": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n \"plugins\": {\n \"postcss-import\": {},"
},
{
"path": "LICENSE",
"chars": 1076,
"preview": "MIT License\n\nCopyright (c) 2018 Rebble (pebble-dev)\n\nPermission is hereby granted, free of charge, to any person obtaini"
},
{
"path": "README.md",
"chars": 1291,
"preview": "# Rebble Store for pebble\n\nThe Rebble Store is a Pebble Appstore replacement.\n\nIf you want to contribute join us on the "
},
{
"path": "build/build.js",
"chars": 1198,
"preview": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = r"
},
{
"path": "build/check-versions.js",
"chars": 1290,
"preview": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.j"
},
{
"path": "build/dev-client.js",
"chars": 245,
"preview": "/* eslint-disable */\nrequire('eventsource-polyfill')\nvar hotClient = require('webpack-hot-middleware/client?noInfo=true&"
},
{
"path": "build/dev-server.js",
"chars": 2194,
"preview": "require('./check-versions')()\nvar config = require('../config')\nif (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.p"
},
{
"path": "build/sprite_module.conf.js",
"chars": 200,
"preview": "import BrowserSprite from 'svg-baker-runtime/browser-sprite';\nimport domready from 'domready'\n\nconst sprite = new Browse"
},
{
"path": "build/utils.js",
"chars": 2768,
"preview": "'use strict'\nconst path = require('path')\nconst config = require('../config')\nconst ExtractTextPlugin = require('extract"
},
{
"path": "build/vue-loader.conf.js",
"chars": 553,
"preview": "'use strict'\nconst utils = require('./utils')\nconst config = require('../config')\nconst isProduction = process.env.NODE_"
},
{
"path": "build/webpack.base.conf.js",
"chars": 2948,
"preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst vue"
},
{
"path": "build/webpack.dev.conf.js",
"chars": 3004,
"preview": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\ncon"
},
{
"path": "build/webpack.prod.conf.js",
"chars": 5196,
"preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst conf"
},
{
"path": "build/webpack.test.conf.js",
"chars": 867,
"preview": "'use strict'\n// This is the webpack config used for unit tests.\n\nconst utils = require('./utils')\nconst webpack = requir"
},
{
"path": "config/dev.env.js",
"chars": 156,
"preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
},
{
"path": "config/index.js",
"chars": 2289,
"preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
},
{
"path": "config/prod.env.js",
"chars": 61,
"preview": "'use strict'\nmodule.exports = {\n NODE_ENV: '\"production\"'\n}\n"
},
{
"path": "config/test.env.js",
"chars": 149,
"preview": "'use strict'\nconst merge = require('webpack-merge')\nconst devEnv = require('./dev.env')\n\nmodule.exports = merge(devEnv, "
},
{
"path": "index.html",
"chars": 1091,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial"
},
{
"path": "package.json",
"chars": 3602,
"preview": "{\n \"name\": \"rebble-app-store\",\n \"version\": \"1.0.0-beta.3\",\n \"description\": \"Rebble Store, a pebble app store replacem"
},
{
"path": "src/404.html",
"chars": 8850,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <!-- Required meta tags always come first -->\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "src/App.vue",
"chars": 5953,
"preview": "<template>\n <div id=\"app\">\n <div v-bind:class=\"$store.state.userParameters.inApp ? 'flex-content main-in-app' : 'fle"
},
{
"path": "src/assets/browserconfig.xml",
"chars": 229,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n <msapplication>\n <tile>\n <square150x150logo src=\"/asset"
},
{
"path": "src/assets/manifest.json",
"chars": 308,
"preview": "{\n\t\"name\": \"Rebble Store\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"\\/assets\\/android-chrome-192x192.png\",\n\t\t\t\"sizes\": \"192x192\",\n\t\t\t\""
},
{
"path": "src/components/Home.vue",
"chars": 2240,
"preview": "<template>\n <div>\n <slider v-bind:banners=\"page.banners\"></slider>\n <main class=\"home apps container\">\n <tag"
},
{
"path": "src/components/Navbar.vue",
"chars": 6981,
"preview": "<template>\n <b-navbar toggleable=\"true\" type=\"dark\" variant=\"dark\" class=\"translucent\" fixed=\"top\">\n <div class="
},
{
"path": "src/components/PageFooter.vue",
"chars": 1537,
"preview": "<template>\n <footer v-bind:class=\"brand ? 'inApp' : 'main'\">\n <div class=\"brand\" to=\"/\">\n Rebble Store <"
},
{
"path": "src/components/SvgContainer.vue",
"chars": 6642,
"preview": "<template>\n <div id=\"svgContainer\">\n <!-- <svg id=\"petRock\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://w"
},
{
"path": "src/components/pages/AppDetails.vue",
"chars": 3171,
"preview": "<template>\n <main v-if=\"Object.entries(app).length !== 0\" class=\"text-center\">\n <screenshot-list v-bind:screenshots="
},
{
"path": "src/components/pages/AppVersions.vue",
"chars": 1048,
"preview": "<template>\n <main class=\"text-center\">\n <div class=\"card subsection text-left p-3 app-details\" v-for=\"(changelog"
},
{
"path": "src/components/pages/AppView.vue",
"chars": 3401,
"preview": "<template>\n <section v-bind:class=\"app.type\" >\n <header class=\"main\" v-bind:class=\"($store.state.userParameters.inAp"
},
{
"path": "src/components/pages/Author.vue",
"chars": 1414,
"preview": "<template>\n <div>\n <header class=\"main\">\n <div v-if=\"page.data !== undefined\" class=\"title-card\">\n <h3>A"
},
{
"path": "src/components/pages/Category.vue",
"chars": 2202,
"preview": "<template>\n <div>\n <header class=\"main\">\n <div class=\"title-card\">\n <h3>{{id | readable-name}}</h3"
},
{
"path": "src/components/pages/Collection.vue",
"chars": 1621,
"preview": "<template>\n <div>\n <header class=\"main\">\n <div class=\"title-card\">\n <h3>Collection: {{slug | reada"
},
{
"path": "src/components/pages/Error.vue",
"chars": 3597,
"preview": "<template>\n <div>\n <main class=\"container text-xs-center\">\n <section>\n <div class=\"page-error page-error"
},
{
"path": "src/components/pages/Search.vue",
"chars": 4083,
"preview": "<template>\n <ais-instant-search :search-client=\"rebbleSearch\" index-name=\"rebble-appstore-production\" :routing=\"routing"
},
{
"path": "src/components/pages/Settings.vue",
"chars": 1383,
"preview": "<template>\n <div>\n <header class=\"main\">\n <div class=\"title-card\">\n <h3>Pebble Type<"
},
{
"path": "src/components/pages/widgets/AppSlider.vue",
"chars": 1952,
"preview": "<template>\n <div v-if=\"banners != null && banners[0] != null\" id=\"banner-carousel\" class=\"carousel slide\" data-ride=\"c"
},
{
"path": "src/components/pages/widgets/AppTitleBar.vue",
"chars": 7908,
"preview": "<template>\n<!-- Fix url args -->\n <div v-if=\"Object.entries(app).length !== 0\" v-bind:class=\"(this.$store.state.userPa"
},
{
"path": "src/components/pages/widgets/CardCollection.vue",
"chars": 2493,
"preview": "<template>\n <section class=\"text-center\">\n <div class=\"header\" v-if=\"showTop\">\n <h6 class=\"text-left\">\n "
},
{
"path": "src/components/pages/widgets/GetAppButton.vue",
"chars": 4584,
"preview": "<template>\n <span>\n <a v-bind:href=\"'pebble://appstore/' + app.id\" class=\"btn btn-outline-pebble btn-download\" v-if="
},
{
"path": "src/components/pages/widgets/HomeSlider.vue",
"chars": 2043,
"preview": "<template>\n <header class=\"main\">\n <div id=\"banner-carousel\" class=\"carousel slide\" data-ride=\"carousel\" v-if=\"bann"
},
{
"path": "src/components/pages/widgets/Pagination.vue",
"chars": 1132,
"preview": "<template>\n <nav v-if=\"links != undefined\">\n <ul class=\"pagination\">\n <li v-bind:class=\"this.$route.params.page"
},
{
"path": "src/components/pages/widgets/ScreenshotList.vue",
"chars": 1218,
"preview": "<template>\n <div v-dragscroll.x=\"!$store.state.userParameters.inApp\" class=\"screenshots\">\n <div id=\"scrollbar\" v-b"
},
{
"path": "src/components/pages/widgets/SingleBanner.vue",
"chars": 1032,
"preview": "<template>\n <div class=\"app-banner\" v-images-loaded:on.done=\"loaded\">\n <img v-show=\"bannerSrc && imageLoaded\" "
},
{
"path": "src/components/pages/widgets/SingleCard.vue",
"chars": 4292,
"preview": "<template>\n <div v-bind:class=\"imageLoaded ? 'loaded' : 'loading'\">\n <vcl-card class=\"loader\"></vcl-card>\n <route"
},
{
"path": "src/components/pages/widgets/SingleScreenshot.vue",
"chars": 2054,
"preview": "<template>\n <div v-if=\"imageSize !== ''\" class=\"screenshot\" :class=\"$store.state.userParameters.hardware === 'chalk' "
},
{
"path": "src/components/pages/widgets/TagList.vue",
"chars": 859,
"preview": "<template>\n <div class=\"row tag-container\">\n <div v-for=\"(tag, index) in tags\" v-bind:key=\"index\" class=\"col-6\"><rou"
},
{
"path": "src/components/pages/widgets/content-loaders/SingleBanner.vue",
"chars": 319,
"preview": "<script>\nimport VueContentLoading from 'vue-content-loading'\nexport default {\n components: {\n VueContentLoading\n }\n"
},
{
"path": "src/components/pages/widgets/content-loaders/SingleCard.vue",
"chars": 573,
"preview": "<script>\nimport VueContentLoading from 'vue-content-loading'\nexport default {\n components: {\n VueContentLoading\n }\n"
},
{
"path": "src/components/pages/widgets/content-loaders/SingleScreenshotRound.vue",
"chars": 307,
"preview": "<script>\nimport VueContentLoading from 'vue-content-loading'\nexport default {\n components: {\n VueContentLoading\n }\n"
},
{
"path": "src/components/pages/widgets/content-loaders/SingleScreenshotSquare.vue",
"chars": 319,
"preview": "<script>\nimport VueContentLoading from 'vue-content-loading'\nexport default {\n components: {\n VueContentLoading\n }\n"
},
{
"path": "src/css/_error.scss",
"chars": 611,
"preview": "// _error.scss\n// Error page styles\n\n\n.page-error {\n // Calculate top margin 58 px ar of the navbar\n margin-top: 40px "
},
{
"path": "src/main.js",
"chars": 2074,
"preview": "import Vue from 'vue'\nimport VueResource from 'vue-resource'\nimport BootstrapVue from 'bootstrap-vue'\n\nimport 'bootstrap"
},
{
"path": "src/mixin/index.js",
"chars": 863,
"preview": "import { Native } from '../services'\nimport { hardwareEnum } from '../store/userParameters'\n\nconst mixins = {\n buildRes"
},
{
"path": "src/router/index.js",
"chars": 1713,
"preview": "import qs from 'qs'\nimport Vue from 'vue'\nimport Router from 'vue-router'\nimport Home from '@/components/Home'\nimport Ca"
},
{
"path": "src/router/search-router.js",
"chars": 903,
"preview": "import vueRouter from './index'\n\nexport const searchRouting = {\n router: {\n read () {\n return vueRouter.current"
},
{
"path": "src/services/index.js",
"chars": 4520,
"preview": "import store from '../store'\nimport router from '../router'\n\nclass NativeService {\n constructor () {\n this.callbacks"
},
{
"path": "src/store/config.js",
"chars": 307,
"preview": "export default {\n namespaced: true,\n state: {\n backendUrl: 'https://appstore-api.rebble.io/api/v1',\n devPortalBa"
},
{
"path": "src/store/index.js",
"chars": 359,
"preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport pathify from 'vuex-pathify'\nimport userParameters from './userParam"
},
{
"path": "src/store/secure.js",
"chars": 349,
"preview": "import VueCookie from 'vue-cookie'\nimport { make } from 'vuex-pathify'\n\nconst state = {\n accessToken: null\n}\n\nexport de"
},
{
"path": "src/store/userParameters.js",
"chars": 1041,
"preview": "import { make } from 'vuex-pathify'\nimport { version } from '../../package.json'\n\nexport const platformEnum = {\n all: '"
},
{
"path": "static/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "static/browserconfig.xml",
"chars": 229,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n <msapplication>\n <tile>\n <square150x150logo src=\"/stati"
},
{
"path": "static/css/_variables.scss",
"chars": 357,
"preview": "// _varaibles.scss\n// Store all the variables in here\n\n// Bootstrap's grid breakpoints\n// Use them this way: map-get($gr"
},
{
"path": "static/manifest.json",
"chars": 308,
"preview": "{\n\t\"name\": \"Rebble Store\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"\\/static\\/android-chrome-192x192.png\",\n\t\t\t\"sizes\": \"192x192\",\n\t\t\t\""
},
{
"path": "test/e2e/custom-assertions/elementCount.js",
"chars": 765,
"preview": "// A custom Nightwatch assertion.\n// The assertion name is the filename.\n// Example usage:\n//\n// browser.assert.elemen"
},
{
"path": "test/e2e/nightwatch.conf.js",
"chars": 1028,
"preview": "require('babel-register')\nvar config = require('../../config')\n\n// http://nightwatchjs.org/gettingstarted#settings-file\n"
},
{
"path": "test/e2e/runner.js",
"chars": 1542,
"preview": "// 1. start the dev server using production config\nprocess.env.NODE_ENV = 'testing'\n\nconst webpack = require('webpack')\n"
},
{
"path": "test/e2e/specs/test.js",
"chars": 422,
"preview": "// For authoring Nightwatch tests, see\n// http://nightwatchjs.org/guide#usage\n\nmodule.exports = {\n 'default e2e tests':"
},
{
"path": "test/unit/.eslintrc",
"chars": 97,
"preview": "{\n \"env\": { \n \"mocha\": true\n },\n \"globals\": { \n \"expect\": true,\n \"sinon\": true\n }\n}\n"
},
{
"path": "test/unit/index.js",
"chars": 487,
"preview": "import Vue from 'vue'\n\nVue.config.productionTip = false\n\n// require all test files (files that ends with .spec.js)\nconst"
},
{
"path": "test/unit/karma.conf.js",
"chars": 1004,
"preview": "// This is a karma config file. For more details see\n// http://karma-runner.github.io/0.13/config/configuration-file.h"
}
]
About this extraction
This page contains the full source code of the pebble-dev/rebble-store GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 78 files (133.5 KB), approximately 38.6k tokens, and a symbol index with 29 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.