start on themes
This commit is contained in:
parent
fb9bc18edf
commit
eaaacdeef5
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -2881,10 +2881,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idb-keyval": {
|
"idb": {
|
||||||
"version": "2.3.0",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/idb/-/idb-2.0.4.tgz",
|
||||||
"integrity": "sha1-TURLgMP4b8vNUTIbTcvJJHxZSMA="
|
"integrity": "sha512-Nw4ykKrrVje6YODRiRm/k2ucFEQeoY+zrkszfOuzVmxx8yyBMtZh2KLaRCKk9r5GzhuF0QlNCVjBewP2n5OZ7Q=="
|
||||||
},
|
},
|
||||||
"ieee754": {
|
"ieee754": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"font-awesome-svg-png": "^1.2.2",
|
"font-awesome-svg-png": "^1.2.2",
|
||||||
"glob": "^7.1.2",
|
"glob": "^7.1.2",
|
||||||
"idb-keyval": "^2.3.0",
|
"idb": "^2.0.4",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.7.2",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
</svg>
|
</svg>
|
||||||
<h1>Pinafore</h1>
|
<h1>Pinafore</h1>
|
||||||
</div>
|
</div>
|
||||||
<p>Pinafore is a web client for <a href="https://joinmastodon.org">Mastodon</a>, optimized for speed and simplicity.</p>
|
<p>Pinafore is a web client for <a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>, optimized for speed and simplicity.</p>
|
||||||
|
|
||||||
<p>To get started, <a href="/settings/instances/add">log in to an instance</a>.</p>
|
<p>To get started, <a href="/settings/instances/add">log in to an instance</a>.</p>
|
||||||
|
|
||||||
<p>Don't have an instance? <a href="https://joinmastodon.org">Join Mastodon!</a></p>
|
<p>Don't have an instance? <a rel="noopener" target="_blank" href="https://joinmastodon.org">Join Mastodon!</a></p>
|
||||||
</FreeTextLayout>
|
</FreeTextLayout>
|
||||||
<style>
|
<style>
|
||||||
.banner {
|
.banner {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../_utils/store'
|
import { store } from '../_utils/store'
|
||||||
import { getHomeTimeline } from '../_utils/mastodon'
|
import { getHomeTimeline } from '../_utils/mastodon/oauth'
|
||||||
import fixture from '../_utils/fixture.json'
|
import fixture from '../_utils/fixture.json'
|
||||||
import Status from './Status.html'
|
import Status from './Status.html'
|
||||||
|
|
||||||
|
|
27
routes/_utils/ajax.js
Normal file
27
routes/_utils/ajax.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export async function post(url, body) {
|
||||||
|
return await (await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})).json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function paramsString(paramsObject) {
|
||||||
|
let params = new URLSearchParams()
|
||||||
|
Object.keys(paramsObject).forEach(key => {
|
||||||
|
params.set(key, paramsObject[value])
|
||||||
|
})
|
||||||
|
return params.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(url, headers = {}) {
|
||||||
|
return await (await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: Object.assign(headers, {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
})
|
||||||
|
})).json()
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
import idbKeyVal from 'idb-keyval'
|
|
||||||
import { blobToBase64 } from '../_utils/binary'
|
|
||||||
|
|
||||||
let databasePromise
|
|
||||||
|
|
||||||
if (process.browser) {
|
|
||||||
databasePromise = Promise.resolve().then(async () => {
|
|
||||||
let token = await idbKeyVal.get('secure_token')
|
|
||||||
if (!token) {
|
|
||||||
let array = new Uint32Array(1028)
|
|
||||||
crypto.getRandomValues(array);
|
|
||||||
let token = await blobToBase64(new Blob([array]))
|
|
||||||
await idbKeyVal.set('secure_token', token)
|
|
||||||
}
|
|
||||||
return idbKeyVal
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
databasePromise = Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
export { databasePromise }
|
|
2
routes/_utils/database/database.js
Normal file
2
routes/_utils/database/database.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import keyval from './idb-keyval'
|
||||||
|
|
57
routes/_utils/database/idb-keyval.js
Normal file
57
routes/_utils/database/idb-keyval.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import idb from 'idb'
|
||||||
|
|
||||||
|
// copypasta'd from https://github.com/jakearchibald/idb#keyval-store
|
||||||
|
|
||||||
|
const dbPromise = idb.open('keyval-store', 1, upgradeDB => {
|
||||||
|
upgradeDB.createObjectStore('keyval')
|
||||||
|
})
|
||||||
|
|
||||||
|
const idbKeyval = {
|
||||||
|
get(key) {
|
||||||
|
return dbPromise.then(db => {
|
||||||
|
return db.transaction('keyval').objectStore('keyval').get(key)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
set(key, val) {
|
||||||
|
return dbPromise.then(db => {
|
||||||
|
const tx = db.transaction('keyval', 'readwrite')
|
||||||
|
tx.objectStore('keyval').put(val, key)
|
||||||
|
return tx.complete
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delete(key) {
|
||||||
|
return dbPromise.then(db => {
|
||||||
|
const tx = db.transaction('keyval', 'readwrite')
|
||||||
|
tx.objectStore('keyval').delete(key)
|
||||||
|
return tx.complete
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
return dbPromise.then(db => {
|
||||||
|
const tx = db.transaction('keyval', 'readwrite')
|
||||||
|
tx.objectStore('keyval').clear()
|
||||||
|
return tx.complete
|
||||||
|
})
|
||||||
|
},
|
||||||
|
keys() {
|
||||||
|
return dbPromise.then(db => {
|
||||||
|
const tx = db.transaction('keyval')
|
||||||
|
const keys = []
|
||||||
|
const store = tx.objectStore('keyval')
|
||||||
|
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
|
||||||
|
// openKeyCursor isn't supported by Safari, so we fall back
|
||||||
|
const iterate = store.iterateKeyCursor || store.iterateCursor
|
||||||
|
iterate.call(store, cursor => {
|
||||||
|
if (!cursor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keys.push(cursor.key)
|
||||||
|
cursor.continue()
|
||||||
|
})
|
||||||
|
|
||||||
|
return tx.complete.then(() => keys)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default idbKeyval
|
|
@ -1,62 +0,0 @@
|
||||||
const WEBSITE = 'https://pinafore.social'
|
|
||||||
const REDIRECT_URI = (typeof location !== 'undefined' ? location.origin : 'https://pinafore.social') + '/settings/instances/add'
|
|
||||||
const SCOPES = 'read write follow'
|
|
||||||
const CLIENT_NAME = 'Pinafore'
|
|
||||||
|
|
||||||
export function registerApplication(instanceName) {
|
|
||||||
const url = `https://${instanceName}/api/v1/apps`
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_name: CLIENT_NAME,
|
|
||||||
redirect_uris: REDIRECT_URI,
|
|
||||||
scopes: SCOPES,
|
|
||||||
website: WEBSITE
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateAuthLink(instanceName, clientId) {
|
|
||||||
let url = `https://${instanceName}/oauth/authorize`
|
|
||||||
|
|
||||||
let params = new URLSearchParams()
|
|
||||||
params.set('client_id', clientId)
|
|
||||||
params.set('redirect_uri', REDIRECT_URI)
|
|
||||||
params.set('response_type', 'code')
|
|
||||||
params.set('scope', SCOPES)
|
|
||||||
url += '?' + params.toString()
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret, code) {
|
|
||||||
let url = `https://${instanceName}/oauth/token`
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
client_id: clientId,
|
|
||||||
client_secret: clientSecret,
|
|
||||||
redirect_uri: REDIRECT_URI,
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: code
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHomeTimeline(instanceName, accessToken) {
|
|
||||||
let url = `https://${instanceName}/api/v1/timelines/home`
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Authorization': `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
43
routes/_utils/mastodon/oauth.js
Normal file
43
routes/_utils/mastodon/oauth.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
const WEBSITE = 'https://pinafore.social'
|
||||||
|
const REDIRECT_URI = (typeof location !== 'undefined' ? location.origin : 'https://pinafore.social') + '/settings/instances'
|
||||||
|
const SCOPES = 'read write follow'
|
||||||
|
const CLIENT_NAME = 'Pinafore'
|
||||||
|
import { post, get, paramsString } from '../ajax'
|
||||||
|
|
||||||
|
export function registerApplication(instanceName) {
|
||||||
|
const url = `https://${instanceName}/api/v1/apps`
|
||||||
|
return post(url, {
|
||||||
|
client_name: CLIENT_NAME,
|
||||||
|
redirect_uris: REDIRECT_URI,
|
||||||
|
scopes: SCOPES,
|
||||||
|
website: WEBSITE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateAuthLink(instanceName, clientId) {
|
||||||
|
let params = paramsString({
|
||||||
|
'client_id': clientId,
|
||||||
|
'redirect_uri': REDIRECT_URI,
|
||||||
|
'response_type': 'code',
|
||||||
|
'scope': SCOPES
|
||||||
|
})
|
||||||
|
return `https://${instanceName}/oauth/authorize?${params}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret, code) {
|
||||||
|
let url = `https://${instanceName}/oauth/token`
|
||||||
|
return post(url, {
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
redirect_uri: REDIRECT_URI,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHomeTimeline(instanceName, accessToken) {
|
||||||
|
let url = `https://${instanceName}/api/v1/timelines/home`
|
||||||
|
return get(url, {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
})
|
||||||
|
}
|
8
routes/_utils/mastodon/user.js
Normal file
8
routes/_utils/mastodon/user.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { get } from '../ajax'
|
||||||
|
|
||||||
|
export function getCurrentUser(instanceName, accessToken) {
|
||||||
|
let url = `https://${instanceName}/api/v1/accounts/verify_credentials`
|
||||||
|
return get(url, {
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
})
|
||||||
|
}
|
|
@ -65,6 +65,15 @@ store.compute(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
store.compute(
|
||||||
|
'currentInstanceData',
|
||||||
|
['currentInstance', 'loggedInInstances'],
|
||||||
|
(currentInstance, loggedInInstances) => {
|
||||||
|
return Object.assign({
|
||||||
|
name: currentInstance
|
||||||
|
}, loggedInInstances[currentInstance])
|
||||||
|
})
|
||||||
|
|
||||||
if (process.browser && process.env.NODE_ENV !== 'production') {
|
if (process.browser && process.env.NODE_ENV !== 'production') {
|
||||||
window.store = store // for debugging
|
window.store = store // for debugging
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
:global(.settings .free-text h1) {
|
:global(.settings .free-text h1) {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
:global(.settings .free-text h2) {
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import SettingsNav from './SettingsNav.html';
|
import SettingsNav from './SettingsNav.html';
|
||||||
|
|
|
@ -5,18 +5,78 @@
|
||||||
<Layout page='settings'>
|
<Layout page='settings'>
|
||||||
<SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}">
|
<SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}">
|
||||||
<h1>{{params.instanceName}}</h1>
|
<h1>{{params.instanceName}}</h1>
|
||||||
|
|
||||||
|
{{#if currentUser}}
|
||||||
|
<h2>Logged in as:</h2>
|
||||||
|
<div class="current-user">
|
||||||
|
<img src="{{currentUser.avatar}}" />
|
||||||
|
<a rel="noopener" target="_blank" href="{{currentUser.url}}">@{{currentUser.acct}}</a>
|
||||||
|
<span class="acct-name">{{currentUser.display_name}}</span>
|
||||||
|
</div>
|
||||||
|
<h2>Theme:</h2>
|
||||||
|
<form class="theme-chooser">
|
||||||
|
<div class="theme-group">
|
||||||
|
<input type="radio" name="current-theme" id="theme-default" value="default">
|
||||||
|
<label for="theme-default">Royal (default)</label>
|
||||||
|
</div>
|
||||||
|
<div class="theme-group">
|
||||||
|
<input type="radio" name="current-theme" id="theme-crimson"
|
||||||
|
value="crimson">
|
||||||
|
<label for="theme-crimson">Crimson and Clover</label>
|
||||||
|
</div>
|
||||||
|
<div class="theme-group">
|
||||||
|
<input type="radio" name="current-theme" id="theme-hotpants"
|
||||||
|
value="hotpants">
|
||||||
|
<label for="theme-hotpants">Hot Pants</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{/if}}
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<style>
|
||||||
|
.current-user {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
.current-user img {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.current-user img, .current-user a, .current-user span {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.theme-chooser {
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
.theme-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.theme-chooser label {
|
||||||
|
margin: 2px 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import { store } from '../../_utils/store'
|
import { store } from '../../_utils/store'
|
||||||
import Layout from '../../_components/Layout.html'
|
import Layout from '../../_components/Layout.html'
|
||||||
import SettingsLayout from '../_components/SettingsLayout.html'
|
import SettingsLayout from '../_components/SettingsLayout.html'
|
||||||
|
import { getCurrentUser } from '../../_utils/mastodon/user'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Layout,
|
Layout,
|
||||||
SettingsLayout
|
SettingsLayout
|
||||||
},
|
},
|
||||||
store: () => store
|
store: () => store,
|
||||||
|
oncreate: async function () {
|
||||||
|
let currentInstanceData = this.store.get('currentInstanceData')
|
||||||
|
let currentUser = await getCurrentUser(currentInstanceData.name, currentInstanceData.access_token)
|
||||||
|
this.set({currentUser: currentUser})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -19,7 +19,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{#if !$isUserLoggedIn}}
|
{{#if !$isUserLoggedIn}}
|
||||||
<p>Don't have an instance? <a href="https://joinmastodon.org">Join Mastodon!</a></p>
|
<p>Don't have an instance? <a rel="noopener" target="_blank" href="https://joinmastodon.org">Join Mastodon!</a></p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -49,8 +49,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Layout from '../../_components/Layout.html';
|
import Layout from '../../_components/Layout.html';
|
||||||
import SettingsLayout from '../_components/SettingsLayout.html'
|
import SettingsLayout from '../_components/SettingsLayout.html'
|
||||||
import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon'
|
import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon/oauth'
|
||||||
import { databasePromise } from '../../_utils/database'
|
|
||||||
import { store } from '../../_utils/store'
|
import { store } from '../../_utils/store'
|
||||||
import { goto } from 'sapper/runtime.js'
|
import { goto } from 'sapper/runtime.js'
|
||||||
|
|
||||||
|
@ -74,12 +73,12 @@
|
||||||
onReceivedOauthCode: async function(code) {
|
onReceivedOauthCode: async function(code) {
|
||||||
let currentRegisteredInstanceName = this.store.get('currentRegisteredInstanceName')
|
let currentRegisteredInstanceName = this.store.get('currentRegisteredInstanceName')
|
||||||
let currentRegisteredInstance = this.store.get('currentRegisteredInstance')
|
let currentRegisteredInstance = this.store.get('currentRegisteredInstance')
|
||||||
let instanceData = await (await getAccessTokenFromAuthCode(
|
let instanceData = await getAccessTokenFromAuthCode(
|
||||||
currentRegisteredInstanceName,
|
currentRegisteredInstanceName,
|
||||||
currentRegisteredInstance.client_id,
|
currentRegisteredInstance.client_id,
|
||||||
currentRegisteredInstance.client_secret,
|
currentRegisteredInstance.client_secret,
|
||||||
code
|
code
|
||||||
)).json()
|
)
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
let loggedInInstances = this.store.get('loggedInInstances')
|
let loggedInInstances = this.store.get('loggedInInstances')
|
||||||
let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder')
|
let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder')
|
||||||
|
|
Loading…
Reference in a new issue