Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85832822
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
41 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/draft/draft.vue b/src/components/draft/draft.vue
index 7a9bdd7af1..7b0878112d 100644
--- a/src/components/draft/draft.vue
+++ b/src/components/draft/draft.vue
@@ -1,171 +1,174 @@
<template>
<article class="Draft">
<div
v-if="!editing"
class="status-content"
>
<div>
<i18n-t
v-if="draft.type === 'reply' || draft.type === 'edit'"
tag="span"
:keypath="draft.type === 'reply' ? 'drafts.replying' : 'drafts.editing'"
>
<template #statusLink>
<router-link
class="faint-link"
:to="{ name: 'conversation', params: { id: draft.refId } }"
>
{{ refStatus ? refStatus.external_url : $t('drafts.unavailable') }}
</router-link>
</template>
</i18n-t>
<StatusContent
v-if="draft.refId && refStatus"
class="status-content"
:status="refStatus"
:compact="true"
/>
</div>
<div class="status-preview">
<span class="status_content">
<p v-if="draft.spoilerText">
<i>
{{ draft.spoilerText }}:
</i>
</p>
<p v-if="draft.status">{{ draft.status }}</p>
- <p v-else class="faint">{{ $t('drafts.empty') }}</p>
+ <p
+ v-else
+ class="faint"
+ >{{ $t('drafts.empty') }}</p>
</span>
<gallery
v-if="draft.files?.length !== 0"
class="attachments media-body"
:compact="true"
:nsfw="nsfwClickthrough"
:attachments="draft.files"
:limit="1"
size="small"
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<div
v-if="draft.poll.options"
class="poll-indicator-container"
:title="$t('drafts.poll_tooltip')"
>
<div class="poll-indicator">
<FAIcon
icon="poll-h"
size="3x"
/>
</div>
</div>
</div>
</div>
<div v-if="editing">
<PostStatusForm
v-if="draft.type !== 'edit'"
:hide-draft="true"
v-bind="postStatusFormProps"
/>
<EditStatusForm
v-else
:hide-draft="true"
:params="postStatusFormProps"
/>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmDialog"
:title="$t('drafts.abandon_confirm_title')"
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
:cancel-text="$t('drafts.abandon_confirm_cancel_button')"
@accepted="doAbandon"
@cancelled="hideConfirmDialog"
>
{{ $t('drafts.abandon_confirm') }}
</confirm-modal>
</teleport>
<div class="actions">
<button
class="btn button-default"
:aria-expanded="editing"
@click.prevent.stop="toggleEditing"
>
{{ editing ? $t('drafts.save') : $t('drafts.continue') }}
</button>
<button
class="btn button-default"
@click.prevent.stop="abandon"
>
{{ $t('drafts.abandon') }}
</button>
</div>
</article>
</template>
<script src="./draft.js"></script>
<style lang="scss">
.Draft {
position: relative;
.status-content {
padding: 0.5em;
margin: 0.5em 0;
}
.status-preview {
display: grid;
grid-template-columns: 1fr;
grid-auto-columns: 10em;
grid-auto-flow: column;
grid-gap: 0.5em;
align-items: start;
max-width: 100%;
p {
word-wrap: break-word;
white-space: normal;
overflow-x: hidden;
}
.poll-indicator-container {
border-radius: var(--roundness);
display: grid;
justify-items: center;
align-items: center;
align-self: start;
height: 0;
padding-bottom: 62.5%;
position: relative;
}
.poll-indicator {
box-sizing: border-box;
border: 1px solid var(--border);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: grid;
justify-items: center;
align-items: center;
width: 100%;
height: 100%;
}
}
.actions {
display: flex;
flex-direction: row;
justify-content: space-evenly;
.btn {
flex: 1;
margin-left: 1em;
margin-right: 1em;
}
}
}
</style>
diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue
index a41355a505..ecf217fb4c 100644
--- a/src/components/font_control/font_control.vue
+++ b/src/components/font_control/font_control.vue
@@ -1,157 +1,157 @@
<template>
<div class="font-control">
<label
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.label', { label }) }}
</label>
{{ ' ' }}
<Checkbox
v-if="typeof fallback !== 'undefined'"
- class="font-checkbox"
:id="name + '-o'"
+ class="font-checkbox"
:model-value="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
>
{{ $t('settings.style.themes3.define') }}
</Checkbox>
<div
v-if="modelValue?.family"
class="font-input"
>
<label
v-if="manualEntry"
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
<i18n-t
keypath="settings.style.themes3.font.entry"
tag="span"
scope="global"
>
<template #fontFamily>
<code>font-family</code>
</template>
</i18n-t>
</label>
<label
v-else
:id="name + '-label'"
:for="manualEntry ? name : name + '-font-switcher'"
class="label"
>
{{ $t('settings.style.themes3.font.select') }}
</label>
{{ ' ' }}
<span
v-if="manualEntry"
class="btn-group"
>
<button
class="btn button-default"
:title="$t('settings.style.themes3.font.lookup_local_fonts')"
@click="toggleManualEntry"
>
<FAIcon
fixed-width
icon="font"
/>
</button>
<input
:id="name"
:model-value="modelValue.family"
class="input custom-font"
type="text"
@update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })"
>
</span>
<span
v-else
class="btn-group"
>
<button
class="btn button-default"
:title="$t('settings.style.themes3.font.enter_manually')"
@click="toggleManualEntry"
>
<FAIcon
fixed-width
icon="keyboard"
/>
</button>
<Select
:id="name + '-local-font-switcher'"
:model-value="modelValue?.family"
class="custom-font"
@update:modelValue="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })"
>
<optgroup
:label="$t('settings.style.themes3.font.group-builtin')"
>
<option
v-for="option in availableOptions"
:key="option"
:value="option"
:style="{ fontFamily: option === 'inherit' ? null : option }"
>
{{ $t('settings.style.themes3.font.builtin.' + option) }}
</option>
</optgroup>
<optgroup
v-if="localFontsSize > 0"
:label="$t('settings.style.themes3.font.group-local')"
>
<option
v-for="option in localFontsList"
:key="option"
:value="option"
:style="{ fontFamily: option }"
>
{{ option }}
</option>
</optgroup>
<optgroup
v-else
:label="$t('settings.style.themes3.font.group-local')"
>
<option disabled>
{{ $t('settings.style.themes3.font.local-unavailable1') }}
</option>
<option disabled>
{{ $t('settings.style.themes3.font.local-unavailable2') }}
</option>
</optgroup>
</Select>
</span>
</div>
</div>
</template>
<script src="./font_control.js"></script>
<style lang="scss">
.font-control {
.custom-font {
min-width: 20em;
max-width: 20em;
}
.font-input {
margin-left: 2em;
margin-top: 0.5em;
}
.font-checkbox {
margin-left: 1em;
}
}
.invalid-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
}
</style>
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index b6b8930e51..585832dbf7 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -1,172 +1,172 @@
/* eslint-env browser */
import statusPosterService from '../../services/status_poster/status_poster.service.js'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons'
library.add(
faUpload,
faCircleNotch
)
const mediaUpload = {
data () {
return {
uploadCount: 0,
uploadReady: true
}
},
computed: {
uploading () {
return this.uploadCount > 0
}
},
methods: {
onClick () {
if (this.uploadReady) {
this.$refs.input.click()
}
},
async resizeImage (file) {
// Skip if not an image or if it's a GIF
if (!file.type.startsWith('image/') || file.type === 'image/gif') {
return file
}
// For PNGs, check if animated
if (file.type === 'image/png') {
const isAnimated = await this.isAnimatedPng(file)
if (isAnimated) {
return file
}
}
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
// Calculate new dimensions
let width = img.width
let height = img.height
const maxSize = 2048
if (width > maxSize || height > maxSize) {
if (width > height) {
height = Math.round((height * maxSize) / width)
width = maxSize
} else {
width = Math.round((width * maxSize) / height)
height = maxSize
}
}
// Create canvas and resize
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height)
// Check WebP support by trying to create a WebP canvas
const testCanvas = document.createElement('canvas')
const supportsWebP = testCanvas.toDataURL('image/webp').startsWith('data:image/webp')
// Convert to WebP if supported, otherwise JPEG
const type = supportsWebP ? 'image/webp' : 'image/jpeg'
const extension = supportsWebP ? '.webp' : '.jpg'
// Remove the original extension and add new one
const newFileName = file.name.replace(/\.[^/.]+$/, '') + extension
canvas.toBlob((blob) => {
resolve(new File([blob], newFileName, {
- type: type,
+ type,
lastModified: Date.now()
}))
}, type, 0.85)
}
img.src = URL.createObjectURL(file)
})
},
async isAnimatedPng (file) {
const buffer = await file.arrayBuffer()
const view = new Uint8Array(buffer)
// Look for animated PNG chunks (acTL)
for (let i = 0; i < view.length - 8; i++) {
if (view[i] === 0x61 && // a
view[i + 1] === 0x63 && // c
view[i + 2] === 0x54 && // T
view[i + 3] === 0x4C) { // L
return true
}
}
return false
},
async uploadFile (file) {
const self = this
const store = this.$store
if (file.size > store.state.instance.uploadlimit) {
const filesize = fileSizeFormatService.fileSizeFormat(file.size)
const allowedsize = fileSizeFormatService.fileSizeFormat(store.state.instance.uploadlimit)
self.$emit('upload-failed', 'file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
return
}
// Resize image if needed
const processedFile = await this.resizeImage(file)
const formData = new FormData()
formData.append('file', processedFile)
self.$emit('uploading')
self.uploadCount++
statusPosterService.uploadMedia({ store, formData })
.then((fileData) => {
self.$emit('uploaded', fileData)
self.decreaseUploadCount()
}, (error) => {
console.error('Error uploading file', error)
self.$emit('upload-failed', 'default')
self.decreaseUploadCount()
})
},
decreaseUploadCount () {
this.uploadCount--
if (this.uploadCount === 0) {
this.$emit('all-uploaded')
}
},
clearFile () {
this.uploadReady = false
this.$nextTick(() => {
this.uploadReady = true
})
},
multiUpload (files) {
for (const file of files) {
this.uploadFile(file)
}
},
change ({ target }) {
this.multiUpload(target.files)
}
},
props: {
dropFiles: Object,
disabled: Boolean,
normalButton: Boolean,
acceptTypes: {
type: String,
default: '*/*'
}
},
watch: {
dropFiles: function (fileInfos) {
if (!this.uploading) {
this.multiUpload(fileInfos)
}
}
}
}
export default mediaUpload
diff --git a/src/components/palette_editor/palette_editor.vue b/src/components/palette_editor/palette_editor.vue
index 3d43b74646..f634e63f76 100644
--- a/src/components/palette_editor/palette_editor.vue
+++ b/src/components/palette_editor/palette_editor.vue
@@ -1,195 +1,195 @@
<template>
<div
class="PaletteEditor"
:class="{ '-compact': compact, '-apply': apply }"
>
<ColorInput
v-for="key in paletteKeys"
:key="key"
:name="key"
:model-value="props.modelValue[key]"
:fallback="fallback(key)"
:label="$t('settings.style.themes3.palette.' + key)"
@update:modelValue="value => updatePalette(key, value)"
/>
<button
class="btn button-default palette-import-button"
@click="importPalette"
>
<FAIcon icon="file-import" />
{{ $t('settings.style.themes3.palette.import') }}
</button>
<button
class="btn button-default palette-export-button"
@click="exportPalette"
>
<FAIcon icon="file-export" />
{{ $t('settings.style.themes3.palette.export') }}
</button>
<button
v-if="apply"
class="btn button-default palette-apply-button"
- @click="applyPalette"
:disabled="disabled"
:class="{ disabled }"
+ @click="applyPalette"
>
{{ $t('settings.style.themes3.palette.apply') }}
</button>
</div>
</template>
<script setup>
import ColorInput from 'src/components/color_input/color_input.vue'
import {
newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFileImport,
faFileExport
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFileImport,
faFileExport
)
const paletteKeys = [
'bg',
'fg',
'text',
'link',
'accent',
'cRed',
'cBlue',
'cGreen',
'cOrange',
'wallpaper'
]
const props = defineProps(['modelValue', 'compact', 'apply', 'disabled'])
const emit = defineEmits(['update:modelValue', 'applyPalette'])
const getExportedObject = () => paletteKeys.reduce((acc, key) => {
const value = props.modelValue[key]
if (value == null) {
return acc
} else {
return { ...acc, [key]: props.modelValue[key] }
}
}, {})
const paletteExporter = newExporter({
filename: 'pleroma_palette',
extension: 'json',
getExportedObject
})
const paletteImporter = newImporter({
accept: '.json',
onImport (parsed, filename) {
emit('update:modelValue', parsed)
}
})
const exportPalette = () => {
paletteExporter.exportData()
}
const importPalette = () => {
paletteImporter.importData()
}
const applyPalette = (data) => {
emit('applyPalette', getExportedObject())
}
const fallback = (key) => {
if (key === 'accent') {
return props.modelValue.link
}
if (key === 'link') {
return props.modelValue.accent
}
if (key.startsWith('extra')) {
return '#FF00FF'
}
if (key.startsWith('wallpaper')) {
return '#008080'
}
}
const updatePalette = (paletteKey, value) => {
emit('update:modelValue', {
...props.modelValue,
[paletteKey]: value
})
}
</script>
<style lang="scss">
.PaletteEditor {
display: grid;
justify-content: space-around;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr) auto;
grid-gap: 0.5em;
align-items: baseline;
.palette-import-button {
grid-column: 1 / span 2;
}
.palette-export-button {
grid-column: 3 / span 2;
}
.palette-apply-button {
grid-column: 1 / span 2;
}
.color-input.style-control {
margin: 0;
}
&.-compact {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(5, 1fr) auto;
.palette-import-button {
grid-column: 1;
}
.palette-export-button {
grid-column: 2;
}
&.-apply {
grid-template-rows: repeat(5, 1fr) auto auto;
.palette-apply-button {
grid-column: 1 / span 2;
}
}
.-mobile & {
grid-template-columns: 1fr;
grid-template-rows: repeat(10, 1fr) auto;
.palette-import-button {
grid-column: 1;
}
.palette-export-button {
grid-column: 1;
}
&.-apply {
.palette-apply-button {
grid-column: 1;
}
}
}
}
}
</style>
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 4a6de32863..20174da0c7 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,395 +1,395 @@
<template>
<div
class="appearance-tab"
:label="$t('settings.general')"
>
<div class="setting-item">
<h2>{{ $t('settings.theme') }}</h2>
<ul
ref="themeList"
class="theme-list"
>
<button
class="button-default theme-preview"
data-theme-key="stock"
- @click="resetTheming"
:class="{ toggled: isStyleActive('stock'), disabled: switchInProgress }"
:disabled="switchInProgress"
+ @click="resetTheming"
>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<!-- eslint-disable vue/no-v-html -->
<component
:is="'style'"
v-html="previewTheme('stock', 'v3')"
/>
<!-- eslint-enable vue/no-v-html -->
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<preview id="theme-preview-stock" />
<h4 class="theme-name">
{{ $t('settings.style.stock_theme_used') }}
<span class="alert neutral version">v3</span>
</h4>
</button>
<button
v-if="isCustomThemeUsed"
disabled
class="button-default theme-preview toggled"
>
<preview />
<h4 class="theme-name">
{{ $t('settings.style.custom_theme_used') }}
<span class="alert neutral version">v2</span>
</h4>
</button>
<button
v-if="isCustomStyleUsed"
disabled
class="button-default theme-preview toggled"
>
<preview />
<h4 class="theme-name">
{{ $t('settings.style.custom_style_used') }}
<span class="alert neutral version">v3</span>
</h4>
</button>
<button
v-for="style in availableStyles"
:key="style.key"
:data-theme-key="style.key"
class="button-default theme-preview"
:class="{ toggled: isThemeActive(style.key), disabled: switchInProgress }"
- @click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
:disabled="switchInProgress"
+ @click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<!-- eslint-disable vue/no-v-html -->
<div v-if="style.ready || noIntersectionObserver">
<component
:is="'style'"
v-html="previewTheme(style.key, style.version, style.data)"
/>
</div>
<!-- eslint-enable vue/no-v-html -->
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<preview :id="'theme-preview-' + style.key" />
<h4 class="theme-name">
{{ style.name }}
<span class="alert neutral version">{{ style.version }}</span>
</h4>
</button>
</ul>
<div class="import-file-container">
<button
class="btn button-default"
- @click="importFile"
:class="{ disabled: switchInProgress }"
:disabled="switchInProgress"
+ @click="importFile"
>
<FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }}
</button>
</div>
<div class="setting-item">
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2>
<div
v-if="customThemeVersion === 'v3'"
class="palettes-container"
>
<h4 v-if="stylePalettes?.length > 0">
{{ $t('settings.style.themes3.palette.style') }}
</h4>
<div class="palettes">
<button
v-for="p in stylePalettes || []"
:key="p.name"
class="btn button-default palette-entry"
:class="{ toggled: isPaletteActive(p.key), disabled: switchInProgress }"
:disabled="switchInProgress"
@click="() => setPalette(p.key, p)"
>
<div class="palette-label">
<label>
{{ p.name ?? $t('settings.style.themes3.palette.user') }}
</label>
</div>
<div class="palette-preview">
<span
v-for="c in palettesKeys"
:key="c"
class="palette-square"
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
/>
</div>
</button>
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4>
<button
v-for="p in bundledPalettes"
:key="p.name"
class="btn button-default palette-entry"
:class="{ toggled: isPaletteActive(p.key), disabled: switchInProgress }"
:disabled="switchInProgress"
@click="() => setPalette(p.key, p)"
>
<div class="palette-label">
<label>
{{ p.name }}
</label>
</div>
<div class="palette-preview">
<span
v-for="c in palettesKeys"
:key="c"
class="palette-square"
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
/>
</div>
</button>
</div>
</div>
<div>
<template v-if="customThemeVersion === 'v3'">
<h4 v-if="expertLevel > 0">
{{ $t('settings.style.themes3.palette.user') }}
</h4>
<PaletteEditor
v-if="expertLevel > 0"
v-model="userPalette"
class="userPalette"
:compact="true"
:apply="true"
- @applyPalette="data => setPaletteCustom(data)"
:disabled="switchInProgress"
+ @applyPalette="data => setPaletteCustom(data)"
/>
</template>
<template v-else-if="customThemeVersion === 'v2'">
<div class="alert neutral theme-notice unsupported-theme-v2">
{{ $t('settings.style.themes3.palette.v2_unsupported') }}
</div>
</template>
</div>
</div>
</div>
<div class="setting-item">
<h2>{{ $t('settings.scale_and_layout') }}</h2>
<div class="alert neutral theme-notice">
{{ $t("settings.style.appearance_tab_note") }}
</div>
<ul class="setting-list">
<li>
<UnitSetting
path="textSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 14, 'rem': 1 }"
timed-apply-mode
>
{{ $t('settings.text_size') }}
</UnitSetting>
<div>
<small>
<i18n-t
scope="global"
keypath="settings.text_size_tip"
tag="span"
>
<code>px</code>
<code>rem</code>
</i18n-t>
<br>
<i18n-t
scope="global"
keypath="settings.text_size_tip2"
tag="span"
>
<code>14px</code>
</i18n-t>
</small>
</div>
</li>
<li>
<UnitSetting
path="emojiSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 32, 'rem': 2.2 }"
>
{{ $t('settings.emoji_size') }}
</UnitSetting>
<ul
class="setting-list suboptions"
>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
expert="1"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
</ul>
</li>
<li>
<UnitSetting
path="navbarSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 55, 'rem': 3.5 }"
>
{{ $t('settings.navbar_size') }}
</UnitSetting>
</li>
<h3>{{ $t('settings.style.interface_font_user_override') }}</h3>
<li>
<FontControl
:model-value="mergedConfig.theme3hacks.fonts.interface"
name="ui"
:label="$t('settings.style.fonts.components.interface')"
:fallback="{ family: 'sans-serif' }"
no-inherit="1"
@update:modelValue="v => updateFont('interface', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.input"
name="input"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.input')"
@update:modelValue="v => updateFont('input', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.post"
name="post"
:fallback="{ family: 'inherit' }"
:label="$t('settings.style.fonts.components.post')"
@update:modelValue="v => updateFont('post', v)"
/>
</li>
<li>
<FontControl
v-if="expertLevel > 0"
:model-value="mergedConfig.theme3hacks.fonts.monospace"
name="postCode"
:fallback="{ family: 'monospace' }"
:label="$t('settings.style.fonts.components.monospace')"
@update:modelValue="v => updateFont('monospace', v)"
/>
</li>
<h3>{{ $t('settings.columns') }}</h3>
<li>
<UnitSetting
path="panelHeaderSize"
:step="0.1"
:units="['px', 'rem']"
:reset-default="{ 'px': 52, 'rem': 3.2 }"
timed-apply-mode
>
{{ $t('settings.panel_header_size') }}
</UnitSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="navbarColumnStretch">
{{ $t('settings.navbar_column_stretch') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li v-if="expertLevel > 0">
{{ $t('settings.column_sizes') }}
<div class="column-settings">
<UnitSetting
v-for="column in columns"
:key="column"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}
</UnitSetting>
</div>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.visual_tweaks') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path="modalMobileCenter">
{{ $t('settings.mobile_center_dialog') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
id="forcedRoundness"
path="forcedRoundness"
:options="forcedRoundnessOptions"
>
{{ $t('settings.style.themes3.hacks.force_interface_roundness') }}
</ChoiceSetting>
</li>
<li>
<ChoiceSetting
id="underlayOverride"
path="theme3hacks.underlay"
:options="underlayOverrideModes"
>
{{ $t('settings.style.themes3.hacks.underlay_overrides') }}
</ChoiceSetting>
</li>
<li v-if="instanceWallpaperUsed">
<BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="forceThemeRecompilation"
:expert="1"
>
{{ $t('settings.force_theme_recompilation_debug') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="themeDebug"
:expert="1"
>
{{ $t('settings.theme_debug') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./appearance_tab.js"></script>
<style lang="scss" src="./appearance_tab.scss"></style>
diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js
index 107b898552..eb17223a1f 100644
--- a/src/components/video_attachment/video_attachment.js
+++ b/src/components/video_attachment/video_attachment.js
@@ -1,52 +1,51 @@
-
const VideoAttachment = {
props: ['attachment', 'controls'],
data () {
return {
blocksSuspend: false,
// Start from true because removing "loop" property seems buggy in Vue
hasAudio: true
}
},
computed: {
loopVideo () {
if (this.$store.getters.mergedConfig.loopVideoSilentOnly) {
return !this.hasAudio
}
return this.$store.getters.mergedConfig.loopVideo
}
},
methods: {
onPlaying (e) {
this.setHasAudio(e)
if (this.loopVideo) {
this.$emit('play', { looping: true })
return
}
this.$emit('play')
},
onPaused (e) {
this.$emit('pause')
},
setHasAudio (e) {
const target = e.srcElement || e.target
// If hasAudio is false, we've already marked this video to not have audio,
// a video can't gain audio out of nowhere so don't bother checking again.
if (!this.hasAudio) return
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track
if (target.webkitAudioDecodedByteCount > 0) return
}
if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
if (target.mozHasAudio) return
}
if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) return
}
this.hasAudio = false
}
}
}
export default VideoAttachment
diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js
index 265a7f25e1..570a1746de 100644
--- a/src/services/gesture_service/gesture_service.js
+++ b/src/services/gesture_service/gesture_service.js
@@ -1,207 +1,206 @@
-
const DIRECTION_LEFT = [-1, 0]
const DIRECTION_RIGHT = [1, 0]
const DIRECTION_UP = [0, -1]
const DIRECTION_DOWN = [0, 1]
const BUTTON_LEFT = 0
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const touchCoord = touch => [touch.screenX, touch.screenY]
const touchEventCoord = e => touchCoord(e.touches[0])
const pointerEventCoord = e => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
const perpendicular = v => [v[1], -v[0]]
const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1]
const project = (v1, v2) => {
const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2))
return [scalar * v2[0], scalar * v2[1]]
}
// direction: either use the constants above or an arbitrary 2d vector.
// threshold: how many Px to move from touch origin before checking if the
// callback should be called.
// divergentTolerance: a scalar for much of divergent direction we tolerate when
// above threshold. for example, with 1.0 we only call the callback if
// divergent component of delta is < 1.0 * direction component of delta.
const swipeGesture = (direction, onSwipe, threshold = 30, perpendicularTolerance = 1.0) => {
return {
direction,
onSwipe,
threshold,
perpendicularTolerance,
_startPos: [0, 0],
_swiping: false
}
}
const beginSwipe = (event, gesture) => {
gesture._startPos = touchEventCoord(event)
gesture._swiping = true
}
const updateSwipe = (event, gesture) => {
if (!gesture._swiping) return
// movement too small
const delta = deltaCoord(gesture._startPos, touchEventCoord(event))
if (vectorLength(delta) < gesture.threshold) return
// movement is opposite from direction
if (dotProduct(delta, gesture.direction) < 0) return
// movement perpendicular to direction is too much
const towardsDir = project(delta, gesture.direction)
const perpendicularDir = perpendicular(gesture.direction)
const towardsPerpendicular = project(delta, perpendicularDir)
if (
vectorLength(towardsDir) * gesture.perpendicularTolerance <
vectorLength(towardsPerpendicular)
) return
gesture.onSwipe()
gesture._swiping = false
}
class SwipeAndClickGesture {
// swipePreviewCallback(offsets: Array[Number])
// offsets: the offset vector which the underlying component should move, from the starting position
// swipeEndCallback(sign: 0|-1|1)
// sign: if the swipe does not meet the threshold, 0
// if the swipe meets the threshold in the positive direction, 1
// if the swipe meets the threshold in the negative direction, -1
constructor ({
direction,
// swipeStartCallback
swipePreviewCallback,
swipeEndCallback,
swipeCancelCallback,
swipelessClickCallback,
threshold = 30,
perpendicularTolerance = 1.0,
disableClickThreshold = 1
}) {
const nop = () => {}
this.direction = direction
this.swipePreviewCallback = swipePreviewCallback || nop
this.swipeEndCallback = swipeEndCallback || nop
this.swipeCancelCallback = swipeCancelCallback || nop
this.swipelessClickCallback = swipelessClickCallback || nop
this.threshold = typeof threshold === 'function' ? threshold : () => threshold
this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
this.perpendicularTolerance = perpendicularTolerance
this._reset()
}
_reset () {
this._startPos = [0, 0]
this._pointerId = -1
this._swiping = false
this._swiped = false
this._preventNextClick = false
}
start (event) {
// Only handle left click
if (event.button !== BUTTON_LEFT) {
return
}
this._startPos = pointerEventCoord(event)
this._pointerId = event.pointerId
this._swiping = true
this._swiped = false
}
move (event) {
if (this._swiping && this._pointerId === event.pointerId) {
this._swiped = true
const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
this.swipePreviewCallback(delta)
}
}
cancel (event) {
if (!this._swiping || this._pointerId !== event.pointerId) {
return
}
this.swipeCancelCallback()
}
end (event) {
if (!this._swiping) {
return
}
if (this._pointerId !== event.pointerId) {
return
}
this._swiping = false
// movement too small
const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
const sign = (() => {
if (vectorLength(delta) < this.threshold()) {
return 0
}
// movement is opposite from direction
const isPositive = dotProduct(delta, this.direction) > 0
// movement perpendicular to direction is too much
const towardsDir = project(delta, this.direction)
const perpendicularDir = perpendicular(this.direction)
const towardsPerpendicular = project(delta, perpendicularDir)
if (
vectorLength(towardsDir) * this.perpendicularTolerance <
vectorLength(towardsPerpendicular)
) {
return 0
}
return isPositive ? 1 : -1
})()
if (this._swiped) {
this.swipeEndCallback(sign)
}
this._reset()
// Only a mouse will fire click event when
// the end point is far from the starting point
// so for other kinds of pointers do not check
// whether we have swiped
if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
this._preventNextClick = true
}
}
click (event) {
if (!this._preventNextClick) {
this.swipelessClickCallback()
}
this._reset()
}
}
const GestureService = {
DIRECTION_LEFT,
DIRECTION_RIGHT,
DIRECTION_UP,
DIRECTION_DOWN,
swipeGesture,
beginSwipe,
updateSwipe,
SwipeAndClickGesture
}
export default GestureService
diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js
index 0c0a66a01e..0f9abaf13a 100644
--- a/src/services/promise_interval/promise_interval.js
+++ b/src/services/promise_interval/promise_interval.js
@@ -1,34 +1,33 @@
-
// promiseInterval - replacement for setInterval for promises, starts counting
// the interval only after a promise is done instead of immediately.
// - promiseCall is a function that returns a promise, it's called the first
// time after the first interval.
// - interval is the interval delay in ms.
export const promiseInterval = (promiseCall, interval) => {
let stopped = false
let timeout = null
const func = () => {
const promise = promiseCall()
// something unexpected happened and promiseCall did not
// return a promise, abort the loop.
if (!(promise && promise.finally)) {
console.warn('promiseInterval: promise call did not return a promise, stopping interval.')
return
}
promise.finally(() => {
if (stopped) return
timeout = window.setTimeout(func, interval)
})
}
const stopFetcher = () => {
stopped = true
window.clearTimeout(timeout)
}
timeout = window.setTimeout(func, interval)
return { stop: stopFetcher }
}
diff --git a/src/services/window_utils/window_utils.js b/src/services/window_utils/window_utils.js
index 909088dbc1..c33785363a 100644
--- a/src/services/window_utils/window_utils.js
+++ b/src/services/window_utils/window_utils.js
@@ -1,10 +1,9 @@
-
export const windowWidth = () =>
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth
export const windowHeight = () =>
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Nov 2, 9:14 AM (8 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1804128
Default Alt Text
(41 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline