mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
ENH - preview Feather post images
This commit is contained in:
@@ -17,6 +17,7 @@ import type { FeatherPost } from '@/types/feather'
|
||||
const props = defineProps<{ post: FeatherPost }>()
|
||||
defineEmits<{
|
||||
follow: [post: FeatherPost]
|
||||
media: [post: FeatherPost, index: number]
|
||||
menu: [post: FeatherPost]
|
||||
open: [post: FeatherPost]
|
||||
profile: [profileId: number]
|
||||
@@ -127,12 +128,20 @@ function relativeTime(timestamp: number): string {
|
||||
class="feather-media"
|
||||
:class="`feather-media--${Math.min(post.media.length, 4)}`"
|
||||
>
|
||||
<img
|
||||
v-for="item in post.media"
|
||||
<button
|
||||
v-for="(item, index) in post.media"
|
||||
:key="item.id"
|
||||
:src="item.url"
|
||||
alt=""
|
||||
/>
|
||||
type="button"
|
||||
class="feather-media__item"
|
||||
:aria-label="
|
||||
phone.t('Apps.feather.openImage', {
|
||||
number: String(index + 1),
|
||||
})
|
||||
"
|
||||
@click.stop="$emit('media', post, index)"
|
||||
>
|
||||
<img :src="item.url" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer class="feather-actions">
|
||||
@@ -307,7 +316,17 @@ function relativeTime(timestamp: number): string {
|
||||
border: 0.5px solid color-mix(in srgb, currentColor 12%, transparent);
|
||||
border-radius: 14px;
|
||||
}
|
||||
.feather-media img {
|
||||
.feather-media__item {
|
||||
min-width: 0;
|
||||
min-height: 105px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.feather-media__item img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 105px;
|
||||
@@ -318,7 +337,7 @@ function relativeTime(timestamp: number): string {
|
||||
.feather-media--4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.feather-media--3 img:first-child {
|
||||
.feather-media--3 .feather-media__item:first-child {
|
||||
grid-row: span 2;
|
||||
}
|
||||
.feather-actions {
|
||||
|
||||
@@ -550,6 +550,11 @@ const defaultLocales: LocaleTree = {
|
||||
bookmarks: 'Bookmarks',
|
||||
noBookmarks: 'No saved posts yet',
|
||||
noBookmarksBody: 'Posts you bookmark will appear here.',
|
||||
imagePreview: 'Image preview',
|
||||
openImage: 'Open image {number}',
|
||||
postImage: 'Post image {number}',
|
||||
previousImage: 'Previous image',
|
||||
nextImage: 'Next image',
|
||||
delete: 'Delete',
|
||||
report: 'Report',
|
||||
block: 'Block @{handle}',
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Camera,
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Feather,
|
||||
@@ -62,7 +64,7 @@ import { useFeatherStore } from '@/stores/feather'
|
||||
import type { FeatherConnectionMode } from '@/stores/feather'
|
||||
import { useMessageMediaStore } from '@/stores/messageMedia'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import type { FeatherPost, FeatherProfile } from '@/types/feather'
|
||||
import type { FeatherMedia, FeatherPost, FeatherProfile } from '@/types/feather'
|
||||
import {
|
||||
filterMailAddressInput,
|
||||
MAIL_ADDRESS_INPUT_MAX_LENGTH,
|
||||
@@ -116,6 +118,7 @@ const menuPost = ref<FeatherPost | null>(null)
|
||||
const reportOpen = ref(false)
|
||||
const reportReason = ref('spam')
|
||||
const reportDetails = ref('')
|
||||
const mediaPreview = ref<{ index: number; items: FeatherMedia[] } | null>(null)
|
||||
const onboarding = ref({ bio: '', displayName: '', handle: '' })
|
||||
const editing = ref({ bio: '', displayName: '' })
|
||||
const authMode = ref<'login' | 'register'>('login')
|
||||
@@ -149,6 +152,10 @@ const displayedProfilePosts = computed(() => {
|
||||
return feather.profilePosts.filter((post) => post.media.length > 0)
|
||||
return feather.profilePosts.filter((post) => !post.reply_to_id)
|
||||
})
|
||||
const previewMedia = computed(() => {
|
||||
if (!mediaPreview.value) return null
|
||||
return mediaPreview.value.items[mediaPreview.value.index] ?? null
|
||||
})
|
||||
const networkSections = computed(() => {
|
||||
const sections: Array<{
|
||||
key: 'results' | 'suggestions'
|
||||
@@ -241,6 +248,28 @@ function t(path: string, params?: Record<string, string>): string {
|
||||
return phone.t(`Apps.feather.${path}`, params)
|
||||
}
|
||||
|
||||
function openMediaPreview(post: FeatherPost, index: number): void {
|
||||
mediaPreview.value = { index, items: post.media }
|
||||
}
|
||||
|
||||
function closeMediaPreview(): void {
|
||||
mediaPreview.value = null
|
||||
}
|
||||
|
||||
function moveMediaPreview(direction: number): void {
|
||||
if (!mediaPreview.value || mediaPreview.value.items.length < 2) return
|
||||
mediaPreview.value.index =
|
||||
(mediaPreview.value.index + direction + mediaPreview.value.items.length) %
|
||||
mediaPreview.value.items.length
|
||||
}
|
||||
|
||||
function handleMediaPreviewKeydown(event: KeyboardEvent): void {
|
||||
if (!mediaPreview.value) return
|
||||
if (event.key === 'Escape') closeMediaPreview()
|
||||
if (event.key === 'ArrowLeft') moveMediaPreview(-1)
|
||||
if (event.key === 'ArrowRight') moveMediaPreview(1)
|
||||
}
|
||||
|
||||
function toast(path: string): void {
|
||||
feedback.value = t(path)
|
||||
window.setTimeout(() => {
|
||||
@@ -651,9 +680,11 @@ watch(
|
||||
onBeforeUnmount(() => {
|
||||
if (exploreSearchTimer !== undefined) window.clearTimeout(exploreSearchTimer)
|
||||
if (networkSearchTimer !== undefined) window.clearTimeout(networkSearchTimer)
|
||||
window.removeEventListener('keydown', handleMediaPreviewKeydown)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', handleMediaPreviewKeydown)
|
||||
const selection =
|
||||
messageMedia.consumeMany<ComposerContext>('feather:composer')
|
||||
if (selection) {
|
||||
@@ -1217,6 +1248,7 @@ onMounted(async () => {
|
||||
<FeatherPostCard
|
||||
:post="feather.thread.post"
|
||||
@follow="feather.followPost"
|
||||
@media="openMediaPreview"
|
||||
@menu="menuPost = $event"
|
||||
@open="() => undefined"
|
||||
@profile="openProfile"
|
||||
@@ -1300,6 +1332,7 @@ onMounted(async () => {
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
@follow="feather.followPost"
|
||||
@media="openMediaPreview"
|
||||
@menu="menuPost = $event"
|
||||
@open="openThread"
|
||||
@profile="openProfile"
|
||||
@@ -1456,6 +1489,7 @@ onMounted(async () => {
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
@follow="feather.followPost"
|
||||
@media="openMediaPreview"
|
||||
@menu="menuPost = $event"
|
||||
@open="openThread"
|
||||
@profile="openProfile"
|
||||
@@ -1800,6 +1834,7 @@ onMounted(async () => {
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
@follow="feather.followPost"
|
||||
@media="openMediaPreview"
|
||||
@menu="menuPost = $event"
|
||||
@open="openThread"
|
||||
@profile="openProfile"
|
||||
@@ -2048,6 +2083,44 @@ onMounted(async () => {
|
||||
</kBlock>
|
||||
</kSheet>
|
||||
|
||||
<div
|
||||
v-if="mediaPreview && previewMedia"
|
||||
class="feather-media-preview"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="t('imagePreview')"
|
||||
@click="closeMediaPreview"
|
||||
>
|
||||
<img
|
||||
:src="previewMedia.url"
|
||||
:alt="t('postImage', { number: String(mediaPreview.index + 1) })"
|
||||
@click.stop
|
||||
/>
|
||||
<template v-if="mediaPreview.items.length > 1">
|
||||
<kButton
|
||||
clear
|
||||
rounded
|
||||
class="feather-media-preview__arrow feather-media-preview__arrow--left"
|
||||
:aria-label="t('previousImage')"
|
||||
@click.stop="moveMediaPreview(-1)"
|
||||
>
|
||||
<ChevronLeft :size="27" />
|
||||
</kButton>
|
||||
<kButton
|
||||
clear
|
||||
rounded
|
||||
class="feather-media-preview__arrow feather-media-preview__arrow--right"
|
||||
:aria-label="t('nextImage')"
|
||||
@click.stop="moveMediaPreview(1)"
|
||||
>
|
||||
<ChevronRight :size="27" />
|
||||
</kButton>
|
||||
<span class="feather-media-preview__count">
|
||||
{{ mediaPreview.index + 1 }} / {{ mediaPreview.items.length }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<kToast :opened="Boolean(feedback)">{{ feedback }}</kToast>
|
||||
</kPage>
|
||||
</template>
|
||||
@@ -2059,6 +2132,57 @@ onMounted(async () => {
|
||||
background: #fff;
|
||||
color: #111923;
|
||||
}
|
||||
.feather-media-preview {
|
||||
position: absolute;
|
||||
z-index: 120;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 64px 12px 42px;
|
||||
background: rgb(0 0 0 / 92%);
|
||||
cursor: zoom-out;
|
||||
}
|
||||
.feather-media-preview > img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 12px;
|
||||
object-fit: contain;
|
||||
box-shadow: 0 18px 50px rgb(0 0 0 / 55%);
|
||||
cursor: default;
|
||||
}
|
||||
.feather-media-preview__arrow {
|
||||
--k-button-bg-color: rgb(22 27 34 / 78%);
|
||||
--k-button-text-color: #fff;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid rgb(255 255 255 / 18%);
|
||||
backdrop-filter: blur(14px);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.feather-media-preview__arrow--left {
|
||||
left: 14px;
|
||||
}
|
||||
.feather-media-preview__arrow--right {
|
||||
right: 14px;
|
||||
}
|
||||
.feather-media-preview__count {
|
||||
position: absolute;
|
||||
bottom: 48px;
|
||||
left: 50%;
|
||||
border: 1px solid rgb(255 255 255 / 14%);
|
||||
border-radius: 999px;
|
||||
padding: 5px 10px;
|
||||
color: #fff;
|
||||
background: rgb(22 27 34 / 78%);
|
||||
backdrop-filter: blur(14px);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
:global(.dark) .feather-app {
|
||||
background: #090d12;
|
||||
color: #f4f7fa;
|
||||
|
||||
@@ -211,7 +211,7 @@ Locales["en"] = {
|
||||
trendKinds = { explore = "Trending in Los Santos", trending = "Popular right now", news = "News · Trending", sports = "Sports · Trending", entertainment = "Entertainment · Trending" },
|
||||
people = "Who to follow", posts = "Posts", followers = "Followers", followingCount = "Following", removeConnection = "Remove", followerRemoved = "Follower removed.", followingRemoved = "Account unfollowed.", noConnections = "Nobody here yet", noConnectionsBody = "Profiles will appear here as connections are made.", follow = "Follow", unfollow = "Following",
|
||||
editProfile = "Edit profile", saveProfile = "Save", joined = "Joined Feather", joinedDate = "Joined August 2026", shareProfile = "Share profile", profileCopied = "Profile handle copied.", postCopied = "Post copied.", verified = "Verified profile", noBio = "No bio yet.", showMore = "Show more",
|
||||
replies = "Replies", noReplies = "No replies yet. Start the conversation.", likes = "Likes", bookmarks = "Bookmarks", noBookmarks = "No saved posts yet", noBookmarksBody = "Posts you bookmark will appear here.", delete = "Delete", report = "Report", block = "Block @{handle}",
|
||||
replies = "Replies", noReplies = "No replies yet. Start the conversation.", likes = "Likes", bookmarks = "Bookmarks", noBookmarks = "No saved posts yet", noBookmarksBody = "Posts you bookmark will appear here.", imagePreview = "Image preview", openImage = "Open image {number}", postImage = "Post image {number}", previousImage = "Previous image", nextImage = "Next image", delete = "Delete", report = "Report", block = "Block @{handle}",
|
||||
reportTitle = "Report feather", reportBody = "Tell us why this feather should be reviewed.", reportSubmit = "Submit report", reported = "Report submitted.", blocked = "Profile blocked.", deleted = "Feather deleted.",
|
||||
reasons = { spam = "Spam", harassment = "Harassment", dangerous = "Dangerous content", illegal = "Illegal content", other = "Other" },
|
||||
noActivity = "No activity yet", activityBody = "Likes, replies and new followers will appear here.",
|
||||
|
||||
Reference in New Issue
Block a user