Verified Commit b2e5588a authored by Eliot Berriot's avatar Eliot Berriot

Merge branch 'release/0.3.5'

parents 353ab726 14b2df74
Pipeline #219 passed with stages
in 4 minutes and 20 seconds
......@@ -212,19 +212,21 @@ def get_text_fields(e):
]
def get_field(raw_name):
content = raw_name.replace(':', '').strip().lower()
content = raw_name.replace(':', '')
search_content = content.strip().lower()
for data in known_fields:
if data.get('startswith', True):
words = content.split(' ')
words = search_content.split(' ')
match = any((
content.startswith(matcher) or words[-1].lower().startswith(matcher)
search_content.startswith(matcher) or words[-1].lower().startswith(matcher)
for matcher in data['matchers']))
else:
match = any((content == matcher for matcher in data['matchers']))
match = any((search_content == matcher for matcher in data['matchers']))
if match:
return {
'name': data['name'],
'code': data['code'],
'title': content,
'keep': data.get('keep', True),
'html': data.get('html', False),
}
......@@ -243,6 +245,7 @@ def get_text_fields(e):
cf = {
'name': field_data['name'],
'code': field_data['code'],
'title': field_data['title'],
'paragraphs': []
}
fields.append(cf)
......
......@@ -88,6 +88,7 @@
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz",
"integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=",
"dev": true,
"requires": {
"co": "4.6.0",
"fast-deep-equal": "1.0.0",
......@@ -1201,7 +1202,8 @@
"big.js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
"integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q=="
"integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
"dev": true
},
"binary-extensions": {
"version": "1.10.0",
......@@ -1726,7 +1728,8 @@
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
"dev": true
},
"coa": {
"version": "1.0.4",
......@@ -2780,7 +2783,8 @@
"emojis-list": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k="
"integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=",
"dev": true
},
"encodeurl": {
"version": "1.0.1",
......@@ -3665,7 +3669,8 @@
"fast-deep-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8="
"integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
"dev": true
},
"fast-levenshtein": {
"version": "2.0.6",
......@@ -5012,12 +5017,14 @@
"json-schema-traverse": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
},
"json-stable-stringify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
"dev": true,
"requires": {
"jsonify": "0.0.0"
}
......@@ -5037,7 +5044,8 @@
"json5": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE="
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
},
"jsonfile": {
"version": "4.0.0",
......@@ -5051,7 +5059,8 @@
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
"jsonpointer": {
"version": "4.0.1",
......@@ -5394,6 +5403,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz",
"integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=",
"dev": true,
"requires": {
"big.js": "3.2.0",
"emojis-list": "2.1.0",
......@@ -5413,8 +5423,7 @@
"lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
"dev": true
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
},
"lodash._arraycopy": {
"version": "3.0.0",
......@@ -9666,6 +9675,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz",
"integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=",
"dev": true,
"requires": {
"ajv": "5.2.3"
}
......@@ -9789,11 +9799,6 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"simple-web-worker": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/simple-web-worker/-/simple-web-worker-1.2.0.tgz",
"integrity": "sha1-Le/9CZiXa4JLFBonzlBNMdDZkQg="
},
"sinon": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-4.0.1.tgz",
......@@ -10840,14 +10845,6 @@
"integrity": "sha512-x3LV3wdmmERhVCYy3quqA57NJW7F3i6faas++pJQWtknWT+n7k30F4TVdHvCLn48peTJFRvCpxs3UuFPqgeELg==",
"dev": true
},
"vue-worker": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/vue-worker/-/vue-worker-1.2.1.tgz",
"integrity": "sha1-3q4UuYqdidqrsg5/xhkGQoSbbZM=",
"requires": {
"simple-web-worker": "1.2.0"
}
},
"watchpack": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz",
......@@ -11194,15 +11191,6 @@
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
"dev": true
},
"worker-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-1.0.0.tgz",
"integrity": "sha512-dUwgs4Rdi1qG3VciM1+EPgAoO8m9USpCXxE3xmpWrnHJSMKGkzpCUNeYLjBRgYcSkf2A5xnXpR450Wqtu+pq0w==",
"requires": {
"loader-utils": "1.1.0",
"schema-utils": "0.3.0"
}
},
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
......
......@@ -17,6 +17,7 @@
"dependencies": {
"compressjs": "^1.0.3",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"lz-string": "^1.4.4",
"semantic-ui-css": "^2.2.12",
"vue": "^2.4.2",
......
......@@ -3,9 +3,18 @@
<div class="ui secondary pointing menu">
<div class="ui container">
<router-link class="ui item" active-class="active" to="/browse">Browse</router-link>
<router-link class="ui item" active-class="active" to="/about">About</router-link>
</div>
</div>
<router-view/>
<div class="ui vertical footer segment form-page">
<div class="ui divider"></div>
<div class="ui text container">
Eliot Berriot, no rights reserved. All SCP data is extracteed from <a href="http://scp-wiki.net/">scp-wiki.net</a> and published under the
<a href="http://creativecommons.org/licenses/by-sa/3.0/">
Creative Commons Attribution-ShareAlike 3.0 License</a>
</div>
</div>
</div>
</template>
......@@ -21,7 +30,9 @@ export default {
#app {
padding: 1rem;
}
#app > .menu {
margin-bottom: 2rem;
}
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
......
<template>
<div class="ui text container">
<h1 class="ui header">
About this project
</h1>
<p>
I worked for many years as a Foundation agent, and I can say without a
doubt I saw some really, really scary stuff during my assignments.
Procedure 110-Montauk isn't even the worst.
<p>
During one mission involving SCP-███, I had access to a full backup
of Foundation files. The Foundation executives are not especially known
for showing mercy in case of unauthorized accesses to highly classified
data.
</p>
<p>
In order to survive, I used my brand new knowledge of two Keter-class
objects to trigger a containment breach and escape.
</p>
<p>
This has been three months since I escaped. I had the time to read most of those files,
and I don't think they should remain secret anymore: people have the right
to know what atrocities the Foundation is dealing with, and commiting
to "protect" them.
</p>
<p>
Today, I'm releasing those files. Hopefully, humanity is now strong
enough to face the truth. They'll probably find me soon though, so I don't
really care anymore.
</p>
<p>By the way, it's not a prank. It's real. <em>Everything</em> is real.</p>
<strong>Chuck</strong>
<div class="ui divider"></div>
<p>
In case you don't believe the previous explanation (I can't blame you),
here is another one.
</p>
<p>
This is simply a pet project. I really like those SCP stories, and I found
it hard sometimes to browse the wiki to find
<router-link :to="{path: '/browse', query: {objectClass: 'neutralized'}}">
all the neutralized objects
</router-link>,
<router-link :to="{path: '/browse', query: {objectClass: 'keter', sort: '-comments'}}">
the most commented Keters
</router-link> or
<router-link :to="{path: '/browse', query: {objectClass: 'thaumiel'}}">
all those fancy Thaumiels.
</router-link>
</p>
<p>
Using SCP browser, you can filter SCPs, read them and navigate between them.
The project itself is not intended to replace scp-wiki.net at all, as
the wiki is the unique and up-to-date data source. Simply think of this as another
interface to browse the same data.
</p>
<p>
Maintaining this is also a way for me to discover new technologies and
programming libraries. The source code behind this is open ad free for anyone
to reuse or hack on.
</p>
<p>Thank you for reading this, here are a few useful links:</p>
<ul class="ui list">
<li>
<a href="https://code.eliotberriot.com/eliotberriot/scp-browser/">
Project repository and issue tracker
</a>
</li>
<li>
<a href="https://scp.eliotberriot.com/data.json">
Data extracted from scp-wiki.net, as a JSON file
</a>
</li>
<li>
<a href="mailto:contact@eliotberriot.com">
contact@eliotberriot.com
</a>
is my email, if you need to contact me for anything regarding this project
</li>
<li>
I'm
<a href="https://mastodon.eliotberriot.com/@eliotberriot">
@eliotberriot@mastodon.eliotberriotcom
</a>
on Mastodon. Feel free to say hi :)
</li>
</ul>
</div>
</template>
<script>
export default {}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
<template>
<div>
<data-loader :loading="scpData.loading"></data-loader>
<div v-if="!scpData.loading" class="ui grid">
<div class="three wide column">
<div v-if="!scpData.loading" class="ui stackable grid">
<div class="four wide tablet column">
<h2>Filters</h2>
<div class="ui form">
<button class="ui fluid basic icon button" @click="scpData.initFilters()">
......@@ -28,13 +28,14 @@
</div>
<div class="ui segment">
<h3 class="ui header">Search</h3>
<div class="ui icon input">
<div class="ui fluid icon input">
<i class="search icon"></i>
<input
type="text"
id="search"
placeholder="666, Red ice, clown, shadow..."
v-model="scpData.filtersById.search.query">
@input="updateSearch"
:value="scpData.filtersById.search.value">
</div>
</div>
<div class='ui segment'>
......@@ -44,12 +45,12 @@
type="checkbox"
@change="toggleAll('objectClass')"
id="objectclass-toggle-all"
:checked="scpData.filtersById.objectClass.choices.length === scpData.filtersById.objectClass.selected.length">
:checked="scpData.filtersById.objectClass.choices.length === scpData.filtersById.objectClass.value.length">
<label for="objectclass-toggle-all">Select all</label>
</div>
<div class="choices">
<div class="inline field" v-for="choice in scpData.filtersById.objectClass.choices">
<input type="checkbox" :id="'objectclass' + choice.id" :value="choice.id" v-model="scpData.filtersById.objectClass.selected">
<input type="checkbox" :id="'objectclass' + choice.id" :value="choice.id" v-model="scpData.filtersById.objectClass.value">
<span :class="['ui', scpData.getObjectClassColor(choice.id), 'empty', 'circular', 'label']"></span>
<label :for="'objectclass' + choice.id">{{ choice.label }}</label>
</div>
......@@ -57,7 +58,7 @@
</div>
<div class='ui segment'>
<h3 class="ui header">Tags</h3>
<select v-model="scpData.filtersById.tags.selected" class="ui fluid tags dropdown" multiple>
<select v-model="scpData.filtersById.tags.value" class="ui fluid tags dropdown" multiple>
<option v-for="tag in scpData.filtersById.tags.choices" :value="tag.id">{{ tag.label }}</option>
</select>
</div>
......@@ -66,14 +67,21 @@
</div>
<div class="twelve wide column">
<h1>{{ filteredScps.length }} SCPs matching your criteria</h1>
<pagination v-if="totalPages > 1" :current="scpData.page" :total="totalPages" @page-selected="selectPage"></pagination>
<div class="ui hidden divider"></div>
<div class="ui link stackable fluid cards">
<div v-for="scp in filteredScps.slice(0, 100)" class="card">
<div v-if="scp.images[0]" class="ui small image" v-lazy:background-image="scp.images[0].src"></div>
<div v-else class="ui small image no-image"></div>
<div v-for="scp in filteredScps.slice((scpData.page - 1) * scpData.pageSize, scpData.page * scpData.pageSize)" class="card">
<router-link
v-if="scp.images[0]"
tag="div"
:to="{name: 'detail', params: {id: scp.id }}"
class="ui small image" v-lazy:background-image="scp.images[0].src"></router-link>
</router-link>
<router-link v-else class="ui small image no-image" :to="{name: 'detail', params: {id: scp.id }}"></router-link>
<div class="content">
<div class="header ellipsis">
<router-link class="header ellipsis" :to="{name: 'detail', params: {id: scp.id }}">
#{{ scp.id }} - {{ scp.name }}
</div>
</router-link>
<div class="meta">
<span :class="['ui', scpData.getObjectClassColor(scp.object_class), 'tiny', 'label']">
{{ scpData.data.object_classes[scp.object_class].label }}
......@@ -102,6 +110,8 @@
</div>
</div>
</div>
<div class="ui hidden divider"></div>
<pagination v-if="totalPages > 1" :current="scpData.page" :total="totalPages" @page-selected="selectPage"></pagination>
</div>
</div>
</div>
......@@ -111,10 +121,16 @@
import scpData from '@/scp-data'
import $ from 'jquery'
import DataLoader from './DataLoader'
import Pagination from './Pagination'
import _ from 'lodash'
export default {
props: {
initialFilters: {type: Object, required: false}
},
components: {
DataLoader
DataLoader,
Pagination
},
data () {
let d = {
......@@ -123,26 +139,65 @@ export default {
return d
},
mounted: function () {
this.scpData.load()
$(this.$el).find('.ui.checkbox').checkbox()
$(this.$el).find('.ui.dropdown').dropdown()
let self = this
let existing = this.scpData.currentFilters()
let filters = this.scpData.filtersFromQs(this.initialFilters)
this.scpData.load(function () {
self.$nextTick(function () {
$(self.$el).find('.ui.dropdown').dropdown()
$(self.$el).find('.ui.checkbox').checkbox()
})
self.updateQs()
}, Object.assign(existing, filters))
},
methods: {
toggleAll: function (filter) {
let f = this.scpData.filtersById[filter]
if (f.selected.length === f.choices.length) {
f.selected = []
if (f.value.length === f.choices.length) {
f.value = []
} else {
f.selected = f.choices.map(e => { return e.id })
f.value = f.choices.map(e => { return e.id })
}
}
},
updateQs: function () {
let query = {}
this.scpData.filters.forEach(f => {
let q = f.toQs(f.value)
if (q.length > 0) {
query[f.id] = q
}
})
if (this.scpData.sortAscending) {
query.sort = this.scpData.sortField
} else {
query.sort = '-' + this.scpData.sortField
}
query.page = this.scpData.page
this.$router.push({query})
},
selectPage: function (page, scroll) {
this.scpData.page = page
window.scrollTo(0, 0)
},
updateSearch: _.debounce(function (e) {
this.scpData.filtersById.search.value = e.target.value
}, 250)
},
computed: {
filteredScps: function () {
return this.scpData.filteredScps()
},
selectedTags: function () {
return this.scpData.filtersById.tags.selected
return this.scpData.filtersById.tags.value
},
filtersValues: function () {
return this.scpData.filters.map(e => { return e.value })
},
sort: function () {
return [this.scpData.sortField, this.scpData.sortAscending]
},
totalPages: function () {
return this.filteredScps.length / this.scpData.pageSize
}
},
watch: {
......@@ -150,6 +205,16 @@ export default {
if (v.length === 0) {
$(this.$el).find('.ui.dropdown.tags').dropdown('set exactly', [])
}
},
filtersValues: function () {
this.scpData.page = 1
this.updateQs()
},
sort: function () {
this.updateQs()
},
'scpData.page': function () {
this.updateQs()
}
}
}
......
......@@ -3,8 +3,8 @@
<data-loader :loading="scpData.loading"></data-loader>
<div v-if="scp" class="ui stackable grid">
<div class="four wide column">
<div class="ui vertical menu">
<router-link to="/browse" class="item">
<div class="ui vertical fluid menu">
<router-link :to="{path: '/browse', query: this.scpData.toQs()}" class="item">
<i class="block list icon"></i>
Back to list
</router-link>
......@@ -63,13 +63,12 @@
<div class="eight wide column">
<div class="ui fluid text container">
<h1>SCP #{{ scp.id }} - {{ scp.name }}</h1>
<template v-if="scpData.getTextField(scp, 'procedures')">
<h2 class="ui header">Special Containment Procedure</h2>
<p v-for="paragraph in scpData.getTextField(scp, 'procedures')">{{ paragraph }}</p>
</template>
<template v-if="scpData.getTextField(scp, 'description')">
<h2 class="ui header">Description</h2>
<p v-for="paragraph in scpData.getTextField(scp, 'description')">{{ paragraph }}</p>
<template v-for="textField in scp.text_fields">
<h2>{{ textField.title }}</h2>
<scp-paragraph
v-for="(paragraph, i) in textField.paragraphs"
:key="scp.id + textField.code + i"
:paragraph="paragraph"></scp-paragraph>
</template>
</div>
</div>
......@@ -85,6 +84,7 @@
<script>
import scpData from '@/scp-data'
import ScpParagraph from './ScpParagraph'
import DataLoader from './DataLoader'
export default {
......@@ -92,7 +92,8 @@ export default {
id: {required: true}
},
components: {
DataLoader
DataLoader,
ScpParagraph
},
beforeRouteUpdate (to, from, next) {
this.realId = parseInt(to.params.id, '10')
......@@ -106,7 +107,7 @@ export default {
return d
},
mounted: function () {
this.scpData.load()
this.scpData.load(function () {}, null)
},
methods: {
browse: function (filterData) {
......@@ -158,5 +159,4 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
<template>
<div class="ui pagination menu">
<span :class="['ui', {'disabled': current <= 1}, 'item']" @click="current > 1 && $emit('page-selected', current - 1)">
<i class="angle left icon"></i>
</span>
<template v-for="p in humanRange">
<span
v-if="!isNaN(parseInt(p, '10'))"
@click="$emit('page-selected', p)"
:class="[{'active': p === current }, 'item']">
{{ p }}</span>
<span v-else class="ui disabled item">{{ ellipsis }}</span>
</template>
<span :class="['ui', {'disabled': current >= total}, 'item']" @click="current < total && $emit('page-selected', current - 1)">
<i class="angle right icon"></i>
</span>
</div>
</template>
<script>
export default {
props: ['total', 'current'],
data: function () {
return {
ellipsis: '...'
}
},
computed: {
humanRange: function () {
let padding = 2
let r = []
for (var i = 1; i <= this.total + 1; i++) {
if (i <= padding) {
r.push(i)
} else if (i > this.current - padding && i < this.current + padding) {
r.push(i)
} else if (i > this.total + 1 - padding) {
r.push(i)
} else {
if (r.slice(-1)[0] !== this.ellipsis) {
r.push(this.ellipsis)
}
}
}
return r
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
<template>
<router-link :to="{name: 'detail', params: {id: realId}}" :data-tooltip="scp.name">
<slot></slot>
</router-link>
</template>
<script>
import scpData from '@/scp-data'
export default {
props: ['id'],
computed: {
realId: function () {
return parseInt(this.id, '10')
},
scp: function () {
return scpData.data.scps[this.realId]
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
<template>
<p><component :is="output"></component></p>
</template>
<script>
import Vue from 'vue'
import ScpLink from './ScpLink'
export default {
props: ['paragraph'],
computed: {
output () {
let content = this.setScpLinks(this.paragraph)
let temp = Vue.compile(`<span>${content}</span>`)
temp.components = {
ScpLink
}
return temp
}
},
methods: {
setScpLinks: function (p) {
let scpRegexp = new RegExp(/SCP-(\d*)(-[\d*])?/, 'gi')
let final = p.replace(scpRegexp, '<scp-link :id="parseInt(\'$1\', \'10\')">SCP-$1$2</scp-link>')
return final
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import About from '@/components/About'
import Browse from '@/components/Browse'
import Detail from '@/components/Detail'
......@@ -8,16 +9,30 @@ Vue.use(Router)
export default new Router({
mode: 'history',
scrollBehavior (to, from, savedPosition) {
return {
x: 0,
y: 0
}
},
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/browse',
name: 'Browse',
component: Browse
component: Browse,
props: (route) => ({
initialFilters: route.query
})
},
{
path: '/browse/:id',
......
......@@ -12,6 +12,8 @@ var scpData = {
searchIndex: null,
sortAscending: true,
sortField: 'id',
page: 1,
pageSize: 40,
sortFields: [