forked from cybrespace/pinafore
add username autocomplete feature
This commit is contained in:
parent
5430fdd189
commit
6fc21e40bf
|
@ -47,3 +47,21 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
store.set({postingStatus: false})
|
store.set({postingStatus: false})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function insertUsername (realm, username, startIndex, endIndex) {
|
||||||
|
let oldText = store.getComposeData(realm, 'text')
|
||||||
|
let pre = oldText.substring(0, startIndex)
|
||||||
|
let post = oldText.substring(endIndex)
|
||||||
|
let newText = `${pre}@${username} ${post}`
|
||||||
|
store.setComposeData(realm, {text: newText})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickSelectedAutosuggestionUsername (realm) {
|
||||||
|
let selectionStart = store.get('composeSelectionStart')
|
||||||
|
let searchText = store.get('composeAutosuggestionSearchText')
|
||||||
|
let selection = store.get('composeAutosuggestionSelected') || 0
|
||||||
|
let account = store.get('composeAutosuggestionSearchResults')[selection]
|
||||||
|
let startIndex = selectionStart - searchText.length
|
||||||
|
let endIndex = selectionStart
|
||||||
|
await insertUsername(realm, account.acct, startIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
<div class="compose-autosuggest {{shown ? 'shown' : ''}}"
|
||||||
|
aria-hidden="true" >
|
||||||
|
<ComposeAutosuggestionList
|
||||||
|
items="{{searchResults}}"
|
||||||
|
on:click="onUserSelected(event)"
|
||||||
|
:selected
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.compose-autosuggest {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.1s linear;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
}
|
||||||
|
.compose-autosuggest.shown {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.compose-autosuggest {
|
||||||
|
/* hack: move this over to the left on mobile so it's easier to see */
|
||||||
|
transform: translateX(-58px); /* avatar size 48px + 10px padding */
|
||||||
|
width: calc(100vw - 20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import { store } from '../../_store/store'
|
||||||
|
import { database } from '../../_database/database'
|
||||||
|
import { insertUsername } from '../../_actions/compose'
|
||||||
|
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||||
|
import { once } from '../../_utils/once'
|
||||||
|
import ComposeAutosuggestionList from './ComposeAutosuggestionList.html'
|
||||||
|
|
||||||
|
const SEARCH_RESULTS_LIMIT = 4
|
||||||
|
const DATABASE_SEARCH_RESULTS_LIMIT = 30
|
||||||
|
const MIN_PREFIX_LENGTH = 1
|
||||||
|
const SEARCH_REGEX = new RegExp(`(?:\\s|^)(@\\S{${MIN_PREFIX_LENGTH},})$`)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
oncreate() {
|
||||||
|
// perf improves for input responsiveness
|
||||||
|
this.observe('composeSelectionStart', () => {
|
||||||
|
scheduleIdleTask(() => {
|
||||||
|
this.set({composeSelectionStartDeferred: this.get('composeSelectionStart')})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.observe('composeFocused', (composeFocused) => {
|
||||||
|
let updateFocusedState = () => {
|
||||||
|
scheduleIdleTask(() => {
|
||||||
|
this.set({composeFocusedDeferred: this.get('composeFocused')})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: hack so that when the user clicks the button, and the textarea blurs,
|
||||||
|
// we don't immediately hide the dropdown which would cause the click to get lost
|
||||||
|
if (composeFocused) {
|
||||||
|
updateFocusedState()
|
||||||
|
} else {
|
||||||
|
Promise.race([
|
||||||
|
new Promise(resolve => setTimeout(resolve, 200)),
|
||||||
|
new Promise(resolve => this.once('userSelected', resolve))
|
||||||
|
]).then(updateFocusedState)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.observe('searchText', async searchText => {
|
||||||
|
if (!searchText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let results = await this.search(searchText)
|
||||||
|
this.store.set({
|
||||||
|
composeAutosuggestionSelected: 0,
|
||||||
|
composeAutosuggestionSearchText: searchText,
|
||||||
|
composeAutosuggestionSearchResults: results
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.observe('shown', shown => {
|
||||||
|
this.store.set({composeAutosuggestionShown: shown})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
once: once,
|
||||||
|
onUserSelected(account) {
|
||||||
|
this.fire('userSelected')
|
||||||
|
let realm = this.get('realm')
|
||||||
|
let selectionStart = this.store.get('composeSelectionStart')
|
||||||
|
let searchText = this.store.get('composeAutosuggestionSearchText')
|
||||||
|
let startIndex = selectionStart - searchText.length
|
||||||
|
let endIndex = selectionStart
|
||||||
|
/* no await */ insertUsername(realm, account.acct, startIndex, endIndex)
|
||||||
|
},
|
||||||
|
async search(searchText) {
|
||||||
|
let currentInstance = this.store.get('currentInstance')
|
||||||
|
let results = await database.searchAccountsByUsername(
|
||||||
|
currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
|
||||||
|
return results.slice(0, SEARCH_RESULTS_LIMIT)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
composeSelectionStart: ($composeSelectionStart) => $composeSelectionStart,
|
||||||
|
composeFocused: ($composeFocused) => $composeFocused,
|
||||||
|
searchResults: ($composeAutosuggestionSearchResults) => $composeAutosuggestionSearchResults || [],
|
||||||
|
selected: ($composeAutosuggestionSelected) => $composeAutosuggestionSelected || 0,
|
||||||
|
searchText: (text, composeSelectionStartDeferred) => {
|
||||||
|
let selectionStart = composeSelectionStartDeferred || 0
|
||||||
|
if (!text || selectionStart < MIN_PREFIX_LENGTH) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let match = text.substring(0, selectionStart).match(SEARCH_REGEX)
|
||||||
|
return match && match[1]
|
||||||
|
},
|
||||||
|
shown: (composeFocusedDeferred, searchText, searchResults) => {
|
||||||
|
return !!(composeFocusedDeferred &&
|
||||||
|
searchText &&
|
||||||
|
searchResults.length)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
store: () => store,
|
||||||
|
components: {
|
||||||
|
ComposeAutosuggestionList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,90 @@
|
||||||
|
<ul class="generic-user-list">
|
||||||
|
{{#each items as account, i @id}}
|
||||||
|
<li class="generic-user-list-item">
|
||||||
|
<button class="generic-user-list-button {{i === selected ? 'selected' : ''}}"
|
||||||
|
tabindex="0"
|
||||||
|
on:click="fire('click', account)">
|
||||||
|
<div class="generic-user-list-grid">
|
||||||
|
<Avatar
|
||||||
|
className="generic-user-list-item-avatar"
|
||||||
|
size="small"
|
||||||
|
:account
|
||||||
|
/>
|
||||||
|
<span class="generic-user-list-display-name">
|
||||||
|
{{account.display_name || account.acct}}
|
||||||
|
</span>
|
||||||
|
<span class="generic-user-list-username">
|
||||||
|
{{'@' + account.acct}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<style>
|
||||||
|
.generic-user-list {
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--compose-autosuggest-outline);
|
||||||
|
}
|
||||||
|
.generic-user-list-item {
|
||||||
|
border-bottom: 1px solid var(--compose-autosuggest-outline);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.generic-user-list-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.generic-user-list-button {
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--settings-list-item-bg);
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.generic-user-list-grid {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-areas: "avatar display-name"
|
||||||
|
"avatar username";
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-column-gap: 10px;
|
||||||
|
grid-row-gap: 5px;
|
||||||
|
}
|
||||||
|
:global(.generic-user-list-item-avatar) {
|
||||||
|
grid-area: avatar;
|
||||||
|
}
|
||||||
|
.generic-user-list-display-name {
|
||||||
|
grid-area: display-name;
|
||||||
|
font-size: 1.1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.generic-user-list-username {
|
||||||
|
grid-area: username;
|
||||||
|
font-size: 1em;
|
||||||
|
color: var(--deemphasized-text-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.generic-user-list-button:hover, .generic-user-list-button.selected {
|
||||||
|
background: var(--compose-autosuggest-item-hover);
|
||||||
|
}
|
||||||
|
.generic-user-list-button:active {
|
||||||
|
background: var(--compose-autosuggest-item-active);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import Avatar from '../Avatar.html'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -8,7 +8,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<ComposeInput :realm :text :autoFocus />
|
<ComposeInput :realm :text :autoFocus />
|
||||||
<ComposeLengthGauge :length :overLimit />
|
<ComposeLengthGauge :length :overLimit />
|
||||||
<ComposeToolbar :realm :postPrivacy :media :contentWarningShown />
|
<ComposeToolbar :realm :postPrivacy :media :contentWarningShown :text />
|
||||||
<ComposeLengthIndicator :length :overLimit />
|
<ComposeLengthIndicator :length :overLimit />
|
||||||
<ComposeMedia :realm :media />
|
<ComposeMedia :realm :media />
|
||||||
<ComposeButton :length :overLimit on:click="onClickPostButton()" />
|
<ComposeButton :length :overLimit on:click="onClickPostButton()" />
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
ref:textarea
|
ref:textarea
|
||||||
bind:value=rawText
|
bind:value=rawText
|
||||||
on:blur="onBlur()"
|
on:blur="onBlur()"
|
||||||
|
on:focus="onFocus()"
|
||||||
|
on:selectionChange="onSelectionChange(event)"
|
||||||
|
on:keydown="onKeydown(event)"
|
||||||
></textarea>
|
></textarea>
|
||||||
<style>
|
<style>
|
||||||
.compose-box-input {
|
.compose-box-input {
|
||||||
|
@ -29,6 +32,8 @@
|
||||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||||
import debounce from 'lodash/debounce'
|
import debounce from 'lodash/debounce'
|
||||||
import { mark, stop } from '../../_utils/marks'
|
import { mark, stop } from '../../_utils/marks'
|
||||||
|
import { selectionChange } from '../../_utils/events'
|
||||||
|
import { clickSelectedAutosuggestionUsername } from '../../_actions/compose'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate() {
|
oncreate() {
|
||||||
|
@ -82,7 +87,53 @@
|
||||||
stop('autosize.destroy()')
|
stop('autosize.destroy()')
|
||||||
},
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.store.set({composeSelectionStart: this.refs.textarea.selectionStart})
|
this.store.set({composeFocused: false})
|
||||||
|
},
|
||||||
|
onFocus() {
|
||||||
|
this.store.set({composeFocused: true})
|
||||||
|
},
|
||||||
|
onSelectionChange(selectionStart) {
|
||||||
|
this.store.set({composeSelectionStart: selectionStart})
|
||||||
|
},
|
||||||
|
onKeydown(e) {
|
||||||
|
let { keyCode } = e
|
||||||
|
switch (keyCode) {
|
||||||
|
case 9: // tab
|
||||||
|
case 13: //enter
|
||||||
|
this.clickSelectedAutosuggestion(e)
|
||||||
|
break
|
||||||
|
case 38: // up
|
||||||
|
this.incrementAutosuggestSelected(-1, e)
|
||||||
|
break
|
||||||
|
case 40: // down
|
||||||
|
this.incrementAutosuggestSelected(1, e)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickSelectedAutosuggestion(event) {
|
||||||
|
let autosuggestionShown = this.store.get('composeAutosuggestionShown')
|
||||||
|
if (!autosuggestionShown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clickSelectedAutosuggestionUsername(this.get('realm'))
|
||||||
|
event.preventDefault()
|
||||||
|
},
|
||||||
|
incrementAutosuggestSelected(increment, event) {
|
||||||
|
let autosuggestionShown = this.store.get('composeAutosuggestionShown')
|
||||||
|
if (!autosuggestionShown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let selected = this.store.get('composeAutosuggestionSelected') || 0
|
||||||
|
let searchResults = this.store.get('composeAutosuggestionSearchResults') || []
|
||||||
|
selected += increment
|
||||||
|
if (selected >= 0) {
|
||||||
|
selected = selected % searchResults.length
|
||||||
|
} else {
|
||||||
|
selected = searchResults.length + selected
|
||||||
|
}
|
||||||
|
this.store.set({composeAutosuggestionSelected: selected})
|
||||||
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
@ -91,6 +142,9 @@
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
postedStatusForRealm: ($postedStatusForRealm) => $postedStatusForRealm
|
postedStatusForRealm: ($postedStatusForRealm) => $postedStatusForRealm
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
selectionChange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,4 +1,5 @@
|
||||||
<div class="compose-box-toolbar">
|
<div class="compose-box-toolbar">
|
||||||
|
<div class="compose-box-toolbar-items">
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Insert emoji"
|
label="Insert emoji"
|
||||||
href="#fa-smile"
|
href="#fa-smile"
|
||||||
|
@ -23,19 +24,32 @@
|
||||||
pressable="true"
|
pressable="true"
|
||||||
pressed="{{contentWarningShown}}"
|
pressed="{{contentWarningShown}}"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<input ref:input
|
<input ref:input
|
||||||
on:change="onFileChange(event)"
|
on:change="onFileChange(event)"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,image/jpeg,image/png,image/gif,video/webm,video/mp4">
|
accept=".jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,image/jpeg,image/png,image/gif,video/webm,video/mp4">
|
||||||
|
<div class="compose-autosuggest-wrapper">
|
||||||
|
<ComposeAutosuggest :realm :text />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.compose-box-toolbar {
|
.compose-box-toolbar {
|
||||||
grid-area: toolbar;
|
grid-area: toolbar;
|
||||||
|
position: relative;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
}
|
||||||
|
.compose-box-toolbar-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.compose-autosuggest-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 0;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import IconButton from '../IconButton.html'
|
import IconButton from '../IconButton.html'
|
||||||
|
@ -44,6 +58,7 @@
|
||||||
import { importDialogs } from '../../_utils/asyncModules'
|
import { importDialogs } from '../../_utils/asyncModules'
|
||||||
import { doMediaUpload } from '../../_actions/media'
|
import { doMediaUpload } from '../../_actions/media'
|
||||||
import { toggleContentWarningShown } from '../../_actions/contentWarnings'
|
import { toggleContentWarningShown } from '../../_actions/contentWarnings'
|
||||||
|
import ComposeAutosuggest from './ComposeAutosuggest.html'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate() {
|
oncreate() {
|
||||||
|
@ -58,7 +73,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
IconButton
|
IconButton,
|
||||||
|
ComposeAutosuggest
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { ACCOUNTS_STORE, RELATIONSHIPS_STORE } from './constants'
|
import {
|
||||||
|
ACCOUNTS_STORE, RELATIONSHIPS_STORE, USERNAME_LOWERCASE
|
||||||
|
} from './constants'
|
||||||
import { accountsCache, relationshipsCache } from './cache'
|
import { accountsCache, relationshipsCache } from './cache'
|
||||||
import { cloneForStorage, getGenericEntityWithId, setGenericEntityWithId } from './helpers'
|
import { cloneForStorage, getGenericEntityWithId, setGenericEntityWithId } from './helpers'
|
||||||
|
import { dbPromise, getDatabase } from './databaseLifecycle'
|
||||||
|
import { createAccountUsernamePrefixKeyRange } from './keys'
|
||||||
|
|
||||||
export async function getAccount (instanceName, accountId) {
|
export async function getAccount (instanceName, accountId) {
|
||||||
return getGenericEntityWithId(ACCOUNTS_STORE, accountsCache, instanceName, accountId)
|
return getGenericEntityWithId(ACCOUNTS_STORE, accountsCache, instanceName, accountId)
|
||||||
|
@ -17,3 +21,25 @@ export async function getRelationship (instanceName, accountId) {
|
||||||
export async function setRelationship (instanceName, relationship) {
|
export async function setRelationship (instanceName, relationship) {
|
||||||
return setGenericEntityWithId(RELATIONSHIPS_STORE, relationshipsCache, instanceName, cloneForStorage(relationship))
|
return setGenericEntityWithId(RELATIONSHIPS_STORE, relationshipsCache, instanceName, cloneForStorage(relationship))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function searchAccountsByUsername (instanceName, usernamePrefix, limit = 20) {
|
||||||
|
const db = await getDatabase(instanceName)
|
||||||
|
return dbPromise(db, ACCOUNTS_STORE, 'readonly', (accountsStore, callback) => {
|
||||||
|
let keyRange = createAccountUsernamePrefixKeyRange(usernamePrefix.toLowerCase())
|
||||||
|
accountsStore.index(USERNAME_LOWERCASE).getAll(keyRange, limit).onsuccess = e => {
|
||||||
|
let results = e.target.result
|
||||||
|
results = results.sort((a, b) => {
|
||||||
|
// accounts you're following go first
|
||||||
|
if (a.following !== b.following) {
|
||||||
|
return a.following ? -1 : 1
|
||||||
|
}
|
||||||
|
// after that, just sort by username
|
||||||
|
if (a[USERNAME_LOWERCASE] !== b[USERNAME_LOWERCASE]) {
|
||||||
|
return a[USERNAME_LOWERCASE] < b[USERNAME_LOWERCASE] ? -1 : 1
|
||||||
|
}
|
||||||
|
return 0 // eslint-disable-line
|
||||||
|
})
|
||||||
|
callback(results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -12,3 +12,4 @@ export const TIMESTAMP = '__pinafore_ts'
|
||||||
export const ACCOUNT_ID = '__pinafore_acct_id'
|
export const ACCOUNT_ID = '__pinafore_acct_id'
|
||||||
export const STATUS_ID = '__pinafore_status_id'
|
export const STATUS_ID = '__pinafore_status_id'
|
||||||
export const REBLOG_ID = '__pinafore_reblog_id'
|
export const REBLOG_ID = '__pinafore_reblog_id'
|
||||||
|
export const USERNAME_LOWERCASE = '__pinafore_acct_lc'
|
||||||
|
|
|
@ -10,7 +10,8 @@ import {
|
||||||
TIMESTAMP,
|
TIMESTAMP,
|
||||||
REBLOG_ID,
|
REBLOG_ID,
|
||||||
THREADS_STORE,
|
THREADS_STORE,
|
||||||
STATUS_ID
|
STATUS_ID,
|
||||||
|
USERNAME_LOWERCASE
|
||||||
} from './constants'
|
} from './constants'
|
||||||
|
|
||||||
import forEach from 'lodash/forEach'
|
import forEach from 'lodash/forEach'
|
||||||
|
@ -18,7 +19,9 @@ import forEach from 'lodash/forEach'
|
||||||
const openReqs = {}
|
const openReqs = {}
|
||||||
const databaseCache = {}
|
const databaseCache = {}
|
||||||
|
|
||||||
const DB_VERSION = 9
|
const DB_VERSION_INITIAL = 9
|
||||||
|
const DB_VERSION_SEARCH_ACCOUNTS = 10
|
||||||
|
const DB_VERSION_CURRENT = 10
|
||||||
|
|
||||||
export function getDatabase (instanceName) {
|
export function getDatabase (instanceName) {
|
||||||
if (!instanceName) {
|
if (!instanceName) {
|
||||||
|
@ -29,7 +32,7 @@ export function getDatabase (instanceName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseCache[instanceName] = new Promise((resolve, reject) => {
|
databaseCache[instanceName] = new Promise((resolve, reject) => {
|
||||||
let req = indexedDB.open(instanceName, DB_VERSION)
|
let req = indexedDB.open(instanceName, DB_VERSION_CURRENT)
|
||||||
openReqs[instanceName] = req
|
openReqs[instanceName] = req
|
||||||
req.onerror = reject
|
req.onerror = reject
|
||||||
req.onblocked = () => {
|
req.onblocked = () => {
|
||||||
|
@ -37,6 +40,7 @@ export function getDatabase (instanceName) {
|
||||||
}
|
}
|
||||||
req.onupgradeneeded = (e) => {
|
req.onupgradeneeded = (e) => {
|
||||||
let db = req.result
|
let db = req.result
|
||||||
|
let tx = e.currentTarget.transaction
|
||||||
|
|
||||||
function createObjectStore (name, init, indexes) {
|
function createObjectStore (name, init, indexes) {
|
||||||
let store = init
|
let store = init
|
||||||
|
@ -49,7 +53,7 @@ export function getDatabase (instanceName) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.oldVersion < DB_VERSION) {
|
if (e.oldVersion < DB_VERSION_INITIAL) {
|
||||||
createObjectStore(STATUSES_STORE, {keyPath: 'id'}, {
|
createObjectStore(STATUSES_STORE, {keyPath: 'id'}, {
|
||||||
[TIMESTAMP]: TIMESTAMP,
|
[TIMESTAMP]: TIMESTAMP,
|
||||||
[REBLOG_ID]: REBLOG_ID
|
[REBLOG_ID]: REBLOG_ID
|
||||||
|
@ -78,6 +82,10 @@ export function getDatabase (instanceName) {
|
||||||
})
|
})
|
||||||
createObjectStore(META_STORE)
|
createObjectStore(META_STORE)
|
||||||
}
|
}
|
||||||
|
if (e.oldVersion < DB_VERSION_SEARCH_ACCOUNTS) {
|
||||||
|
tx.objectStore(ACCOUNTS_STORE)
|
||||||
|
.createIndex(USERNAME_LOWERCASE, USERNAME_LOWERCASE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
req.onsuccess = () => resolve(req.result)
|
req.onsuccess = () => resolve(req.result)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { dbPromise, getDatabase } from './databaseLifecycle'
|
import { dbPromise, getDatabase } from './databaseLifecycle'
|
||||||
import { getInCache, hasInCache, setInCache } from './cache'
|
import { getInCache, hasInCache, setInCache } from './cache'
|
||||||
import { ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP } from './constants'
|
import { ACCOUNT_ID, REBLOG_ID, STATUS_ID, TIMESTAMP, USERNAME_LOWERCASE } from './constants'
|
||||||
|
|
||||||
export async function getGenericEntityWithId (store, cache, instanceName, id) {
|
export async function getGenericEntityWithId (store, cache, instanceName, id) {
|
||||||
if (hasInCache(cache, instanceName, id)) {
|
if (hasInCache(cache, instanceName, id)) {
|
||||||
|
@ -41,6 +41,10 @@ export function cloneForStorage (obj) {
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
res[REBLOG_ID] = value.id
|
res[REBLOG_ID] = value.id
|
||||||
break
|
break
|
||||||
|
case 'acct':
|
||||||
|
res[key] = value
|
||||||
|
res[USERNAME_LOWERCASE] = value.toLowerCase()
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
res[key] = value
|
res[key] = value
|
||||||
break
|
break
|
||||||
|
|
|
@ -45,3 +45,14 @@ export function createPinnedStatusKeyRange (accountId) {
|
||||||
accountId + '\u0000\uffff'
|
accountId + '\u0000\uffff'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// accounts
|
||||||
|
//
|
||||||
|
|
||||||
|
export function createAccountUsernamePrefixKeyRange (accountUsernamePrefix) {
|
||||||
|
return IDBKeyRange.bound(
|
||||||
|
accountUsernamePrefix,
|
||||||
|
accountUsernamePrefix + '\uffff'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -52,3 +52,20 @@ export function blurWithCapture (node, callback) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectionChange (node, callback) {
|
||||||
|
let events = ['keyup', 'click', 'focus', 'blur']
|
||||||
|
let listener = () => {
|
||||||
|
callback(node.selectionStart)
|
||||||
|
}
|
||||||
|
for (let event of events) {
|
||||||
|
node.addEventListener(event, listener)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
teardown () {
|
||||||
|
for (let event of events) {
|
||||||
|
node.removeEventListener(event, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
// svelte helper to add a .once() method similar to .on, but only fires once
|
||||||
|
|
||||||
|
export function once (eventName, callback) {
|
||||||
|
let listener = this.on(eventName, eventValue => {
|
||||||
|
listener.cancel()
|
||||||
|
callback(eventValue)
|
||||||
|
})
|
||||||
|
}
|
|
@ -74,4 +74,8 @@
|
||||||
--muted-modal-bg: transparent;
|
--muted-modal-bg: transparent;
|
||||||
--muted-modal-focus: #999;
|
--muted-modal-focus: #999;
|
||||||
--muted-modal-hover: rgba(255, 255, 255, 0.2);
|
--muted-modal-hover: rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
--compose-autosuggest-item-hover: $compose-background;
|
||||||
|
--compose-autosuggest-item-active: darken($compose-background, 5%);
|
||||||
|
--compose-autosuggest-outline: lighten($focus-outline, 5%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 30%);
|
$focus-outline: lighten($main-theme-color, 30%);
|
||||||
|
$compose-background: lighten($main-theme-color, 32%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 15%);
|
$focus-outline: lighten($main-theme-color, 15%);
|
||||||
|
$compose-background: lighten($main-theme-color, 17%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 30%);
|
$focus-outline: lighten($main-theme-color, 30%);
|
||||||
|
$compose-background: lighten($main-theme-color, 32%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 15%);
|
$focus-outline: lighten($main-theme-color, 15%);
|
||||||
|
$compose-background: lighten($main-theme-color, 17%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 30%);
|
$focus-outline: lighten($main-theme-color, 30%);
|
||||||
|
$compose-background: lighten($main-theme-color, 32%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 30%);
|
$focus-outline: lighten($main-theme-color, 30%);
|
||||||
|
$compose-background: lighten($main-theme-color, 32%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 30%);
|
$focus-outline: lighten($main-theme-color, 30%);
|
||||||
|
$compose-background: lighten($main-theme-color, 32%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ $secondary-text-color: white;
|
||||||
$toast-border: #fafafa;
|
$toast-border: #fafafa;
|
||||||
$toast-bg: #333;
|
$toast-bg: #333;
|
||||||
$focus-outline: lighten($main-theme-color, 50%);
|
$focus-outline: lighten($main-theme-color, 50%);
|
||||||
|
$compose-background: lighten($main-theme-color, 52%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* auto-generated w/ build-sass.js */
|
/* auto-generated w/ build-sass.js */
|
||||||
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#90a8ee;--action-button-fill-color-hover:#a2b6f0;--action-button-fill-color-active:#577ae4;--action-button-fill-color-pressed:#2351dc;--action-button-fill-color-pressed-hover:#3862e0;--action-button-fill-color-pressed-active:#1d44b8;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#c5d1f6;--very-deemphasized-link-color:rgba(65,105,225,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#d2dcf8;--main-theme-color:#4169e1;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2)}
|
body{--button-primary-bg:#6081e6;--button-primary-text:#fff;--button-primary-border:#132c76;--button-primary-bg-active:#456ce2;--button-primary-bg-hover:#6988e7;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#4169e1;--main-bg:#fff;--body-bg:#e8edfb;--body-text-color:#333;--main-border:#dadada;--svg-fill:#4169e1;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#4169e1;--nav-border:#214cce;--nav-a-border:#4169e1;--nav-a-selected-border:#fff;--nav-a-selected-bg:#6d8ce8;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#839deb;--nav-a-bg-hover:#577ae4;--nav-a-border-hover:#4169e1;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#90a8ee;--action-button-fill-color-hover:#a2b6f0;--action-button-fill-color-active:#577ae4;--action-button-fill-color-pressed:#2351dc;--action-button-fill-color-pressed-hover:#3862e0;--action-button-fill-color-pressed-active:#1d44b8;--settings-list-item-bg:#fff;--settings-list-item-text:#4169e1;--settings-list-item-text-hover:#4169e1;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#c5d1f6;--very-deemphasized-link-color:rgba(65,105,225,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#d2dcf8;--main-theme-color:#4169e1;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2);--compose-autosuggest-item-hover:#ced8f7;--compose-autosuggest-item-active:#b8c7f4;--compose-autosuggest-outline:#dbe3f9}
|
||||||
body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:72px;left:0;right:0;bottom:0}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:20px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}50%{transform:rotate(180deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 2s infinite linear}.ellipsis::after{content:"\2026"}
|
body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);position:fixed;left:0;right:0;bottom:0;top:0}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:72px;left:0;right:0;bottom:0}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:20px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}50%{transform:rotate(180deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 2s infinite linear}.ellipsis::after{content:"\2026"}
|
||||||
body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#c7c7c7;--action-button-fill-color-hover:#d1d1d1;--action-button-fill-color-active:#a6a6a6;--action-button-fill-color-pressed:#878787;--action-button-fill-color-pressed-hover:#949494;--action-button-fill-color-pressed-active:#737373;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#bfbfbf;--very-deemphasized-link-color:rgba(153,153,153,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#ededed;--main-theme-color:#999;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2)}
|
body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline{--button-primary-bg:#ababab;--button-primary-text:#fff;--button-primary-border:#4d4d4d;--button-primary-bg-active:#9c9c9c;--button-primary-bg-hover:#b0b0b0;--button-bg:#e6e6e6;--button-text:#333;--button-border:#a7a7a7;--button-bg-active:#bfbfbf;--button-bg-hover:#f2f2f2;--input-border:#dadada;--anchor-text:#999;--main-bg:#fff;--body-bg:#fafafa;--body-text-color:#333;--main-border:#dadada;--svg-fill:#999;--form-bg:#f7f7f7;--form-border:#c1c1c1;--nav-bg:#999;--nav-border:gray;--nav-a-border:#999;--nav-a-selected-border:#fff;--nav-a-selected-bg:#b3b3b3;--nav-svg-fill:#fff;--nav-text-color:#fff;--nav-a-selected-border-hover:#fff;--nav-a-selected-bg-hover:#bfbfbf;--nav-a-bg-hover:#a6a6a6;--nav-a-border-hover:#999;--nav-svg-fill-hover:#fff;--nav-text-color-hover:#fff;--action-button-fill-color:#c7c7c7;--action-button-fill-color-hover:#d1d1d1;--action-button-fill-color-active:#a6a6a6;--action-button-fill-color-pressed:#878787;--action-button-fill-color-pressed-hover:#949494;--action-button-fill-color-pressed-active:#737373;--settings-list-item-bg:#fff;--settings-list-item-text:#999;--settings-list-item-text-hover:#999;--settings-list-item-border:#dadada;--settings-list-item-bg-active:#e6e6e6;--settings-list-item-bg-hover:#fafafa;--toast-bg:#333;--toast-border:#fafafa;--toast-text:#fff;--mask-bg:#333;--mask-svg-fill:#fff;--mask-opaque-bg:rgba(51,51,51,0.8);--loading-bg:#ededed;--deemphasized-text-color:#666;--focus-outline:#bfbfbf;--very-deemphasized-link-color:rgba(153,153,153,0.6);--very-deemphasized-text-color:rgba(102,102,102,0.6);--status-direct-background:#ededed;--main-theme-color:#999;--warning-color:#e01f19;--alt-input-bg:rgba(255,255,255,0.7);--muted-modal-bg:transparent;--muted-modal-focus:#999;--muted-modal-hover:rgba(255,255,255,0.2);--compose-autosuggest-item-hover:#c4c4c4;--compose-autosuggest-item-active:#b8b8b8;--compose-autosuggest-outline:#ccc}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue