♻️ Migrate the exploring page
This commit is contained in:
16
app/components/AttachmentItem.vue
Normal file
16
app/components/AttachmentItem.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<img v-if="itemType == 'image'" :src="remoteSource" class="rounded-md">
|
||||
<audio v-else-if="itemType == 'audio'" :src="remoteSource" controls />
|
||||
<video v-else-if="itemType == 'video'" :src="remoteSource" controls />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ item: any }>()
|
||||
|
||||
const itemType = computed(() => props.item.mime_type.split('/')[0] ?? 'unknown')
|
||||
|
||||
const apiBase = useSolarNetworkUrl();
|
||||
const remoteSource = computed(() => `${apiBase}/drive/files/${props.item.id}?original=true`)
|
||||
</script>
|
131
app/components/PostEditor.vue
Normal file
131
app/components/PostEditor.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<pub-select v-model:value="publisher" />
|
||||
<v-textarea
|
||||
v-model="content"
|
||||
placeholder="What's happended?!"
|
||||
@keydown.meta.enter.exact="submit"
|
||||
@keydown.ctrl.enter.exact="submit"
|
||||
/>
|
||||
<div v-if="fileList.length > 0" class="d-flex gap-2 flex-wrap">
|
||||
<v-img
|
||||
v-for="file in fileList"
|
||||
:key="file.name"
|
||||
:src="file.url"
|
||||
width="100"
|
||||
height="100"
|
||||
class="rounded"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-2">
|
||||
<v-file-input
|
||||
v-model="selectedFiles"
|
||||
multiple
|
||||
accept="image/*,video/*,audio/*"
|
||||
label="Upload files"
|
||||
prepend-icon="mdi-upload"
|
||||
hide-details
|
||||
density="compact"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
<v-btn type="primary" :loading="submitting" @click="submit">
|
||||
Post
|
||||
<template #append>
|
||||
<v-icon>mdi-send</v-icon>
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import * as tus from 'tus-js-client'
|
||||
import { useSolarNetwork } from '~/composables/useSolarNetwork'
|
||||
|
||||
import PubSelect from './PubSelect.vue'
|
||||
|
||||
const emits = defineEmits(['posted'])
|
||||
|
||||
const publisher = ref<string | undefined>()
|
||||
const content = ref('')
|
||||
|
||||
const selectedFiles = ref<File[]>([])
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submit() {
|
||||
submitting.value = true
|
||||
const api = useSolarNetwork()
|
||||
await api(`/posts?pub=${publisher.value}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content.value,
|
||||
attachments: fileList.value
|
||||
.filter((e) => e.url != null)
|
||||
.map((e) => e.url!.split('/').reverse()[0]),
|
||||
}),
|
||||
})
|
||||
|
||||
submitting.value = false
|
||||
content.value = ''
|
||||
fileList.value = []
|
||||
emits('posted')
|
||||
}
|
||||
|
||||
function handleFileSelect() {
|
||||
selectedFiles.value.forEach(file => {
|
||||
uploadFile(file)
|
||||
})
|
||||
selectedFiles.value = []
|
||||
}
|
||||
|
||||
function uploadFile(file: File) {
|
||||
const upload = new tus.Upload(file, {
|
||||
endpoint: '/cgi/drive/tus',
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
removeFingerprintOnSuccess: false,
|
||||
uploadDataDuringCreation: false,
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
'content-type': file.type ?? 'application/octet-stream',
|
||||
},
|
||||
headers: {
|
||||
'X-DirectUpload': 'true',
|
||||
},
|
||||
onShouldRetry: () => false,
|
||||
onError: function (error) {
|
||||
console.error('[DRIVE] Upload failed:', error)
|
||||
},
|
||||
onProgress: function (bytesUploaded, bytesTotal) {
|
||||
// Could show progress
|
||||
},
|
||||
onSuccess: function (payload) {
|
||||
const rawInfo = payload.lastResponse.getHeader('x-fileinfo')
|
||||
const jsonInfo = JSON.parse(rawInfo as string)
|
||||
console.log('[DRIVE] Upload successful: ', jsonInfo)
|
||||
fileList.value.push({
|
||||
name: file.name,
|
||||
url: `/cgi/drive/files/${jsonInfo.id}`,
|
||||
type: jsonInfo.mime_type,
|
||||
})
|
||||
},
|
||||
onBeforeRequest: function (req) {
|
||||
const xhr = req.getUnderlyingObject()
|
||||
xhr.withCredentials = true
|
||||
},
|
||||
})
|
||||
upload.findPreviousUploads().then(function (previousUploads) {
|
||||
if (previousUploads.length) {
|
||||
upload.resumeFromPreviousUpload(previousUploads[0])
|
||||
}
|
||||
upload.start()
|
||||
})
|
||||
}
|
||||
</script>
|
28
app/components/PostHeader.vue
Normal file
28
app/components/PostHeader.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="flex gap-3 items-center">
|
||||
<v-avatar :image="publisherAvatar" size="40" />
|
||||
<div class="flex-grow-1 flex flex-col">
|
||||
<p class="flex gap-1 items-baseline">
|
||||
<span class="font-bold">{{ props.item.publisher.nick }}</span>
|
||||
<span class="text-xs">@{{ props.item.publisher.name }}</span>
|
||||
</p>
|
||||
<p class="text-xs flex gap-1">
|
||||
<span>{{ DateTime.fromISO(props.item.created_at).toRelative() }}</span>
|
||||
<span class="font-bold">·</span>
|
||||
<span>{{ DateTime.fromISO(props.item.created_at).toLocaleString() }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const props = defineProps<{ item: any }>()
|
||||
|
||||
const apiBase = useSolarNetworkUrl();
|
||||
const publisherAvatar = computed(() =>
|
||||
props.item.publisher.picture ? `${apiBase}/drive/files/${props.item.publisher.picture.id}` : undefined,
|
||||
)
|
||||
</script>
|
50
app/components/PostItem.vue
Normal file
50
app/components/PostItem.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="flex flex-col gap-3">
|
||||
<post-header :item="props.item" />
|
||||
|
||||
<div v-if="props.item.title || props.item.description">
|
||||
<h2 v-if="props.item.title" class="text-lg">{{ props.item.title }}</h2>
|
||||
<p v-if="props.item.description" class="text-sm">
|
||||
{{ props.item.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<article v-if="htmlContent" class="prose prose-sm dark:prose-invert prose-slate prose-p:m-0">
|
||||
<div v-html="htmlContent"/>
|
||||
</article>
|
||||
|
||||
<div v-if="props.item.attachments" class="d-flex gap-2 flex-wrap">
|
||||
<attachment-item
|
||||
v-for="attachment in props.item.attachments"
|
||||
:key="attachment.id"
|
||||
:item="attachment"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Marked } from 'marked'
|
||||
|
||||
import PostHeader from './PostHeader.vue'
|
||||
import AttachmentItem from './AttachmentItem.vue'
|
||||
|
||||
const props = defineProps<{ item: any }>()
|
||||
|
||||
const marked = new Marked()
|
||||
|
||||
const htmlContent = ref<string>('')
|
||||
|
||||
watch(
|
||||
props.item,
|
||||
async (value) => {
|
||||
if (value.content) htmlContent.value = await marked.parse(value.content)
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
</script>
|
55
app/components/PubSelect.vue
Normal file
55
app/components/PubSelect.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<v-select
|
||||
:items="pubStore.publishers"
|
||||
item-title="nick"
|
||||
item-value="name"
|
||||
:model-value="props.value"
|
||||
@update:model-value="(v) => emits('update:value', v)"
|
||||
>
|
||||
<template #item="{ props: itemProps, item }">
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
:image="item.raw.picture ? `${apiBase}/api/drive/files/${item.raw.picture.id}` : undefined"
|
||||
size="small"
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.raw?.nick }}</v-list-item-title>
|
||||
<v-list-item-subtitle>@{{ item.raw?.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #selection="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
:image="item.raw.picture ? `${apiBase}/api/drive/files/${item.raw.picture.id}` : undefined"
|
||||
size="24"
|
||||
rounded
|
||||
class="me-2"
|
||||
/>
|
||||
{{ item.raw?.nick }}
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePubStore } from '~/stores/pub'
|
||||
import { watch } from 'vue'
|
||||
|
||||
const pubStore = usePubStore()
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
|
||||
const props = defineProps<{ value: string | undefined }>()
|
||||
const emits = defineEmits(['update:value'])
|
||||
|
||||
watch(
|
||||
pubStore,
|
||||
(value) => {
|
||||
if (!props.value && value.publishers) {
|
||||
emits('update:value', pubStore.publishers[0]?.name)
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
</script>
|
Reference in New Issue
Block a user