start on themes
This commit is contained in:
parent
fb9bc18edf
commit
eaaacdeef5
|
@ -2881,10 +2881,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"idb-keyval": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-2.3.0.tgz",
|
||||
"integrity": "sha1-TURLgMP4b8vNUTIbTcvJJHxZSMA="
|
||||
"idb": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-2.0.4.tgz",
|
||||
"integrity": "sha512-Nw4ykKrrVje6YODRiRm/k2ucFEQeoY+zrkszfOuzVmxx8yyBMtZh2KLaRCKk9r5GzhuF0QlNCVjBewP2n5OZ7Q=="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.8",
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"font-awesome-svg-png": "^1.2.2",
|
||||
"glob": "^7.1.2",
|
||||
"idb-keyval": "^2.3.0",
|
||||
"idb": "^2.0.4",
|
||||
"node-fetch": "^1.7.3",
|
||||
"node-sass": "^4.7.2",
|
||||
"npm-run-all": "^4.1.2",
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
</svg>
|
||||
<h1>Pinafore</h1>
|
||||
</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>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>
|
||||
<style>
|
||||
.banner {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</style>
|
||||
<script>
|
||||
import { store } from '../_utils/store'
|
||||
import { getHomeTimeline } from '../_utils/mastodon'
|
||||
import { getHomeTimeline } from '../_utils/mastodon/oauth'
|
||||
import fixture from '../_utils/fixture.json'
|
||||
import Status from './Status.html'
|
||||
|
||||
|
|
|
@ -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 }
|
|
@ -0,0 +1,2 @@
|
|||
import keyval from './idb-keyval'
|
||||
|
|
@ -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}`
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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}`
|
||||
})
|
||||
}
|
|
@ -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') {
|
||||
window.store = store // for debugging
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
:global(.settings .free-text h1) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
:global(.settings .free-text h2) {
|
||||
margin: 20px 0 10px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import SettingsNav from './SettingsNav.html';
|
||||
|
|
|
@ -5,18 +5,78 @@
|
|||
<Layout page='settings'>
|
||||
<SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}">
|
||||
<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>
|
||||
</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>
|
||||
import { store } from '../../_utils/store'
|
||||
import Layout from '../../_components/Layout.html'
|
||||
import SettingsLayout from '../_components/SettingsLayout.html'
|
||||
import { getCurrentUser } from '../../_utils/mastodon/user'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
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>
|
|
@ -19,7 +19,7 @@
|
|||
</form>
|
||||
|
||||
{{#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}}
|
||||
</SettingsLayout>
|
||||
</Layout>
|
||||
|
@ -49,8 +49,7 @@
|
|||
<script>
|
||||
import Layout from '../../_components/Layout.html';
|
||||
import SettingsLayout from '../_components/SettingsLayout.html'
|
||||
import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon'
|
||||
import { databasePromise } from '../../_utils/database'
|
||||
import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon/oauth'
|
||||
import { store } from '../../_utils/store'
|
||||
import { goto } from 'sapper/runtime.js'
|
||||
|
||||
|
@ -74,12 +73,12 @@
|
|||
onReceivedOauthCode: async function(code) {
|
||||
let currentRegisteredInstanceName = this.store.get('currentRegisteredInstanceName')
|
||||
let currentRegisteredInstance = this.store.get('currentRegisteredInstance')
|
||||
let instanceData = await (await getAccessTokenFromAuthCode(
|
||||
let instanceData = await getAccessTokenFromAuthCode(
|
||||
currentRegisteredInstanceName,
|
||||
currentRegisteredInstance.client_id,
|
||||
currentRegisteredInstance.client_secret,
|
||||
code
|
||||
)).json()
|
||||
)
|
||||
// TODO: handle error
|
||||
let loggedInInstances = this.store.get('loggedInInstances')
|
||||
let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder')
|
||||
|
|
Loading…
Reference in New Issue